diff --git a/code/__DEFINES/mapexporting.dm b/code/__DEFINES/mapexporting.dm
new file mode 100644
index 0000000000..cc0e2c0282
--- /dev/null
+++ b/code/__DEFINES/mapexporting.dm
@@ -0,0 +1,14 @@
+//Bits to save
+#define SAVE_OBJECTS (1 << 1) //Save objects?
+#define SAVE_MOBS (1 << 2) //Save Mobs?
+#define SAVE_TURFS (1 << 3) //Save turfs?
+#define SAVE_AREAS (1 << 4) //Save areas?
+#define SAVE_SPACE (1 << 5) //Save space areas? (If not they will be saved as NOOP)
+#define SAVE_OBJECT_PROPERTIES (1 << 6) //Save custom properties of objects (obj.on_object_saved() output)
+
+#define SAVE_ALL SAVE_OBJECTS | SAVE_MOBS | SAVE_TURFS | SAVE_AREAS | SAVE_SPACE | SAVE_OBJECT_PROPERTIES
+
+//Ignore turf if it contains
+#define SAVE_SHUTTLEAREA_DONTCARE 0
+#define SAVE_SHUTTLEAREA_IGNORE 1
+#define SAVE_SHUTTLEAREA_ONLY 2
diff --git a/code/datums/components/storage/storage.dm b/code/datums/components/storage/storage.dm
index 97d6748dc7..01a8f41c89 100644
--- a/code/datums/components/storage/storage.dm
+++ b/code/datums/components/storage/storage.dm
@@ -732,3 +732,15 @@
*/
/datum/component/storage/proc/get_max_volume()
return max_volume || AUTO_SCALE_STORAGE_VOLUME(max_w_class, max_combined_w_class)
+
+/obj/item/storage/on_object_saved(depth)
+ if(depth >= 10)
+ return ""
+ var/dat = ""
+ for(var/obj/item in contents)
+ var/metadata = generate_tgm_metadata(item)
+ dat += "[dat ? ",\n" : ""][item.type][metadata]"
+ //Save the contents of things inside the things inside us, EG saving the contents of bags inside lockers
+ var/custom_data = item.on_object_saved(depth++)
+ dat += "[custom_data ? ",\n[custom_data]" : ""]"
+ return dat
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 58c571d9e6..e263678943 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -383,3 +383,34 @@
/obj/proc/plunger_act(obj/item/plunger/P, mob/living/user, reinforced)
return
+
+
+//For returning special data when the object is saved
+//For example, or silos will return a list of their materials which will be dumped on top of them
+//Can be customised if you have something that contains something you want saved
+//If you put an incorrect format it will break outputting, so don't use this if you don't know what you are doing
+//NOTE: Contents is automatically saved, so if you store your things in the contents var, don't worry about this
+//====Output Format Examples====:
+//===Single Object===
+// "/obj/item/folder/blue"
+//===Multiple Objects===
+// "/obj/item/folder/blue,\n
+// /obj/item/folder/red"
+//===Single Object with metadata===
+// "/obj/item/folder/blue{\n
+// \tdir = 8;\n
+// \tname = "special folder"\n
+// \t}"
+//===Multiple Objects with metadata===
+// "/obj/item/folder/blue{\n
+// \tdir = 8;\n
+// \tname = "special folder"\n
+// \t},\n
+// /obj/item/folder/red"
+//====How to save easily====:
+// return "[thing.type][generate_tgm_metadata(thing)]"
+//Where thing is the additional thing you want to same (For example ores inside an ORM)
+//Just add ,\n between each thing
+//generate_tgm_metadata(thing) handles everything inside the {} for you
+/obj/proc/on_object_saved(depth)
+ return ""
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index fe1fb03426..f5512f2301 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -672,3 +672,15 @@
if(allowed(user))
return TRUE
to_chat(user, "Access denied.")
+
+/obj/structure/closet/on_object_saved(depth)
+ if(depth >= 10)
+ return ""
+ var/dat = ""
+ for(var/obj/item in contents)
+ var/metadata = generate_tgm_metadata(item)
+ dat += "[dat ? ",\n" : ""][item.type][metadata]"
+ //Save the contents of things inside the things inside us, EG saving the contents of bags inside lockers
+ var/custom_data = item.on_object_saved(depth++)
+ dat += "[custom_data ? ",\n[custom_data]" : ""]"
+ return dat
diff --git a/code/modules/buildmode/submodes/save_area.dm b/code/modules/buildmode/submodes/save_area.dm
new file mode 100644
index 0000000000..6cd235f161
--- /dev/null
+++ b/code/modules/buildmode/submodes/save_area.dm
@@ -0,0 +1,65 @@
+GLOBAL_VAR_INIT(save_area_executing, FALSE)
+
+/datum/mapGenerator/save_area
+ buildmode_name = "Save Area"
+ modules = list(/datum/mapGeneratorModule/save_area)
+ var/min_x = 0
+ var/min_y = 0
+ var/max_x = 0
+ var/max_y = 0
+
+/datum/mapGenerator/save_area/defineRegion(turf/Start, turf/End, replace = 0)
+ min_x = min(Start.x,End.x)
+ min_y = min(Start.y,End.y)
+ max_x = max(Start.x,End.x)
+ max_y = max(Start.y,End.y)
+ ..()
+
+/datum/mapGeneratorModule/save_area
+ var/areaName = "default.dm"
+
+//This could be optimised by making turfs that are the same go in the same, but this is a quick bodge solution so yea, fun job for coder here :)
+/datum/mapGeneratorModule/save_area/generate()
+ var/datum/mapGenerator/save_area/L = mother
+ if(!istype(L))
+ return
+ //If someone somehow gets build mode, stop them from using this.
+ if(!check_rights(R_ADMIN))
+ message_admins("[ckey(usr)] tried to run the map save generator but was rejected due to insufficient perms.")
+ to_chat(usr, "You must have R_ADMIN privellages to use this.")
+ return
+ //Emergency check
+ if(L.map.len > 1600)
+ var/confirm = alert("Uhm, are you sure, the area is quiet large?", "Run generator", "Yes", "No")
+ if(confirm != "Yes")
+ return
+
+ if(GLOB.save_area_executing)
+ to_chat(usr, "Someone is already running the generator! Try again in a bit.")
+ return
+
+ to_chat(usr, "Saving, please wait...")
+ GLOB.save_area_executing = TRUE
+
+ //Log just in case something happens
+ log_game("[key_name(usr)] ran the save level map generator on [L.map.len] turfs.")
+ message_admins("[key_name(usr)] ran the save level map generator on [L.map.len] turfs.")
+
+ //Step 1: Get the data (This can take a while)
+ var/dat = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE\n"
+ dat += convert_map_to_tgm(L.map)
+
+ //Step 2: Write the data to a file
+ var/filedir = file("data/temp.dmm")
+ if(fexists(filedir))
+ fdel(filedir)
+ WRITE_FILE(filedir, "[dat]")
+
+ //Step 3: Give the file to client for download
+ usr << ftp(filedir)
+
+ //Step 4: Remove the file from the server (hopefully we can find a way to avoid step)
+ fdel(filedir)
+ log_game("[L.map.len] turfs have been saved by [ckey(usr)]")
+ alert("Area saved successfully.", "Action Successful!", "Ok")
+ GLOB.save_area_executing = FALSE
diff --git a/code/modules/mapexporting/mapexporter.dm b/code/modules/mapexporting/mapexporter.dm
new file mode 100644
index 0000000000..d21caa0f6c
--- /dev/null
+++ b/code/modules/mapexporting/mapexporter.dm
@@ -0,0 +1,184 @@
+//Map exporter
+//Inputting a list of turfs into convert_map_to_tgm() will output a string
+//with the turfs and their objects / areas on said turf into the TGM mapping format
+//for .dmm files. This file can then be opened in the map editor or imported
+//back into the game.
+//============================
+//This has been made semi-modular so you should be able to use these functions
+//elsewhere in code if you ever need to get a file in the .dmm format
+/atom/proc/get_save_vars()
+ return
+
+GLOBAL_LIST_INIT(save_file_chars, list(
+ "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"
+))
+
+//Converts a list of turfs into TGM file format
+/proc/convert_map_to_tgm(list/map,\
+ save_flag = SAVE_ALL, \
+ shuttle_area_flag = SAVE_SHUTTLEAREA_DONTCARE, \
+ list/vars_to_save = list("pixel_x", "pixel_y", "dir", "name", "req_access", "req_access_txt", "piping_layer", "color", "icon_state", "pipe_color", "amount"),\
+ list/obj_blacklist = list())
+ //Calculate the bounds
+ var/minx = 1024
+ var/miny = 1024
+ var/maxx = -1
+ var/maxy = -1
+ for(var/turf/place in map)
+ minx = min(place.x, minx)
+ miny = min(place.y, miny)
+ maxx = max(place.x, maxx)
+ maxy = max(place.y, maxy)
+ var/width = maxx - minx + 1
+ var/height = maxy - miny + 1
+
+ //Sort the map so weird shaped / bad inputted maps can be handled
+ var/list/sortedmap = sort_map(map, minx, miny, maxx, maxy)
+
+ //Step 0: Calculate the amount of letters we need (26 ^ n > turf count)
+ var/turfsNeeded = width * height
+ var/layers = FLOOR(log(GLOB.save_file_chars.len, turfsNeeded) + 0.999,1)
+
+ //Step 1: Run through the area and generate file data
+ var/list/header_chars = list() //The characters of the header
+ var/list/header_dat = list() //The data of the header, lines up with chars
+ var/header = "" //The actual header in text
+ var/contents = "" //The contents in text (bit at the end)
+ var/index = 1
+ for(var/x in 1 to width)
+ contents += "\n([x],1,1) = {\"\n"
+ for(var/y in height to 1 step -1)
+ //====Get turfs Data====
+ var/turf/place = sortedmap[x][y]
+ var/area/location
+ var/list/objects
+ //If there is nothing there, save as a noop (For odd shapes)
+ if(!place)
+ place = /turf/template_noop
+ location = /area/template_noop
+ objects = list()
+ //Ignore things in space, must be a space turf and the area has to be empty space
+ else if(istype(place, /turf/open/space) && istype(get_area(place), /area/space) && !(save_flag & SAVE_SPACE))
+ place = /turf/template_noop
+ location = /area/template_noop
+ //Stuff to add
+ else
+ var/area/location_type = get_area(place)
+ location = location_type.type
+ objects = place
+ place = place.type
+ //====Saving shuttles only / non shuttles only====
+ var/is_shuttle_area = istype(location, /area/shuttle)
+ if((is_shuttle_area && shuttle_area_flag == SAVE_SHUTTLEAREA_IGNORE) || (!is_shuttle_area && shuttle_area_flag == SAVE_SHUTTLEAREA_ONLY))
+ place = /turf/template_noop
+ location = /area/template_noop
+ objects = list()
+ //====For toggling not saving areas and turfs====
+ if(!(save_flag & SAVE_AREAS))
+ location = /area/template_noop
+ if(!(save_flag & SAVE_TURFS))
+ place = /turf/template_noop
+ //====Generate Header Character====
+ var/header_char = calculate_tgm_header_index(index, layers) //The characters of the header
+ var/current_header = "(\n" //The actual stuff inside the header
+ //Add objects to the header file
+ var/empty = TRUE
+ //====SAVING OBJECTS====
+ if(save_flag & SAVE_OBJECTS)
+ for(var/obj/thing in objects)
+ if(thing.type in obj_blacklist)
+ continue
+ var/metadata = generate_tgm_metadata(thing, vars_to_save)
+ current_header += "[empty?"":",\n"][thing.type][metadata]"
+ empty = FALSE
+ //====SAVING SPECIAL DATA====
+ //This is what causes lockers and machines to save stuff inside of them
+ if(save_flag & SAVE_OBJECT_PROPERTIES)
+ var/custom_data = thing.on_object_saved()
+ current_header += "[custom_data ? ",\n[custom_data]" : ""]"
+ //====SAVING MOBS====
+ if(save_flag & SAVE_MOBS)
+ for(var/mob/living/thing in objects)
+ if(istype(thing, /mob/living/carbon)) //Ignore people, but not animals
+ continue
+ var/metadata = generate_tgm_metadata(thing, vars_to_save)
+ current_header += "[empty?"":",\n"][thing.type][metadata]"
+ empty = FALSE
+ current_header += "[empty?"":",\n"][place],\n[location])\n"
+ //====Fill the contents file====
+ //Compression is done here
+ var/position_of_header = header_dat.Find(current_header)
+ if(position_of_header)
+ //If the header has already been saved, change the character to the other saved header
+ header_char = header_chars[position_of_header]
+ else
+ header += "\"[header_char]\" = [current_header]"
+ header_chars += header_char
+ header_dat += current_header
+ index ++
+ contents += "[header_char]\n"
+ contents += "\"}"
+ return "[header][contents]"
+
+//Sorts maps in terms of their positions, so scrambled / odd shaped maps can be saved
+/proc/sort_map(list/map, minx, miny, maxx, maxy)
+ var/width = maxx - minx + 1
+ var/height = maxy - miny + 1
+ var/allTurfs = new/list(width, height)
+ for(var/turf/place in map)
+ allTurfs[place.x - minx + 1][place.y - miny + 1] = place
+ return allTurfs
+
+//vars_to_save = list() to save all vars
+/proc/generate_tgm_metadata(atom/O, list/vars_to_save = list("pixel_x", "pixel_y", "dir", "name", "req_access", "req_access_txt", "piping_layer", "color", "icon_state", "pipe_color", "amount"))
+ var/dat = ""
+ var/data_to_add = list()
+ for(var/V in O.vars)
+ if(O.get_save_vars())
+ if(!(V in O.get_save_vars()))
+ continue
+ else
+ if(!(V in vars_to_save) && vars_to_save)
+ continue
+ var/value = O.vars[V]
+ if(!value)
+ continue
+ if(value == initial(O.vars[V]) || !issaved(O.vars[V]))
+ continue
+ var/symbol = ""
+ if(istext(value))
+ symbol = "\""
+ value = sanitize_simple(value, list("{"="", "}"="", "\""="", ";"="", ","=""))
+ else if(isicon(value) || isfile(value))
+ symbol = "'"
+ else if(!(isnum(value) || ispath(value)))
+ continue
+ //Prevent symbols from being because otherwise you can name something [";},/obj/item/gun/energy/laser/instakill{name="da epic gun] and spawn yourself an instakill gun.
+ data_to_add += "[V] = [symbol][value][symbol]"
+ //Process data to add
+ var/first = TRUE
+ for(var/data in data_to_add)
+ dat += "[first ? "" : ";\n"]\t[data]"
+ first = FALSE
+ if(dat)
+ dat = "{\n[dat]\n\t}"
+ return dat
+
+/proc/calculate_tgm_header_index(index, layers)
+ var/output = ""
+ for(var/i in 1 to layers)
+ var/l = GLOB.save_file_chars.len
+ var/c = FLOOR((index-1) / (l ** (i - 1)), 1)
+ c = (c % l) + 1
+ output = "[GLOB.save_file_chars[c]][output]"
+ return output
diff --git a/code/modules/mining/machine_silo.dm b/code/modules/mining/machine_silo.dm
index 34b349b198..e6281bb695 100644
--- a/code/modules/mining/machine_silo.dm
+++ b/code/modules/mining/machine_silo.dm
@@ -243,3 +243,17 @@ GLOBAL_LIST_EMPTY(silo_access_logs)
sep = ", "
msg += "[amount < 0 ? "-" : "+"][val] [M.name]"
formatted = msg.Join()
+
+/obj/machinery/ore_silo/on_object_saved(var/depth = 0)
+ if(depth >= 10)
+ return ""
+ var/dat
+ var/datum/component/material_container/material_holder = GetComponent(/datum/component/material_container)
+ for(var/each in material_holder.materials)
+ var/amount = material_holder.materials[each] / MINERAL_MATERIAL_AMOUNT
+ var/datum/material/material_datum = each
+ while(amount > 0)
+ var/amount_in_stack = max(1, min(50, amount))
+ amount -= amount_in_stack
+ dat += "[dat ? ",\n" : ""][material_datum.sheet_type]{\n\tamount = [amount_in_stack]\n\t}"
+ return dat
diff --git a/tgstation.dme b/tgstation.dme
index 30c413815b..0c72aa5c46 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -69,6 +69,7 @@
#include "code\__DEFINES\loadout.dm"
#include "code\__DEFINES\logging.dm"
#include "code\__DEFINES\machines.dm"
+#include "code\__DEFINES\mapexporting.dm"
#include "code\__DEFINES\maps.dm"
#include "code\__DEFINES\materials.dm"
#include "code\__DEFINES\maths.dm"
@@ -1909,6 +1910,7 @@
#include "code\modules\buildmode\submodes\boom.dm"
#include "code\modules\buildmode\submodes\copy.dm"
#include "code\modules\buildmode\submodes\fill.dm"
+#include "code\modules\buildmode\submodes\save_area.dm"
#include "code\modules\buildmode\submodes\mapgen.dm"
#include "code\modules\buildmode\submodes\throwing.dm"
#include "code\modules\buildmode\submodes\variable_edit.dm"
@@ -2453,6 +2455,7 @@
#include "code\modules\mafia\_defines.dm"
#include "code\modules\mafia\controller.dm"
#include "code\modules\mafia\map_pieces.dm"
+#include "code\modules\mapexporting\mapexporter.dm"
#include "code\modules\mafia\outfits.dm"
#include "code\modules\mafia\roles.dm"
#include "code\modules\mapping\map_config.dm"