From e698c965b366f5f0dcf3368e51b7cc653081ce7a Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 17 May 2025 00:43:09 -0500 Subject: [PATCH] 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 :cl: fix: Fix `map_export` admin verb not properly saving a massive amount of objects. /:cl: --- code/game/machinery/buttons.dm | 3 + code/game/machinery/doors/airlock.dm | 6 ++ code/game/machinery/doors/door.dm | 6 ++ .../effects/decals/cleanable/humans.dm | 3 + .../structures/crates_lockers/closets.dm | 8 ++ code/game/objects/structures/false_walls.dm | 5 + code/modules/admin/verbs/map_export.dm | 98 +++++++++++++------ code/modules/art/paintings.dm | 3 + .../machinery/air_alarm/_air_alarm.dm | 3 + .../machinery/components/components_base.dm | 12 +++ .../living/basic/ruin_defender/dark_wizard.dm | 3 + code/modules/photography/photos/photo.dm | 3 + code/modules/power/apc/apc_main.dm | 23 ++++- code/modules/power/apc/apc_power_proc.dm | 2 +- code/modules/power/power_store.dm | 6 ++ code/modules/power/smes.dm | 8 ++ .../stationary_port/stationary_port.dm | 19 ++-- 17 files changed, 172 insertions(+), 39 deletions(-) diff --git a/code/game/machinery/buttons.dm b/code/game/machinery/buttons.dm index 66f1b64795a..e1ae2b9c441 100644 --- a/code/game/machinery/buttons.dm +++ b/code/game/machinery/buttons.dm @@ -37,6 +37,9 @@ fire = 90 acid = 70 +/obj/machinery/button/get_save_vars() + return ..() + NAMEOF(src, id) + /** * INITIALIZATION */ diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index 69bdbd563f8..9b24dad6ca8 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -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) . = ..() diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 3490de72673..1f8349e8745 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -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) . = ..() diff --git a/code/game/objects/effects/decals/cleanable/humans.dm b/code/game/objects/effects/decals/cleanable/humans.dm index 5e1edd12002..e3f8b1b2c3a 100644 --- a/code/game/objects/effects/decals/cleanable/humans.dm +++ b/code/game/objects/effects/decals/cleanable/humans.dm @@ -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 . = ..() diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index ef53e089922..ff448b58444 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -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) . = ..() diff --git a/code/game/objects/structures/false_walls.dm b/code/game/objects/structures/false_walls.dm index b37628996ed..2f5a048b4fb 100644 --- a/code/game/objects/structures/false_walls.dm +++ b/code/game/objects/structures/false_walls.dm @@ -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) diff --git a/code/modules/admin/verbs/map_export.dm b/code/modules/admin/verbs/map_export.dm index 057a8476892..eb14fd965a3 100644 --- a/code/modules/admin/verbs/map_export.dm +++ b/code/modules/admin/verbs/map_export.dm @@ -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) diff --git a/code/modules/art/paintings.dm b/code/modules/art/paintings.dm index 3eddd7b02ab..b6b36e41d69 100644 --- a/code/modules/art/paintings.dm +++ b/code/modules/art/paintings.dm @@ -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 diff --git a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm index 66ae237886d..ea9e4e82b5a 100644 --- a/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm +++ b/code/modules/atmospherics/machinery/air_alarm/_air_alarm.dm @@ -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)) diff --git a/code/modules/atmospherics/machinery/components/components_base.dm b/code/modules/atmospherics/machinery/components/components_base.dm index 6001042453b..bc2e5ee425b 100644 --- a/code/modules/atmospherics/machinery/components/components_base.dm +++ b/code/modules/atmospherics/machinery/components/components_base.dm @@ -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) diff --git a/code/modules/mob/living/basic/ruin_defender/dark_wizard.dm b/code/modules/mob/living/basic/ruin_defender/dark_wizard.dm index 6e88219244a..df4282ea224 100644 --- a/code/modules/mob/living/basic/ruin_defender/dark_wizard.dm +++ b/code/modules/mob/living/basic/ruin_defender/dark_wizard.dm @@ -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) . = ..() diff --git a/code/modules/photography/photos/photo.dm b/code/modules/photography/photos/photo.dm index 6b0db46e21b..82de4fc01a8 100644 --- a/code/modules/photography/photos/photo.dm +++ b/code/modules/photography/photos/photo.dm @@ -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. diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm index b0c05a40d83..a181e5f93e5 100644 --- a/code/modules/power/apc/apc_main.dm +++ b/code/modules/power/apc/apc_main.dm @@ -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!") diff --git a/code/modules/power/apc/apc_power_proc.dm b/code/modules/power/apc/apc_power_proc.dm index 2f1182d01a5..b4ee895db35 100644 --- a/code/modules/power/apc/apc_power_proc.dm +++ b/code/modules/power/apc/apc_power_proc.dm @@ -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 diff --git a/code/modules/power/power_store.dm b/code/modules/power/power_store.dm index 277c5c031b4..9dd60708a37 100644 --- a/code/modules/power/power_store.dm +++ b/code/modules/power/power_store.dm @@ -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) diff --git a/code/modules/power/smes.dm b/code/modules/power/smes.dm index 9e708855d57..0277fe9bf20 100644 --- a/code/modules/power/smes.dm +++ b/code/modules/power/smes.dm @@ -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: diff --git a/code/modules/shuttle/stationary_port/stationary_port.dm b/code/modules/shuttle/stationary_port/stationary_port.dm index 6e68cf5dc0c..dfb40108ad2 100644 --- a/code/modules/shuttle/stationary_port/stationary_port.dm +++ b/code/modules/shuttle/stationary_port/stationary_port.dm @@ -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()