code:
/obj/machinery/atmospherics/components/tank
	icon = 'icons/obj/atmospherics/stationary_canisters.dmi'
	icon_state = "smooth"

	name = "pressure tank"
	desc = "A large vessel containing pressurized gas."

	max_integrity = 800
	integrity_failure = 0.2
	density = TRUE
	layer = ABOVE_WINDOW_LAYER

	custom_materials = list(/datum/material/iron = 20000) // plasteel is not a material to prevent two bugs: one where the default pressure is 1.5 times higher as plasteel's material modifier is added, and a second one where the tank names could be "plasteel plasteel" tanks
	material_flags = MATERIAL_EFFECTS | MATERIAL_GREYSCALE | MATERIAL_ADD_PREFIX | MATERIAL_AFFECT_STATISTICS

	pipe_flags = PIPING_ONE_PER_TURF
	device_type = QUATERNARY
	initialize_directions = NONE
	custom_reconcilation = TRUE

	smoothing_flags = SMOOTH_CORNERS | SMOOTH_OBJ
	smoothing_groups = list(SMOOTH_GROUP_GAS_TANK)
	canSmoothWith = list(SMOOTH_GROUP_GAS_TANK)
	appearance_flags = KEEP_TOGETHER

	greyscale_config = /datum/greyscale_config/stationary_canister
	greyscale_colors = "#ffffff"

	///The image showing the gases inside of the tank
	var/image/window

	/// The volume of the gas mixture
	var/volume = 2500 //in liters
	/// The max pressure of the gas mixture before damaging the tank
	var/max_pressure = 20000
	/// The typepath of the gas this tank should be filled with.
	var/gas_type = null

	///Reference to the gas mix inside the tank
	var/datum/gas_mixture/air_contents

	/// The sounds that play when the tank is breaking from overpressure
	var/static/list/breaking_sounds = list(
		'sound/effects/structure_stress/pop1.ogg',
		'sound/effects/structure_stress/pop2.ogg',
		'sound/effects/structure_stress/pop3.ogg',
	)

	/// Shared images for the knob overlay representing a side of the tank that is open to connections
	var/static/list/knob_overlays

	/// Number of crack states to fill the list with. This exists because I'm lazy and didn't want to keeping adding more things manually to the below list.
	var/crack_states_count = 10
	/// The icon states for the cracks in the tank dmi
	var/static/list/crack_states

	/// The merger id used to create/get the merger group in charge of handling tanks that share an internal gas storage
	var/merger_id = "stationary_tanks"
	/// The typecache of types which are allowed to merge internal storage
	var/static/list/merger_typecache

/obj/machinery/atmospherics/components/tank/Initialize(mapload)
	. = ..()

	if(!knob_overlays)
		knob_overlays = list()
		for(var/dir in GLOB.cardinals)
			knob_overlays["[dir]"] = image('icons/obj/atmospherics/stationary_canisters.dmi', icon_state = "knob", dir = dir, layer = FLOAT_LAYER)

	if(!crack_states)
		crack_states = list()
		for(var/i in 1 to crack_states_count)
			crack_states += "crack[i]"

	if(!merger_typecache)
		merger_typecache = typecacheof(/obj/machinery/atmospherics/components/tank)

	AddComponent(/datum/component/gas_leaker, leak_rate = 0.05)
	AddElement(/datum/element/volatile_gas_storage)
	AddElement(/datum/element/crackable, 'icons/obj/atmospherics/stationary_canisters.dmi', crack_states)

	RegisterSignal(src, COMSIG_MERGER_ADDING, .proc/merger_adding)
	RegisterSignal(src, COMSIG_MERGER_REMOVING, .proc/merger_removing)
	RegisterSignal(src, COMSIG_ATOM_SMOOTHED_ICON, .proc/smoothed)

	air_contents = new
	air_contents.temperature = T20C
	air_contents.volume = volume
	refresh_pressure_limit()

	if(gas_type)
		fill_to_pressure(gas_type)

	QUEUE_SMOOTH(src)
	QUEUE_SMOOTH_NEIGHBORS(src)

	// Mapped in tanks should automatically connect to adjacent pipenets in the direction set in dir
	if(mapload)
		initialize_directions = dir

	return INITIALIZE_HINT_LATELOAD

// We late initialize here so all stationary tanks have time to set up their
// initial gas mixes and signal registrations.
/obj/machinery/atmospherics/components/tank/LateInitialize()
	. = ..()
	GetMergeGroup(merger_id, merger_typecache)

/obj/machinery/atmospherics/components/tank/Destroy()
	QUEUE_SMOOTH_NEIGHBORS(src)
	return ..()

/obj/machinery/atmospherics/components/tank/examine(mob/user, thats)
	. = ..()
	var/wrench_hint = EXAMINE_HINT("wrench")
	if(!initialize_directions)
		. += span_notice("A pipe port can be opened with a [wrench_hint].")
	else
		. += span_notice("The pipe port can be moved or closed with a [wrench_hint].")
	. += span_notice("A holographic sticker on it says that its maximum safe pressure is: [siunit_pressure(max_pressure, 0)].")

/obj/machinery/atmospherics/components/tank/set_custom_materials(list/materials, multiplier)
	. = ..()
	refresh_pressure_limit()

/// Recalculates pressure based on the current max integrity compared to original
/obj/machinery/atmospherics/components/tank/proc/refresh_pressure_limit()
	var/max_pressure_multiplier = max_integrity / initial(max_integrity)
	max_pressure = max_pressure_multiplier * initial(max_pressure)

/// Fills the tank to the maximum safe pressure.
/// Safety margin is a multiplier for the cap for the purpose of this proc so it doesn't have to be filled completely.
/obj/machinery/atmospherics/components/tank/proc/fill_to_pressure(gastype, safety_margin = 0.5)
	var/pressure_limit = max_pressure * safety_margin

	var/moles_to_add = (pressure_limit * air_contents.volume) / (R_IDEAL_GAS_EQUATION * air_contents.temperature)
	air_contents.assert_gas(gastype)
	air_contents.gases[gastype][MOLES] += moles_to_add
	air_contents.archive()

/obj/machinery/atmospherics/components/tank/process_atmos()
	if(air_contents.react(src))
		update_parents()

	if(air_contents.return_pressure() > max_pressure)
		take_damage(0.1, BRUTE, sound_effect = FALSE)
		if(prob(40))
			playsound(src, pick(breaking_sounds), 30, vary = TRUE)

	refresh_window()

///////////////////////////////////////////////////////////////////
// Pipenet stuff

/obj/machinery/atmospherics/components/tank/return_analyzable_air()
	return air_contents

/obj/machinery/atmospherics/components/tank/return_airs_for_reconcilation(datum/pipeline/requester)
	. = ..()
	if(!air_contents)
		return
	. += air_contents

/obj/machinery/atmospherics/components/tank/return_pipenets_for_reconcilation(datum/pipeline/requester)
	. = ..()
	var/datum/merger/merge_group = GetMergeGroup(merger_id, merger_typecache)
	for(var/obj/machinery/atmospherics/components/tank/tank as anything in merge_group.members)
		. += tank.parents

/obj/machinery/atmospherics/components/tank/proc/toggle_side_port(new_dir)
	if(initialize_directions & new_dir)
		initialize_directions &= ~new_dir
	else
		initialize_directions |= new_dir

	for(var/i in 1 to length(nodes))
		var/obj/machinery/atmospherics/components/node = nodes[i]
		if(!node)
			continue
		if(src in node.nodes)
			node.disconnect(src)
		nodes[i] = null
		if(parents[i])
			nullify_pipenet(parents[i])

	atmos_init()

	for(var/obj/machinery/atmospherics/components/node as anything in nodes)
		if(!node)
			continue
		node.atmos_init()
		node.add_member(src)
	SSair.add_to_rebuild_queue(src)

	update_parents()

///////////////////////////////////////////////////////////////////
// Merger handling

/obj/machinery/atmospherics/components/tank/proc/merger_adding(obj/machinery/atmospherics/components/tank/us, datum/merger/new_merger)
	SIGNAL_HANDLER
	if(new_merger.id != merger_id)
		return
	RegisterSignal(new_merger, COMSIG_MERGER_REFRESH_COMPLETE, .proc/merger_refresh_complete)

/obj/machinery/atmospherics/components/tank/proc/merger_removing(obj/machinery/atmospherics/components/tank/us, datum/merger/old_merger)
	SIGNAL_HANDLER
	if(old_merger.id != merger_id)
		return
	UnregisterSignal(old_merger, COMSIG_MERGER_REFRESH_COMPLETE)

/// Handles the combined gas tank for the entire merger group, only the origin tank actualy runs this.
/obj/machinery/atmospherics/components/tank/proc/merger_refresh_complete(datum/merger/merger, list/leaving_members, list/joining_members)
	SIGNAL_HANDLER
	if(merger.origin != src)
		return
	var/shares = length(merger.members) + length(leaving_members) - length(joining_members)
	for(var/obj/machinery/atmospherics/components/tank/leaver as anything in leaving_members)
		var/datum/gas_mixture/gas_share = air_contents.remove_ratio(1 / shares--)
		air_contents.volume -= leaver.volume
		leaver.air_contents = gas_share
		leaver.update_appearance()

	for(var/obj/machinery/atmospherics/components/tank/joiner as anything in joining_members)
		if(joiner == src)
			continue
		var/datum/gas_mixture/joiner_share = joiner.air_contents
		if(joiner_share)
			air_contents.merge(joiner_share)
		joiner.air_contents = air_contents
		air_contents.volume += joiner.volume
		joiner.update_appearance()

	for(var/dir in GLOB.cardinals)
		if(dir & initialize_directions & merger.members[src])
			toggle_side_port(dir)

///////////////////////////////////////////////////////////////////
// Appearance stuff

/obj/machinery/atmospherics/components/tank/proc/smoothed()
	SIGNAL_HANDLER
	refresh_window()

/obj/machinery/atmospherics/components/tank/update_appearance()
	. = ..()
	refresh_window()

/obj/machinery/atmospherics/components/tank/update_overlays()
	. = ..()
	if(!initialize_directions)
		return
	for(var/dir in GLOB.cardinals)
		if(initialize_directions & dir)
			. += knob_overlays["[dir]"]

/obj/machinery/atmospherics/components/tank/update_greyscale()
	. = ..()
	refresh_window()

/obj/machinery/atmospherics/components/tank/proc/refresh_window()
	cut_overlay(window)

	if(!air_contents)
		window = null
		return

	window = image(icon, icon_state = "window-bg", layer = FLOAT_LAYER)

	var/list/new_underlays = list()
	for(var/obj/effect/overlay/gas/gas as anything in air_contents.return_visuals())
		var/image/new_underlay = image(gas.icon, icon_state = gas.icon_state, layer = FLOAT_LAYER)
		new_underlay.filters = alpha_mask_filter(icon = icon(icon, icon_state = "window-bg"))
		new_underlays += new_underlay

	var/image/foreground = image(icon, icon_state = "window-fg", layer = FLOAT_LAYER)
	foreground.underlays = new_underlays
	window.overlays = list(foreground)

	add_overlay(window)

///////////////////////////////////////////////////////////////////
// Tool interactions

/obj/machinery/atmospherics/components/tank/wrench_act(mob/living/user, obj/item/item)
	. = TRUE
	var/new_dir = get_dir(src, user)

	if(new_dir in GLOB.diagonals)
		return

	item.play_tool_sound(src, 10)
	if(!item.use_tool(src, user, 3 SECONDS))
		return

	toggle_side_port(new_dir)

	item.play_tool_sound(src, 50)

