Files
SmArtKarandnevimer 5a2becfa67 Fixes random runtimes from RPD usage (#92246)
## About The Pull Request

SSair inits some pipes for its directional cache when RPD is used which
runtimes when atmos components try to update their icon due to assuming
they have a loc while being nullspaced.

## Changelog
🆑
fix: Fixed RPD causing runtimes when placing atmos components for the
first time.
/🆑

---------

Co-authored-by: Lucy <lucy@absolucy.moe>
(cherry picked from commit 99797151d7)
2025-08-08 15:29:12 -04:00

385 lines
13 KiB
Plaintext

// So much of atmospherics.dm was used solely by components, so separating this makes things all a lot cleaner.
// On top of that, now people can add component-speciic procs/vars if they want!
/obj/machinery/atmospherics/components
hide = FALSE
layer = GAS_PUMP_LAYER
///Is the component welded?
var/welded = FALSE
///Current underfloor_accessibility state, determines if the component should show the pipe underneath it and what plane it renders on.
var/underfloor_state = UNDERFLOOR_INTERACTABLE
///When the component is on a non default layer should we shift everything? Or just the underlay pipe
var/shift_underlay_only = TRUE
///Stores the parent pipeline, used in components
var/list/datum/pipeline/parents
///If this is queued for a rebuild this var signifies whether parents should be updated after it's done
var/update_parents_after_rebuild = FALSE
///Stores the gasmix for each node, used in components
var/list/datum/gas_mixture/airs
///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)
parents = new(device_type)
airs = new(device_type)
. = ..()
for(var/i in 1 to device_type)
if(airs[i])
continue
var/datum/gas_mixture/component_mixture = new
component_mixture.volume = 200
airs[i] = component_mixture
update_appearance()
// Iconnery
/**
* Called by update_icon(), used individually by each component to determine the icon state without the pipe in consideration
*/
/obj/machinery/atmospherics/components/proc/update_icon_nopipes()
return
/obj/machinery/atmospherics/components/on_hide(datum/source, underfloor_accessibility)
hide_pipe(underfloor_accessibility)
return ..()
/**
* Called in on_hide(), set the underfloor_state var to true or false depending on the situation, calls update_icon()
*/
/obj/machinery/atmospherics/components/proc/hide_pipe(underfloor_accessibility)
underfloor_state = underfloor_accessibility
if(underfloor_state)
REMOVE_TRAIT(src, TRAIT_UNDERFLOOR, REF(src))
else
ADD_TRAIT(src, TRAIT_UNDERFLOOR, REF(src))
update_appearance(UPDATE_ICON)
/obj/machinery/atmospherics/components/update_icon()
update_icon_nopipes()
underlays.Cut()
color = null
var/uncovered_turf = loc && HAS_TRAIT(loc, TRAIT_UNCOVERED_TURF)
SET_PLANE_IMPLICIT(src, (underfloor_state == UNDERFLOOR_INTERACTABLE && !uncovered_turf) ? GAME_PLANE : FLOOR_PLANE)
// Layer is handled in update_layer()
if(!underfloor_state)
return ..()
if(pipe_flags & PIPING_DISTRO_AND_WASTE_LAYERS)
return ..()
var/connected = 0 //Direction bitset
var/underlay_pipe_layer = shift_underlay_only ? piping_layer : 3
for(var/i in 1 to device_type) //adds intact pieces
if(!nodes[i])
continue
var/obj/machinery/atmospherics/node = nodes[i]
var/node_dir = get_dir(src, node)
var/mutable_appearance/pipe_appearance = mutable_appearance('icons/obj/pipes_n_cables/pipe_underlays.dmi', "intact_[node_dir]_[underlay_pipe_layer]", appearance_flags = RESET_COLOR|KEEP_APART)
pipe_appearance.color = (node.pipe_color == ATMOS_COLOR_OMNI || istype(node, /obj/machinery/atmospherics/pipe/color_adapter)) ? pipe_color : node.pipe_color
if (underfloor_state == UNDERFLOOR_VISIBLE || uncovered_turf)
pipe_appearance.layer = BELOW_CATWALK_LAYER + get_pipe_layer_offset()
SET_PLANE_EXPLICIT(pipe_appearance, FLOOR_PLANE, src)
underlays += pipe_appearance
connected |= node_dir
for(var/direction in GLOB.cardinals)
if((initialize_directions & direction) && !(connected & direction))
var/mutable_appearance/pipe_appearance = mutable_appearance('icons/obj/pipes_n_cables/pipe_underlays.dmi', "exposed_[direction]_[underlay_pipe_layer]", appearance_flags = RESET_COLOR|KEEP_APART)
pipe_appearance.color = pipe_color
if (underfloor_state == UNDERFLOOR_VISIBLE || uncovered_turf)
pipe_appearance.layer = BELOW_CATWALK_LAYER + get_pipe_layer_offset()
SET_PLANE_EXPLICIT(pipe_appearance, FLOOR_PLANE, src)
underlays += pipe_appearance
if(!shift_underlay_only)
PIPING_LAYER_SHIFT(src, piping_layer)
return ..()
/obj/machinery/atmospherics/components/get_pipe_image(iconfile, iconstate, direction, color, piping_layer, trinary)
var/mutable_appearance/pipe_appearance = ..()
if (underfloor_state == UNDERFLOOR_VISIBLE || (loc && HAS_TRAIT(loc, TRAIT_UNCOVERED_TURF)))
pipe_appearance.layer = BELOW_CATWALK_LAYER + get_pipe_layer_offset()
SET_PLANE_EXPLICIT(pipe_appearance, FLOOR_PLANE, src)
return pipe_appearance
// Pipenet stuff; housekeeping
/obj/machinery/atmospherics/components/nullify_node(i)
if(parents[i])
nullify_pipenet(parents[i])
airs[i] = null
return ..()
/obj/machinery/atmospherics/components/on_construction(mob/user)
. = ..()
update_parents()
/obj/machinery/atmospherics/components/on_deconstruction(disassembled)
relocate_airs()
return ..()
/obj/machinery/atmospherics/components/rebuild_pipes()
. = ..()
if(update_parents_after_rebuild)
update_parents()
/obj/machinery/atmospherics/components/get_rebuild_targets()
var/list/to_return = list()
for(var/i in 1 to device_type)
if(parents[i])
continue
parents[i] = new /datum/pipeline()
to_return += parents[i]
return to_return
/**
* Called by nullify_node(), used to remove the pipeline the component is attached to
* Arguments:
* * -reference: the pipeline the component is attached to
*/
/obj/machinery/atmospherics/components/proc/nullify_pipenet(datum/pipeline/reference)
if(!reference)
CRASH("nullify_pipenet(null) called by [type] on [COORD(src)]")
for (var/i in 1 to parents.len)
if (parents[i] == reference)
reference.other_airs -= airs[i] // Disconnects from the pipeline side
parents[i] = null // Disconnects from the machinery side.
reference.other_atmos_machines -= src
if(custom_reconcilation)
reference.require_custom_reconcilation -= src
/**
* We explicitly qdel pipeline when this particular pipeline
* is projected to have no member and cause GC problems.
* We have to do this because components don't qdel pipelines
* while pipes must and will happily wreck and rebuild everything
* again every time they are qdeleted.
*/
if(!length(reference.other_atmos_machines) && !length(reference.members))
if(QDESTROYING(reference))
CRASH("nullify_pipenet() called on qdeleting [reference]")
qdel(reference)
/obj/machinery/atmospherics/components/return_pipenet_airs(datum/pipeline/reference)
var/list/returned_air = list()
for (var/i in 1 to parents.len)
if (parents[i] == reference)
returned_air += airs[i]
return returned_air
/obj/machinery/atmospherics/components/pipeline_expansion(datum/pipeline/reference)
if(reference)
return list(nodes[parents.Find(reference)])
return ..()
/obj/machinery/atmospherics/components/set_pipenet(datum/pipeline/reference, obj/machinery/atmospherics/target_component)
parents[nodes.Find(target_component)] = reference
/obj/machinery/atmospherics/components/return_pipenet(obj/machinery/atmospherics/target_component = nodes[1]) //returns parents[1] if called without argument
return parents[nodes.Find(target_component)]
/obj/machinery/atmospherics/components/replace_pipenet(datum/pipeline/Old, datum/pipeline/New)
parents[parents.Find(Old)] = New
// Helpers
/**
* Called in most atmos processes and gas handling situations, update the parents pipelines of the devices connected to the source component
* This way gases won't get stuck
*/
/obj/machinery/atmospherics/components/proc/update_parents()
if(!SSair.initialized)
return
if(rebuilding)
update_parents_after_rebuild = TRUE
return
for(var/i in 1 to device_type)
var/datum/pipeline/parent = parents[i]
if(!parent)
WARNING("Component is missing a pipenet! Rebuilding...")
SSair.add_to_rebuild_queue(src)
else
parent.update = TRUE
/obj/machinery/atmospherics/components/return_pipenets()
. = list()
for(var/i in 1 to device_type)
. += return_pipenet(nodes[i])
/// When this machine is in a pipenet that is reconciling airs, this proc can add pipelines to the calculation.
/// Can be either a list of pipenets or a single pipenet.
/obj/machinery/atmospherics/components/proc/return_pipenets_for_reconcilation(datum/pipeline/requester)
return list()
/// When this machine is in a pipenet that is reconciling airs, this proc can add airs to the calculation.
/// Can be either a list of airs or a single air mix.
/obj/machinery/atmospherics/components/proc/return_airs_for_reconcilation(datum/pipeline/requester)
return list()
// UI Stuff
/obj/machinery/atmospherics/components/ui_status(mob/user, datum/ui_state/state)
if(allowed(user))
return ..()
to_chat(user, span_danger("Access denied."))
return UI_CLOSE
// Tool acts
/obj/machinery/atmospherics/components/return_analyzable_air()
return airs
/**
* Handles machinery deconstruction and unsafe pressure release
*/
/obj/machinery/atmospherics/components/proc/crowbar_deconstruction_act(mob/living/user, obj/item/tool, internal_pressure = 0)
if(!panel_open)
balloon_alert(user, "open panel!")
return ITEM_INTERACT_SUCCESS
var/unsafe_wrenching = FALSE
var/filled_pipe = FALSE
var/datum/gas_mixture/environment_air = loc.return_air()
for(var/i in 1 to device_type)
var/datum/gas_mixture/inside_air = airs[i]
if(inside_air.total_moles() > 0 || internal_pressure)
filled_pipe = TRUE
if(!nodes[i] || (istype(nodes[i], /obj/machinery/atmospherics/components/unary/portables_connector) && !portable_device_connected(i)))
internal_pressure = internal_pressure > airs[i].return_pressure() ? internal_pressure : airs[i].return_pressure()
if(!filled_pipe)
default_deconstruction_crowbar(tool)
return ITEM_INTERACT_SUCCESS
to_chat(user, span_notice("You begin to unfasten \the [src]..."))
internal_pressure -= environment_air.return_pressure()
if(internal_pressure > 2 * ONE_ATMOSPHERE)
to_chat(user, span_warning("As you begin deconstructing \the [src] a gush of air blows in your face... maybe you should reconsider?"))
unsafe_wrenching = TRUE
if(!do_after(user, 2 SECONDS, src))
return
if(unsafe_wrenching)
unsafe_pressure_release(user, internal_pressure)
tool.play_tool_sound(src, 50)
deconstruct(TRUE)
return ITEM_INTERACT_SUCCESS
/obj/machinery/atmospherics/components/default_change_direction_wrench(mob/user, obj/item/I)
. = ..()
if(!.)
return FALSE
set_init_directions()
reconnect_nodes()
return TRUE
/obj/machinery/atmospherics/components/proc/reconnect_nodes()
for(var/i in 1 to device_type)
var/obj/machinery/atmospherics/node = nodes[i]
if(node)
if(src in node.nodes)
node.disconnect(src)
nodes[i] = null
if(parents[i])
nullify_pipenet(parents[i])
for(var/i in 1 to device_type)
var/obj/machinery/atmospherics/node = nodes[i]
atmos_init()
node = nodes[i]
if(node)
node.atmos_init()
node.add_member(src)
update_parents()
SSair.add_to_rebuild_queue(src)
/**
* Disconnects all nodes from ourselves, remove us from the node's nodes.
* Nullify our parent pipenet
*/
/obj/machinery/atmospherics/components/proc/disconnect_nodes()
for(var/i in 1 to device_type)
var/obj/machinery/atmospherics/node = nodes[i]
if(node)
if(src in node.nodes) //Only if it's actually connected. On-pipe version would is one-sided.
node.disconnect(src)
nodes[i] = null
if(parents[i])
nullify_pipenet(parents[i])
/**
* Connects all nodes to ourselves, add us to the node's nodes.
* Calls atmos_init() on the node and on us.
*/
/obj/machinery/atmospherics/components/proc/connect_nodes()
atmos_init()
for(var/i in 1 to device_type)
var/obj/machinery/atmospherics/node = nodes[i]
if(node)
node.atmos_init()
node.add_member(src)
SSair.add_to_rebuild_queue(src)
/**
* Easy way to toggle nodes connection and disconnection.
*
* Arguments:
* * disconnect - if TRUE, disconnects all nodes. If FALSE, connects all nodes.
*/
/obj/machinery/atmospherics/components/proc/change_nodes_connection(disconnect)
if(disconnect)
disconnect_nodes()
return
connect_nodes()
/obj/machinery/atmospherics/components/update_layer()
if (!underfloor_state)
layer = BELOW_CATWALK_LAYER
else if (PLANE_TO_TRUE(plane) == FLOOR_PLANE)
layer = ABOVE_OPEN_TURF_LAYER
else
layer = initial(layer)
layer += get_pipe_layer_offset()
/obj/machinery/atmospherics/components/proc/get_pipe_layer_offset()
return (piping_layer - PIPING_LAYER_DEFAULT) * PIPING_LAYER_LCHANGE + (GLOB.pipe_colors_ordered[pipe_color] * 0.001)
/**
* Handles air relocation to the pipenet/environment
*/
/obj/machinery/atmospherics/components/proc/relocate_airs(datum/gas_mixture/to_release)
var/turf/local_turf = get_turf(src)
for(var/i in 1 to device_type)
var/datum/gas_mixture/air = airs[i]
if(!nodes[i] || (istype(nodes[i], /obj/machinery/atmospherics/components/unary/portables_connector) && !portable_device_connected(i)))
if(!to_release)
to_release = air
continue
to_release.merge(air)
continue
var/datum/gas_mixture/parents_air = parents[i].air
parents_air.merge(air)
if(to_release)
local_turf.assume_air(to_release)