map_export admin verb can now save atmos for turfs and canisters (#91051)

## About The Pull Request
Due to the atmos complexity, this was split off from:
- #90998

This takes the atmos gas mixture from a turf and converts it into a
string for saving via the `map_export` verb. The behavior is as follows:
- Space and wall turfs atmos are skipped
- Rounds all gases to 0.01 decimal for their individual mole value
- Any gases less than 0.01 moles get skipped

This does not keep track of active fires since those have hotspots,
active turfs, etc., which is way more complicated to save. Another
caveat is the amount of characters the map reader needs to write is much
larger since every turf (besides space/walls) will have different atmos
due to mobs breathing, airlocks opening, etc. so I made this setting
into a `SAVE_ATMOS` flag that you can apply when saving.

This also keeps track of air inside of canisters.

## Why It's Good For The Game
Atmospherics changes to the station can now be saved properly. 

## Changelog
🆑
qol: `map_export` admin verb can now save atmos for turfs and air
canisters
/🆑
This commit is contained in:
Tim
2025-05-26 08:32:11 -05:00
committed by Roxy
parent 008c1f07b5
commit f981acda23
6 changed files with 60 additions and 4 deletions

View File

@@ -5,6 +5,7 @@
#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)
#define SAVE_ATMOS (1 << 7) //! Save turf atmos
//Ignore turf if it contains
#define SAVE_SHUTTLEAREA_DONTCARE 0

View File

@@ -100,6 +100,13 @@ ADMIN_VERB(map_export, R_DEBUG, "Map Export", "Select a part of the map by coord
. += NAMEOF(src, anchored)
return .
/turf/open/get_save_vars()
. = ..()
var/datum/gas_mixture/turf_gasmix = return_air()
initial_gas_mix = turf_gasmix.to_string()
. += NAMEOF(src, initial_gas_mix)
return .
/obj/get_save_vars()
. = ..()
. += NAMEOF(src, req_access)
@@ -292,7 +299,12 @@ GLOBAL_LIST_INIT(save_file_chars, list(
var/metadata = generate_tgm_metadata(thing)
current_header += "[empty ? "" : ",\n"][thing.type][metadata]"
empty = FALSE
current_header += "[empty ? "" : ",\n"][place],\n[location])\n"
current_header += "[empty ? "" : ",\n"][place]"
//====SAVING ATMOS====
if((save_flag & SAVE_TURFS) && (save_flag & SAVE_ATMOS) && !isspaceturf(pull_from))
var/metadata = generate_tgm_metadata(pull_from)
current_header += "[metadata]"
current_header += ",\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

View File

@@ -743,3 +743,26 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache())
current_reaction.react(air_mixture = src, working_power = working_power, electrolyzer_args = electrolyzer_args)
garbage_collect()
/// Convert a gas mixture to a string (ie. "o2=22;n2=82;TEMP=180")
/// Rounds all temperature and gases to 0.01 and skips any gases less than that amount
/datum/gas_mixture/proc/to_string()
var/list/cached_gases = gases
var/rounded_temp = round(temperature, 0.01)
var/list/atmos_contents = list()
var/temperature_str = "TEMP=[num2text(rounded_temp)]"
if(!length(cached_gases) || total_moles() < 0.01)
return temperature_str
for(var/gas_path in cached_gases)
var/gas_moles = cached_gases[gas_path][MOLES]
var/gas_id = cached_gases[gas_path][GAS_META][META_GAS_ID]
gas_moles = round(gas_moles, 0.01)
if(gas_moles >= 0.01)
atmos_contents += "[gas_id]=[num2text(gas_moles)]"
atmos_contents += temperature_str
return atmos_contents.Join(";")

View File

@@ -49,13 +49,20 @@
fire = 80
acid = 50
/obj/machinery/portable_atmospherics/canister/get_save_vars()
. = ..()
. += NAMEOF(src, valve_open)
. += NAMEOF(src, release_pressure)
return .
/obj/machinery/portable_atmospherics/canister/Initialize(mapload)
. = ..()
if(mapload)
internal_cell = new /obj/item/stock_parts/power_store/cell/high(src)
create_gas()
if(!initial_gas_mix)
create_gas()
if(ispath(gas_type, /datum/gas))
desc = "[GLOB.meta_gas_info[gas_type][META_GAS_NAME]]. [GLOB.meta_gas_info[gas_type][META_GAS_DESC]]"

View File

@@ -13,6 +13,8 @@
///Stores the gas mixture of the portable component. Don't access this directly, use return_air() so you support the temporary processing it provides
var/datum/gas_mixture/air_contents
///Stores the gas mixture in string form for map loading and saving
var/initial_gas_mix
///Stores the reference of the connecting port
var/obj/machinery/atmospherics/components/unary/portables_connector/connected_port
///Stores the reference of the tank the machine is holding
@@ -42,11 +44,21 @@
fire = 60
acid = 30
/obj/machinery/portable_atmospherics/get_save_vars()
. = ..()
var/datum/gas_mixture/gasmix = air_contents
initial_gas_mix = gasmix.to_string()
. += NAMEOF(src, initial_gas_mix)
return .
/obj/machinery/portable_atmospherics/Initialize(mapload)
. = ..()
air_contents = new
if(initial_gas_mix)
air_contents = SSair.parse_gas_string(initial_gas_mix)
else
air_contents = new
air_contents.temperature = T20C
air_contents.volume = volume
air_contents.temperature = T20C
SSair.start_processing_machine(src)
AddElement(/datum/element/climbable, climb_time = 3 SECONDS, climb_stun = 3 SECONDS)
AddElement(/datum/element/elevation, pixel_shift = 8)

View File

@@ -14,6 +14,7 @@
"Area Saving" = SAVE_AREAS,
"Space Turf Saving" = SAVE_SPACE,
"Object Property Saving" = SAVE_OBJECT_PROPERTIES,
"Atmos Saving" = SAVE_ATMOS,
)
var/what_to_change = tgui_input_list(builder, "What export setting would you like to toggle?", "Map Exporter", options)
if (!what_to_change)