Files
Bubberstation/code/modules/admin/verbs/map_export.dm
SkyratBot 782a04859a [MIRROR] Macro Optimizes Map Saving (100x) DO NOT CHURN STRINGS Edition (#26189)
* Macro Optimizes Map Saving (100x) DO NOT CHURN STRINGS Edition  (#80845)

## About The Pull Request

Yello!
This one is reasonably quick, tho I did some fixes too

This is the big one, fixes the buildmode tool sometimes locking disabled
for the whole round.
We do this by replacing the static var on buildmode with global var and
a global proc
This keeps a harddel on the buildmode datum from permalocking is_running
to TRUE

Also makes flipping the var BACK if something breaks significantly
easier for admins, so that's nice

Alright, smaller things now

Fixes lists of numbers failing to encoded improperly This was fixed on
shiptest, we failed to actually port their most recent revision
Fixes the shuttle flag not actually working because it used istype
instead of ispath
Changes obj_blacklist to a typecache for optimization's sake
Renames/moves some vars around to prevent weird double typing things
Removes a checktick in key gen, it's just costing more time then it
would save in overtime
Properly handles lists. We were only doing var encoding one layer deep,
need to do it alll the way down

Alright, now the optimizations

This proc is fucking HOT, and it's for really dumb reasons

This is a text gen proc, and it makes the mistake of generating text and
concatinating it with MORE text.
This is HORRIFICALLY EXPENSIVE because byond caches strings (can only be
one of each) and string churn fucks up that caching system something
fierce
Moving from strings to lists of strings we join at the end takes us from
like idk 100 seconds to save bare metastation to like 1.5
This is applied basically everywhere for obvious reasons

While I'm here, storing keys in a flat list and then using find to find
them, then using that index to lookup into another flat list is a bit
silly. Let's just make it an assoc list. Faster lookup, cleaner.

Oh also rather then iterating over all the vars on an object, let's
iterate over just the ones we care about yeah?

Let's see... no sense genning a key we'll never use, and having suffixes
be often non existent is silly just embrace the slight mess.

That's it I think, this takes us from 100 seconds to save metastation to
2.5 seconds to save ALL of metastation (I removed the vars limiter so I
could make sure var saving didn't fuck me up)

## Why It's Good For The Game

Cleans up some issues that we failed to port the fixes for, MASSIVELY
optimizes this (so it can finish in like 5/10 seconds and not 300!) and
ensures admins can always use the thing and don't risk dropping their
pet buildastation to the void.

Worth noting, this tool really should not be used for station mapping
outside an event context. It produces sorta buggy var edits, and WILL
fail to pull over context for shit. Please don't use it as such

Profiles (csv files I promise)

[Before](https://github.com/tgstation/tgstation/files/13853313/profiler.json)

[After](https://github.com/tgstation/tgstation/files/13853271/profiler.json)

I'd include my line by lines but I don't know how much you'd get out of
them. Here's an image tho

![image](https://github.com/tgstation/tgstation/assets/58055496/3f3148c5-8b1e-4bda-aa65-3983f9944a91)

## Changelog
🆑
fix: The map saving tool will no longer lock up and prevent all further
action at random
fix: Map saving now takes on the order of seconds, not minutes
fix: Fixes an issue with lists that caused strongdmm to report saved
maps as broken
/🆑

* Macro Optimizes Map Saving (100x) DO NOT CHURN STRINGS Edition

---------

Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
2024-01-20 23:39:52 +01:00

307 lines
10 KiB
Plaintext

/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/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()