Files
Bubberstation/code/modules/atmospherics/machinery/components/tank.dm
tonty 395964a8b3 Pressure Tanks no longer runtime when shuttleRotated, Remedies constant CI fails (#88413)
## About The Pull Request

Fixes #71092 

## Why It's Good For The Game

Flaky tests bad, fixman good...

## Changelog

🆑
fix: Pubby's whiteship no longer breaks when it tries to dock
/🆑
2024-12-27 17:26:08 +01:00

634 lines
22 KiB
Plaintext

#define TANK_PLATING_SHEETS 12
/obj/machinery/atmospherics/components/tank
icon = 'icons/obj/pipes_n_cables/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 = TANK_PLATING_SHEETS * SHEET_MATERIAL_AMOUNT) // 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 = SMOOTH_GROUP_GAS_TANK
canSmoothWith = SMOOTH_GROUP_GAS_TANK
appearance_flags = KEEP_TOGETHER|LONG_GLIDE
greyscale_config = /datum/greyscale_config/stationary_canister
greyscale_colors = "#ffffff"
///The image showing the gases inside of the tank
var/image/window
/// The open node directions of the tank, assuming that the tank is facing NORTH.
var/open_ports = NONE
/// 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 = 46000
/// 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/pipes_n_cables/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/pipes_n_cables/stationary_canisters.dmi', crack_states)
RegisterSignal(src, COMSIG_MERGER_ADDING, PROC_REF(merger_adding))
RegisterSignal(src, COMSIG_MERGER_REMOVING, PROC_REF(merger_removing))
RegisterSignal(src, COMSIG_ATOM_SMOOTHED_ICON, PROC_REF(smoothed))
air_contents = new
air_contents.temperature = T20C
air_contents.volume = volume
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)
set_portdir_relative(dir, TRUE)
set_init_directions()
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/post_machine_initialize()
. = ..()
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/finalize_material_effects(list/materials)
. = ..()
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()
///////////////////////////////////////////////////////////////////
// Port stuff
/**
* Enables/Disables a port direction in var/open_ports. \
* Use this, then call set_init_directions() instead of setting initialize_directions directly \
* This system exists because tanks not having all initialize_directions set correctly breaks shuttle rotations
*/
/obj/machinery/atmospherics/components/tank/proc/set_portdir_relative(relative_port_dir, enable)
ASSERT(!isnull(enable), "Did not receive argument enable")
// Rotate the given dir so that it's relative to north
var/port_dir
if(dir == NORTH) // We're already facing north, no rotation needed
port_dir = relative_port_dir
else
var/offnorth_angle = dir2angle(dir)
port_dir = turn(relative_port_dir, offnorth_angle)
if(enable)
open_ports |= port_dir
else
open_ports &= ~port_dir
/**
* Toggles a port direction in var/open_ports \
* Use this, then call set_init_directions() instead of setting initialize_directions directly \
* This system exists because tanks not having all initialize_directions set correctly breaks shuttle rotations
*/
/obj/machinery/atmospherics/components/tank/proc/toggle_portdir_relative(relative_port_dir)
var/toggle = ((initialize_directions & relative_port_dir) ? FALSE : TRUE)
set_portdir_relative(relative_port_dir, toggle)
/obj/machinery/atmospherics/components/tank/set_init_directions()
if(!open_ports)
initialize_directions = NONE
return
//We're rotating open_ports relative to dir, and
//setting initialize_directions to that rotated dir
var/relative_port_dirs = NONE
var/dir_angle = dir2angle(dir)
for(var/cardinal in GLOB.cardinals)
var/current_dir = cardinal & open_ports
if(!current_dir)
continue
var/rotated_dir = turn(current_dir, -dir_angle)
relative_port_dirs |= rotated_dir
initialize_directions = relative_port_dirs
/obj/machinery/atmospherics/components/tank/proc/toggle_side_port(port_dir)
toggle_portdir_relative(port_dir)
set_init_directions()
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()
///////////////////////////////////////////////////////////////////
// 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
///////////////////////////////////////////////////////////////////
// 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_REF(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(get_turf(src)))
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, heat_required = HIGH_TEMPERATURE_REQUIRED))
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
var/unsafe = FALSE
var/internal_pressure = air_contents.return_pressure() - airmix.return_pressure()
if(internal_pressure > 2 * ONE_ATMOSPHERE)
time_taken *= 2
to_chat(user, span_warning("The tank seems to be pressurized, are you sure this is a good idea?"))
unsafe = TRUE
if(!tool.use_tool(src, user, time_taken, volume = 60))
return
if(unsafe)
unsafe_pressure_release(user, internal_pressure)
deconstruct(disassembled=TRUE)
to_chat(user, span_notice("You finish cutting open the sealed gas tank, revealing the innards."))
/obj/machinery/atmospherics/components/tank/on_deconstruction(disassembled)
var/turf/location = drop_location()
. = ..()
location.assume_air(air_contents)
if(!disassembled)
return
var/obj/structure/tank_frame/frame = new(location)
frame.construction_state = TANK_PLATING_UNSECURED
for(var/datum/material/material as anything in custom_materials)
if (frame.material_end_product)
// If something looks fishy, you get nothing
message_admins("\The [src] had multiple materials set. Unless you were messing around with VV, yell at a coder")
frame.material_end_product = null
frame.construction_state = TANK_FRAME
break
else
frame.material_end_product = material
frame.update_appearance()
///////////////////////////////////////////////////////////////////
// Gas tank variants
/obj/machinery/atmospherics/components/tank/air
name = "pressure tank (Air)"
/obj/machinery/atmospherics/components/tank/air/layer1
piping_layer = 1
/obj/machinery/atmospherics/components/tank/air/layer2
piping_layer = 2
/obj/machinery/atmospherics/components/tank/air/layer4
piping_layer = 4
/obj/machinery/atmospherics/components/tank/air/layer5
piping_layer = 5
/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/nitrium
gas_type = /datum/gas/nitrium
/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/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/pipes_n_cables/stationary_canisters.dmi'
icon_state = "frame"
anchored = FALSE
density = TRUE
custom_materials = list(/datum/material/alloy/plasteel = 4 * SHEET_MATERIAL_AMOUNT)
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/atom_deconstruct(disassembled)
if(disassembled)
for(var/datum/material/mat as anything in custom_materials)
new mat.sheet_type(drop_location(), custom_materials[mat] / SHEET_MATERIAL_AMOUNT)
/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 && isstack(item) && add_plating(user, item))
return
return ..()
/obj/structure/tank_frame/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool, time = 0.5 SECONDS)
return ITEM_INTERACT_SUCCESS
/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
if(!stack.material_type)
balloon_alert(user, "invalid material!")
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(TANK_PLATING_SHEETS))
var/amount_more
switch(100 * stack.amount / TANK_PLATING_SHEETS)
if(0) // Wat?
amount_more = "any at all"
if(1 to 25)
amount_more = "a lot more"
if(26 to 50)
amount_more = "about four times as much"
if(51 to 75)
amount_more = "about twice as much"
if(76 to 100)
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(), TANK_PLATING_SHEETS)
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, heat_required = HIGH_TEMPERATURE_REQUIRED))
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((material_end_product) = TANK_PLATING_SHEETS * SHEET_MATERIAL_AMOUNT)
new_tank.set_custom_materials(new_custom_materials)
new_tank.on_construction(user, 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)
#undef TANK_PLATING_SHEETS