From 6cefa2b9abc9ea33be69c13497fe3621394ea448 Mon Sep 17 00:00:00 2001
From: SkyratBot <59378654+SkyratBot@users.noreply.github.com>
Date: Tue, 2 Jan 2024 23:13:11 +0100
Subject: [PATCH] [MIRROR] Map export admin verb/buildmode [MDB IGNORE]
(#25965)
* Map export admin verb/buildmode (#80326)
## About The Pull Request
The base for the PR was taken from
https://github.com/shiptest-ss13/Shiptest/pull/206 and thank them for
that.
The point of this verb is to save pieces of the map to your computer for
further use. It's not that necessary, but rarely, it can be useful.
[Video](https://i.imgur.com/M6mdDTC.mp4)
## Why It's Good For The Game
Transferring buildings from one round to another, preserving the decor
made in the game.
## Changelog
:cl: Vishenka0704
admin: The ability to export a part(or z-level) of the map has been
added.
/:cl:
* Map export admin verb/buildmode
---------
Co-authored-by: Yaroslav Nurkov <78199449+AnywayFarus@users.noreply.github.com>
---
code/__DEFINES/map_exporter.dm | 14 +
code/modules/admin/admin_verbs.dm | 1 +
code/modules/admin/verbs/map_export.dm | 298 ++++++++++++++++++
code/modules/buildmode/submodes/map_export.dm | 80 +++++
icons/misc/buildmode.dmi | Bin 3174 -> 6299 bytes
tgstation.dme | 3 +
6 files changed, 396 insertions(+)
create mode 100644 code/__DEFINES/map_exporter.dm
create mode 100644 code/modules/admin/verbs/map_export.dm
create mode 100644 code/modules/buildmode/submodes/map_export.dm
diff --git a/code/__DEFINES/map_exporter.dm b/code/__DEFINES/map_exporter.dm
new file mode 100644
index 00000000000..becedcd23e5
--- /dev/null
+++ b/code/__DEFINES/map_exporter.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)
+
+//Ignore turf if it contains
+#define SAVE_SHUTTLEAREA_DONTCARE 0
+#define SAVE_SHUTTLEAREA_IGNORE 1
+#define SAVE_SHUTTLEAREA_ONLY 2
+
+#define DMM2TGM_MESSAGE "MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE"
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index d3a31c5175f..5353d9788f7 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -231,6 +231,7 @@ GLOBAL_PROTECT(admin_verbs_debug)
/client/proc/get_dynex_range, /*debug verbs for dynex explosions.*/
/client/proc/jump_to_ruin,
/client/proc/load_circuit,
+ /client/proc/map_export,
/client/proc/map_template_load,
/client/proc/map_template_upload,
/client/proc/modify_goals,
diff --git a/code/modules/admin/verbs/map_export.dm b/code/modules/admin/verbs/map_export.dm
new file mode 100644
index 00000000000..88e80414235
--- /dev/null
+++ b/code/modules/admin/verbs/map_export.dm
@@ -0,0 +1,298 @@
+/client/proc/map_export()
+ set category = "Debug"
+ set name = "Map Export"
+ set desc = "Select a part of the map by coordinates and download it."
+
+ var/z_level = tgui_input_number(usr, "Export Which Z-Level?", "Map Exporter", usr.z || 2)
+ var/start_x = tgui_input_number(usr, "Start X?", "Map Exporter", usr.x || 1, world.maxx, 1)
+ var/start_y = tgui_input_number(usr, "Start Y?", "Map Exporter", usr.y || 1, world.maxy, 1)
+ var/end_x = tgui_input_number(usr, "End X?", "Map Exporter", usr.x || 1, world.maxx, 1)
+ var/end_y = tgui_input_number(usr, "End Y?", "Map Exporter", usr.y || 1, world.maxy, 1)
+ var/date = time2text(world.timeofday, "YYYY-MM-DD_hh-mm-ss")
+ var/file_name = sanitize_filename(tgui_input_text(usr, "Filename?", "Map Exporter", "exported_map_[date]"))
+ var/confirm = tgui_alert(usr, "Are you sure you want to do this? This will cause extreme lag!", "Map Exporter", list("Yes", "No"))
+
+ if(confirm != "Yes" || !check_rights(R_DEBUG))
+ return
+
+ var/map_text = write_map(start_x, start_y, z_level, end_x, end_y, z_level)
+ log_admin("Build Mode: [key_name(usr)] is exporting the map area from ([start_x], [start_y], [z_level]) through ([end_x], [end_y], [z_level])")
+ send_exported_map(usr, file_name, map_text)
+
+/**
+ * A procedure for saving DMM text to a file and then sending it to the user.
+ * Arguments:
+ * * user - a user which get map
+ * * name - name of file + .dmm
+ * * map - text with DMM format
+ */
+/proc/send_exported_map(user, name, map)
+ var/file_path = "data/[name].dmm"
+ rustg_file_write(map, file_path)
+ DIRECT_OUTPUT(user, ftp(file_path, "[name].dmm"))
+ var/file_to_delete = file(file_path)
+ fdel(file_to_delete)
+
+/proc/sanitize_filename(text)
+ return hashtag_newlines_and_tabs(text, list("\n"="", "\t"="", "/"="", "\\"="", "?"="", "%"="", "*"="", ":"="", "|"="", "\""="", "<"="", ">"=""))
+
+/proc/hashtag_newlines_and_tabs(text, list/repl_chars = list("\n"="#","\t"="#"))
+ for(var/char in repl_chars)
+ var/index = findtext(text, char)
+ while(index)
+ text = copytext(text, 1, index) + repl_chars[char] + copytext(text, index + length(char))
+ index = findtext(text, char, index + length(char))
+ return text
+
+/**
+ * A procedure for saving non-standard properties of an object.
+ * For example, saving ore into a silo, and further spavn by coordinates of metal stacks objects
+ */
+/obj/proc/on_object_saved()
+ return null
+
+// Save resources in silo
+/obj/machinery/ore_silo/on_object_saved()
+ var/data
+ 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] / 100
+ var/datum/material/material_datum = each
+ while(amount > 0)
+ var/amount_in_stack = max(1, min(50, amount))
+ amount -= amount_in_stack
+ data += "[data ? ",\n" : ""][material_datum.sheet_type]{\n\tamount = [amount_in_stack]\n\t}"
+ return data
+
+/**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 list(
+ NAMEOF(src, color),
+ NAMEOF(src, dir),
+ NAMEOF(src, icon),
+ NAMEOF(src, icon_state),
+ NAMEOF(src, name),
+ NAMEOF(src, pixel_x),
+ NAMEOF(src, pixel_y),
+ )
+
+/obj/get_save_vars()
+ return ..() + NAMEOF(src, req_access)
+
+/obj/item/stack/get_save_vars()
+ return ..() + NAMEOF(src, amount)
+
+/obj/docking_port/get_save_vars()
+ return ..() + list(
+ NAMEOF(src, dheight),
+ NAMEOF(src, dwidth),
+ NAMEOF(src, height),
+ NAMEOF(src, shuttle_id),
+ NAMEOF(src, width),
+ )
+/obj/docking_port/stationary/get_save_vars()
+ return ..() + NAMEOF(src, roundstart_template)
+
+/obj/machinery/atmospherics/get_save_vars()
+ return ..() + list(
+ NAMEOF(src, piping_layer),
+ NAMEOF(src, pipe_color),
+ )
+
+/obj/item/pipe/get_save_vars()
+ return ..() + list(
+ NAMEOF(src, piping_layer),
+ NAMEOF(src, pipe_color),
+ )
+
+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",
+))
+
+/proc/to_list_string(list/future_string)
+ . = "list("
+ var/first_entry = TRUE
+ for(var/item in future_string)
+ if(!first_entry)
+ . += ", "
+ if(future_string[item])
+ . += hashtag_newlines_and_tabs("[item] = [future_string[item]]", list("{"="", "}"="", "\""="", ";"="", ","=""))
+ else
+ . += hashtag_newlines_and_tabs("[item]", list("{"="", "}"="", "\""="", ";"="", ","=""))
+ first_entry = FALSE
+ . += ")"
+
+/**
+ *Procedure for converting a coordinate-selected part of the map into text for the .dmi format
+ */
+/proc/write_map(
+ minx,
+ miny,
+ minz,
+ maxx,
+ maxy,
+ maxz,
+ save_flag = ALL,
+ shuttle_area_flag = SAVE_SHUTTLEAREA_DONTCARE,
+ list/obj_blacklist = list(),
+)
+
+ var/width = maxx - minx
+ var/height = maxy - miny
+ var/depth = maxz - minz
+
+ //Step 0: Calculate the amount of letters we need (26 ^ n > turf count)
+ var/turfs_needed = width * height
+ var/layers = FLOOR(log(GLOB.save_file_chars.len, turfs_needed) + 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/z in 0 to depth)
+ for(var/x in 0 to width)
+ contents += "\n([x + 1],1,[z + 1]) = {\"\n"
+ for(var/y in height to 0 step -1)
+ CHECK_TICK
+ //====Get turfs Data====
+ var/turf/place = locate((minx + x), (miny + y), (minz + z))
+ var/area/location
+ var/list/objects
+ var/area/place_area = get_area(place)
+ //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
+ else if(istype(place, /turf/open/space) && !(save_flag & SAVE_SPACE))
+ place = /turf/template_noop
+ location = /area/template_noop
+ //Stuff to add
+ else
+ location = place_area.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)
+ CHECK_TICK
+ if(thing.type in obj_blacklist)
+ continue
+ var/metadata = generate_tgm_metadata(thing)
+ 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)
+ CHECK_TICK
+ if(istype(thing, /mob/living/carbon)) //Ignore people, but not animals
+ continue
+ var/metadata = generate_tgm_metadata(thing)
+ 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 "//[DMM2TGM_MESSAGE]\n[header][contents]"
+
+//vars_to_save = list() to save all vars
+/proc/generate_tgm_metadata(atom/object)
+ var/dat = ""
+ var/data_to_add = list()
+ var/list/vars_to_save = object.get_save_vars()
+ if(!vars_to_save)
+ return
+ for(var/variable in object.vars)
+ CHECK_TICK
+ if(!(variable in vars_to_save))
+ continue
+ var/value = object.vars[variable]
+ if(!value)
+ continue
+ if(value == initial(object.vars[variable]) || !issaved(object.vars[variable]))
+ continue
+ if(variable == "icon_state" && object.smoothing_flags)
+ continue
+ var/symbol = ""
+ if(istext(value))
+ symbol = "\""
+ value = hashtag_newlines_and_tabs(value, list("{"="", "}"="", "\""="", ";"="", ","=""))
+ else if(islist(value))
+ value = to_list_string(value)
+ 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 += "[variable] = [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)
+ CHECK_TICK
+ var/length = GLOB.save_file_chars.len
+ var/calculated = FLOOR((index-1) / (length ** (i - 1)), 1)
+ calculated = (calculated % length) + 1
+ output = "[GLOB.save_file_chars[calculated]][output]"
+ return output
diff --git a/code/modules/buildmode/submodes/map_export.dm b/code/modules/buildmode/submodes/map_export.dm
new file mode 100644
index 00000000000..3e167c0f637
--- /dev/null
+++ b/code/modules/buildmode/submodes/map_export.dm
@@ -0,0 +1,80 @@
+/datum/buildmode_mode/map_export
+ key = "mapexport"
+ use_corner_selection = TRUE
+ /// Variable with the flag value to understand how to treat the shuttle zones.
+ var/shuttle_flag = SAVE_SHUTTLEAREA_DONTCARE
+ /// Variable with a flag value to indicate what should be saved (for example, only objects or only mobs).
+ var/save_flag = ALL
+ /// A guard variable to prevent more than one map export process from occurring at the same time.
+ var/static/is_running = FALSE
+
+/datum/buildmode_mode/map_export/change_settings(client/builder)
+ var/static/list/options = list(
+ "Object Saving" = SAVE_OBJECTS,
+ "Mob Saving" = SAVE_MOBS,
+ "Turf Saving" = SAVE_TURFS,
+ "Area Saving" = SAVE_AREAS,
+ "Space Turf Saving" = SAVE_SPACE,
+ "Object Property Saving" = SAVE_OBJECT_PROPERTIES,
+ )
+ var/what_to_change = tgui_input_list(builder, "What export setting would you like to toggle?", "Map Exporter", options)
+ save_flag ^= options[what_to_change]
+ to_chat(builder, "[what_to_change] is now [save_flag & options[what_to_change] ? "ENABLED" : "DISABLED"].")
+
+/datum/buildmode_mode/map_export/show_help(client/builder)
+ to_chat(builder, span_purple(examine_block(
+ "[span_bold("Select corner")] -> Left Mouse Button on obj/turf/mob\n\
+ [span_bold("Set export options")] -> Right Mouse Button on buildmode button"))
+ )
+
+/datum/buildmode_mode/map_export/handle_selected_area(client/builder, params)
+ var/list/listed_params = params2list(params)
+ var/left_click = listed_params.Find("left")
+
+ //Ensure the selection is actually done
+ if(!left_click)
+ to_chat(builder, span_warning("Invalid selection."))
+ return
+
+ //If someone somehow gets build mode, stop them from using this.
+ if(!check_rights(R_DEBUG))
+ message_admins("[ckey(builder)] tried to run the map save generator but was rejected due to insufficient perms.")
+ to_chat(builder, span_warning("You must have +ADMIN rights to use this."))
+ return
+ //Emergency check
+ if(get_dist(cornerA, cornerB) > 60 || cornerA.z != cornerB.z)
+ var/confirm = tgui_alert(builder, "Are you sure about this? Exporting large maps may take quite a while.", "Map Exporter", list("Yes", "No"))
+ if(confirm != "Yes")
+ return
+
+ if(cornerA == cornerB)
+ return
+
+ if(is_running)
+ to_chat(builder, span_warning("Someone is already running the generator! Try again in a little bit."))
+ return
+
+ to_chat(builder, span_warning("Saving, please wait..."))
+ is_running = TRUE
+
+ log_admin("Build Mode: [key_name(builder)] is exporting the map area from [AREACOORD(cornerA)] through [AREACOORD(cornerB)]") //I put this before the actual saving of the map because it likely won't log if it crashes the fucking server
+
+ //oversimplified for readability and understandibility
+
+ var/minx = min(cornerA.x, cornerB.x)
+ var/miny = min(cornerA.y, cornerB.y)
+ var/minz = min(cornerA.z, cornerB.z)
+
+ var/maxx = max(cornerA.x, cornerB.x)
+ var/maxy = max(cornerA.y, cornerB.y)
+ var/maxz = max(cornerA.z, cornerB.z)
+
+ //Step 1: Get the data (This can take a while)
+ var/dat = write_map(minx, miny, minz, maxx, maxy, maxz, save_flag, shuttle_flag)
+
+ //Step 2: Write the data to a file and give map to client
+ var/date = time2text(world.timeofday, "YYYY-MM-DD_hh-mm-ss")
+ var/file_name = sanitize_filename(tgui_input_text(builder, "Filename?", "Map Exporter", "exported_map_[date]"))
+ send_exported_map(builder, file_name, dat)
+ to_chat(builder, span_green("The map was successfully saved!"))
+ is_running = FALSE
diff --git a/icons/misc/buildmode.dmi b/icons/misc/buildmode.dmi
index dd794c02aaa30f6ef1f3cdcdf4331b742f31537e..7d56918846b2094dc492b7aa96a67dc47f7edd85 100644
GIT binary patch
literal 6299
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