/obj/machinery/atmospherics/components/tank/welder_act(mob/living/user, obj/item/tool)
	. = ..()
	. = TRUE
	if(atom_integrity >= max_integrity)
		return
	if(!tool.tool_start_check(user, amount = 0))
		return
	to_chat(user, span_notice("You begin to repair the cracks in the gas tank..."))
	var/repair_amount = max_integrity / 10
	do
		if(!tool.use_tool(src, user, 2.5 SECONDS, volume = 40))
			return
	while(repair_damage(repair_amount))
	to_chat(user, span_notice("The gas tank has been fully repaired and all cracks sealed."))

/obj/machinery/atmospherics/components/tank/welder_act_secondary(mob/living/user, obj/item/tool)
	. = ..()
	. = TRUE
	to_chat(user, span_notice("You begin cutting open the gas tank..."))
	var/turf/current_location = get_turf(src)
	var/datum/gas_mixture/airmix = current_location.return_air()

	var/time_taken = 4 SECONDS

	if(air_contents.return_pressure() > airmix.return_pressure())
		time_taken *= 2
		to_chat(user, span_warning("The tank seems to be pressurized, are you sure this is a good idea?"))

	if(!tool.use_tool(src, user, time_taken, volume =  60))
		return

	deconstruct(disassembled=TRUE)
	to_chat(user, span_notice("You finish cutting open the sealed gas tank, revealing the innards."))

/obj/machinery/atmospherics/components/tank/deconstruct(disassembled)
	var/turf/location = drop_location()
	. = ..()
	if(!disassembled)
		return
	var/obj/structure/tank_frame/frame = new(location)
	frame.construction_state = TANK_PLATING_UNSECURED
	frame.material_end_product = custom_materials[2].type
	frame.update_appearance()

///////////////////////////////////////////////////////////////////
// Gas tank variants

/obj/machinery/atmospherics/components/tank/air
	name = "pressure tank (Air)"

/obj/machinery/atmospherics/components/tank/air/Initialize(mapload)
	. = ..()
	fill_to_pressure(/datum/gas/oxygen, safety_margin = (O2STANDARD * 0.5))
	fill_to_pressure(/datum/gas/nitrogen, safety_margin = (N2STANDARD * 0.5))

/obj/machinery/atmospherics/components/tank/carbon_dioxide
	gas_type = /datum/gas/carbon_dioxide

/obj/machinery/atmospherics/components/tank/plasma
	gas_type = /datum/gas/plasma

/obj/machinery/atmospherics/components/tank/nitrogen
	gas_type = /datum/gas/nitrogen

/obj/machinery/atmospherics/components/tank/oxygen
	gas_type = /datum/gas/oxygen

/obj/machinery/atmospherics/components/tank/nitrous
	gas_type = /datum/gas/nitrous_oxide

/obj/machinery/atmospherics/components/tank/bz
	gas_type = /datum/gas/bz

/obj/machinery/atmospherics/components/tank/freon
	gas_type = /datum/gas/freon

/obj/machinery/atmospherics/components/tank/halon
	gas_type = /datum/gas/halon

/obj/machinery/atmospherics/components/tank/healium
	gas_type = /datum/gas/healium

/obj/machinery/atmospherics/components/tank/hydrogen
	gas_type = /datum/gas/hydrogen

/obj/machinery/atmospherics/components/tank/hypernoblium
	gas_type = /datum/gas/hypernoblium

/obj/machinery/atmospherics/components/tank/miasma
	gas_type = /datum/gas/miasma

/obj/machinery/atmospherics/components/tank/nitryl
	gas_type = /datum/gas/nitryl

/obj/machinery/atmospherics/components/tank/pluoxium
	gas_type = /datum/gas/pluoxium

/obj/machinery/atmospherics/components/tank/proto_nitrate
	gas_type = /datum/gas/proto_nitrate

/obj/machinery/atmospherics/components/tank/stimulum
	gas_type = /datum/gas/stimulum

/obj/machinery/atmospherics/components/tank/tritium
	gas_type = /datum/gas/tritium

/obj/machinery/atmospherics/components/tank/water_vapor
	gas_type = /datum/gas/water_vapor

/obj/machinery/atmospherics/components/tank/zauker
	gas_type = /datum/gas/zauker

/obj/machinery/atmospherics/components/tank/helium
	gas_type = /datum/gas/helium

/obj/machinery/atmospherics/components/tank/antinoblium
	gas_type = /datum/gas/antinoblium

///////////////////////////////////////////////////////////////////
// Tank Frame Structure

/obj/structure/tank_frame
	icon = 'icons/obj/atmospherics/stationary_canisters.dmi'
	icon_state = "frame"
	anchored = FALSE
	density = TRUE
	custom_materials = list(/datum/material/alloy/plasteel = 4000)
	var/construction_state = TANK_FRAME
	var/datum/material/material_end_product

/obj/structure/tank_frame/examine(mob/user)
	. = ..()
	var/wrenched_hint = EXAMINE_HINT("wrenched")

	if(!anchored)
		. += span_notice("[src] has not been [wrenched_hint] to the floor yet.")
	else
		. += span_notice("[src] is [wrenched_hint] to the floor.")

	switch(construction_state)
		if(TANK_FRAME)
			var/screwed_hint = EXAMINE_HINT("screwed")
			var/plating_hint = EXAMINE_HINT("metal plating")
			. += span_notice("[src] is [screwed_hint] together and now just needs some [plating_hint].")
		if(TANK_PLATING_UNSECURED)
			var/crowbar_hint = EXAMINE_HINT("crowbar")
			var/welder_hint = EXAMINE_HINT("welder")
			. += span_notice("The plating has been firmly attached and would need a [crowbar_hint] to detach, but still needs to be sealed by a [welder_hint].")

/obj/structure/tank_frame/deconstruct(disassembled)
	if(disassembled)
		for(var/datum/material/mat as anything in custom_materials)
			new mat.sheet_type(drop_location())
	return ..()

/obj/structure/tank_frame/update_icon(updates)
	. = ..()
	switch(construction_state)
		if(TANK_FRAME)
			icon_state = "frame"
		if(TANK_PLATING_UNSECURED)
			icon_state = "plated_frame"

/obj/structure/tank_frame/attackby(obj/item/item, mob/living/user, params)
	if(construction_state == TANK_FRAME && istype(item, /obj/item/stack) && add_plating(user, item))
		return
	return ..()

/obj/structure/tank_frame/wrench_act(mob/living/user, obj/item/tool)
	. = ..()
	return default_unfasten_wrench(user, tool, 0.5 SECONDS)

/obj/structure/tank_frame/screwdriver_act_secondary(mob/living/user, obj/item/tool)
	. = ..()
	if(construction_state != TANK_FRAME)
		return
	. = TRUE
	to_chat(user, span_notice("You begin taking apart [src]."))
	if(!tool.use_tool(src, user, 1 SECONDS))
		return
	deconstruct(TRUE)
	to_chat(user, span_notice("[src] has been taken apart."))

/obj/structure/tank_frame/proc/add_plating(mob/living/user, obj/item/stack/stack)
	. = FALSE
	var/datum/material/stack_mat = GET_MATERIAL_REF(stack.material_type)
	if(!(MAT_CATEGORY_RIGID in stack_mat.categories))
		to_chat(user, span_notice("This material doesn't seem rigid enough to hold the shape of a tank..."))
		return

	. = TRUE
	to_chat(user, span_notice("You begin adding [stack] to [src]..."))
	if(!stack.use_tool(src, user, 3 SECONDS))
		return
	if(!stack.use(20))
		var/amount_more
		switch(stack.amount)
			if(0) // Wat?
				amount_more = "any at all"
			if(1 to 4)
				amount_more = "a lot more"
			if(5 to 9)
				amount_more = "about four times as much"
			if(10 to 15)
				amount_more = "about twice as much"
			if(16 to 20)
				amount_more = "just a bit more"
			else
				amount_more = "an indeterminate amount more"
		to_chat(user, span_notice("You don't have enough [stack] to add all the plating. Maybe [amount_more]."))
		return

	material_end_product = stack_mat
	construction_state = TANK_PLATING_UNSECURED
	update_appearance()
	to_chat(user, span_notice("You finish attaching [stack] to [src]."))

/obj/structure/tank_frame/crowbar_act_secondary(mob/living/user, obj/item/tool)
	. = ..()
	if(construction_state != TANK_PLATING_UNSECURED)
		return
	. = TRUE
	to_chat(user, span_notice("You start prying off the outer plating..."))
	if(!tool.use_tool(src, user, 2 SECONDS))
		return
	construction_state = TANK_FRAME
	new material_end_product.sheet_type(drop_location(), 20)
	material_end_product = null
	update_appearance()

/obj/structure/tank_frame/welder_act(mob/living/user, obj/item/tool)
	. = ..()
	if(construction_state != TANK_PLATING_UNSECURED)
		return
	. = TRUE
	if(!anchored)
		to_chat(user, span_notice("You need to <b>wrench</b> [src] to the floor before finishing."))
		return
	if(!tool.tool_start_check(user, amount = 0))
		return
	to_chat(user, span_notice("You begin sealing the outer plating with the welder..."))
	if(!tool.use_tool(src, user, 2 SECONDS, volume = 60))
		return

	var/turf/build_location = drop_location()
	if(!isturf(build_location))
		return
	var/obj/machinery/atmospherics/components/tank/new_tank = new(build_location)
	var/list/new_custom_materials = list()
	new_custom_materials[material_end_product] = 20000
	new_tank.set_custom_materials(new_custom_materials)
	new_tank.on_construction(new_tank.pipe_color, new_tank.piping_layer)
	to_chat(user, span_notice("[new_tank] has been sealed and is ready to accept gases."))
	qdel(src)


code:
/obj/machinery/atmospherics/components/trinary/filter
	icon_state = "filter_off-0"
	density = FALSE

	name = "gas filter"
	desc = "Very useful for filtering gasses."

	can_unwrench = TRUE
	construction_type = /obj/item/pipe/trinary/flippable
	pipe_state = "filter"

	///Rate of transfer of the gases to the outputs
	var/transfer_rate = MAX_TRANSFER_RATE
	///What gases are we filtering, by typepath
	var/list/filter_type = list()
	///Frequency id for connecting to the NTNet
	var/frequency = 0
	///Reference to the radio datum
	var/datum/radio_frequency/radio_connection

/obj/machinery/atmospherics/components/trinary/filter/CtrlClick(mob/user)
	if(can_interact(user))
		on = !on
		investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
		update_appearance()
	return ..()

/obj/machinery/atmospherics/components/trinary/filter/AltClick(mob/user)
	if(can_interact(user))
		transfer_rate = MAX_TRANSFER_RATE
		investigate_log("was set to [transfer_rate] L/s by [key_name(user)]", INVESTIGATE_ATMOS)
		balloon_alert(user, "volume output set to [transfer_rate] L/s")
		update_appearance()
	return ..()

/obj/machinery/atmospherics/components/trinary/filter/proc/set_frequency(new_frequency)
	SSradio.remove_object(src, frequency)
	frequency = new_frequency
	if(frequency)
		radio_connection = SSradio.add_object(src, frequency, RADIO_ATMOSIA)

/obj/machinery/atmospherics/components/trinary/filter/Destroy()
	SSradio.remove_object(src,frequency)
	return ..()

