ADMIN_VERB(map_export, R_DEBUG, "Map Export", "Select a part of the map by coordinates and download it.", ADMIN_CATEGORY_DEBUG) var/user_x = user.mob.x var/user_y = user.mob.y var/user_z = user.mob.z var/z_level = tgui_input_number(user, "Export Which Z-Level?", "Map Exporter", user_z || 2) var/start_x = tgui_input_number(user, "Start X?", "Map Exporter", user_x || 1, world.maxx, 1) var/start_y = tgui_input_number(user, "Start Y?", "Map Exporter", user_y || 1, world.maxy, 1) var/end_x = tgui_input_number(user, "End X?", "Map Exporter", user_x || 1, world.maxx, 1) var/end_y = tgui_input_number(user, "End Y?", "Map Exporter", user_y || 1, world.maxy, 1) var/date = time2text(world.timeofday, "YYYY-MM-DD_hh-mm-ss") var/file_name = sanitize_filename(tgui_input_text(user, "Filename?", "Map Exporter", "exported_map_[date]")) var/confirm = tgui_alert(user, "Are you sure you want to do this? This will cause extreme lag!", "Map Exporter", list("Yes", "No")) if(confirm != "Yes") return var/map_text = write_map(start_x, start_y, z_level, end_x, end_y, z_level) log_admin("Build Mode: [key_name(user)] is exporting the map area from ([start_x], [start_y], [z_level]) through ([end_x], [end_y], [z_level])") send_exported_map(user, 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 ..() + list(NAMEOF(src, req_access), NAMEOF(src, id_tag)) /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/build_from) var/list/build_into = list() build_into += "list(" var/first_entry = TRUE for(var/item in build_from) CHECK_TICK if(!first_entry) build_into += ", " if(isnum(item) || !build_from[item]) build_into += "[tgm_encode(item)]" else build_into += "[tgm_encode(item)] = [tgm_encode(build_from[item])]" first_entry = FALSE build_into += ")" return build_into.Join("") /// Takes a constant, encodes it into a TGM valid string /proc/tgm_encode(value) if(istext(value)) //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. return "\"[hashtag_newlines_and_tabs("[value]", list("{"="", "}"="", "\""="", ";"="", ","=""))]\"" if(isnum(value) || ispath(value)) return "[value]" if(islist(value)) return to_list_string(value) if(isnull(value)) return "null" if(isicon(value) || isfile(value)) return "'[value]'" // not handled: // - pops: /obj{name="foo"} // - new(), newlist(), icon(), matrix(), sound() // fallback: string return tgm_encode("[value]") /** *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_data = list() //holds the data of a header -> to its key var/list/header = list() //The actual header in text var/list/contents = list() //The contents in text (bit at the end) var/key_index = 1 // How many keys we've generated so far 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 var/area/location var/turf/pull_from = locate((minx + x), (miny + y), (minz + z)) //If there is nothing there, save as a noop (For odd shapes) if(isnull(pull_from)) place = /turf/template_noop location = /area/template_noop //Ignore things in space, must be a space turf else if(istype(pull_from, /turf/open/space) && !(save_flag & SAVE_SPACE)) place = /turf/template_noop location = /area/template_noop pull_from = null //Stuff to add else var/area/place_area = get_area(pull_from) location = place_area.type place = pull_from.type //====Saving shuttles only / non shuttles only==== var/is_shuttle_area = ispath(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 pull_from = null //====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==== // Info that describes this turf and all its contents // Unique, will be checked for existing later var/list/current_header = list() current_header += "(\n" //Add objects to the header file var/empty = TRUE //====SAVING OBJECTS==== if(save_flag & SAVE_OBJECTS) for(var/obj/thing in pull_from) CHECK_TICK if(obj_blacklist[thing.type]) 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 pull_from) 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==== var/textiftied_header = current_header.Join() // If we already know this header just use its key, otherwise we gotta make a new one var/key = header_data[textiftied_header] if(!key) key = calculate_tgm_header_index(key_index, layers) key_index++ header += "\"[key]\" = [textiftied_header]" header_data[textiftied_header] = key contents += "[key]\n" contents += "\"}" return "//[DMM2TGM_MESSAGE]\n[header.Join()][contents.Join()]" /proc/generate_tgm_metadata(atom/object) var/list/data_to_add = list() var/list/vars_to_save = object.get_save_vars() for(var/variable in vars_to_save) CHECK_TICK var/value = object.vars[variable] if(value == initial(object.vars[variable]) || !issaved(object.vars[variable])) continue if(variable == "icon_state" && object.smoothing_flags) continue var/text_value = tgm_encode(value) if(!text_value) continue data_to_add += "[variable] = [text_value]" if(!length(data_to_add)) return return "{\n\t[data_to_add.Join(";\n\t")]\n\t}" // Could be inlined, not a massive cost tho so it's fine /// Generates a key matching our index /proc/calculate_tgm_header_index(index, key_length) var/list/output = list() // We want to stick the first one last, so we walk backwards var/list/pull_from = GLOB.save_file_chars var/length = length(pull_from) for(var/i in key_length to 1 step -1) var/calculated = FLOOR((index-1) / (length ** (i - 1)), 1) calculated = (calculated % length) + 1 output += pull_from[calculated] return output.Join()