Fix map_export admin verb not saving objects properly (#90998)

## About The Pull Request

When using the `map_export` admin verb the following things are fixed:
- All objects density, anchored, opacity, atom_integrity, and
resistance_flags vars are saved
- Multi-tile objects being spammed on all tiles the sprite reaches
- Dirt decals error icon
- Airlocks error icon and to save welded state
- Dark Wizard Simple Mobs error icon
- Closets to save welded, open, and locked states
- Air alarms to save name
- Air scrubbers/vents to save name and welded states
- APCs to save name, charge, cell, lighting, equipment, and
environmental states
- APCs spawning a duplicated terminal underneath it when one already
exists
- SMES to save charge, input, and output states
- Holodecks to revert any holodeck turfs to the empty turf and skip
saving any hologram items
- Photos and Paintings error icons
- Bloody Footprints error icons
- False Walls error icons
- Docking Ports runtimes because the map template var would change
- Effects (lasers, portals, beams, sparks, etc.) saving when they should
be omitted

I would have loved to get `component_parts` to save for machines and
turf decals, but perhaps that is for another day since it requires
complicated solutions.

Here are some before and after pictures:


![StrongDMM_209k6PXSaQ](https://github.com/user-attachments/assets/27f0a80b-3cbc-4862-a218-612d52fa0e4f)


![StrongDMM_5PdRfLTZ4l](https://github.com/user-attachments/assets/3bbfd724-4b51-47c5-8cff-02687250fc1e)


![StrongDMM_F4DPOGz5K7](https://github.com/user-attachments/assets/1130ded8-3062-469a-ad4a-d437d89a68da)


![StrongDMM_BIa554dsGv](https://github.com/user-attachments/assets/440d6b38-dbbf-47c0-ad9a-5165504d104e)

## Why It's Good For The Game
Better map saving code.

## Changelog
🆑
fix: Fix `map_export` admin verb not properly saving a massive amount of
objects.
/🆑
This commit is contained in:
Tim
2025-05-16 22:43:09 -07:00
committed by GitHub
parent 62fcace82c
commit e698c965b3
17 changed files with 172 additions and 39 deletions
+3
View File
@@ -37,6 +37,9 @@
fire = 90
acid = 70
/obj/machinery/button/get_save_vars()
return ..() + NAMEOF(src, id)
/**
* INITIALIZATION
*/
+6
View File
@@ -157,6 +157,12 @@
flags_1 = HTML_USE_INITAL_ICON_1
rad_insulation = RAD_MEDIUM_INSULATION
/obj/machinery/door/airlock/get_save_vars()
. = ..()
. -= NAMEOF(src, icon_state) // airlocks ignore icon_state and instead use get_airlock_overlay()
// TODO save the wire data but need to include states for cute wires, signalers attached to wires, etc.
return .
/obj/machinery/door/airlock/Initialize(mapload)
. = ..()
+6
View File
@@ -82,6 +82,11 @@
fire = 80
acid = 70
/obj/machinery/door/get_save_vars()
. = ..()
. += NAMEOF(src, welded)
return .
/obj/machinery/door/Initialize(mapload)
AddElement(/datum/element/blocks_explosives)
. = ..()
@@ -118,6 +123,7 @@
)
AddElement(/datum/element/connect_loc, loc_connections)
AddElement(/datum/element/can_barricade)
update_appearance()
/obj/machinery/door/examine(mob/user)
. = ..()
@@ -293,6 +293,9 @@
dryname = "dried footprints"
drydesc = "HMM... SOMEONE WAS HERE!"
/obj/effect/decal/cleanable/blood/footprints/get_save_vars()
return ..() - NAMEOF(src, icon_state)
/obj/effect/decal/cleanable/blood/footprints/Initialize(mapload, footprint_sprite)
src.footprint_sprite = footprint_sprite
. = ..()
@@ -109,6 +109,14 @@ GLOBAL_LIST_EMPTY(roundstart_station_closets)
fire = 70
acid = 60
/obj/structure/closet/get_save_vars()
. = ..()
. += NAMEOF(src, welded)
. += NAMEOF(src, opened)
. += NAMEOF(src, locked)
. += NAMEOF(src, anchorable)
return .
/obj/structure/closet/Initialize(mapload)
. = ..()
@@ -26,6 +26,10 @@
var/girder_type = /obj/structure/girder/displaced
var/opening = FALSE
/obj/structure/falsewall/get_save_vars()
. = ..()
. -= NAMEOF(src, icon)
return .
/obj/structure/falsewall/Initialize(mapload)
. = ..()
@@ -33,6 +37,7 @@
set_custom_materials(initialized_mineral.mats_per_unit, mineral_amount)
qdel(initialized_mineral)
air_update_turf(TRUE, TRUE)
update_appearance()
/obj/structure/falsewall/attack_hand(mob/user, list/modifiers)
if(opening)
+68 -30
View File
@@ -74,44 +74,63 @@ ADMIN_VERB(map_export, R_DEBUG, "Map Export", "Select a part of the map by coord
**/
/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),
)
. = 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)
. += NAMEOF(src, density)
. += NAMEOF(src, opacity)
if(uses_integrity)
if(atom_integrity != max_integrity) // Only save if atom_integrity differs from max_integrity to avoid redundant saving
. += NAMEOF(src, atom_integrity)
. += NAMEOF(src, max_integrity)
. += NAMEOF(src, integrity_failure)
. += NAMEOF(src, damage_deflection)
. += NAMEOF(src, resistance_flags)
return .
/atom/movable/get_save_vars()
. = ..()
. += NAMEOF(src, anchored)
return .
/obj/get_save_vars()
return ..() + list(NAMEOF(src, req_access), NAMEOF(src, id_tag))
. = ..()
. += NAMEOF(src, req_access)
. += NAMEOF(src, id_tag)
return .
/obj/item/stack/get_save_vars()
return ..() + NAMEOF(src, amount)
. = ..()
. += NAMEOF(src, amount)
return .
/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)
. = ..()
. += NAMEOF(src, dheight)
. += NAMEOF(src, dwidth)
. += NAMEOF(src, height)
. += NAMEOF(src, shuttle_id)
. += NAMEOF(src, width)
return .
/obj/machinery/atmospherics/get_save_vars()
return ..() + list(
NAMEOF(src, piping_layer),
NAMEOF(src, pipe_color),
)
. = ..()
. += NAMEOF(src, piping_layer)
. += NAMEOF(src, pipe_color)
return .
/obj/item/pipe/get_save_vars()
return ..() + list(
NAMEOF(src, piping_layer),
NAMEOF(src, pipe_color),
)
. = ..()
. += NAMEOF(src, piping_layer)
. += NAMEOF(src, pipe_color)
return .
GLOBAL_LIST_INIT(save_file_chars, list(
"a","b","c","d","e",
@@ -176,12 +195,20 @@ GLOBAL_LIST_INIT(save_file_chars, list(
maxz,
save_flag = ALL,
shuttle_area_flag = SAVE_SHUTTLEAREA_DONTCARE,
list/obj_blacklist = list(),
list/obj_blacklist = typesof(/obj/effect),
)
var/width = maxx - minx
var/height = maxy - miny
var/depth = maxz - minz
if(!islist(obj_blacklist))
CRASH("Non-list being used as object blacklist for map writing")
// we want to keep crayon writings, blood splatters, cobwebs, etc.
obj_blacklist -= typesof(/obj/effect/decal)
obj_blacklist -= typesof(/obj/effect/turf_decal)
obj_blacklist -= typesof(/obj/effect/landmark) // most landmarks get deleted except for latejoin arrivals shuttle
//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)
@@ -221,6 +248,10 @@ GLOBAL_LIST_INIT(save_file_chars, list(
place = /turf/template_noop
location = /area/template_noop
pull_from = null
//====Saving holodeck areas====
// All hologram objects get skipped and floor tiles get replaced with empty plating
if(ispath(location, /area/station/holodeck) && istype(place, /turf/open/floor/holofloor))
place = /turf/open/floor/holofloor/plating
//====For toggling not saving areas and turfs====
if(!(save_flag & SAVE_AREAS))
location = /area/template_noop
@@ -239,6 +270,11 @@ GLOBAL_LIST_INIT(save_file_chars, list(
CHECK_TICK
if(obj_blacklist[thing.type])
continue
if(thing.flags_1 & HOLOGRAM_1)
continue
if(is_multi_tile_object(thing) && (thing.loc != pull_from))
continue
var/metadata = generate_tgm_metadata(thing)
current_header += "[empty ? "" : ",\n"][thing.type][metadata]"
empty = FALSE
@@ -272,8 +308,8 @@ GLOBAL_LIST_INIT(save_file_chars, list(
/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]
@@ -281,6 +317,8 @@ GLOBAL_LIST_INIT(save_file_chars, list(
continue
if(variable == "icon_state" && object.smoothing_flags)
continue
if(variable == "icon" && object.smoothing_flags)
continue
var/text_value = tgm_encode(value)
if(!text_value)
+3
View File
@@ -706,6 +706,9 @@
/// the type of wallframe it 'disassembles' into
var/wallframe_type = /obj/item/wallframe/painting
/obj/structure/sign/painting/get_save_vars()
return ..() - NAMEOF(src, icon)
/obj/structure/sign/painting/Initialize(mapload, dir, building)
. = ..()
SSpersistent_paintings.painting_frames += src
@@ -87,6 +87,9 @@ GLOBAL_LIST_EMPTY_TYPED(air_alarms, /obj/machinery/airalarm)
fire = 90
acid = 30
/obj/machinery/airalarm/get_save_vars()
return ..() - NAMEOF(src, name)
/obj/machinery/airalarm/Initialize(mapload, ndir, nbuild)
. = ..()
set_wires(new /datum/wires/airalarm(src))
@@ -19,6 +19,18 @@
///Handles whether the custom reconcilation handling should be used
var/custom_reconcilation = FALSE
/obj/machinery/atmospherics/components/get_save_vars()
. = ..()
if(!override_naming)
// Prevents saving the dynamic name with \proper due to it converting to "???"
. -= NAMEOF(src, name)
. += NAMEOF(src, welded)
return .
/obj/machinery/atmospherics/components/Initialize(mapload)
. = ..()
update_appearance()
/obj/machinery/atmospherics/components/New()
parents = new(device_type)
airs = new(device_type)
@@ -24,6 +24,9 @@
unsuitable_heat_damage = 0
ai_controller = /datum/ai_controller/basic_controller/dark_wizard
/mob/living/basic/dark_wizard/get_save_vars()
return ..() - NAMEOF(src, icon_state) // icon_state is applied via apply_dynamic_human_appearance()
/mob/living/basic/dark_wizard/Initialize(mapload)
. = ..()
+3
View File
@@ -15,6 +15,9 @@
var/datum/picture/picture
var/scribble //Scribble on the back.
/obj/item/photo/get_save_vars()
return ..() - NAMEOF(src, icon)
/obj/item/photo/Initialize(mapload, datum/picture/P, datum_name = TRUE, datum_desc = TRUE)
set_picture(P, datum_name, datum_desc, TRUE)
//Photos are quite rarer than papers, so they're more likely to be added to the queue to make things even.
+22 -1
View File
@@ -151,6 +151,26 @@
fire = 90
acid = 50
/obj/machinery/power/apc/get_save_vars()
. = ..()
if(!auto_name)
. -= NAMEOF(src, name)
. += NAMEOF(src, opened)
. += NAMEOF(src, coverlocked)
. += NAMEOF(src, lighting)
. += NAMEOF(src, equipment)
. += NAMEOF(src, environ)
. += NAMEOF(src, cell_type)
if(cell_type)
start_charge = cell.charge / cell.maxcharge // only used in Initialize() so direct edit is fine
. += NAMEOF(src, start_charge)
// TODO save the wire data but need to include states for cute wires, signalers attached to wires, etc.
//. += NAMEOF(src, shorted)
//. += NAMEOF(src, locked)
return .
/obj/machinery/power/apc/Initialize(mapload, ndir)
. = ..()
//APCs get added to their own processing tasks for the machines subsystem.
@@ -212,7 +232,8 @@
if(cell_type)
cell = new cell_type(src)
cell.charge = start_charge * cell.maxcharge / 100 // (convert percentage to actual value)
make_terminal()
if(!locate(/obj/machinery/power/terminal) in loc)
make_terminal()
///This is how we test to ensure that mappers use the directional subtypes of APCs, rather than use the parent and pixel-shift it themselves.
if(abs(offset_old) != APC_PIXEL_OFFSET)
log_mapping("APC: ([src]) at [AREACOORD(src)] with dir ([dir] | [uppertext(dir2text(dir))]) has pixel_[dir & (WEST|EAST) ? "x" : "y"] value [offset_old] - should be [dir & (SOUTH|EAST) ? "-" : ""][APC_PIXEL_OFFSET]. Use the directional/ helpers!")
+1 -1
View File
@@ -10,7 +10,7 @@
/obj/machinery/power/apc/proc/make_terminal(terminal_cable_layer = cable_layer)
// create a terminal object at the same position as original turf loc
// wires will attach to this
terminal = new/obj/machinery/power/terminal(loc)
terminal = new /obj/machinery/power/terminal(loc)
terminal.cable_layer = terminal_cable_layer
terminal.setDir(dir)
terminal.master = src
+6
View File
@@ -38,6 +38,12 @@
/obj/item/stock_parts/power_store/get_cell()
return src
/obj/item/stock_parts/power_store/get_save_vars()
. = ..()
. += NAMEOF(src, charge)
. += NAMEOF(src, rigged)
return .
/obj/item/stock_parts/power_store/Initialize(mapload, override_maxcharge)
. = ..()
create_reagents(5, INJECTABLE | DRAINABLE)
+8
View File
@@ -49,6 +49,14 @@
if(!terminal)
. += span_warning("This [src] has no power terminal!")
/obj/machinery/power/smes/get_save_vars()
. = ..()
. += NAMEOF(src, charge)
. += NAMEOF(src, capacity)
. += NAMEOF(src, input_level)
. += NAMEOF(src, output_level)
return .
/obj/machinery/power/smes/Initialize(mapload)
. = ..()
dir_loop:
@@ -6,11 +6,16 @@
/// Map template to load when the dock is loaded
var/datum/map_template/shuttle/roundstart_template
/// The shuttle template id to use after roundstart
var/shuttle_template_id
/// Used to check if the shuttle template is enabled in the config file
var/json_key
///If true, the shuttle can always dock at this docking port, despite its area checks, or if something is already docked
var/override_can_dock_checks = FALSE
/obj/docking_port/stationary/get_save_vars()
return ..() + NAMEOF(src, roundstart_template)
/obj/docking_port/stationary/Initialize(mapload)
. = ..()
register()
@@ -76,18 +81,18 @@
/obj/docking_port/stationary/proc/load_roundstart()
if(json_key)
var/sid = SSmapping.current_map.shuttles[json_key]
roundstart_template = SSmapping.shuttle_templates[sid]
if(!roundstart_template)
shuttle_template_id = SSmapping.shuttle_templates[sid]
if(!shuttle_template_id)
CRASH("json_key:[json_key] value \[[sid]\] resulted in a null shuttle template for [src]")
else if(roundstart_template) // passed a PATH
var/sid = "[initial(roundstart_template.port_id)]_[initial(roundstart_template.suffix)]"
roundstart_template = SSmapping.shuttle_templates[sid]
if(!roundstart_template)
CRASH("Invalid path ([sid]/[roundstart_template]) passed to docking port.")
shuttle_template_id = SSmapping.shuttle_templates[sid]
if(!shuttle_template_id)
CRASH("Invalid path ([sid]/[shuttle_template_id]) passed to docking port.")
if(roundstart_template)
SSshuttle.action_load(roundstart_template, src)
if(shuttle_template_id)
SSshuttle.action_load(shuttle_template_id, src)
//returns first-found touching shuttleport
/obj/docking_port/stationary/get_docked()