/obj/machinery/atmospherics/components/trinary/filter/update_overlays()
	. = ..()
	for(var/direction in GLOB.cardinals)
		if(!(direction & initialize_directions))
			continue

		. += get_pipe_image(icon, "cap", direction, pipe_color, piping_layer, TRUE)

/obj/machinery/atmospherics/components/trinary/filter/update_icon_nopipes()
	var/on_state = on && nodes[1] && nodes[2] && nodes[3] && is_operational
	icon_state = "filter_[on_state ? "on" : "off"]-[set_overlay_offset(piping_layer)][flipped ? "_f" : ""]"

/obj/machinery/atmospherics/components/trinary/filter/process_atmos()
	..()
	if(!on || !(nodes[1] && nodes[2] && nodes[3]) || !is_operational)
		return

	//Early return
	var/datum/gas_mixture/air1 = airs[1]
	if(!air1 || air1.temperature <= 0)
		return

	var/datum/gas_mixture/air2 = airs[2]
	var/datum/gas_mixture/air3 = airs[3]

	var/transfer_ratio = transfer_rate / air1.volume

	if(transfer_ratio <= 0)
		return

	// Attempt to transfer the gas.

	// If the main output is full, we try to send filtered output to the side port (air2).
	// If the side output is full, we try to send the non-filtered gases to the main output port (air3).
	// Any gas that can't be moved due to its destination being too full is sent back to the input (air1).

	var/side_output_full = air2.return_pressure() >= MAX_OUTPUT_PRESSURE
	var/main_output_full = air3.return_pressure() >= MAX_OUTPUT_PRESSURE

	// If both output ports are full, there's nothing we can do. Don't bother removing anything from the input.
	if (side_output_full && main_output_full)
		return

	var/datum/gas_mixture/removed = air1.remove_ratio(transfer_ratio)

	if(!removed || !removed.total_moles())
		return

	var/filtering = TRUE
	if(!filter_type.len)
		filtering = FALSE

	// Process if we have a filter set.
	// If no filter is set, we just try to forward everything to air3 to avoid gas being outright lost.
	if(filtering)
		var/datum/gas_mixture/filtered_out = new

		for(var/gas in removed.gases & filter_type)
			var/datum/gas_mixture/removing = removed.remove_specific_ratio(gas, 1)
			if(removing)
				filtered_out.merge(removing)
		// Send things to the side output if we can, return them to the input if we can't.
		// This means that other gases continue to flow to the main output if the side output is blocked.
		if (side_output_full)
			air1.merge(filtered_out)
		else
			air2.merge(filtered_out)
		// Make sure we don't send any now-empty gas entries to the main output
		removed.garbage_collect()

	// Send things to the main output if we can, return them to the input if we can't.
	// This lets filtered gases continue to flow to the side output in a manner consistent with the main output behavior.
	if (main_output_full)
		air1.merge(removed)
	else
		air3.merge(removed)

	update_parents()

/obj/machinery/atmospherics/components/trinary/filter/atmos_init()
	set_frequency(frequency)
	return ..()

/obj/machinery/atmospherics/components/trinary/filter/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "AtmosFilter", name)
		ui.open()

/obj/machinery/atmospherics/components/trinary/filter/ui_data()
	var/data = list()
	data["on"] = on
	data["rate"] = round(transfer_rate)
	data["max_rate"] = round(MAX_TRANSFER_RATE)

	data["filter_types"] = list()
	for(var/path in GLOB.meta_gas_info)
		var/list/gas = GLOB.meta_gas_info[path]
		data["filter_types"] += list(list("name" = gas[META_GAS_NAME], "gas_id" = gas[META_GAS_ID], "enabled" = (path in filter_type)))

	return data

/obj/machinery/atmospherics/components/trinary/filter/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("power")
			on = !on
			investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", INVESTIGATE_ATMOS)
			. = TRUE
		if("rate")
			var/rate = params["rate"]
			if(rate == "max")
				rate = MAX_TRANSFER_RATE
				. = TRUE
			else if(text2num(rate) != null)
				rate = text2num(rate)
				. = TRUE
			if(.)
				transfer_rate = clamp(rate, 0, MAX_TRANSFER_RATE)
				investigate_log("was set to [transfer_rate] L/s by [key_name(usr)]", INVESTIGATE_ATMOS)
		if("toggle_filter")
			if(!gas_id2path(params["val"]))
				return TRUE
			filter_type ^= gas_id2path(params["val"])
			var/change
			if(gas_id2path(params["val"]) in filter_type)
				change = "added"
			else
				change = "removed"
			var/gas_name = GLOB.meta_gas_info[gas_id2path(params["val"])][META_GAS_NAME]
			investigate_log("[key_name(usr)] [change] [gas_name] from the filter type.", INVESTIGATE_ATMOS)
			. = TRUE
	update_appearance()

/obj/machinery/atmospherics/components/trinary/filter/can_unwrench(mob/user)
	. = ..()
	if(. && on && is_operational)
		to_chat(user, span_warning("You cannot unwrench [src], turn it off first!"))
		return FALSE

// mapping

/obj/machinery/atmospherics/components/trinary/filter/layer2
	piping_layer = 2
	icon_state = "filter_off_map-2"
/obj/machinery/atmospherics/components/trinary/filter/layer4
	piping_layer = 4
	icon_state = "filter_off_map-4"

/obj/machinery/atmospherics/components/trinary/filter/on
	on = TRUE
	icon_state = "filter_on-0"

/obj/machinery/atmospherics/components/trinary/filter/on/layer2
	piping_layer = 2
	icon_state = "filter_on_map-2"
/obj/machinery/atmospherics/components/trinary/filter/on/layer4
	piping_layer = 4
	icon_state = "filter_on_map-4"

/obj/machinery/atmospherics/components/trinary/filter/flipped
	icon_state = "filter_off-0_f"
	flipped = TRUE

/obj/machinery/atmospherics/components/trinary/filter/flipped/layer2
	piping_layer = 2
	icon_state = "filter_off_f_map-2"
/obj/machinery/atmospherics/components/trinary/filter/flipped/layer4
	piping_layer = 4
	icon_state = "filter_off_f_map-4"

/obj/machinery/atmospherics/components/trinary/filter/flipped/on
	on = TRUE
	icon_state = "filter_on-0_f"

/obj/machinery/atmospherics/components/trinary/filter/flipped/on/layer2
	piping_layer = 2
	icon_state = "filter_on_f_map-2"
/obj/machinery/atmospherics/components/trinary/filter/flipped/on/layer4
	piping_layer = 4
	icon_state = "filter_on_f_map-4"

/obj/machinery/atmospherics/components/trinary/filter/atmos //Used for atmos waste loops
	on = TRUE
	icon_state = "filter_on-0"
/obj/machinery/atmospherics/components/trinary/filter/atmos/n2
	name = "nitrogen filter"
	filter_type = list(/datum/gas/nitrogen)
/obj/machinery/atmospherics/components/trinary/filter/atmos/o2
	name = "oxygen filter"
	filter_type = list(/datum/gas/oxygen)
/obj/machinery/atmospherics/components/trinary/filter/atmos/co2
	name = "carbon dioxide filter"
	filter_type = list(/datum/gas/carbon_dioxide)
/obj/machinery/atmospherics/components/trinary/filter/atmos/n2o
	name = "nitrous oxide filter"
	filter_type = list(/datum/gas/nitrous_oxide)
/obj/machinery/atmospherics/components/trinary/filter/atmos/plasma
	name = "plasma filter"
	filter_type = list(/datum/gas/plasma)
/obj/machinery/atmospherics/components/trinary/filter/atmos/bz
	name = "bz filter"
	filter_type = list(/datum/gas/bz)
/obj/machinery/atmospherics/components/trinary/filter/atmos/freon
	name = "freon filter"
	filter_type = list(/datum/gas/freon)
/obj/machinery/atmospherics/components/trinary/filter/atmos/halon
	name = "halon filter"
	filter_type = list(/datum/gas/halon)
