diff --git a/code/game/jobs/job/science.dm b/code/game/jobs/job/science.dm
index eaa1c4b7936..1f69a5ee14d 100644
--- a/code/game/jobs/job/science.dm
+++ b/code/game/jobs/job/science.dm
@@ -133,33 +133,3 @@
H.equip_to_slot_or_del(new /obj/item/weapon/storage/belt/utility/full(H), slot_belt)
H.equip_to_slot_or_del(new /obj/item/clothing/suit/storage/toggle/labcoat(H), slot_wear_suit)
return TRUE
-
-/datum/job/geneticist
- title = "Geneticist"
- flag = GENETICIST
- department = "Science"
- department_flag = MEDSCI
- faction = "Station"
- total_positions = 0
- spawn_positions = 0
- supervisors = "the research director"
- selection_color = "#ffeeff"
- economic_modifier = 7
- access = list(access_genetics, access_tox, access_tox_storage, access_morgue, access_research)
- minimal_access = list(access_morgue, access_genetics, access_research)
-
- bag_type = /obj/item/weapon/storage/backpack/genetics
- satchel_type = /obj/item/weapon/storage/backpack/satchel_gen
- duffel_type = /obj/item/weapon/storage/backpack/duffel/gen
- messenger_bag_type = /obj/item/weapon/storage/backpack/messenger/gen
-
- equip(var/mob/living/carbon/human/H)
- if(!H)
- return FALSE
- H.equip_to_slot_or_del(new /obj/item/device/radio/headset/headset_sci(H), slot_l_ear)
- H.equip_to_slot_or_del(new /obj/item/clothing/under/rank/geneticist(H), slot_w_uniform)
- H.equip_to_slot_or_del(new /obj/item/clothing/shoes/white(H), slot_shoes)
- H.equip_to_slot_or_del(new /obj/item/device/pda/geneticist(H), slot_belt)
- H.equip_to_slot_or_del(new /obj/item/clothing/suit/storage/toggle/labcoat/genetics(H), slot_wear_suit)
- H.equip_to_slot_or_del(new /obj/item/device/flashlight/pen(H), slot_s_store)
- return TRUE
diff --git a/code/game/jobs/jobs.dm b/code/game/jobs/jobs.dm
index 8736fa16874..c7c84264c75 100644
--- a/code/game/jobs/jobs.dm
+++ b/code/game/jobs/jobs.dm
@@ -23,14 +23,13 @@ var/const/SCIENTIST =(1<<1)
var/const/CHEMIST =(1<<2)
var/const/CMO =(1<<3)
var/const/DOCTOR =(1<<4)
-var/const/GENETICIST =(1<<5)
-var/const/VIROLOGIST =(1<<6)
-var/const/PSYCHIATRIST =(1<<7)
-var/const/ROBOTICIST =(1<<8)
-var/const/XENOBIOLOGIST =(1<<9)
-var/const/PARAMEDIC =(1<<10)
-var/const/INTERN_MED =(1<<11)
-var/const/INTERN_SCI =(1<<12)
+var/const/VIROLOGIST =(1<<5)
+var/const/PSYCHIATRIST =(1<<6)
+var/const/ROBOTICIST =(1<<7)
+var/const/XENOBIOLOGIST =(1<<8)
+var/const/PARAMEDIC =(1<<9)
+var/const/INTERN_MED =(1<<10)
+var/const/INTERN_SCI =(1<<11)
var/const/CIVILIAN =(1<<2)
@@ -85,7 +84,6 @@ var/list/medical_positions = list(
var/list/science_positions = list(
"Research Director",
"Scientist",
- "Geneticist", //Part of both medical and science
"Roboticist",
"Xenobiologist",
"Lab Assistant"
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 21bce1f0d1b..bc75fdc929b 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -33,7 +33,7 @@
return null
var/atom/T = null
- var/lowest_health = 100
+ var/lowest_health = INFINITY // Max you can get
stop_automated_movement = 0
for(var/atom/A in targets)
@@ -192,6 +192,8 @@ mob/living/simple_animal/hostile/hitby(atom/movable/AM as mob|obj,var/speed = TH
if(HOSTILE_STANCE_IDLE)
targets = ListTargets(10)
target_mob = FindTarget()
+ if(destroy_surroundings && isnull(target_mob))
+ DestroySurroundings()
if(HOSTILE_STANCE_ATTACK)
if(destroy_surroundings)
diff --git a/code/modules/power/smes.dm b/code/modules/power/smes.dm
index f1d3fd386b2..d565ec72e94 100644
--- a/code/modules/power/smes.dm
+++ b/code/modules/power/smes.dm
@@ -333,7 +333,10 @@
// this is the data which will be sent to the ui
var/data[0]
data["nameTag"] = name_tag
- data["storedCapacity"] = round(100.0*charge/capacity, 0.1)
+ if(capacity)
+ data["storedCapacity"] = round(100.0*charge/capacity, 0.1)
+ else
+ data["storedCapacity"] = 0
data["charging"] = inputting
data["chargeMode"] = input_attempt
data["chargeLevel"] = input_level
diff --git a/code/modules/recycling/disposal.dm b/code/modules/recycling/disposal.dm
index 13a42ccca75..e7d1b095e57 100644
--- a/code/modules/recycling/disposal.dm
+++ b/code/modules/recycling/disposal.dm
@@ -1412,9 +1412,13 @@
var/turf/target // this will be where the output objects are 'thrown' to.
var/mode = 0
+ var/spread = 0
+ var/spread_point = 10
+
+
/obj/structure/disposaloutlet/Initialize()
. = ..()
- target = get_ranged_target_turf(src, dir, 10)
+ target = get_ranged_target_turf(src, dir, spread_point)
var/obj/structure/disposalpipe/trunk/trunk = locate() in src.loc
if(trunk)
@@ -1437,7 +1441,12 @@
AM.pipe_eject(dir)
if(!istype(AM,/mob/living/silicon/robot/drone)) //Drones keep smashing windows from being fired out of chutes. Bad for the station. ~Z
spawn(5)
- AM.throw_at(target, 3, 1)
+ if(spread)
+ var/turf/new_turf_target = get_step(target,turn(src.dir, rand(-spread,spread)))
+ AM.throw_at(new_turf_target, 3, 1)
+ else
+ AM.throw_at(target, 3, 1)
+
H.vent_gas(src.loc)
qdel(H)
diff --git a/code/modules/research/designs/circuit_designs.dm b/code/modules/research/designs/circuit_designs.dm
index 0a76c264fe4..b898445ba99 100644
--- a/code/modules/research/designs/circuit_designs.dm
+++ b/code/modules/research/designs/circuit_designs.dm
@@ -79,12 +79,6 @@
build_path = /obj/item/weapon/circuitboard/pandemic
sort_string = "FAEAA"
-/datum/design/circuit/scan_console
- name = "DNA machine"
- id = "scan_console"
- build_path = /obj/item/weapon/circuitboard/scan_consolenew
- sort_string = "FAGAA"
-
/datum/design/circuit/clonecontrol
name = "cloning control console"
id = "clonecontrol"
diff --git a/html/changelog.html b/html/changelog.html
index 8c4868a4f59..fe3aabae566 100644
--- a/html/changelog.html
+++ b/html/changelog.html
@@ -56,6 +56,13 @@
-->
+
03 June 2018
+
BurgerBB updated:
+
+ - Added vents to the cargo warehouse connector to prevent underpressure from disposal inlets.
+ - Disposal outlets now spread out items so all the trash doesn't pile up on one tile.
+
+
29 May 2018
Kaedwuff updated:
diff --git a/html/changelogs/.all_changelog.yml b/html/changelogs/.all_changelog.yml
index 26008dbe624..7e46944753a 100644
--- a/html/changelogs/.all_changelog.yml
+++ b/html/changelogs/.all_changelog.yml
@@ -5523,3 +5523,9 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py.
2018-05-29:
Kaedwuff:
- bugfix: You can no longer slice fruit (or anything else) with a syringe.
+2018-06-03:
+ BurgerBB:
+ - bugfix: Added vents to the cargo warehouse connector to prevent underpressure
+ from disposal inlets.
+ - bugfix: Disposal outlets now spread out items so all the trash doesn't pile up
+ on one tile.
diff --git a/maps/aurora/aurora-4_mainlevel.dmm b/maps/aurora/aurora-4_mainlevel.dmm
index bd0d67ddcbd..08cd2efd739 100644
--- a/maps/aurora/aurora-4_mainlevel.dmm
+++ b/maps/aurora/aurora-4_mainlevel.dmm
@@ -2223,6 +2223,14 @@
},
/turf/simulated/floor/tiled,
/area/security/brig)
+"aet" = (
+/obj/machinery/vending/hydroseeds{
+ prices = list();
+ restock_items = 1
+ },
+/obj/effect/floor_decal/industrial/outline/yellow,
+/turf/simulated/floor/tiled,
+/area/hydroponics)
"aeu" = (
/obj/machinery/atmospherics/pipe/manifold/hidden/supply{
dir = 4
@@ -2311,6 +2319,27 @@
dir = 4
},
/area/security/brig)
+"aeD" = (
+/obj/item/weapon/material/hatchet,
+/obj/item/weapon/material/minihoe,
+/obj/effect/floor_decal/corner/green{
+ icon_state = "corner_white";
+ dir = 9
+ },
+/obj/structure/table/standard,
+/obj/structure/disposalpipe/segment{
+ dir = 1;
+ icon_state = "pipe-c"
+ },
+/obj/machinery/newscaster{
+ pixel_x = -32
+ },
+/obj/item/watertank,
+/obj/item/weapon/screwdriver,
+/obj/item/weapon/wrench,
+/obj/item/weapon/shovel/spade,
+/turf/simulated/floor/tiled,
+/area/hydroponics)
"aeE" = (
/obj/structure/cable/green{
d1 = 1;
@@ -2580,6 +2609,11 @@
},
/turf/simulated/floor/tiled,
/area/security/brig)
+"afd" = (
+/obj/effect/floor_decal/industrial/outline/yellow,
+/obj/machinery/vending/hydronutrients,
+/turf/simulated/floor/tiled,
+/area/hydroponics)
"afe" = (
/turf/simulated/wall/r_wall,
/area/security/investigations)
@@ -2726,6 +2760,11 @@
},
/turf/simulated/floor/tiled,
/area/security/brig)
+"afy" = (
+/obj/effect/floor_decal/industrial/outline/yellow,
+/obj/machinery/seed_extractor,
+/turf/simulated/floor/tiled,
+/area/hydroponics)
"afC" = (
/obj/structure/closet{
name = "Evidence Closet"
@@ -7116,41 +7155,6 @@
/obj/machinery/shield_gen/external,
/turf/simulated/floor/plating,
/area/engineering/storage)
-"apt" = (
-/obj/structure/table/rack,
-/obj/item/clothing/suit/space/void/atmos,
-/obj/item/clothing/head/helmet/space/void/atmos,
-/obj/item/clothing/shoes/magboots,
-/obj/item/clothing/mask/breath,
-/obj/structure/window/reinforced{
- icon_state = "rwindow";
- dir = 1
- },
-/obj/effect/floor_decal/industrial/outline/yellow,
-/obj/machinery/door/window/southleft{
- dir = 4;
- icon_state = "left";
- name = "Atmospheric Hardsuits";
- req_access = list(24)
- },
-/obj/structure/table/rack,
-/obj/item/clothing/suit/space/void/atmos,
-/obj/item/clothing/head/helmet/space/void/atmos,
-/obj/item/clothing/shoes/magboots,
-/obj/item/clothing/mask/breath,
-/obj/structure/window/reinforced{
- icon_state = "rwindow";
- dir = 1
- },
-/obj/effect/floor_decal/industrial/outline/yellow,
-/obj/machinery/door/window/southleft{
- dir = 4;
- icon_state = "left";
- name = "Atmospheric Hardsuits";
- req_access = list(24)
- },
-/turf/simulated/floor/tiled,
-/area/engineering/atmos/storage)
"apu" = (
/obj/machinery/atmospherics/unary/vent_scrubber/on{
dir = 4
@@ -7719,33 +7723,6 @@
/obj/machinery/portable_atmospherics/canister/nitrogen,
/turf/simulated/floor/plating,
/area/engineering/storage)
-"aqH" = (
-/obj/structure/table/rack,
-/obj/item/clothing/suit/space/void/atmos,
-/obj/item/clothing/head/helmet/space/void/atmos,
-/obj/item/clothing/shoes/magboots,
-/obj/item/clothing/mask/breath,
-/obj/effect/floor_decal/industrial/outline/yellow,
-/obj/machinery/door/window/southleft{
- dir = 4;
- icon_state = "left";
- name = "Atmospheric Hardsuits";
- req_access = list(24)
- },
-/obj/structure/table/rack,
-/obj/item/clothing/suit/space/void/atmos,
-/obj/item/clothing/head/helmet/space/void/atmos,
-/obj/item/clothing/shoes/magboots,
-/obj/item/clothing/mask/breath,
-/obj/effect/floor_decal/industrial/outline/yellow,
-/obj/machinery/door/window/southleft{
- dir = 4;
- icon_state = "left";
- name = "Atmospheric Hardsuits";
- req_access = list(24)
- },
-/turf/simulated/floor/tiled,
-/area/engineering/atmos/storage)
"aqI" = (
/obj/machinery/light,
/obj/machinery/atmospherics/pipe/simple/hidden/red,
@@ -32678,23 +32655,6 @@
/obj/item/weapon/circuitboard/arcade/orion_trail,
/turf/simulated/floor/plating,
/area/storage/tech)
-"bgk" = (
-/obj/structure/table/rack{
- dir = 8;
- layer = 2.9
- },
-/obj/item/weapon/circuitboard/cloning{
- pixel_x = 0
- },
-/obj/item/weapon/circuitboard/clonescanner,
-/obj/item/weapon/circuitboard/clonepod,
-/obj/item/weapon/circuitboard/scan_consolenew,
-/obj/item/weapon/circuitboard/med_data{
- pixel_x = 3;
- pixel_y = -3
- },
-/turf/simulated/floor/plating,
-/area/storage/tech)
"bgl" = (
/obj/structure/table/rack{
dir = 8;
@@ -57389,18 +57349,6 @@
/obj/item/weapon/wrapping_paper,
/turf/simulated/floor/tiled,
/area/quartermaster/office)
-"cad" = (
-/obj/effect/floor_decal/corner/brown{
- icon_state = "corner_white";
- dir = 5
- },
-/obj/effect/floor_decal/industrial/loading{
- icon_state = "loadingarea";
- dir = 1
- },
-/obj/effect/floor_decal/industrial/outline/yellow,
-/turf/simulated/floor/tiled,
-/area/quartermaster/office)
"cae" = (
/obj/effect/floor_decal/corner/brown/full{
icon_state = "corner_white_full";
@@ -57714,17 +57662,6 @@
/obj/effect/large_stock_marker,
/turf/simulated/floor/tiled,
/area/quartermaster/office)
-"caM" = (
-/obj/machinery/camera/network/supply{
- c_tag = "Cargo - Warehouse Entrence";
- dir = 1
- },
-/obj/effect/floor_decal/corner/brown{
- dir = 10
- },
-/obj/machinery/light/small,
-/turf/simulated/floor/tiled,
-/area/quartermaster/storage)
"caN" = (
/obj/effect/floor_decal/corner/brown/full{
icon_state = "corner_white_full";
@@ -58283,17 +58220,6 @@
},
/turf/simulated/floor/tiled,
/area/maintenance/disposal)
-"ccd" = (
-/obj/structure/lattice/catwalk,
-/turf/simulated/open,
-/area/maintenance/disposal)
-"cce" = (
-/obj/structure/lattice/catwalk,
-/obj/structure/sign/drop{
- pixel_y = 32
- },
-/turf/simulated/open,
-/area/maintenance/disposal)
"ccf" = (
/turf/simulated/open,
/area/maintenance/disposal)
@@ -58490,15 +58416,6 @@
},
/turf/simulated/floor/tiled,
/area/maintenance/disposal)
-"ccA" = (
-/obj/structure/disposalpipe/trunk{
- dir = 4
- },
-/obj/structure/disposaloutlet{
- dir = 8
- },
-/turf/simulated/floor/plating,
-/area/maintenance/disposal)
"ccB" = (
/obj/structure/cable{
d1 = 1;
@@ -58725,11 +58642,6 @@
},
/turf/simulated/floor/tiled,
/area/maintenance/disposal)
-"ccW" = (
-/obj/machinery/light/small/emergency,
-/obj/structure/lattice/catwalk,
-/turf/simulated/open,
-/area/maintenance/disposal)
"ccX" = (
/obj/machinery/light/small/emergency,
/obj/structure/closet/crate,
@@ -58801,15 +58713,6 @@
},
/turf/simulated/wall/r_wall,
/area/maintenance/disposal)
-"cdi" = (
-/obj/structure/disposaloutlet,
-/obj/structure/disposalpipe/trunk{
- dir = 8
- },
-/turf/simulated/floor/tiled/airless{
- icon_state = "asteroidfloor"
- },
-/area/maintenance/disposal)
"cdj" = (
/obj/machinery/light/small/emergency{
dir = 8
@@ -64577,21 +64480,40 @@
/turf/simulated/floor,
/area/security/security_office)
"csM" = (
+/obj/structure/table/rack,
+/obj/item/clothing/suit/space/void/atmos,
+/obj/item/clothing/head/helmet/space/void/atmos,
+/obj/item/clothing/shoes/magboots,
+/obj/item/clothing/mask/breath,
+/obj/structure/window/reinforced{
+ icon_state = "rwindow";
+ dir = 1
+ },
+/obj/effect/floor_decal/industrial/outline/yellow,
+/obj/machinery/door/window/southleft{
+ dir = 4;
+ icon_state = "left";
+ name = "Atmospheric Hardsuits";
+ req_access = list(24)
+ },
+/turf/simulated/floor/tiled,
+/area/engineering/atmos/storage)
+"csN" = (
/turf/simulated/wall,
/area/security/security_office)
-"csN" = (
+"csO" = (
/obj/machinery/vending/coffee,
/obj/effect/floor_decal/industrial/hatch/yellow,
/turf/simulated/floor/tiled,
/area/security/security_office)
-"csO" = (
-/turf/simulated/floor/tiled,
-/area/security/security_office)
"csP" = (
-/obj/machinery/light,
/turf/simulated/floor/tiled,
/area/security/security_office)
"csQ" = (
+/obj/machinery/light,
+/turf/simulated/floor/tiled,
+/area/security/security_office)
+"csR" = (
/obj/machinery/requests_console{
department = "Security";
departmentType = 5;
@@ -64599,7 +64521,7 @@
},
/turf/simulated/floor/tiled,
/area/security/security_office)
-"csR" = (
+"csS" = (
/obj/machinery/camera/network/security{
c_tag = "Security - Security Office";
dir = 1;
@@ -64620,7 +64542,7 @@
},
/turf/simulated/floor/tiled,
/area/security/security_office)
-"csS" = (
+"csT" = (
/obj/effect/floor_decal/corner/blue{
icon_state = "corner_white";
dir = 10
@@ -64637,7 +64559,7 @@
},
/turf/simulated/floor/tiled,
/area/security/security_office)
-"csT" = (
+"csU" = (
/obj/machinery/hologram/holopad,
/obj/effect/floor_decal/corner/blue{
icon_state = "corner_white";
@@ -64655,7 +64577,7 @@
},
/turf/simulated/floor/tiled,
/area/security/security_office)
-"csU" = (
+"csV" = (
/obj/structure/flora/pottedplant/random,
/obj/effect/floor_decal/corner/blue{
icon_state = "corner_white";
@@ -64666,7 +64588,7 @@
},
/turf/simulated/floor/tiled,
/area/security/security_office)
-"csV" = (
+"csW" = (
/obj/structure/grille,
/obj/structure/window/reinforced,
/obj/structure/window/reinforced{
@@ -64679,7 +64601,7 @@
/obj/machinery/door/firedoor,
/turf/simulated/floor,
/area/security/security_office)
-"csW" = (
+"csX" = (
/obj/effect/floor_decal/corner/blue,
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,
/obj/machinery/atmospherics/pipe/manifold/hidden/supply{
@@ -64696,24 +64618,39 @@
},
/turf/simulated/floor/tiled,
/area/security/brig)
-"csX" = (
-/turf/simulated/wall,
-/area/security/security_office)
"csY" = (
-/turf/simulated/wall,
-/area/security/security_office)
+/obj/item/clothing/suit/space/void/atmos,
+/obj/item/clothing/head/helmet/space/void/atmos,
+/obj/item/clothing/shoes/magboots,
+/obj/effect/floor_decal/industrial/outline/yellow,
+/obj/machinery/door/window/southleft{
+ dir = 4;
+ icon_state = "left";
+ name = "Atmospheric Hardsuits";
+ req_access = list(24)
+ },
+/obj/structure/table/rack,
+/obj/item/clothing/mask/breath,
+/turf/simulated/floor/tiled,
+/area/engineering/atmos/storage)
"csZ" = (
-/obj/structure/grille,
-/obj/structure/window/reinforced,
-/obj/structure/window/reinforced{
- dir = 1
- },
-/obj/structure/window/reinforced{
- dir = 8
- },
-/turf/simulated/floor/plating,
+/turf/simulated/wall,
/area/security/security_office)
"cta" = (
+/turf/simulated/wall,
+/area/security/security_office)
+"ctb" = (
+/obj/structure/grille,
+/obj/structure/window/reinforced,
+/obj/structure/window/reinforced{
+ dir = 1
+ },
+/obj/structure/window/reinforced{
+ dir = 8
+ },
+/turf/simulated/floor/plating,
+/area/security/security_office)
+"ctc" = (
/obj/structure/grille,
/obj/structure/window/reinforced,
/obj/structure/window/reinforced{
@@ -64725,43 +64662,11 @@
},
/turf/simulated/floor/plating,
/area/security/security_office)
-"ctb" = (
-/turf/simulated/wall,
-/area/security/security_office)
-"ctc" = (
-/turf/simulated/wall,
-/area/security/security_office)
"ctd" = (
-/obj/structure/grille,
-/obj/structure/window/reinforced,
-/obj/structure/window/reinforced{
- dir = 1
- },
-/obj/structure/window/reinforced{
- dir = 4
- },
-/obj/structure/window/reinforced{
- icon_state = "rwindow";
- dir = 8
- },
-/obj/machinery/door/firedoor,
-/turf/simulated/floor,
+/turf/simulated/wall,
/area/security/security_office)
"cte" = (
-/obj/machinery/door/airlock/glass_security{
- name = "Security Office";
- req_access = list(63)
- },
-/obj/structure/cable/green{
- d1 = 1;
- d2 = 2;
- icon_state = "1-2"
- },
-/obj/machinery/atmospherics/pipe/simple/hidden/supply,
-/obj/structure/disposalpipe/segment,
-/obj/machinery/door/firedoor,
-/obj/machinery/atmospherics/pipe/simple/hidden,
-/turf/simulated/floor/tiled,
+/turf/simulated/wall,
/area/security/security_office)
"ctf" = (
/obj/structure/grille,
@@ -64780,47 +64685,128 @@
/turf/simulated/floor,
/area/security/security_office)
"ctg" = (
-/turf/simulated/wall,
+/obj/machinery/door/airlock/glass_security{
+ name = "Security Office";
+ req_access = list(63)
+ },
+/obj/structure/cable/green{
+ d1 = 1;
+ d2 = 2;
+ icon_state = "1-2"
+ },
+/obj/machinery/atmospherics/pipe/simple/hidden/supply,
+/obj/structure/disposalpipe/segment,
+/obj/machinery/door/firedoor,
+/obj/machinery/atmospherics/pipe/simple/hidden,
+/turf/simulated/floor/tiled,
/area/security/security_office)
"cth" = (
-/obj/item/weapon/material/hatchet,
-/obj/item/weapon/material/minihoe,
-/obj/effect/floor_decal/corner/green{
- icon_state = "corner_white";
- dir = 9
+/obj/structure/grille,
+/obj/structure/window/reinforced,
+/obj/structure/window/reinforced{
+ dir = 1
},
-/obj/structure/table/standard,
-/obj/structure/disposalpipe/segment{
- dir = 1;
- icon_state = "pipe-c"
+/obj/structure/window/reinforced{
+ dir = 4
},
-/obj/machinery/newscaster{
- pixel_x = -32
+/obj/structure/window/reinforced{
+ icon_state = "rwindow";
+ dir = 8
},
-/obj/item/watertank,
-/obj/item/weapon/screwdriver,
-/obj/item/weapon/wrench,
-/obj/item/weapon/shovel/spade,
-/turf/simulated/floor/tiled,
-/area/hydroponics)
+/obj/machinery/door/firedoor,
+/turf/simulated/floor,
+/area/security/security_office)
"cti" = (
-/obj/machinery/vending/hydroseeds{
- prices = list();
- restock_items = 1
+/turf/simulated/wall,
+/area/security/security_office)
+"ctj" = (
+/obj/structure/table/rack{
+ dir = 8;
+ layer = 2.9
+ },
+/obj/item/weapon/circuitboard/cloning{
+ pixel_x = 0
+ },
+/obj/item/weapon/circuitboard/clonescanner,
+/obj/item/weapon/circuitboard/clonepod,
+/obj/item/weapon/circuitboard/med_data{
+ pixel_x = 3;
+ pixel_y = -3
+ },
+/turf/simulated/floor/plating,
+/area/storage/tech)
+"ctk" = (
+/obj/effect/floor_decal/corner/brown{
+ icon_state = "corner_white";
+ dir = 5
+ },
+/obj/effect/floor_decal/industrial/loading{
+ icon_state = "loadingarea";
+ dir = 1
},
/obj/effect/floor_decal/industrial/outline/yellow,
+/obj/machinery/atmospherics/unary/vent_pump/on,
/turf/simulated/floor/tiled,
-/area/hydroponics)
-"ctj" = (
-/obj/effect/floor_decal/industrial/outline/yellow,
-/obj/machinery/vending/hydronutrients,
+/area/quartermaster/office)
+"ctl" = (
+/obj/structure/cable/green{
+ d1 = 4;
+ d2 = 8;
+ icon_state = "4-8"
+ },
+/obj/machinery/atmospherics/pipe/manifold/hidden/supply{
+ dir = 1
+ },
+/obj/machinery/atmospherics/pipe/manifold/hidden/scrubbers,
/turf/simulated/floor/tiled,
-/area/hydroponics)
-"ctk" = (
-/obj/effect/floor_decal/industrial/outline/yellow,
-/obj/machinery/seed_extractor,
+/area/quartermaster/office)
+"ctm" = (
+/obj/machinery/camera/network/supply{
+ c_tag = "Cargo - Warehouse Entrence";
+ dir = 1
+ },
+/obj/effect/floor_decal/corner/brown{
+ dir = 10
+ },
+/obj/machinery/light/small,
+/obj/machinery/atmospherics/unary/vent_scrubber/on{
+ dir = 1
+ },
/turf/simulated/floor/tiled,
-/area/hydroponics)
+/area/quartermaster/storage)
+"ctn" = (
+/obj/structure/sign/drop{
+ pixel_y = 32
+ },
+/turf/simulated/open,
+/area/maintenance/disposal)
+"cto" = (
+/obj/structure/disposalpipe/trunk{
+ dir = 4
+ },
+/obj/structure/disposaloutlet{
+ dir = 8;
+ spread = 360;
+ spread_point = 2
+ },
+/turf/simulated/floor/plating,
+/area/maintenance/disposal)
+"ctp" = (
+/obj/machinery/light/small/emergency,
+/turf/simulated/open,
+/area/maintenance/disposal)
+"ctq" = (
+/obj/structure/disposaloutlet{
+ spread = 180;
+ spread_point = 1
+ },
+/obj/structure/disposalpipe/trunk{
+ dir = 8
+ },
+/turf/simulated/floor/tiled/airless{
+ icon_state = "asteroidfloor"
+ },
+/area/maintenance/disposal)
(1,1,1) = {"
aaa
@@ -84198,8 +84184,8 @@ ali
amf
anh
aoh
-apt
-aqH
+csM
+csY
arV
atB
avi
@@ -84226,7 +84212,7 @@ baH
bcg
bdD
beP
-bgk
+ctj
bhD
bcc
bki
@@ -89407,7 +89393,7 @@ bdJ
bRL
col
coy
-cth
+aeD
coW
cpi
cpr
@@ -89424,9 +89410,9 @@ bWF
bWm
cih
cim
-cad
-bXT
-caM
+ctk
+ctl
+ctm
bWm
cbs
cbM
@@ -91209,7 +91195,7 @@ coF
bSL
cpc
bSL
-cti
+aet
cpI
bSQ
cqe
@@ -91723,8 +91709,8 @@ coH
adu
adz
bSL
-ctj
-ctk
+afd
+afy
bSQ
cqg
bRL
@@ -94569,9 +94555,9 @@ bUz
bPO
bPK
cbD
-ccd
ccf
-ccd
+ccf
+ccf
cdh
aaa
aab
@@ -94826,9 +94812,9 @@ bUz
caW
bPL
cbE
-cce
+ctn
ccf
-ccW
+ctp
cdh
aab
aab
@@ -95341,7 +95327,7 @@ bPO
bPL
cbE
cbE
-ccA
+cto
cbE
cdh
cdt
@@ -95600,7 +95586,7 @@ cbF
ccg
cbD
cbE
-cdi
+ctq
cdu
cdE
aab
@@ -96531,11 +96517,11 @@ aja
aja
crQ
crZ
-crQ
-csr
-csr
-csr
-csr
+csi
+css
+csC
+csN
+csZ
asw
auk
avI
@@ -96791,8 +96777,8 @@ csa
csj
cst
csD
-csN
-csr
+csO
+cta
asx
auk
avJ
@@ -97045,11 +97031,11 @@ cmy
crO
crS
csb
-crU
-crU
-crU
-crU
-csZ
+csk
+csu
+csE
+csP
+ctb
asy
aul
avK
@@ -97305,8 +97291,8 @@ csc
csl
csv
csF
-csP
-cta
+csQ
+ctc
asz
alz
avL
@@ -97562,8 +97548,8 @@ csd
csm
csw
csG
-csQ
-csr
+csR
+ctd
asA
aum
avK
@@ -97819,8 +97805,8 @@ cse
csn
csx
csH
-csR
-csr
+csS
+cte
asB
aun
avM
@@ -98076,8 +98062,8 @@ csf
cso
csy
csI
-csS
-crQ
+csT
+ctf
asC
auo
avN
@@ -98331,10 +98317,10 @@ cmI
crX
csg
csp
-csp
-csp
-csT
-cte
+csz
+csJ
+csU
+ctg
cnQ
aun
avM
@@ -98577,12 +98563,12 @@ aaV
adY
clC
aja
-crf
+crg
crm
cru
crC
cmD
-crC
+crN
cmD
aja
crY
@@ -98590,8 +98576,8 @@ csh
csq
csA
csK
-csU
-crQ
+csV
+cth
asE
aup
avK
@@ -98845,10 +98831,10 @@ afe
afe
afe
csr
-csr
+csB
csL
-csV
-csr
+csW
+cti
asF
aun
avM
@@ -99618,7 +99604,7 @@ alI
amH
anD
aoQ
-csW
+csX
cnP
asH
auq
diff --git a/tools/hooks/README.md b/tools/hooks/README.md
new file mode 100644
index 00000000000..844f3a3952f
--- /dev/null
+++ b/tools/hooks/README.md
@@ -0,0 +1,36 @@
+# Git Integration Hooks
+
+This folder contains installable scripts for [Git hooks] and [merge drivers].
+Use of these hooks and drivers is optional and they must be installed
+explicitly before they take effect.
+
+To install the current set of hooks, or update if new hooks are added, run
+`install.bat` (Windows) or `install.sh` (Unix-like) as appropriate.
+
+Hooks expect a Unix-like environment on the backend. Usually this is handled
+automatically by GUI tools like TortoiseGit and GitHub for Windows, but
+[Git for Windows] is an option if you prefer to use a CLI even on Windows.
+
+## Current Hooks
+
+* **Pre-commit**: Runs [mapmerge2] on changed maps, if any.
+
+## Adding New Hooks
+
+New [Git hooks] may be added by creating a file named `.hook` in
+this directory. Git determines what hooks are available and what their names
+are. The install script copies the `.hook` file into `.git/hooks`, so editing
+the `.hook` file will require a reinstall.
+
+New [merge drivers] may be added by adding a shell script named `.merge`
+and updating `.gitattributes` in the root of the repository to include the line
+`*. merge=`. The install script will set up the merge driver to point
+to the `.merge` file directly, and editing it will not require a reinstall.
+
+`tools/hooks/python.sh` may be used as a trampoline to ensure that the correct
+version of Python is found.
+
+[Git hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
+[merge drivers]: https://git-scm.com/docs/gitattributes#_performing_a_three_way_merge
+[Git for Windows]: https://gitforwindows.org/
+[mapmerge2]: ../mapmerge2/README.md
diff --git a/tools/hooks/install.bat b/tools/hooks/install.bat
new file mode 100644
index 00000000000..c4f864b5c68
--- /dev/null
+++ b/tools/hooks/install.bat
@@ -0,0 +1,14 @@
+@echo off
+cd %~dp0
+for %%f in (*.hook) do (
+ echo Installing hook: %%~nf
+ copy %%f ..\..\.git\hooks\%%~nf >nul
+)
+for %%f in (*.merge) do (
+ echo Installing merge driver: %%~nf
+ echo [merge "%%~nf"]^
+
+ driver = tools/hooks/%%f %%P %%O %%A %%B %%L >> ..\..\.git\config
+)
+echo Done
+pause
diff --git a/tools/hooks/install.sh b/tools/hooks/install.sh
new file mode 100644
index 00000000000..32183a7ce89
--- /dev/null
+++ b/tools/hooks/install.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+shopt -s nullglob
+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/hooks/$f %P %O %A %B %L"
+done
+echo "Done"
diff --git a/tools/hooks/pre-commit.hook b/tools/hooks/pre-commit.hook
new file mode 100644
index 00000000000..7eccda6f58d
--- /dev/null
+++ b/tools/hooks/pre-commit.hook
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec tools/hooks/python.sh -m precommit
diff --git a/tools/hooks/python.sh b/tools/hooks/python.sh
new file mode 100644
index 00000000000..32557070f48
--- /dev/null
+++ b/tools/hooks/python.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+set -e
+if command -v python3 >/dev/null 2>&1; then
+ PY=python3
+else
+ PY=python
+fi
+PATHSEP=$($PY - <<'EOF'
+import sys, os
+if sys.version_info.major != 3 or sys.version_info.minor < 6:
+ sys.stderr.write("Python 3.6+ is required: " + sys.version + "\n")
+ exit(1)
+print(os.pathsep)
+EOF
+)
+export PYTHONPATH=tools/mapmerge2/${PATHSEP}${PYTHONPATH}
+$PY "$@"
diff --git a/tools/mapmerge2/Prepare Maps - Aurora.bat b/tools/mapmerge2/Prepare Maps - Aurora.bat
new file mode 100644
index 00000000000..5cc98794819
--- /dev/null
+++ b/tools/mapmerge2/Prepare Maps - Aurora.bat
@@ -0,0 +1,12 @@
+@echo off
+cd ../../maps/aurora
+
+for /R %%f in (*.dmm) do copy "%%f" "%%f.backup"
+
+cls
+echo All dmm files in the maps/aurora directory have been backed up.
+echo Now you can make your changes...
+echo ---
+echo Remember to run mapmerge.bat just before you commit your changes!
+echo ---
+pause
diff --git a/tools/mapmerge2/Prepare Maps - Exodus.bat b/tools/mapmerge2/Prepare Maps - Exodus.bat
new file mode 100644
index 00000000000..bc3e0179f87
--- /dev/null
+++ b/tools/mapmerge2/Prepare Maps - Exodus.bat
@@ -0,0 +1,12 @@
+@echo off
+cd ../../maps/exodus
+
+for /R %%f in (*.dmm) do copy "%%f" "%%f.backup"
+
+cls
+echo All dmm files in the maps/exodus directory have been backed up.
+echo Now you can make your changes...
+echo ---
+echo Remember to run mapmerge.bat just before you commit your changes!
+echo ---
+pause
diff --git a/tools/mapmerge2/Prepare Maps - Runtime.bat b/tools/mapmerge2/Prepare Maps - Runtime.bat
new file mode 100644
index 00000000000..2c3e15a68d4
--- /dev/null
+++ b/tools/mapmerge2/Prepare Maps - Runtime.bat
@@ -0,0 +1,12 @@
+@echo off
+cd ../../maps/runtime
+
+for /R %%f in (*.dmm) do copy "%%f" "%%f.backup"
+
+cls
+echo All dmm files in the maps/runtime directory have been backed up.
+echo Now you can make your changes...
+echo ---
+echo Remember to run mapmerge.bat just before you commit your changes!
+echo ---
+pause
diff --git a/tools/mapmerge2/README.md b/tools/mapmerge2/README.md
new file mode 100644
index 00000000000..0ff4d21ac2e
--- /dev/null
+++ b/tools/mapmerge2/README.md
@@ -0,0 +1,52 @@
+# Map Merge 2
+
+**Map Merge 2** is an improvement over previous map merging scripts, with
+better merge-conflict prevention, multi-Z support, and automatic handling of
+key overflow. For up-to-date tips and tricks, also visit the [Map Merger] wiki article.
+
+## What Map Merging Is
+
+The "map merge" operation describes the process of rewriting a map file written
+by the DreamMaker map editor to A) use a format more amenable to Git's conflict
+resolution and B) differ in the least amount textually from the previous
+version of the map while maintaining all the actual changes. It requires an old
+version of the map to use as a reference and a new version of the map which
+contains the desired changes.
+
+## Installation
+
+To install Python dependencies, run `requirements-install.bat`, or run
+`python -m pip install -r requirements.txt` directly. See the [Git hooks]
+documentation to install the Git pre-commit hook which runs the map merger
+automatically, or use `tools/mapmerge/Prepare Maps.bat` to save backups before
+running `mapmerge.bat`.
+
+For up-to-date installation and detailed troubleshooting instructions, visit
+the [Map Merger] wiki article.
+
+## Code Structure
+
+Frontend scripts are meant to be run directly. They obey the environment
+variables `TGM` to set whether files are saved in TGM (1) or DMM (0) format,
+and `MAPROOT` to determine where maps are kept. By default, TGM is used and
+the map root is autodetected. Each script may either prompt for the desired map
+or be run with command-line parameters indicating which maps to act on. The
+scripts include:
+
+* `convert.py` for converting maps to and from the TGM format. Used by
+ `tgm2dmm.bat` and `dmm2tgm.bat`.
+* `mapmerge.py` for running the map merge on map backups saved by
+ `Prepare Maps.bat`. Used by `mapmerge.bat`
+
+Implementation modules:
+
+* `dmm.py` includes the map reader and writer.
+* `mapmerge.py` includes the implementation of the map merge operation.
+* `frontend.py` includes the common code for the frontend scripts.
+
+`precommit.py` is run by the [Git hooks] if installed, and merges the new
+version of any map saved in the index (`git add`ed) with the old version stored
+in Git when run.
+
+[Map Merger]: https://tgstation13.org/wiki/Map_Merger
+[Git hooks]: ../hooks/README.md
diff --git a/tools/mapmerge2/convert.py b/tools/mapmerge2/convert.py
new file mode 100644
index 00000000000..35e5dda4433
--- /dev/null
+++ b/tools/mapmerge2/convert.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+import frontend
+import dmm
+
+if __name__ == '__main__':
+ settings = frontend.read_settings()
+ for fname in frontend.process(settings, "convert"):
+ dmm.DMM.from_file(fname).to_file(fname, settings.tgm)
diff --git a/tools/mapmerge2/dmm.py b/tools/mapmerge2/dmm.py
new file mode 100644
index 00000000000..d76f07e32c6
--- /dev/null
+++ b/tools/mapmerge2/dmm.py
@@ -0,0 +1,488 @@
+# 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):
+ self._presave_checks()
+ 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):
+ self._presave_checks()
+ 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):
+ free_keys = self._ensure_free_keys(1)
+ # 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")
+
+ def _presave_checks(self):
+ # last-second handling of bogus keys to help prevent and fix broken maps
+ self._ensure_free_keys(0)
+ max_key = max_key_for(self.key_length)
+ bad_keys = {key: 0 for key in self.dictionary.keys() if key > max_key}
+ if bad_keys:
+ print(f"Warning: fixing {len(bad_keys)} overflowing keys")
+ for k in bad_keys:
+ # create a new non-bogus key and transfer that value to it
+ new_key = bad_keys[k] = self.generate_new_key()
+ self.dictionary.forceput(new_key, self.dictionary[k])
+ print(f" {num_to_key(k, self.key_length, True)} -> {num_to_key(new_key, self.key_length)}")
+ for k, v in self.grid.items():
+ # reassign the grid entries which used the old key
+ self.grid[k] = bad_keys.get(v, v)
+
+ def _ensure_free_keys(self, desired):
+ # ensure that free keys exist by increasing the key length if necessary
+ free_keys = max_key_for(self.key_length) - len(self.dictionary)
+ while free_keys < desired:
+ if self.key_length >= MAX_KEY_LENGTH:
+ raise KeyTooLarge(f"can't expand beyond key length {MAX_KEY_LENGTH} ({len(self.dictionary)} keys)")
+ self.key_length += 1
+ free_keys = max_key_for(self.key_length) - len(self.dictionary)
+ return free_keys
+
+ @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
+MAX_KEY_LENGTH = 3 # things will get ugly fast if you exceed this
+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, allow_overflow=False):
+ if num >= (BASE ** key_length if allow_overflow else max_key_for(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
+
+def max_key_for(key_length):
+ # keys only go up to "ymo" = 65534, under-estimated just in case
+ # https://secure.byond.com/forum/?post=2340796#comment23770802
+ return min(65530, BASE ** key_length)
+
+class KeyTooLarge(Exception):
+ pass
+
+# ----------
+# An actual atom parser
+
+def parse_map_atom(atom):
+ try:
+ i = atom.index('{')
+ except ValueError:
+ return atom, {}
+
+ path, rest = atom[:i], atom[i+1:]
+ vars = {}
+
+ in_string = False
+ in_name = False
+ escaping = False
+ current_name = ''
+ current = ''
+ for ch in rest:
+ if escaping:
+ escaping = False
+ current += ch
+ elif ch == '\\':
+ escaping = True
+ elif ch == '"':
+ in_string = not in_string
+ current += ch
+ elif in_string:
+ current += ch
+ elif ch == ';':
+ vars[current_name.strip()] = current.strip()
+ current_name = current = ''
+ elif ch == '=':
+ current_name = current
+ current = ''
+ elif ch == '}':
+ vars[current_name.strip()] = current.strip()
+ break
+ elif ch not in ' ':
+ current += ch
+
+ return path, vars
+
+# ----------
+# TGM writer
+
+def save_tgm(dmm, output):
+ output.write(f"{TGM_HEADER}\n")
+ if dmm.header:
+ output.write(f"{dmm.header}\n")
+
+ # write dictionary in tgm format
+ for key, value in sorted(dmm.dictionary.items()):
+ output.write(f'"{num_to_key(key, dmm.key_length)}" = (\n')
+ for idx, thing in enumerate(value):
+ in_quote_block = False
+ in_varedit_block = False
+ for char in thing:
+ if in_quote_block:
+ if char == '"':
+ in_quote_block = False
+ output.write(char)
+ elif char == '"':
+ in_quote_block = True
+ output.write(char)
+ elif not in_varedit_block:
+ if char == "{":
+ in_varedit_block = True
+ output.write("{\n\t")
+ else:
+ output.write(char)
+ elif char == ";":
+ output.write(";\n\t")
+ elif char == "}":
+ output.write("\n\t}")
+ in_varedit_block = False
+ else:
+ output.write(char)
+ if idx < len(value) - 1:
+ output.write(",\n")
+ output.write(")\n")
+
+ # thanks to YotaXP for finding out about this one
+ max_x, max_y, max_z = dmm.size
+ for z in range(1, max_z + 1):
+ output.write("\n")
+ for x in range(1, max_x + 1):
+ output.write(f"({x},{1},{z}) = {{\"\n")
+ for y in range(1, max_y + 1):
+ output.write(f"{num_to_key(dmm.grid[x, y, z], dmm.key_length)}\n")
+ output.write("\"}\n")
+
+# ----------
+# DMM writer
+
+def save_dmm(dmm, output):
+ if dmm.header:
+ output.write(f"{dmm.header}\n")
+
+ # writes a tile dictionary the same way Dreammaker does
+ for key, value in sorted(dmm.dictionary.items()):
+ output.write(f'"{num_to_key(key, dmm.key_length)}" = ({",".join(value)})\n')
+
+ output.write("\n")
+
+ # writes a map grid the same way Dreammaker does
+ max_x, max_y, max_z = dmm.size
+ for z in range(1, max_z + 1):
+ output.write(f"(1,1,{z}) = {{\"\n")
+
+ for y in range(1, max_y + 1):
+ for x in range(1, max_x + 1):
+ try:
+ output.write(num_to_key(dmm.grid[x, y, z], dmm.key_length))
+ except KeyError:
+ print(f"Key error: ({x}, {y}, {z})")
+ output.write("\n")
+ output.write("\"}\n")
+
+# ----------
+# Parser
+
+def _parse(map_raw_text):
+ in_comment_line = False
+ comment_trigger = False
+
+ in_quote_block = False
+ in_key_block = False
+ in_data_block = False
+ in_varedit_block = False
+ after_data_block = False
+ escaping = False
+ skip_whitespace = False
+
+ dictionary = bidict.bidict()
+ duplicate_keys = {}
+ curr_key_len = 0
+ curr_key = 0
+ curr_datum = ""
+ curr_data = list()
+
+ in_map_block = False
+ in_coord_block = False
+ in_map_string = False
+ iter_x = 0
+ adjust_y = True
+
+ curr_num = ""
+ reading_coord = "x"
+
+ key_length = 0
+
+ maxx = 0
+ maxy = 0
+ maxz = 0
+
+ curr_x = 0
+ curr_y = 0
+ curr_z = 0
+ grid = dict()
+
+ it = iter(map_raw_text)
+
+ # map block
+ for char in it:
+ if char == "\n":
+ in_comment_line = False
+ comment_trigger = False
+ continue
+ elif in_comment_line:
+ continue
+ elif char == "\t":
+ continue
+
+ if char == "/" and not in_quote_block:
+ if comment_trigger:
+ in_comment_line = True
+ continue
+ else:
+ comment_trigger = True
+ else:
+ comment_trigger = False
+
+ if in_data_block:
+
+ if in_varedit_block:
+
+ if in_quote_block:
+ if char == "\\":
+ curr_datum = curr_datum + char
+ escaping = True
+
+ elif escaping:
+ curr_datum = curr_datum + char
+ escaping = False
+
+ elif char == "\"":
+ curr_datum = curr_datum + char
+ in_quote_block = False
+
+ else:
+ curr_datum = curr_datum + char
+
+ else:
+ if skip_whitespace and char == " ":
+ skip_whitespace = False
+ continue
+ skip_whitespace = False
+
+ if char == "\"":
+ curr_datum = curr_datum + char
+ in_quote_block = True
+
+ elif char == ";":
+ skip_whitespace = True
+ curr_datum = curr_datum + char
+
+ elif char == "}":
+ curr_datum = curr_datum + char
+ in_varedit_block = False
+
+ else:
+ curr_datum = curr_datum + char
+
+ elif char == "{":
+ curr_datum = curr_datum + char
+ in_varedit_block = True
+
+ elif char == ",":
+ curr_data.append(curr_datum)
+ curr_datum = ""
+
+ elif char == ")":
+ curr_data.append(curr_datum)
+ curr_data = tuple(curr_data)
+ try:
+ dictionary[curr_key] = curr_data
+ except bidict.ValueDuplicationError:
+ # if the map has duplicate values, eliminate them now
+ duplicate_keys[curr_key] = dictionary.inv[curr_data]
+ curr_data = list()
+ curr_datum = ""
+ curr_key = 0
+ curr_key_len = 0
+ in_data_block = False
+ after_data_block = True
+
+ else:
+ curr_datum = curr_datum + char
+
+ elif in_key_block:
+ if char == "\"":
+ in_key_block = False
+ if key_length == 0:
+ key_length = curr_key_len
+ else:
+ assert key_length == curr_key_len
+ else:
+ curr_key = BASE * curr_key + base52_r[char]
+ curr_key_len += 1
+
+ # else we're looking for a key block, a data block or the map block
+ elif char == "\"":
+ in_key_block = True
+ after_data_block = False
+
+ elif char == "(":
+ if after_data_block:
+ in_coord_block = True
+ after_data_block = False
+ curr_key = 0
+ curr_key_len = 0
+ break
+ else:
+ in_data_block = True
+ after_data_block = False
+
+ # grid block
+ for char in it:
+ if in_coord_block:
+ if char == ",":
+ if reading_coord == "x":
+ curr_x = int(curr_num)
+ if curr_x > maxx:
+ maxx = curr_x
+ iter_x = 0
+ curr_num = ""
+ reading_coord = "y"
+ elif reading_coord == "y":
+ curr_y = int(curr_num)
+ if curr_y > maxy:
+ maxy = curr_y
+ curr_num = ""
+ reading_coord = "z"
+ else:
+ raise ValueError("too many dimensions")
+
+ elif char == ")":
+ curr_z = int(curr_num)
+ if curr_z > maxz:
+ maxz = curr_z
+ in_coord_block = False
+ reading_coord = "x"
+ curr_num = ""
+
+ else:
+ curr_num = curr_num + char
+
+ elif in_map_string:
+ if char == "\"":
+ in_map_string = False
+ adjust_y = True
+ curr_y -= 1
+
+ elif char == "\n":
+ if adjust_y:
+ adjust_y = False
+ else:
+ curr_y += 1
+ if curr_x > maxx:
+ maxx = curr_x
+ if iter_x > 1:
+ curr_x = 1
+ iter_x = 0
+
+ else:
+ curr_key = BASE * curr_key + base52_r[char]
+ curr_key_len += 1
+ if curr_key_len == key_length:
+ iter_x += 1
+ if iter_x > 1:
+ curr_x += 1
+
+ grid[curr_x, curr_y, curr_z] = duplicate_keys.get(curr_key, curr_key)
+ curr_key = 0
+ curr_key_len = 0
+
+ # else look for coordinate block or a map string
+ elif char == "(":
+ in_coord_block = True
+ elif char == "\"":
+ in_map_string = True
+
+ if curr_y > maxy:
+ maxy = curr_y
+
+ data = DMM(key_length, Coordinate(maxx, maxy, maxz))
+ data.dictionary = dictionary
+ data.grid = grid
+ return data
diff --git a/tools/mapmerge2/dmm2tgm.bat b/tools/mapmerge2/dmm2tgm.bat
new file mode 100644
index 00000000000..b17b03afa08
--- /dev/null
+++ b/tools/mapmerge2/dmm2tgm.bat
@@ -0,0 +1,5 @@
+@echo off
+set MAPROOT=../../maps/
+set TGM=1
+python convert.py
+pause
diff --git a/tools/mapmerge2/frontend.py b/tools/mapmerge2/frontend.py
new file mode 100644
index 00000000000..4d44eab951e
--- /dev/null
+++ b/tools/mapmerge2/frontend.py
@@ -0,0 +1,127 @@
+# Common code for the frontend interface of map tools
+import sys
+import os
+import pathlib
+import shutil
+from collections import namedtuple
+
+Settings = namedtuple('Settings', ['map_folder', 'tgm'])
+MapsToRun = namedtuple('MapsToRun', ['files', 'indices'])
+
+def string_to_num(s):
+ try:
+ return int(s)
+ except ValueError:
+ return -1
+
+def read_settings():
+ # discover map folder if needed
+ try:
+ map_folder = os.environ['MAPROOT']
+ except KeyError:
+ map_folder = '_maps/'
+ for _ in range(8):
+ if os.path.exists(map_folder):
+ break
+ map_folder = os.path.join('..', map_folder)
+ else:
+ map_folder = None
+
+ # assume TGM is True by default
+ tgm = os.environ.get('TGM', "1") == "1"
+
+ return Settings(map_folder, tgm)
+
+def pretty_path(settings, path_str):
+ if settings.map_folder:
+ return path_str[len(os.path.commonpath([settings.map_folder, path_str]))+1:]
+ else:
+ return path_str
+
+def prompt_maps(settings, verb):
+ if not settings.map_folder:
+ print("Could not autodetect the _maps folder, set MAPROOT")
+ exit(1)
+
+ list_of_files = list()
+ for root, directories, filenames in os.walk(settings.map_folder):
+ for filename in [f for f in filenames if f.endswith(".dmm")]:
+ list_of_files.append(pathlib.Path(root, filename))
+
+ last_dir = ""
+ for i, this_file in enumerate(list_of_files):
+ this_dir = this_file.parent
+ if last_dir != this_dir:
+ print("--------------------------------")
+ last_dir = this_dir
+ print("[{}]: {}".format(i, pretty_path(settings, str(this_file))))
+
+ print("--------------------------------")
+ in_list = input("List the maps you want to " + verb + " (example: 1,3-5,12):\n")
+ in_list = in_list.replace(" ", "")
+ in_list = in_list.split(",")
+
+ valid_indices = list()
+ for m in in_list:
+ index_range = m.split("-")
+ if len(index_range) == 1:
+ index = string_to_num(index_range[0])
+ if index >= 0 and index < len(list_of_files):
+ valid_indices.append(index)
+ elif len(index_range) == 2:
+ index0 = string_to_num(index_range[0])
+ index1 = string_to_num(index_range[1])
+ if index0 >= 0 and index0 <= index1 and index1 < len(list_of_files):
+ valid_indices.extend(range(index0, index1 + 1))
+
+ return MapsToRun(list_of_files, valid_indices)
+
+def process(settings, verb, *, modify=True, backup=None):
+ if backup is None:
+ backup = modify # by default, backup when we modify
+ assert modify or not backup # doesn't make sense to backup when not modifying
+
+ if len(sys.argv) > 1:
+ maps = sys.argv[1:]
+ else:
+ maps = prompt_maps(settings, verb)
+ maps = [str(maps.files[i]) for i in maps.indices]
+ print()
+
+ if not maps:
+ print("No maps selected.")
+ return
+
+ if modify:
+ print(f"Maps WILL{'' if settings.tgm else ' NOT'} be converted to tgm.")
+ if backup:
+ print("Backups will be created with a \".before\" extension.")
+ else:
+ print("Warning: backups are NOT being taken.")
+
+ print(f"\nWill {verb} these maps:")
+ for path_str in maps:
+ print(pretty_path(settings, path_str))
+
+ try:
+ confirm = input(f"\nPress Enter to {verb}...\n")
+ except KeyboardInterrupt:
+ confirm = "^C"
+ if confirm != "":
+ print(f"\nAborted.")
+ return
+
+ for path_str in maps:
+ print(f' - {pretty_path(settings, path_str)}')
+
+ if backup:
+ shutil.copyfile(path_str, path_str + ".before")
+
+ try:
+ yield path_str
+ except Exception as e:
+ print(f"Error: {e}")
+ else:
+ print("Succeeded.")
+
+ print("\nFinished.")
diff --git a/tools/mapmerge2/mapmerge.bat b/tools/mapmerge2/mapmerge.bat
new file mode 100644
index 00000000000..e12c500bc83
--- /dev/null
+++ b/tools/mapmerge2/mapmerge.bat
@@ -0,0 +1,5 @@
+@echo off
+set MAPROOT=../../maps/
+set TGM=1
+python mapmerge.py
+pause
diff --git a/tools/mapmerge2/mapmerge.py b/tools/mapmerge2/mapmerge.py
new file mode 100644
index 00000000000..f449bd948f0
--- /dev/null
+++ b/tools/mapmerge2/mapmerge.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+import frontend
+import shutil
+from dmm import *
+from collections import defaultdict
+
+def merge_map(new_map, old_map, delete_unused=False):
+ if new_map.key_length != old_map.key_length:
+ print("Warning: Key lengths differ, taking new map")
+ print(f" Old: {old_map.key_length}")
+ print(f" New: {new_map.key_length}")
+ return new_map
+
+ if new_map.size != old_map.size:
+ print("Warning: Map dimensions differ, taking new map")
+ print(f" Old: {old_map.size}")
+ print(f" New: {new_map.size}")
+ return new_map
+
+ key_length, size = old_map.key_length, old_map.size
+ merged = DMM(key_length, size)
+ merged.dictionary = old_map.dictionary.copy()
+
+ known_keys = dict() # mapping fron 'new' key to 'merged' key
+ unused_keys = set(old_map.dictionary.keys()) # keys going unused
+
+ # step one: parse the new version, compare it to the old version, merge both
+ for z, y, x in new_map.coords_zyx:
+ new_key = new_map.grid[x, y, z]
+ # if this key has been processed before, it can immediately be merged
+ try:
+ merged.grid[x, y, z] = known_keys[new_key]
+ continue
+ except KeyError:
+ pass
+
+ def select_key(assigned):
+ merged.grid[x, y, z] = known_keys[new_key] = assigned
+
+ old_key = old_map.grid[x, y, z]
+ old_tile = old_map.dictionary[old_key]
+ new_tile = new_map.dictionary[new_key]
+
+ # this tile is the exact same as before, so the old key is used
+ if new_tile == old_tile:
+ select_key(old_key)
+ unused_keys.remove(old_key)
+
+ # the tile is different here, but if it exists in the merged dictionary, that key can be used
+ elif new_tile in merged.dictionary.inv:
+ newold_key = merged.dictionary.inv[new_tile]
+ select_key(newold_key)
+ unused_keys.remove(newold_key)
+
+ # the tile is brand new and it needs a new key, but if the old key isn't being used any longer it can be used instead
+ elif old_tile not in new_map.dictionary.inv and old_key in unused_keys:
+ merged.dictionary[old_key] = new_tile
+ select_key(old_key)
+ unused_keys.remove(old_key)
+
+ # all other options ruled out, a brand new key is generated for the brand new tile
+ else:
+ fresh_key = merged.generate_new_key()
+ merged.dictionary[fresh_key] = new_tile
+ select_key(fresh_key)
+
+ # step two: delete unused keys
+ if unused_keys:
+ print(f"Notice: Trimming {len(unused_keys)} unused dictionary keys.")
+ for key in unused_keys:
+ del merged.dictionary[key]
+
+ # sanity check: that the merged map equals the new map
+ for z, y, x in new_map.coords_zyx:
+ new_tile = new_map.dictionary[new_map.grid[x, y, z]]
+ merged_tile = merged.dictionary[merged.grid[x, y, z]]
+ if new_tile != merged_tile:
+ print(f"Error: the map has been mangled! This is a mapmerge bug!")
+ print(f"At {x},{y},{z}.")
+ print(f"Should be {new_tile}")
+ print(f"Instead is {merged_tile}")
+ raise RuntimeError()
+
+ return merged
+
+def main(settings):
+ for fname in frontend.process(settings, "merge", backup=True):
+ shutil.copyfile(fname, fname + ".before")
+ old_map = DMM.from_file(fname + ".backup")
+ new_map = DMM.from_file(fname)
+ merge_map(new_map, old_map).to_file(fname, settings.tgm)
+
+if __name__ == '__main__':
+ main(frontend.read_settings())
diff --git a/tools/mapmerge2/precommit.py b/tools/mapmerge2/precommit.py
new file mode 100644
index 00000000000..381f1ea8a3e
--- /dev/null
+++ b/tools/mapmerge2/precommit.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+import os
+import pygit2
+import dmm
+from mapmerge import merge_map
+
+def main(repo):
+ if repo.index.conflicts:
+ print("You need to resolve merge conflicts first.")
+ return 1
+
+ changed = 0
+ for path, status in repo.status().items():
+ if path.endswith(".dmm") and (status & (pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_INDEX_NEW)):
+ # read the index
+ index_entry = repo.index[path]
+ index_map = dmm.DMM.from_bytes(repo[index_entry.id].read_raw())
+
+ try:
+ head_blob = repo[repo[repo.head.target].tree[path].id]
+ except KeyError:
+ # New map, no entry in HEAD
+ print(f"Converting new map: {path}")
+ assert (status & pygit2.GIT_STATUS_INDEX_NEW)
+ merged_map = index_map
+ else:
+ # Entry in HEAD, merge the index over it
+ print(f"Merging map: {path}")
+ assert not (status & pygit2.GIT_STATUS_INDEX_NEW)
+ head_map = dmm.DMM.from_bytes(head_blob.read_raw())
+ merged_map = merge_map(index_map, head_map)
+
+ # write to the index
+ blob_id = repo.create_blob(merged_map.to_bytes())
+ repo.index.add(pygit2.IndexEntry(path, blob_id, index_entry.mode))
+ changed += 1
+
+ # write to the working directory if that's clean
+ if status & (pygit2.GIT_STATUS_WT_DELETED | pygit2.GIT_STATUS_WT_MODIFIED):
+ print(f"Warning: {path} has unindexed changes, not overwriting them")
+ else:
+ merged_map.to_file(os.path.join(repo.workdir, path))
+
+ if changed:
+ repo.index.write()
+ print(f"Merged {changed} maps.")
+ return 0
+
+if __name__ == '__main__':
+ exit(main(pygit2.Repository(pygit2.discover_repository(os.getcwd()))))
diff --git a/tools/mapmerge2/requirements-install.bat b/tools/mapmerge2/requirements-install.bat
new file mode 100644
index 00000000000..71341af8792
--- /dev/null
+++ b/tools/mapmerge2/requirements-install.bat
@@ -0,0 +1,3 @@
+@echo off
+python -m pip install -r requirements.txt
+pause
diff --git a/tools/mapmerge2/requirements.txt b/tools/mapmerge2/requirements.txt
new file mode 100644
index 00000000000..d01a2c6ccff
--- /dev/null
+++ b/tools/mapmerge2/requirements.txt
@@ -0,0 +1,2 @@
+pygit2==0.26.0
+bidict==0.13.1
diff --git a/tools/mapmerge2/tgm2dmm.bat b/tools/mapmerge2/tgm2dmm.bat
new file mode 100644
index 00000000000..5bf5677453c
--- /dev/null
+++ b/tools/mapmerge2/tgm2dmm.bat
@@ -0,0 +1,5 @@
+@echo off
+set MAPROOT=../../maps/
+set TGM=0
+python convert.py
+pause