/obj/machinery/atmospherics/components/trinary/filter/atmos/healium
	name = "healium filter"
	filter_type = list(/datum/gas/healium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/h2
	name = "hydrogen filter"
	filter_type = list(/datum/gas/hydrogen)
/obj/machinery/atmospherics/components/trinary/filter/atmos/hypernoblium
	name = "hypernoblium filter"
	filter_type = list(/datum/gas/hypernoblium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/miasma
	name = "miasma filter"
	filter_type = list(/datum/gas/miasma)
/obj/machinery/atmospherics/components/trinary/filter/atmos/no2
	name = "nitryl filter"
	filter_type = list(/datum/gas/nitryl)
/obj/machinery/atmospherics/components/trinary/filter/atmos/pluoxium
	name = "pluoxium filter"
	filter_type = list(/datum/gas/pluoxium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/proto_nitrate
	name = "proto-nitrate filter"
	filter_type = list(/datum/gas/proto_nitrate)
/obj/machinery/atmospherics/components/trinary/filter/atmos/stimulum
	name = "stimulum filter"
	filter_type = list(/datum/gas/stimulum)
/obj/machinery/atmospherics/components/trinary/filter/atmos/tritium
	name = "tritium filter"
	filter_type = list(/datum/gas/tritium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/h2o
	name = "water vapor filter"
	filter_type = list(/datum/gas/water_vapor)
/obj/machinery/atmospherics/components/trinary/filter/atmos/zauker
	name = "zauker filter"
	filter_type = list(/datum/gas/zauker)

/obj/machinery/atmospherics/components/trinary/filter/atmos/helium
	name = "helium filter"
	filter_type = list(/datum/gas/helium)

/obj/machinery/atmospherics/components/trinary/filter/atmos/antinoblium
	name = "antinoblium filter"
	filter_type = list(/datum/gas/antinoblium)

/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped //This feels wrong, I know
	icon_state = "filter_on-0_f"
	flipped = TRUE
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/n2
	name = "nitrogen filter"
	filter_type = list(/datum/gas/nitrogen)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/o2
	name = "oxygen filter"
	filter_type = list(/datum/gas/oxygen)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/co2
	name = "carbon dioxide filter"
	filter_type = list(/datum/gas/carbon_dioxide)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/n2o
	name = "nitrous oxide filter"
	filter_type = list(/datum/gas/nitrous_oxide)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/plasma
	name = "plasma filter"
	filter_type = list(/datum/gas/plasma)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/bz
	name = "bz filter"
	filter_type = list(/datum/gas/bz)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/freon
	name = "freon filter"
	filter_type = list(/datum/gas/freon)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/halon
	name = "halon filter"
	filter_type = list(/datum/gas/halon)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/healium
	name = "healium filter"
	filter_type = list(/datum/gas/healium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/h2
	name = "hydrogen filter"
	filter_type = list(/datum/gas/hydrogen)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/hypernoblium
	name = "hypernoblium filter"
	filter_type = list(/datum/gas/hypernoblium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/miasma
	name = "miasma filter"
	filter_type = list(/datum/gas/miasma)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/no2
	name = "nitryl filter"
	filter_type = list(/datum/gas/nitryl)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/pluoxium
	name = "pluoxium filter"
	filter_type = list(/datum/gas/pluoxium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/proto_nitrate
	name = "proto-nitrate filter"
	filter_type = list(/datum/gas/proto_nitrate)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/stimulum
	name = "stimulum filter"
	filter_type = list(/datum/gas/stimulum)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/tritium
	name = "tritium filter"
	filter_type = list(/datum/gas/tritium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/h2o
	name = "water vapor filter"
	filter_type = list(/datum/gas/water_vapor)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/zauker
	name = "zauker filter"
	filter_type = list(/datum/gas/zauker)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/helium
	name = "helium filter"
	filter_type = list(/datum/gas/helium)
/obj/machinery/atmospherics/components/trinary/filter/atmos/flipped/antinoblium
	name = "antinoblium filter"
	filter_type = list(/datum/gas/antinoblium)

// These two filter types have critical_machine flagged to on and thus causes the area they are in to be exempt from the Grid Check event.

/obj/machinery/atmospherics/components/trinary/filter/critical
	critical_machine = TRUE

/obj/machinery/atmospherics/components/trinary/filter/flipped/critical
	critical_machine = TRUE

repo_name: AnalWerewolf/sundown

license: mit


code:
/obj/machinery/atmospherics/components/trinary/mixer
	icon_state = "mixer_off-0"
	density = FALSE

	name = "gas mixer"
	desc = "Very useful for mixing gasses."

	can_unwrench = TRUE
	construction_type = /obj/item/pipe/trinary/flippable
	pipe_state = "mixer"

	///Output pressure target
	var/target_pressure = ONE_ATMOSPHERE
	///Ratio between the node 1 and 2, determines the amount of gas transfered, sums up to 1
	var/node1_concentration = 0.5
	///Ratio between the node 1 and 2, determines the amount of gas transfered, sums up to 1
	var/node2_concentration = 0.5
	//node 3 is the outlet, nodes 1 & 2 are intakes

/obj/machinery/atmospherics/components/trinary/mixer/CtrlClick(mob/user)
	if(can_interact(user))
		on = !on
		investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
		update_appearance()
	return ..()

/obj/machinery/atmospherics/components/trinary/mixer/AltClick(mob/user)
	if(can_interact(user))
		target_pressure = MAX_OUTPUT_PRESSURE
		investigate_log("was set to [target_pressure] kPa by [key_name(user)]", INVESTIGATE_ATMOS)
		balloon_alert(user, "pressure output on set to [target_pressure] kPa")
		update_appearance()
	return ..()

/obj/machinery/atmospherics/components/trinary/mixer/update_overlays()
	. = ..()
	for(var/direction in GLOB.cardinals)
		if(!(direction & initialize_directions))
			continue

		. += get_pipe_image(icon, "cap", direction, pipe_color, piping_layer, TRUE)

/obj/machinery/atmospherics/components/trinary/mixer/update_icon_nopipes()
	var/on_state = on && nodes[1] && nodes[2] && nodes[3] && is_operational
	icon_state = "mixer_[on_state ? "on" : "off"]-[set_overlay_offset(piping_layer)][flipped ? "_f" : ""]"

/obj/machinery/atmospherics/components/trinary/mixer/New()
	..()
	var/datum/gas_mixture/air3 = airs[3]
	air3.volume = 300
	airs[3] = air3

/obj/machinery/atmospherics/components/trinary/mixer/process_atmos()
	..()
	if(!on || !(nodes[1] && nodes[2] && nodes[3]) && !is_operational)
		return

	//Get those gases, mah boiiii
	var/datum/gas_mixture/air1 = airs[1]
	var/datum/gas_mixture/air2 = airs[2]

	if(!air1 || !air2)
		return

	var/datum/gas_mixture/air3 = airs[3]

	var/output_starting_pressure = air3.return_pressure()

	if(output_starting_pressure >= target_pressure)
		//No need to mix if target is already full!
		return

	//Calculate necessary moles to transfer using PV=nRT
	var/general_transfer = (target_pressure - output_starting_pressure) * air3.volume / R_IDEAL_GAS_EQUATION

	var/transfer_moles1 = air1.temperature ? node1_concentration * general_transfer / air1.temperature : 0
	var/transfer_moles2 = air2.temperature ? node2_concentration * general_transfer / air2.temperature : 0

	var/air1_moles = air1.total_moles()
	var/air2_moles = air2.total_moles()

	if(!node2_concentration)
		if(air1.temperature <= 0)
			return
		transfer_moles1 = min(transfer_moles1, air1_moles)
		transfer_moles2 = 0
	else if(!node1_concentration)
		if(air2.temperature <= 0)
			return
		transfer_moles2 = min(transfer_moles2, air2_moles)
		transfer_moles1 = 0
	else
		if(air1.temperature <= 0 || air2.temperature <= 0)
			return
		if((transfer_moles2 <= 0) || (transfer_moles1 <= 0))
			return
		if((air1_moles < transfer_moles1) || (air2_moles < transfer_moles2))
			var/ratio = 0
			ratio = min(air1_moles / transfer_moles1, air2_moles / transfer_moles2)
			transfer_moles1 *= ratio
			transfer_moles2 *= ratio

	//Actually transfer the gas

	if(transfer_moles1)
		var/datum/gas_mixture/removed1 = air1.remove(transfer_moles1)
		air3.merge(removed1)
		var/datum/pipeline/parent1 = parents[1]
		parent1.update = TRUE

	if(transfer_moles2)
		var/datum/gas_mixture/removed2 = air2.remove(transfer_moles2)
		air3.merge(removed2)
		var/datum/pipeline/parent2 = parents[2]
		parent2.update = TRUE

	var/datum/pipeline/parent3 = parents[3]
	parent3.update = TRUE

/obj/machinery/atmospherics/components/trinary/mixer/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "AtmosMixer", name)
		ui.open()

/obj/machinery/atmospherics/components/trinary/mixer/ui_data()
	var/data = list()
	data["on"] = on
	data["set_pressure"] = round(target_pressure)
	data["max_pressure"] = round(MAX_OUTPUT_PRESSURE)
	data["node1_concentration"] = round(node1_concentration*100, 1)
	data["node2_concentration"] = round(node2_concentration*100, 1)
	return data

/obj/machinery/atmospherics/components/trinary/mixer/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("power")
			on = !on
			investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", INVESTIGATE_ATMOS)
			. = TRUE
		if("pressure")
			var/pressure = params["pressure"]
			if(pressure == "max")
				pressure = MAX_OUTPUT_PRESSURE
				. = TRUE
			else if(text2num(pressure) != null)
				pressure = text2num(pressure)
				. = TRUE
			if(.)
				target_pressure = clamp(pressure, 0, MAX_OUTPUT_PRESSURE)
				investigate_log("was set to [target_pressure] kPa by [key_name(usr)]", INVESTIGATE_ATMOS)
		if("node1")
			var/value = text2num(params["concentration"])
			adjust_node1_value(value)
			investigate_log("was set to [node1_concentration] % on node 1 by [key_name(usr)]", INVESTIGATE_ATMOS)
			. = TRUE
		if("node2")
			var/value = text2num(params["concentration"])
			adjust_node1_value(100 - value)
			investigate_log("was set to [node2_concentration] % on node 2 by [key_name(usr)]", INVESTIGATE_ATMOS)
			. = TRUE
	update_appearance()

/obj/machinery/atmospherics/components/trinary/mixer/proc/adjust_node1_value(newValue)
	node1_concentration = newValue / 100
	node2_concentration = 1 - node1_concentration

/obj/machinery/atmospherics/components/trinary/mixer/can_unwrench(mob/user)
	. = ..()
	if(. && on && is_operational)
		to_chat(user, span_warning("You cannot unwrench [src], turn it off first!"))
		return FALSE

// mapping

/obj/machinery/atmospherics/components/trinary/mixer/layer2
	piping_layer = 2
	icon_state = "mixer_off_map-2"
/obj/machinery/atmospherics/components/trinary/mixer/layer4
	piping_layer = 4
	icon_state = "mixer_off_map-4"

/obj/machinery/atmospherics/components/trinary/mixer/on
	on = TRUE
	icon_state = "mixer_on-0"

/obj/machinery/atmospherics/components/trinary/mixer/on/layer2
	piping_layer = 2
	icon_state = "mixer_on_map-2"
/obj/machinery/atmospherics/components/trinary/mixer/on/layer4
	piping_layer = 4
	icon_state = "mixer_on_map-4"

/obj/machinery/atmospherics/components/trinary/mixer/flipped
	icon_state = "mixer_off-0_f"
	flipped = TRUE

/obj/machinery/atmospherics/components/trinary/mixer/flipped/layer2
	piping_layer = 2
	icon_state = "mixer_off_f_map-2"
/obj/machinery/atmospherics/components/trinary/mixer/flipped/layer4
	piping_layer = 4
	icon_state = "mixer_off_f_map-4"

/obj/machinery/atmospherics/components/trinary/mixer/flipped/on
	on = TRUE
	icon_state = "mixer_on-0_f"

/obj/machinery/atmospherics/components/trinary/mixer/flipped/on/layer2
	piping_layer = 2
	icon_state = "mixer_on_f_map-2"
/obj/machinery/atmospherics/components/trinary/mixer/flipped/on/layer4
	piping_layer = 4
	icon_state = "mixer_on_f_map-4"

/obj/machinery/atmospherics/components/trinary/mixer/airmix //For standard airmix to distro
	name = "air mixer"
	icon_state = "mixer_on-0"
	node1_concentration = N2STANDARD
	node2_concentration = O2STANDARD
	target_pressure = MAX_OUTPUT_PRESSURE
	on = TRUE

/obj/machinery/atmospherics/components/trinary/mixer/airmix/inverse
	node1_concentration = O2STANDARD
	node2_concentration = N2STANDARD

/obj/machinery/atmospherics/components/trinary/mixer/airmix/flipped
	icon_state = "mixer_on-0_f"
	flipped = TRUE

/obj/machinery/atmospherics/components/trinary/mixer/airmix/flipped/inverse
	node1_concentration = O2STANDARD
	node2_concentration = N2STANDARD


code:
/obj/machinery/atmospherics/components/trinary
	icon = 'icons/obj/atmospherics/components/trinary_devices.dmi'
	dir = SOUTH
	initialize_directions = SOUTH|NORTH|WEST
	use_power = IDLE_POWER_USE
	device_type = TRINARY
	layer = GAS_FILTER_LAYER
	pipe_flags = PIPING_ONE_PER_TURF
	vent_movement = NONE

	///Flips the node connections so that the first and third ports are swapped
	var/flipped = FALSE

/obj/machinery/atmospherics/components/trinary/set_init_directions()
	switch(dir)
		if(NORTH)
			initialize_directions = EAST|NORTH|SOUTH
		if(SOUTH)
			initialize_directions = SOUTH|WEST|NORTH
		if(EAST)
			initialize_directions = EAST|WEST|SOUTH
		if(WEST)
			initialize_directions = WEST|NORTH|EAST

/*
Housekeeping and pipe network stuff
*/

/obj/machinery/atmospherics/components/trinary/get_node_connects()

	//Mixer:
	//1 and 2 is input
	//Node 3 is output
	//If we flip the mixer, 1 and 3 shall exchange positions

	//Filter:
	//Node 1 is input
	//Node 2 is filtered output
	//Node 3 is rest output
	//If we flip the filter, 1 and 3 shall exchange positions

	var/node1_connect = turn(dir, -180)
	var/node2_connect = turn(dir, -90)
	var/node3_connect = dir

	if(flipped)
		node1_connect = turn(node1_connect, 180)
		node3_connect = turn(node3_connect, 180)

	return list(node1_connect, node2_connect, node3_connect)

/obj/machinery/atmospherics/components/trinary/proc/set_overlay_offset(pipe_layer)
	switch(pipe_layer)
		if(1)
			return 1
		if(5)
			return 5
		else
			return 0


code:
/obj/machinery/atmospherics/components/unary/bluespace_sender
	icon = 'icons/obj/atmospherics/components/bluespace_gas_selling.dmi'
	icon_state = "bluespace_sender_off"
	name = "Bluespace Gas Sender"
	desc = "Sends gases to the bluespace network to be shared with the connected vendors, who knows what's beyond!"

	density = TRUE
	max_integrity = 300
	armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 100, BOMB = 0, BIO = 100, RAD = 100, FIRE = 80, ACID = 30)
	layer = OBJ_LAYER
	circuit = /obj/item/circuitboard/machine/bluespace_sender
	pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY

	///Base icon name for updating the appearance
	var/base_icon = "bluespace_sender"
	///Gas mixture containing the inserted gases and that is connected to the vendors
	var/datum/gas_mixture/bluespace_network
	///Rate of gas transfer inside the network (from 0 to 1)
	var/gas_transfer_rate = 0.5
	///A base price for each and every gases, in case you don't want to change them
	var/list/base_prices = list()
	///List storing all the vendors connected to the machine
	var/list/vendors
	///Amount of credits gained from each vendor
	var/credits_gained = 0

/obj/machinery/atmospherics/components/unary/bluespace_sender/Initialize(mapload)
	. = ..()
	initialize_directions = dir
	bluespace_network = new
	for(var/gas_id in GLOB.meta_gas_info)
		bluespace_network.assert_gas(gas_id)
	for(var/gas_id in GLOB.meta_gas_info)
		var/datum/gas/gas = gas_id
		base_prices[gas_id] = initial(gas.base_value)

	update_appearance()

/obj/machinery/atmospherics/components/unary/bluespace_sender/Destroy()
	if(bluespace_network.total_moles())
		var/turf/local_turf = get_turf(src)
		local_turf.assume_air(bluespace_network)
	return ..()

/obj/machinery/atmospherics/components/unary/bluespace_sender/update_icon_state()
	if(panel_open)
		icon_state = "[base_icon]_open"
		return ..()
	if(on && is_operational)
		icon_state = "[base_icon]_on"
		return ..()
	icon_state =  "[base_icon]_off"
	return ..()

/obj/machinery/atmospherics/components/unary/bluespace_sender/update_overlays()
	. = ..()
	. += get_pipe_image(icon, "pipe", dir, , piping_layer)
	if(showpipe)
		. += get_pipe_image(icon, "pipe", initialize_directions)

/obj/machinery/atmospherics/components/unary/bluespace_sender/process_atmos()
	if(!is_operational || !on || !nodes[1])  //if it has no power or its switched off, dont process atmos
		return

	var/datum/gas_mixture/content = airs[1]
	var/datum/gas_mixture/remove = content.remove_ratio(gas_transfer_rate)
	bluespace_network.merge(remove)
	bluespace_network.temperature = T20C
	update_parents()

/obj/machinery/atmospherics/components/unary/bluespace_sender/attackby(obj/item/item, mob/user, params)
	if(!on)
		if(default_deconstruction_screwdriver(user, "[base_icon]_open", "[base_icon]_off", item))
			update_appearance()
			return
	if(default_change_direction_wrench(user, item))
		return
	if(item.tool_behaviour == TOOL_CROWBAR && panel_open && bluespace_network.total_moles() > 0)
		say("WARNING - Bluespace network can contain hazardous gases, deconstruct with caution!")
		if(!do_after(user, 3 SECONDS, src))
			return
	if(default_deconstruction_crowbar(item))
		return
	return ..()

/obj/machinery/atmospherics/components/unary/bluespace_sender/default_change_direction_wrench(mob/user, obj/item/item)
	if(!..())
		return FALSE
	set_init_directions()
	var/obj/machinery/atmospherics/node = nodes[1]
	if(node)
		if(src in node.nodes) //Only if it's actually connected. On-pipe version would is one-sided.
			node.disconnect(src)
		nodes[1] = null
	if(parents[1])
		nullify_pipenet(parents[1])

	atmos_init()
	node = nodes[1]
	if(node)
		node.atmos_init()
		node.add_member(src)
	SSair.add_to_rebuild_queue(src)
	return TRUE

/obj/machinery/atmospherics/components/unary/bluespace_sender/multitool_act(mob/living/user, obj/item/item)
	var/obj/item/multitool/multitool = item
	multitool.buffer = src
	to_chat(user, span_notice("You store linkage information in [item]'s buffer."))
	return TRUE

/obj/machinery/atmospherics/components/unary/bluespace_sender/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "BluespaceSender", name)
		ui.open()

/obj/machinery/atmospherics/components/unary/bluespace_sender/ui_data(mob/user)
	var/list/data = list()
	data["on"] = on
	data["gas_transfer_rate"] = gas_transfer_rate
	var/list/bluespace_gasdata = list()
	if(bluespace_network.total_moles())
		for(var/gas_id in bluespace_network.gases)
			bluespace_gasdata.Add(list(list(
			"name" = bluespace_network.gases[gas_id][GAS_META][META_GAS_NAME],
			"id" = bluespace_network.gases[gas_id][GAS_META][META_GAS_ID],
			"amount" = round(bluespace_network.gases[gas_id][MOLES], 0.01),
			"price" = base_prices[gas_id],
			)))
	else
		for(var/gas_id in bluespace_network.gases)
			bluespace_gasdata.Add(list(list(
				"name" = bluespace_network.gases[gas_id][GAS_META][META_GAS_NAME],
				"id" = "",
				"amount" = 0,
				"price" = 0,
				)))
	data["bluespace_network_gases"] = bluespace_gasdata
	var/list/vendors_list = list()
	if(vendors)
		for(var/obj/machinery/bluespace_vendor/vendor in vendors)
			vendors_list.Add(list(list(
				"name" = vendor.name,
				"area" = get_area(vendor),
			)))
	data["vendors_list"] = vendors_list
	data["credits"] = credits_gained
	return data

/obj/machinery/atmospherics/components/unary/bluespace_sender/ui_act(action, params)
	. = ..()
	if(.)
		return

	switch(action)
		if("power")
			on = !on
			investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", INVESTIGATE_ATMOS)
			update_appearance()
			. = TRUE

		if("rate")
			gas_transfer_rate = clamp(params["rate"], 0, 1)
			. = TRUE

		if("price")
			var/gas_type = gas_id2path(params["gas_type"])
			base_prices[gas_type] = clamp(params["gas_price"], 0, 100)
			. = TRUE

		if("retrieve")
			if(bluespace_network.total_moles() > 0)
				var/datum/gas_mixture/remove = bluespace_network.remove(bluespace_network.total_moles())
				airs[1].merge(remove)
				update_parents()
				bluespace_network.garbage_collect()
			. = TRUE


code:
///Max temperature allowed inside the cryotube, should break before reaching this heat
#define MAX_TEMPERATURE 4000
// Multiply factor is used with efficiency to multiply Tx quantity
// Tx quantity is how much volume should be removed from the cell's beaker - multiplied by delta_time
// Throttle Counter Max is how many calls of process() between ones that inject reagents.
// These three defines control how fast and efficient cryo is
#define CRYO_MULTIPLY_FACTOR 25
#define CRYO_TX_QTY 0.5
// The minimum O2 moles in the cryotube before it switches off.
#define CRYO_MIN_GAS_MOLES 5
#define CRYO_BREAKOUT_TIME 30 SECONDS

/// This is a visual helper that shows the occupant inside the cryo cell.
/atom/movable/visual/cryo_occupant
	icon = 'icons/obj/cryogenics.dmi'
	// Must be tall, otherwise the filter will consider this as a 32x32 tile
	// and will crop the head off.
	icon_state = "mask_bg"
	layer = MOB_LAYER + 0.01
	mouse_opacity = MOUSE_OPACITY_TRANSPARENT
	pixel_y = 22
	appearance_flags = KEEP_TOGETHER
	/// The current occupant being presented
	var/mob/living/occupant

/atom/movable/visual/cryo_occupant/Initialize(mapload, obj/machinery/atmospherics/components/unary/cryo_cell/parent)
	. = ..()
	// Alpha masking
	// It will follow this as the animation goes, but that's no problem as the "mask" icon state
	// already accounts for this.
	add_filter("alpha_mask", 1, list("type" = "alpha", "icon" = icon('icons/obj/cryogenics.dmi', "mask"), "y" = -22))
	RegisterSignal(parent, COMSIG_MACHINERY_SET_OCCUPANT, .proc/on_set_occupant)
	RegisterSignal(parent, COMSIG_CRYO_SET_ON, .proc/on_set_on)

/// COMSIG_MACHINERY_SET_OCCUPANT callback
/atom/movable/visual/cryo_occupant/proc/on_set_occupant(datum/source, mob/living/new_occupant)
	SIGNAL_HANDLER

	if(occupant)
		vis_contents -= occupant
		REMOVE_TRAIT(occupant, TRAIT_IMMOBILIZED, CRYO_TRAIT)
		REMOVE_TRAIT(occupant, TRAIT_FORCED_STANDING, CRYO_TRAIT)

	occupant = new_occupant
	if(!occupant)
		return

	occupant.setDir(SOUTH)
	vis_contents += occupant
	pixel_y = 22
	ADD_TRAIT(occupant, TRAIT_IMMOBILIZED, CRYO_TRAIT)
	// Keep them standing! They'll go sideways in the tube when they fall asleep otherwise.
	ADD_TRAIT(occupant, TRAIT_FORCED_STANDING, CRYO_TRAIT)

/// COMSIG_CRYO_SET_ON callback
/atom/movable/visual/cryo_occupant/proc/on_set_on(datum/source, on)
	SIGNAL_HANDLER

	if(on)
		animate(src, pixel_y = 24, time = 20, loop = -1)
		animate(pixel_y = 22, time = 20)
	else
		animate(src)

/// Cryo cell
/obj/machinery/atmospherics/components/unary/cryo_cell
	name = "cryo cell"
	icon = 'icons/obj/cryogenics.dmi'
	icon_state = "pod-off"
	density = TRUE
	max_integrity = 350
	armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 100, BOMB = 0, BIO = 100, RAD = 100, FIRE = 30, ACID = 30)
	layer = MOB_LAYER
	state_open = FALSE
	circuit = /obj/item/circuitboard/machine/cryo_tube
	pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY
	occupant_typecache = list(/mob/living/carbon, /mob/living/simple_animal)
	processing_flags = NONE

	showpipe = FALSE

	var/autoeject = TRUE
	var/volume = 100

	var/efficiency = 1
	var/sleep_factor = 0.00125
	var/unconscious_factor = 0.001
	var/heat_capacity = 20000
	var/conduction_coefficient = 0.3

	var/obj/item/reagent_containers/glass/beaker = null
	var/consume_gas = FALSE

	var/obj/item/radio/radio
	var/radio_key = /obj/item/encryptionkey/headset_med
	var/radio_channel = RADIO_CHANNEL_MEDICAL
	vent_movement = NONE

	/// Visual content - Occupant
	var/atom/movable/visual/cryo_occupant/occupant_vis

	var/message_cooldown
	///Cryo will continue to treat people with 0 damage but existing wounds, but will sound off when damage healing is done in case doctors want to directly treat the wounds instead
	var/treating_wounds = FALSE
	fair_market_price = 10
	payment_department = ACCOUNT_MED

/obj/machinery/atmospherics/components/unary/cryo_cell/Initialize(mapload)
	. = ..()
	initialize_directions = dir
	if(is_operational)
		begin_processing()

	radio = new(src)
	radio.keyslot = new radio_key
	radio.subspace_transmission = TRUE
	radio.canhear_range = 0
	radio.recalculateChannels()

	occupant_vis = new(null, src)
	vis_contents += occupant_vis

/obj/machinery/atmospherics/components/unary/cryo_cell/set_occupant(atom/movable/new_occupant)
	. = ..()
	update_appearance()

/obj/machinery/atmospherics/components/unary/cryo_cell/on_construction()
	..(dir, dir)

/obj/machinery/atmospherics/components/unary/cryo_cell/RefreshParts()
	var/C
	for(var/obj/item/stock_parts/matter_bin/M in component_parts)
		C += M.rating

	efficiency = initial(efficiency) * C
	sleep_factor = initial(sleep_factor) * C
	unconscious_factor = initial(unconscious_factor) * C
	heat_capacity = initial(heat_capacity) / C
	conduction_coefficient = initial(conduction_coefficient) * C

/obj/machinery/atmospherics/components/unary/cryo_cell/examine(mob/user) //this is leaving out everything but efficiency since they follow the same idea of "better beaker, better results"
	. = ..()
	if(in_range(user, src) || isobserver(user))
		. += span_notice("The status display reads: Efficiency at [efficiency*100]%.")

/obj/machinery/atmospherics/components/unary/cryo_cell/Destroy()
	vis_contents.Cut()

	QDEL_NULL(occupant_vis)
	QDEL_NULL(radio)
	QDEL_NULL(beaker)
	///Take the turf the cryotube is on
	var/turf/T = get_turf(src)
	if(T)
		///Take the air composition inside the cryotube
		var/datum/gas_mixture/air1 = airs[1]
		T.assume_air(air1)

	return ..()

/obj/machinery/atmospherics/components/unary/cryo_cell/contents_explosion(severity, target)
	. = ..()
	if(!beaker)
		return

	switch(severity)
		if(EXPLODE_DEVASTATE)
			SSexplosions.high_mov_atom += beaker
		if(EXPLODE_HEAVY)
			SSexplosions.med_mov_atom += beaker
		if(EXPLODE_LIGHT)
			SSexplosions.low_mov_atom += beaker

/obj/machinery/atmospherics/components/unary/cryo_cell/handle_atom_del(atom/A)
	..()
	if(A == beaker)
		beaker = null
		updateUsrDialog()

/obj/machinery/atmospherics/components/unary/cryo_cell/on_deconstruction()
	if(beaker)
		beaker.forceMove(drop_location())
		beaker = null

/obj/machinery/atmospherics/components/unary/cryo_cell/update_icon_state()
	icon_state = (state_open) ? "pod-open" : ((on && is_operational) ? "pod-on" : "pod-off")
	return ..()

/obj/machinery/atmospherics/components/unary/cryo_cell/update_icon()
	. = ..()
	plane = initial(plane)

GLOBAL_VAR_INIT(cryo_overlay_cover_on, mutable_appearance('icons/obj/cryogenics.dmi', "cover-on", layer = MOB_LAYER + 0.02))
GLOBAL_VAR_INIT(cryo_overlay_cover_off, mutable_appearance('icons/obj/cryogenics.dmi', "cover-off", layer = MOB_LAYER + 0.02))

/obj/machinery/atmospherics/components/unary/cryo_cell/update_overlays()
	. = ..()
	if(panel_open)
		. += "pod-panel"
	if(state_open)
		return
	. += (on && is_operational) ? GLOB.cryo_overlay_cover_on : GLOB.cryo_overlay_cover_off

/obj/machinery/atmospherics/components/unary/cryo_cell/nap_violation(mob/violator)
	open_machine()

/obj/machinery/atmospherics/components/unary/cryo_cell/proc/set_on(new_value)
	if(on == new_value)
		return
	SEND_SIGNAL(src, COMSIG_CRYO_SET_ON, new_value)
	. = on
	on = new_value
	update_appearance()

/obj/machinery/atmospherics/components/unary/cryo_cell/on_set_is_operational(old_value)
	if(old_value) //Turned off
		set_on(FALSE)
		end_processing()
	else //Turned on
		begin_processing()

/obj/machinery/atmospherics/components/unary/cryo_cell/process(delta_time)
	..()

	if(!on)
		return
	if(!occupant)
		return

	var/mob/living/mob_occupant = occupant
	if(mob_occupant.on_fire)
		mob_occupant.extinguish_mob()
	if(!check_nap_violations())
		return
	if(mob_occupant.stat == DEAD) // We don't bother with dead people.
		return
	if(mob_occupant.get_organic_health() >= mob_occupant.getMaxHealth()) // Don't bother with fully healed people.
		if(iscarbon(mob_occupant))
			var/mob/living/carbon/C = mob_occupant
			if(C.all_wounds)
				if(!treating_wounds) // if we have wounds and haven't already alerted the doctors we're only dealing with the wounds, let them know
					treating_wounds = TRUE
					playsound(src, 'sound/machines/cryo_warning.ogg', volume) // Bug the doctors.
					var/msg = "Patient vitals fully recovered, continuing automated wound treatment."
					radio.talk_into(src, msg, radio_channel)
			else // otherwise if we were only treating wounds and now we don't have any, turn off treating_wounds so we can boot 'em out
				treating_wounds = FALSE

		if(!treating_wounds)
			set_on(FALSE)
			playsound(src, 'sound/machines/cryo_warning.ogg', volume) // Bug the doctors.
			var/msg = "Patient fully restored."
			if(autoeject) // Eject if configured.
				msg += " Auto ejecting patient now."
				open_machine()
			radio.talk_into(src, msg, radio_channel)
			return

	var/datum/gas_mixture/air1 = airs[1]

	if(air1.gases[/datum/gas/oxygen][MOLES] > CRYO_MIN_GAS_MOLES)
		if(mob_occupant.bodytemperature < T0C) // Sleepytime. Why? More cryo magic.
			mob_occupant.Sleeping((mob_occupant.bodytemperature * sleep_factor) * 1000 * delta_time)
			mob_occupant.Unconscious((mob_occupant.bodytemperature * unconscious_factor) * 1000 * delta_time)
		if(beaker)
			beaker.reagents.trans_to(occupant, (CRYO_TX_QTY / (efficiency * CRYO_MULTIPLY_FACTOR)) * delta_time, efficiency * CRYO_MULTIPLY_FACTOR, methods = VAPOR) // Transfer reagents.
			consume_gas = TRUE
	return TRUE

/obj/machinery/atmospherics/components/unary/cryo_cell/process_atmos()
	..()

	if(!on)
		return

	var/datum/gas_mixture/air1 = airs[1]

	if(!nodes[1] || !airs[1] || !air1.gases.len || air1.gases[/datum/gas/oxygen][MOLES] < CRYO_MIN_GAS_MOLES) // Turn off if the machine won't work.
		var/msg = "Insufficient cryogenic gas, shutting down."
		radio.talk_into(src, msg, radio_channel)
		set_on(FALSE)
		return

	if(occupant)
		var/mob/living/mob_occupant = occupant
		var/cold_protection = 0
		var/temperature_delta = air1.temperature - mob_occupant.bodytemperature // The only semi-realistic thing here: share temperature between the cell and the occupant.

		if(ishuman(mob_occupant))
			var/mob/living/carbon/human/H = mob_occupant
			cold_protection = H.get_cold_protection(air1.temperature)

		if(abs(temperature_delta) > 1)
			var/air_heat_capacity = air1.heat_capacity()

			var/heat = ((1 - cold_protection) * 0.1 + conduction_coefficient) * temperature_delta * (air_heat_capacity * heat_capacity / (air_heat_capacity + heat_capacity))

			air1.temperature = clamp(air1.temperature - heat / air_heat_capacity, TCMB, MAX_TEMPERATURE)
			mob_occupant.adjust_bodytemperature(heat / heat_capacity, TCMB)

			//lets have the core temp match the body temp in humans
			if(ishuman(mob_occupant))
				var/mob/living/carbon/human/humi = mob_occupant
				humi.adjust_coretemperature(humi.bodytemperature - humi.coretemperature)

		if(consume_gas) // Transferring reagent costs us extra gas
			air1.gases[/datum/gas/oxygen][MOLES] -= max(0, 2 / efficiency + 1 / efficiency) // Magically consume gas? Why not, we run on cryo magic.
			consume_gas = FALSE
		if(!consume_gas)
			air1.gases[/datum/gas/oxygen][MOLES] -= max(0, 2 / efficiency)
		air1.garbage_collect()

		if(air1.temperature > 2000)
			take_damage(clamp((air1.temperature)/200, 10, 20), BURN)

		update_parents()

/obj/machinery/atmospherics/components/unary/cryo_cell/relaymove(mob/living/user, direction)
	if(message_cooldown <= world.time)
		message_cooldown = world.time + 50
		to_chat(user, span_warning("[src]'s door won't budge!"))

/obj/machinery/atmospherics/components/unary/cryo_cell/open_machine(drop = FALSE)
	if(!state_open && !panel_open)
		set_on(FALSE)
	for(var/mob/M in contents) //only drop mobs
		M.forceMove(get_turf(src))
	set_occupant(null)
	flick("pod-open-anim", src)
	..()

/obj/machinery/atmospherics/components/unary/cryo_cell/close_machine(mob/living/carbon/user)
	treating_wounds = FALSE
	if((isnull(user) || istype(user)) && state_open && !panel_open)
		flick("pod-close-anim", src)
		..(user)
		return occupant

/obj/machinery/atmospherics/components/unary/cryo_cell/container_resist_act(mob/living/user)
	user.changeNext_move(CLICK_CD_BREAKOUT)
	user.last_special = world.time + CLICK_CD_BREAKOUT
	user.visible_message(span_notice("You see [user] kicking against the glass of [src]!"), \
		span_notice("You struggle inside [src], kicking the release with your foot... (this will take about [DisplayTimeText(CRYO_BREAKOUT_TIME)].)"), \
		span_hear("You hear a thump from [src]."))
	if(do_after(user, CRYO_BREAKOUT_TIME, target = src))
		if(!user || user.stat != CONSCIOUS || user.loc != src )
			return
		user.visible_message(span_warning("[user] successfully broke out of [src]!"), \
			span_notice("You successfully break out of [src]!"))
		open_machine()

/obj/machinery/atmospherics/components/unary/cryo_cell/examine(mob/user)
	. = ..()
	if(occupant)
		if(on)
			. += "Someone's inside [src]!"
		else
			. += "You can barely make out a form floating in [src]."
	else
		. += "[src] seems empty."

/obj/machinery/atmospherics/components/unary/cryo_cell/MouseDrop_T(mob/target, mob/user)
	if(user.incapacitated() || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !ISADVANCEDTOOLUSER(user))
		return
	if(isliving(target))
		var/mob/living/L = target
		if(L.incapacitated())
			close_machine(target)
	else
		user.visible_message(span_notice("[user] starts shoving [target] inside [src]."), span_notice("You start shoving [target] inside [src]."))
		if (do_after(user, 2.5 SECONDS, target=target))
			close_machine(target)

/obj/machinery/atmospherics/components/unary/cryo_cell/attackby(obj/item/I, mob/user, params)
	if(istype(I, /obj/item/reagent_containers/glass))
		. = 1 //no afterattack
		if(beaker)
			to_chat(user, span_warning("A beaker is already loaded into [src]!"))
			return
		if(!user.transferItemToLoc(I, src))
			return
		beaker = I
		user.visible_message(span_notice("[user] places [I] in [src]."), \
							span_notice("You place [I] in [src]."))
		var/reagentlist = pretty_string_from_reagent_list(I.reagents.reagent_list)
		log_game("[key_name(user)] added an [I] to cryo containing [reagentlist]")
		return
	if(!on && !occupant && !state_open && (default_deconstruction_screwdriver(user, "pod-off", "pod-off", I)) \
		|| default_change_direction_wrench(user, I) \
		|| default_pry_open(I) \
		|| default_deconstruction_crowbar(I))
		update_appearance()
		return
	else if(I.tool_behaviour == TOOL_SCREWDRIVER)
		to_chat(user, "You can't access the maintenance panel while the pod is " \
		+ (on ? "active" : (occupant ? "full" : "open")) + "!")
		return
	return ..()

/obj/machinery/atmospherics/components/unary/cryo_cell/ui_state(mob/user)
	return GLOB.notcontained_state

/obj/machinery/atmospherics/components/unary/cryo_cell/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "Cryo", name)
		ui.open()

/obj/machinery/atmospherics/components/unary/cryo_cell/ui_data()
	var/list/data = list()
	data["isOperating"] = on
	data["hasOccupant"] = occupant ? TRUE : FALSE
	data["isOpen"] = state_open
	data["autoEject"] = autoeject

	data["occupant"] = list()
	if(occupant)
		var/mob/living/mob_occupant = occupant
		data["occupant"]["name"] = mob_occupant.name
		switch(mob_occupant.stat)
			if(CONSCIOUS)
				data["occupant"]["stat"] = "Conscious"
				data["occupant"]["statstate"] = "good"
			if(SOFT_CRIT)
				data["occupant"]["stat"] = "Conscious"
				data["occupant"]["statstate"] = "average"
			if(UNCONSCIOUS, HARD_CRIT)
				data["occupant"]["stat"] = "Unconscious"
				data["occupant"]["statstate"] = "average"
			if(DEAD)
				data["occupant"]["stat"] = "Dead"
				data["occupant"]["statstate"] = "bad"
		data["occupant"]["health"] = round(mob_occupant.health, 1)
		data["occupant"]["maxHealth"] = mob_occupant.maxHealth
		data["occupant"]["minHealth"] = HEALTH_THRESHOLD_DEAD
		data["occupant"]["bruteLoss"] = round(mob_occupant.getBruteLoss(), 1)
		data["occupant"]["oxyLoss"] = round(mob_occupant.getOxyLoss(), 1)
		data["occupant"]["toxLoss"] = round(mob_occupant.getToxLoss(), 1)
		data["occupant"]["fireLoss"] = round(mob_occupant.getFireLoss(), 1)
		data["occupant"]["bodyTemperature"] = round(mob_occupant.bodytemperature, 1)
		if(mob_occupant.bodytemperature < TCRYO)
			data["occupant"]["temperaturestatus"] = "good"
		else if(mob_occupant.bodytemperature < T0C)
			data["occupant"]["temperaturestatus"] = "average"
		else
			data["occupant"]["temperaturestatus"] = "bad"

	var/datum/gas_mixture/air1 = airs[1]
	data["cellTemperature"] = round(air1.temperature, 1)

	data["isBeakerLoaded"] = beaker ? TRUE : FALSE
	var/beakerContents = list()
	if(beaker && beaker.reagents && beaker.reagents.reagent_list.len)
		for(var/datum/reagent/R in beaker.reagents.reagent_list)
			beakerContents += list(list("name" = R.name, "volume" = R.volume))
	data["beakerContents"] = beakerContents
	return data

/obj/machinery/atmospherics/components/unary/cryo_cell/ui_act(action, params)
	. = ..()
	if(.)
		return
	switch(action)
		if("power")
			if(on)
				set_on(FALSE)
			else if(!state_open)
				set_on(TRUE)
			. = TRUE
		if("door")
			if(state_open)
				close_machine()
			else
				open_machine()
			. = TRUE
		if("autoeject")
			autoeject = !autoeject
			. = TRUE
		if("ejectbeaker")
			if(beaker)
				beaker.forceMove(drop_location())
				if(Adjacent(usr) && !issilicon(usr))
					usr.put_in_hands(beaker)
				beaker = null
				. = TRUE

/obj/machinery/atmospherics/components/unary/cryo_cell/can_interact(mob/user)
	return ..() && user.loc != src

/obj/machinery/atmospherics/components/unary/cryo_cell/CtrlClick(mob/user)
	if(can_interact(user) && !state_open)
		set_on(!on)
	return ..()

/obj/machinery/atmospherics/components/unary/cryo_cell/AltClick(mob/user)
	if(can_interact(user))
		if(state_open)
			close_machine()
		else
			open_machine()
	return ..()

/obj/machinery/atmospherics/components/unary/cryo_cell/update_remote_sight(mob/living/user)
	return // we don't see the pipe network while inside cryo.

/obj/machinery/atmospherics/components/unary/cryo_cell/get_remote_view_fullscreens(mob/user)
	user.overlay_fullscreen("remote_view", /atom/movable/screen/fullscreen/impaired, 1)

/obj/machinery/atmospherics/components/unary/cryo_cell/can_see_pipes()
	return FALSE // you can't see the pipe network when inside a cryo cell.

/obj/machinery/atmospherics/components/unary/cryo_cell/return_temperature()
	var/datum/gas_mixture/G = airs[1]

	if(G.total_moles() > 10)
		return G.temperature
	return ..()

/obj/machinery/atmospherics/components/unary/cryo_cell/default_change_direction_wrench(mob/user, obj/item/wrench/W)
	. = ..()
	if(.)
		set_init_directions()
		var/obj/machinery/atmospherics/node = nodes[1]
		if(node)
			node.disconnect(src)
			nodes[1] = null
			if(parents[1])
				nullify_pipenet(parents[1])

		atmos_init()
		node = nodes[1]
		if(node)
			node.atmos_init()
			node.add_member(src)
		SSair.add_to_rebuild_queue(src)

#undef MAX_TEMPERATURE
#undef CRYO_MULTIPLY_FACTOR
#undef CRYO_TX_QTY
#undef CRYO_MIN_GAS_MOLES
#undef CRYO_BREAKOUT_TIME


code:
/obj/machinery/atmospherics/components/unary/heat_exchanger

	icon_state = "he1"

	name = "heat exchanger"
	desc = "Exchanges heat between two input gases. Set up for fast heat transfer."

	can_unwrench = TRUE
	shift_underlay_only = FALSE // not really used

	layer = LOW_OBJ_LAYER

	var/datum/weakref/partner_ref = null
	var/update_cycle

	pipe_state = "heunary"

/obj/machinery/atmospherics/components/unary/heat_exchanger/layer2
	piping_layer = 2
	icon_state = "he_map-2"

/obj/machinery/atmospherics/components/unary/heat_exchanger/layer4
	piping_layer = 4
	icon_state = "he_map-4"

/obj/machinery/atmospherics/components/unary/heat_exchanger/update_icon_state()
	icon_state = "he[nodes[1] ? 1 : 0]"
	return ..()

/obj/machinery/atmospherics/components/unary/heat_exchanger/update_icon()
	. = ..()
	if(nodes[1])
		var/obj/machinery/atmospherics/node = nodes[1]
		add_atom_colour(node.color, FIXED_COLOUR_PRIORITY)
	PIPING_LAYER_SHIFT(src, piping_layer)

/obj/machinery/atmospherics/components/unary/heat_exchanger/atmos_init()
	var/obj/machinery/atmospherics/components/unary/heat_exchanger/partner = partner_ref?.resolve()
	if(!partner)
		partner_ref = null
		var/partner_connect = turn(dir,180)

		for(var/obj/machinery/atmospherics/components/unary/heat_exchanger/target in get_step(src,partner_connect))
			if(target.dir & get_dir(src,target))
				partner_ref = WEAKREF(target)
				target.partner_ref = WEAKREF(src)
				break

	. = ..()

/obj/machinery/atmospherics/components/unary/heat_exchanger/process_atmos()
	var/obj/machinery/atmospherics/components/unary/heat_exchanger/partner = partner_ref?.resolve()
	if(!partner)
		partner_ref = null
		return
	if(SSair.times_fired <= update_cycle)
		return

	update_cycle = SSair.times_fired
	partner.update_cycle = SSair.times_fired

	var/datum/gas_mixture/air_contents = airs[1]
	var/datum/gas_mixture/partnerair_contents = partner.airs[1]

	var/air_heat_capacity = air_contents.heat_capacity()
	var/other_air_heat_capacity = partnerair_contents.heat_capacity()
	var/combined_heat_capacity = other_air_heat_capacity + air_heat_capacity

	var/old_temperature = air_contents.temperature
	var/other_old_temperature = partnerair_contents.temperature

	if(combined_heat_capacity > 0)
		var/combined_energy = partnerair_contents.temperature * other_air_heat_capacity + air_heat_capacity * air_contents.temperature

		var/new_temperature = combined_energy / combined_heat_capacity
		air_contents.temperature = new_temperature
		partnerair_contents.temperature = new_temperature

	if(abs(old_temperature - air_contents.temperature) > 1)
		update_parents()

	if(abs(other_old_temperature - partnerair_contents.temperature) > 1)
		partner.update_parents()


code:
/obj/machinery/atmospherics/components/unary/outlet_injector
	icon_state = "inje_map-3"

	name = "air injector"
	desc = "Has a valve and pump attached to it."

	use_power = IDLE_POWER_USE
	can_unwrench = TRUE
	shift_underlay_only = FALSE
	hide = TRUE
	layer = GAS_SCRUBBER_LAYER
	pipe_state = "injector"
	resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF //really helpful in building gas chambers for xenomorphs

	///Variable used for radio frequency injection
	var/injecting = FALSE
	///Rate of operation of the device
	var/volume_rate = 50

	///Frequency id for connecting to the NTNet
	var/frequency = 0
	///Reference to the radio datum
	var/datum/radio_frequency/radio_connection
	///Id of the radio connection
	var/id = null

/obj/machinery/atmospherics/components/unary/outlet_injector/CtrlClick(mob/user)
	if(can_interact(user))
		on = !on
		investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS)
		update_appearance()
	return ..()

/obj/machinery/atmospherics/components/unary/outlet_injector/AltClick(mob/user)
	if(can_interact(user))
		volume_rate = MAX_TRANSFER_RATE
		investigate_log("was set to [volume_rate] L/s by [key_name(user)]", INVESTIGATE_ATMOS)
		balloon_alert(user, "volume output set to [volume_rate] L/s")
		update_appearance()
	return ..()

/obj/machinery/atmospherics/components/unary/outlet_injector/Destroy()
	SSradio.remove_object(src,frequency)
	return ..()

/obj/machinery/atmospherics/components/unary/outlet_injector/update_icon_nopipes()
	cut_overlays()
	if(showpipe)
		// everything is already shifted so don't shift the cap
		add_overlay(get_pipe_image(icon, "inje_cap", initialize_directions, pipe_color))

	if(!nodes[1] || !on || !is_operational)
		icon_state = "inje_off"
	else
		icon_state = "inje_on"

/obj/machinery/atmospherics/components/unary/outlet_injector/process_atmos()
	..()

	injecting = 0

	if(!on || !is_operational)
		return

	var/turf/location = get_turf(loc)
	if(isclosedturf(location))
		return

	var/datum/gas_mixture/air_contents = airs[1]

	if(air_contents.temperature > 0)
		var/transfer_moles = (air_contents.return_pressure() * volume_rate) / (air_contents.temperature * R_IDEAL_GAS_EQUATION)

		if(!transfer_moles)
			return

		var/datum/gas_mixture/removed = air_contents.remove(transfer_moles)

		location.assume_air(removed)

		update_parents()

/obj/machinery/atmospherics/components/unary/outlet_injector/proc/inject()

	if(on || injecting || !is_operational)
		return

	var/datum/gas_mixture/air_contents = airs[1]

	injecting = 1
	flick("inje_inject", src)

	if(air_contents.temperature > 0)
		var/transfer_moles = (air_contents.return_pressure() * volume_rate) / (air_contents.temperature * R_IDEAL_GAS_EQUATION)

		if(!transfer_moles)
			return

		var/datum/gas_mixture/removed = air_contents.remove(transfer_moles)
		loc.assume_air(removed)
		update_parents()

/obj/machinery/atmospherics/components/unary/outlet_injector/proc/set_frequency(new_frequency)
	SSradio.remove_object(src, frequency)
	frequency = new_frequency
	if(frequency)
		radio_connection = SSradio.add_object(src, frequency)

/obj/machinery/atmospherics/components/unary/outlet_injector/proc/broadcast_status()

	if(!radio_connection)
		return

	var/datum/signal/signal = new(list(
		"tag" = id,
		"device" = "AO",
		"power" = on,
		"volume_rate" = volume_rate,
		//"timestamp" = world.time,
		"sigtype" = "status"
	))
	radio_connection.post_signal(src, signal)

/obj/machinery/atmospherics/components/unary/outlet_injector/atmos_init()
	set_frequency(frequency)
	broadcast_status()
	..()

/obj/machinery/atmospherics/components/unary/outlet_injector/receive_signal(datum/signal/signal)

	if(!signal.data["tag"] || (signal.data["tag"] != id) || (signal.data["sigtype"]!="command"))
		return

	if("power" in signal.data)
		on = text2num(signal.data["power"])

	if("power_toggle" in signal.data)
		on = !on

	if("inject" in signal.data)
		INVOKE_ASYNC(src, .proc/inject)
		return

	if("set_volume_rate" in signal.data)
		var/number = text2num(signal.data["set_volume_rate"])
		var/datum/gas_mixture/air_contents = airs[1]
		volume_rate = clamp(number, 0, air_contents.volume)

	addtimer(CALLBACK(src, .proc/broadcast_status), 2)

	if(!("status" in signal.data)) //do not update_icon
		update_appearance()

/obj/machinery/atmospherics/components/unary/outlet_injector/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "AtmosPump", name)
		ui.open()

/obj/machinery/atmospherics/components/unary/outlet_injector/ui_data()
	var/data = list()
	data["on"] = on
	data["rate"] = round(volume_rate)
	data["max_rate"] = round(MAX_TRANSFER_RATE)
	return data

/obj/machinery/atmospherics/components/unary/outlet_injector/ui_act(action, params)
	. = ..()
	if(.)
		return

	switch(action)
		if("power")
			on = !on
			investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", INVESTIGATE_ATMOS)
			. = TRUE
		if("rate")
			var/rate = params["rate"]
			if(rate == "max")
				rate = MAX_TRANSFER_RATE
				. = TRUE
			else if(text2num(rate) != null)
				rate = text2num(rate)
				. = TRUE
			if(.)
				volume_rate = clamp(rate, 0, MAX_TRANSFER_RATE)
				investigate_log("was set to [volume_rate] L/s by [key_name(usr)]", INVESTIGATE_ATMOS)
	update_appearance()
	broadcast_status()

/obj/machinery/atmospherics/components/unary/outlet_injector/can_unwrench(mob/user)
	. = ..()
	if(. && on && is_operational)
		to_chat(user, span_warning("You cannot unwrench [src], turn it off first!"))
		return FALSE

// mapping

/obj/machinery/atmospherics/components/unary/outlet_injector/layer2
	piping_layer = 2
	icon_state = "inje_map-2"

/obj/machinery/atmospherics/components/unary/outlet_injector/layer4
	piping_layer = 4
	icon_state = "inje_map-4"

/obj/machinery/atmospherics/components/unary/outlet_injector/on
	on = TRUE

/obj/machinery/atmospherics/components/unary/outlet_injector/on/layer2
	piping_layer = 2
	icon_state = "inje_map-2"

/obj/machinery/atmospherics/components/unary/outlet_injector/on/layer4
	piping_layer = 4
	icon_state = "inje_map-4"

/obj/machinery/atmospherics/components/unary/outlet_injector/atmos
	frequency = FREQ_ATMOS_STORAGE
	on = TRUE
	volume_rate = MAX_TRANSFER_RATE

/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/plasma_input
	name = "plasma tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_PLAS
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/oxygen_input
	name = "oxygen tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_O2
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/nitrogen_input
	name = "nitrogen tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_N2
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/mix_input
	name = "mix tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_MIX
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/nitrous_input
	name = "nitrous oxide tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_N2O
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/air_input
	name = "air mix tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_AIR
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/carbon_input
	name = "carbon dioxide tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_CO2
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/bz_input
	name = "bz tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_BZ
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/freon_input
	name = "freon tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_FREON
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/halon_input
	name = "halon tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_HALON
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/healium_input
	name = "healium tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_HEALIUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/hydrogen_input
	name = "hydrogen tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_H2
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/hypernoblium_input
	name = "hypernoblium tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_HYPERNOBLIUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/miasma_input
	name = "miasma tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_MIASMA
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/nitryl_input
	name = "nitryl tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_NO2
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/pluoxium_input
	name = "pluoxium tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_PLUOXIUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/proto_nitrate_input
	name = "proto-nitrate tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_PROTO_NITRATE
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/stimulum_input
	name = "stimulum tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_STIMULUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/tritium_input
	name = "tritium tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_TRITIUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/water_vapor_input
	name = "water vapor tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_H2O
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/zauker_input
	name = "zauker tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_ZAUKER
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/helium_input
	name = "helium tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_HELIUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/antinoblium_input
	name = "antinoblium tank input injector"
	id = ATMOS_GAS_MONITOR_INPUT_ANTINOBLIUM
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/incinerator_input
	name = "incinerator chamber input injector"
	id = ATMOS_GAS_MONITOR_INPUT_INCINERATOR
/obj/machinery/atmospherics/components/unary/outlet_injector/atmos/ordnance_mixing_input
	name = "ordnance mixing input injector"
	id = ATMOS_GAS_MONITOR_INPUT_ORDNANCE_LAB


code:
/obj/machinery/atmospherics/components/unary/passive_vent
	icon_state = "passive_vent_map-3"

	name = "passive vent"
	desc = "It is an open vent."

	can_unwrench = TRUE
	hide = TRUE
	layer = GAS_SCRUBBER_LAYER
	shift_underlay_only = FALSE

	pipe_state = "pvent"
	vent_movement = VENTCRAWL_ALLOWED | VENTCRAWL_CAN_SEE | VENTCRAWL_ENTRANCE_ALLOWED

/obj/machinery/atmospherics/components/unary/passive_vent/update_icon_nopipes()
	cut_overlays()
	if(showpipe)
		var/image/cap = get_pipe_image(icon, "vent_cap", initialize_directions, pipe_color)
		add_overlay(cap)
	icon_state = "passive_vent"

/obj/machinery/atmospherics/components/unary/passive_vent/process_atmos()
	var/turf/location = get_turf(loc)
	if(isclosedturf(location))
		return

	var/datum/gas_mixture/external = location.return_air()
	var/datum/gas_mixture/internal = airs[1]

	if(internal.equalize(external))
		air_update_turf(FALSE, FALSE)
		update_parents()

/obj/machinery/atmospherics/components/unary/passive_vent/layer2
	piping_layer = 2
	icon_state = "passive_vent_map-2"

/obj/machinery/atmospherics/components/unary/passive_vent/layer4
	piping_layer = 4
	icon_state = "passive_vent_map-4"


code:
/obj/machinery/atmospherics/components/unary/portables_connector
	icon_state = "connector_map-3"

	name = "connector port"
	desc = "For connecting portables devices related to atmospherics control."

	can_unwrench = TRUE

	use_power = NO_POWER_USE
	layer = GAS_FILTER_LAYER
	hide = TRUE
	shift_underlay_only = FALSE

	pipe_flags = PIPING_ONE_PER_TURF
	pipe_state = "connector"
	custom_reconcilation = TRUE

	///Reference to the connected device
	var/obj/machinery/portable_atmospherics/connected_device

/obj/machinery/atmospherics/components/unary/portables_connector/New()
	. = ..()
	var/datum/gas_mixture/air_contents = airs[1]
	air_contents.volume = 0

/obj/machinery/atmospherics/components/unary/portables_connector/Destroy()
	if(connected_device)
		connected_device.disconnect()
	return ..()

/obj/machinery/atmospherics/components/unary/portables_connector/update_icon_nopipes()
	icon_state = "connector"
	if(showpipe)
		cut_overlays()
		var/image/cap = get_pipe_image(icon, "connector_cap", initialize_directions, pipe_color)
		add_overlay(cap)

/obj/machinery/atmospherics/components/unary/portables_connector/process_atmos()
	if(!connected_device)
		return
	update_parents()

/obj/machinery/atmospherics/components/unary/portables_connector/return_airs_for_reconcilation(datum/pipeline/requester)
	. = ..()
	if(!connected_device)
		return
	. += connected_device.return_air()

/obj/machinery/atmospherics/components/unary/portables_connector/can_unwrench(mob/user)
	. = ..()
	if(. && connected_device)
		to_chat(user, span_warning("You cannot unwrench [src], detach [connected_device] first!"))
		return FALSE

/obj/machinery/atmospherics/components/unary/portables_connector/layer2
	piping_layer = 2
	icon_state = "connector_map-2"

/obj/machinery/atmospherics/components/unary/portables_connector/layer4
	piping_layer = 4
	icon_state = "connector_map-4"

/obj/machinery/atmospherics/components/unary/portables_connector/visible
	hide = FALSE

/obj/machinery/atmospherics/components/unary/portables_connector/visible/layer2
	piping_layer = 2
	icon_state = "connector_map-2"

/obj/machinery/atmospherics/components/unary/portables_connector/visible/layer4
	piping_layer = 4
	icon_state = "connector_map-4"


code:
/obj/machinery/atmospherics/components/unary
	icon = 'icons/obj/atmospherics/components/unary_devices.dmi'
	dir = SOUTH
	initialize_directions = SOUTH
	device_type = UNARY
	pipe_flags = PIPING_ONE_PER_TURF
	construction_type = /obj/item/pipe/directional
	///Unique id of the device
	var/uid
	///Increases to prevent duplicated Ids
	var/static/gl_uid = 1

/obj/machinery/atmospherics/components/unary/set_init_directions()
	initialize_directions = dir

/obj/machinery/atmospherics/components/unary/on_construction()
	..()
	update_appearance()

/obj/machinery/atmospherics/components/unary/proc/assign_uid_vents()
	uid = num2text(gl_uid++)
	return uid