From 5d2d2a01e7e957e4e54a90c4440628d527099348 Mon Sep 17 00:00:00 2001
From: 13spacemen <46101244+13spacemen@users.noreply.github.com>
Date: Mon, 25 Dec 2023 06:13:25 +0500
Subject: [PATCH] Canisters: Wires, Rigging and Screentips (#80265)
## About The Pull Request
Added 6 wires to gas canisters, accessible when you screw open the panel
Toggle valve, toggle shielding, min pressure, max pressure, eject tank,
toggle reaction suppression (only works with hypernob crystal'd
canisters)
Pulsing the wires only works if the canister has a cell
You can attach dual assembly combos to canisters too, doesn't have to be
an igniter combo, could be a prox-signaler or timer-signaler. These are
visible on the canister sprite and can be removed by right-clicking the
canister. Visible assemblies don't trigger anything inside the canister,
it's like taping it to the canister, also looks more menacing.
Also added screentips to canisters
## Why It's Good For The Game
Emergent gameplay, more possibilities with atmos automation and
contraptions, can make traps like a proximity sensor N2O canister in
maint for cult conversions, anything
AND YES EVERYTHING IS LOGGED NO STEALTH GRIEFING POSSIBLE
## Video
https://github.com/tgstation/tgstation/assets/46101244/922465b3-9f9c-4b7b-8769-fca6df3b87a3
## Changelog
:cl:
add: Canisters now have wires! You can pulse wires to do various
canister functions like opening/closing the valve. Make sure it has a
cell though.
add: You can rig assembly combos (igniter-timer, prox-signaler, etc.)
onto canisters
qol: Canisters now have screentips
/:cl:
---
code/__DEFINES/wires.dm | 6 +
code/datums/wires/canister.dm | 33 +++
.../machinery/portable/canister.dm | 233 +++++++++++++-----
tgstation.dme | 1 +
4 files changed, 209 insertions(+), 64 deletions(-)
create mode 100644 code/datums/wires/canister.dm
diff --git a/code/__DEFINES/wires.dm b/code/__DEFINES/wires.dm
index 2b4c528abc2..633dcc360c2 100644
--- a/code/__DEFINES/wires.dm
+++ b/code/__DEFINES/wires.dm
@@ -64,6 +64,12 @@
#define WIRE_ZAP1 "High Voltage Circuit 1"
#define WIRE_ZAP2 "High Voltage Circuit 2"
#define WIRE_OVERCLOCK "Overclock"
+#define WIRE_VALVE "Valve"
+#define WIRE_SHIELDING "Shielding"
+#define WIRE_REGULATOR_MIN "Minimize Regulator Pressure"
+#define WIRE_REGULATOR_MAX "Maximize Regulator Pressure"
+#define WIRE_TANK_EJECT "Eject Tank"
+#define WIRE_REACTION_SUPPRESSION "Reaction Suppression"
// Wire states for the AI
#define AI_WIRE_NORMAL 0
diff --git a/code/datums/wires/canister.dm b/code/datums/wires/canister.dm
new file mode 100644
index 00000000000..c692ff22872
--- /dev/null
+++ b/code/datums/wires/canister.dm
@@ -0,0 +1,33 @@
+/datum/wires/canister
+ holder_type = /obj/machinery/portable_atmospherics/canister
+ proper_name = "Canister"
+
+/datum/wires/canister/New(atom/holder)
+ wires = list(WIRE_VALVE, WIRE_SHIELDING, WIRE_REGULATOR_MIN, WIRE_REGULATOR_MAX, WIRE_TANK_EJECT, WIRE_REACTION_SUPPRESSION)
+ ..()
+
+/datum/wires/canister/on_pulse(wire)
+ var/obj/machinery/portable_atmospherics/canister/the_canister = holder
+ if(!the_canister.internal_cell)
+ return
+ switch(wire)
+ if(WIRE_VALVE)
+ the_canister.toggle_valve(usr, wire_pulsed = TRUE)
+ if(WIRE_SHIELDING)
+ the_canister.toggle_shielding(usr, wire_pulsed = TRUE)
+ if(WIRE_TANK_EJECT)
+ the_canister.eject_tank(usr, wire_pulsed = TRUE)
+ if(WIRE_REGULATOR_MIN)
+ the_canister.release_pressure = CAN_MIN_RELEASE_PRESSURE
+ the_canister.investigate_log("was set to [the_canister.release_pressure] kPa by [key_name(usr)] via wire pulse.", INVESTIGATE_ATMOS)
+ if(WIRE_REGULATOR_MAX)
+ the_canister.release_pressure = CAN_MAX_RELEASE_PRESSURE
+ the_canister.investigate_log("was set to [the_canister.release_pressure] kPa by [key_name(usr)] via wire pulse.", INVESTIGATE_ATMOS)
+ if(WIRE_REACTION_SUPPRESSION)
+ the_canister.toggle_reaction_suppression(usr, wire_pulsed = TRUE)
+
+/datum/wires/canister/can_reveal_wires(mob/user)
+ if(HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
+ return TRUE
+
+ return ..()
diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 7479b92c420..bef8eb7300d 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -36,10 +36,14 @@
var/shielding_powered = FALSE
///The powercell used to enable shielding
var/obj/item/stock_parts/cell/internal_cell
- ///Is the cell hatch opened
- var/cell_container_opened = FALSE
///used while processing to update appearance only when its pressure state changes
var/current_pressure_state
+ /// An assembly attached to the canister
+ var/obj/item/assembly_holder/rig = null
+ /// The person who attached an assembly to this canister, usually for ignition purposes in tandem with valve wire pulse
+ var/mob/last_rigger = ""
+ //overlay of attached assemblies
+ var/mutable_appearance/assemblies_overlay
/datum/armor/portable_atmospherics_canister
melee = 50
@@ -56,6 +60,8 @@
if(mapload)
internal_cell = new /obj/item/stock_parts/cell/high(src)
+ set_wires(new /datum/wires/canister(src))
+
if(existing_mixture)
air_contents.copy_from(existing_mixture)
else
@@ -73,6 +79,18 @@
AddElement(/datum/element/atmos_sensitive, mapload)
AddElement(/datum/element/volatile_gas_storage)
AddComponent(/datum/component/gas_leaker, leak_rate=0.01)
+ register_context()
+
+/obj/machinery/portable_atmospherics/canister/Destroy()
+ QDEL_NULL(wires)
+ return ..()
+
+/obj/machinery/portable_atmospherics/canister/Exited(atom/movable/gone)
+ if(gone == rig)
+ rig = null
+ last_rigger = null
+ cut_overlays(assemblies_overlay)
+ return ..()
/obj/machinery/portable_atmospherics/canister/interact(mob/user)
. = ..()
@@ -81,6 +99,30 @@
playsound(src, 'sound/misc/compiler-failure.ogg', 50, TRUE)
return
+/obj/machinery/portable_atmospherics/canister/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(rig)
+ context[SCREENTIP_CONTEXT_RMB] = "Remove rigged device"
+ if(holding)
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Remove tank"
+ if(!held_item)
+ return CONTEXTUAL_SCREENTIP_SET
+ if(istype(held_item, /obj/item/assembly_holder))
+ context[SCREENTIP_CONTEXT_LMB] = "Rig"
+ if(istype(held_item, /obj/item/stock_parts/cell))
+ context[SCREENTIP_CONTEXT_LMB] = "Insert cell"
+ switch(held_item.tool_behaviour)
+ if(TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] hatch"
+ if(TOOL_CROWBAR)
+ if(panel_open && internal_cell)
+ context[SCREENTIP_CONTEXT_LMB] = "Remove cell"
+ if(TOOL_WELDER)
+ context[SCREENTIP_CONTEXT_LMB] = "Repair"
+ context[SCREENTIP_CONTEXT_RMB] = "Dismantle"
+
+ return CONTEXTUAL_SCREENTIP_SET
+
/obj/machinery/portable_atmospherics/canister/examine(user)
. = ..()
. += span_notice("A sticker on its side says MAX SAFE PRESSURE: [siunit_pressure(initial(pressure_limit), 0)]; MAX SAFE TEMPERATURE: [siunit(temp_limit, "K", 0)].")
@@ -88,8 +130,13 @@
. += span_notice("The internal cell has [internal_cell.percent()]% of its total charge.")
else
. += span_notice("Warning, no cell installed, use a screwdriver to open the hatch and insert one.")
- if(cell_container_opened)
- . += span_notice("Cell hatch open, close it with a screwdriver.")
+ if(panel_open)
+ . += span_notice("Hatch open, close it with a screwdriver.")
+ if(get_dist(user, src) <= 2)
+ if(rig)
+ . += span_warning("There is some kind of device rigged to the canister!")
+ else
+ . += span_notice("It looks like you could rig a device to the canister.")
// Please keep the canister types sorted
// Basic canister per gas below here
@@ -286,7 +333,7 @@
. += mutable_appearance('icons/obj/pipes_n_cables/canisters.dmi', "shielding")
. += emissive_appearance('icons/obj/pipes_n_cables/canisters.dmi', "shielding", src)
- if(cell_container_opened)
+ if(panel_open)
. += mutable_appearance('icons/obj/pipes_n_cables/canisters.dmi', "cell_hatch")
///Function is used to actually set the overlays
@@ -352,30 +399,69 @@
/obj/machinery/portable_atmospherics/canister/attackby(obj/item/item, mob/user, params)
if(istype(item, /obj/item/stock_parts/cell))
var/obj/item/stock_parts/cell/active_cell = item
- if(!cell_container_opened)
- balloon_alert(user, "open the hatch first")
+ if(!panel_open)
+ balloon_alert(user, "open hatch first!")
return TRUE
if(!user.transferItemToLoc(active_cell, src))
return TRUE
if(internal_cell)
user.put_in_hands(internal_cell)
- balloon_alert(user, "you successfully replace the cell")
+ balloon_alert(user, "you replace the cell")
else
- balloon_alert(user, "you successfully install the cell")
+ balloon_alert(user, "you install the cell")
internal_cell = active_cell
return TRUE
+ if(is_wire_tool(item) && panel_open)
+ wires.interact(user)
+ return
+ if(istype(item, /obj/item/assembly_holder))
+ if(rig)
+ balloon_alert(user, "another device is in the way!")
+ return ..()
+ var/obj/item/assembly_holder/holder = item
+
+ user.balloon_alert_to_viewers("attaching rig...")
+ add_fingerprint(user)
+ if(!do_after(user, 2 SECONDS, target = src) || !user.transferItemToLoc(holder, src))
+ return
+ rig = holder
+ holder.master = src
+ holder.on_attach()
+ assemblies_overlay = holder
+ assemblies_overlay.layer = 3.3
+ assemblies_overlay.pixel_y -= 5
+ add_overlay(assemblies_overlay)
+ log_bomber(user, "attached [holder.name] to ", src)
+ last_rigger = user
+ user.balloon_alert_to_viewers("attached rig")
+ return
return ..()
+/obj/machinery/portable_atmospherics/canister/attack_hand_secondary(mob/user, list/modifiers)
+ if(!rig)
+ return
+ // mousetrap rigs only make sense if you can set them off, can't step on them
+ // If you see a mousetrap-rigged fuel tank, just leave it alone
+ rig.on_found()
+ if(QDELETED(src))
+ return
+ user.balloon_alert_to_viewers("detaching rig...")
+ if(!do_after(user, 2 SECONDS, target = src))
+ return
+ user.balloon_alert_to_viewers("detached rig")
+ user.log_message("detached [rig] from [src].", LOG_GAME)
+ if(!user.put_in_hands(rig))
+ rig.forceMove(get_turf(user))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
/obj/machinery/portable_atmospherics/canister/screwdriver_act(mob/living/user, obj/item/screwdriver)
- screwdriver.play_tool_sound(src, 50)
- cell_container_opened = !cell_container_opened
- to_chat(user, span_notice("You [cell_container_opened ? "open" : "close"] the cell container hatch of [src]."))
- update_appearance()
- return ITEM_INTERACT_SUCCESS
+ if(default_deconstruction_screwdriver(user, icon_state, icon_state, screwdriver))
+ update_appearance()
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/portable_atmospherics/canister/crowbar_act(mob/living/user, obj/item/tool)
- if(!cell_container_opened || !internal_cell)
+ if(!panel_open || !internal_cell)
return ITEM_INTERACT_BLOCKING
internal_cell.forceMove(drop_location())
@@ -606,64 +692,19 @@
investigate_log("was set to [release_pressure] kPa by [key_name(usr)].", INVESTIGATE_ATMOS)
if("valve")
- var/logmsg
- var/admin_msg
- var/danger = FALSE
- var/n = 0
- valve_open = !valve_open
- if(valve_open)
- SSair.start_processing_machine(src)
- logmsg = "Valve was opened by [key_name(usr)], starting a transfer into \the [holding || "air"].
"
- if(!holding)
- var/list/gaseslog = list() //list for logging all gases in canister
- for(var/id in air_contents.gases)
- var/gas = air_contents.gases[id]
- gaseslog[gas[GAS_META][META_GAS_NAME]] = gas[MOLES] //adds gases to gaseslog
- if(!gas[GAS_META][META_GAS_DANGER])
- continue
- if(gas[MOLES] > (gas[GAS_META][META_GAS_MOLES_VISIBLE] || MOLES_GAS_VISIBLE)) //if moles_visible is undefined, default to default visibility
- danger = TRUE //at least 1 danger gas
- logmsg = "[key_name(usr)] opened a canister that contains the following:"
- admin_msg = "[ADMIN_LOOKUPFLW(usr)] opened a canister that contains the following at [ADMIN_VERBOSEJMP(src)]:"
- for(var/name in gaseslog)
- n = n + 1
- logmsg += "\n[name]: [gaseslog[name]] moles."
- if(n <= 5) //the first five gases added
- admin_msg += "\n[name]: [gaseslog[name]] moles."
- if(n == 5 && length(gaseslog) > 5) //message added if more than 5 gases
- admin_msg += "\nToo many gases to log. Check investigate log."
- if(danger) //sent to admin's chat if contains dangerous gases
- message_admins(admin_msg)
- else
- logmsg = "valve was closed by [key_name(usr)], stopping the transfer into \the [holding || "air"].
"
- investigate_log(logmsg, INVESTIGATE_ATMOS)
- release_log += logmsg
+ toggle_valve(usr)
. = TRUE
if("eject")
- if(holding)
- if(valve_open)
- message_admins("[ADMIN_LOOKUPFLW(usr)] removed [holding] from [src] with valve still open at [ADMIN_VERBOSEJMP(src)] releasing contents into the [span_boldannounce("air")].")
- usr.investigate_log("removed the [holding], leaving the valve open and transferring into the [span_boldannounce("air")].", INVESTIGATE_ATMOS)
- replace_tank(usr, FALSE)
+ if(eject_tank(usr))
. = TRUE
if("shielding")
- shielding_powered = !shielding_powered
- SSair.start_processing_machine(src)
- message_admins("[ADMIN_LOOKUPFLW(usr)] turned [shielding_powered ? "on" : "off"] the [src] powered shielding.")
- usr.investigate_log("turned [shielding_powered ? "on" : "off"] the [src] powered shielding.")
- update_appearance()
+ toggle_shielding(usr)
. = TRUE
if("reaction_suppression")
- if(!nob_crystal_inserted)
- stack_trace("[usr] tried to toggle reaction suppression on a canister without a noblium crystal inside, possible href exploit attempt.")
- return
- suppress_reactions = !suppress_reactions
- SSair.start_processing_machine(src)
- message_admins("[ADMIN_LOOKUPFLW(usr)] turned [suppress_reactions ? "on" : "off"] the [src] reaction suppression.")
- usr.investigate_log("turned [suppress_reactions ? "on" : "off"] the [src] reaction suppression.")
+ toggle_reaction_suppression(usr)
. = TRUE
if("recolor")
@@ -682,6 +723,70 @@
update_appearance()
+
+/// Opens/closes the canister valve
+/obj/machinery/portable_atmospherics/canister/proc/toggle_valve(mob/user, wire_pulsed = FALSE)
+ var/logmsg
+ var/admin_msg
+ var/danger = FALSE
+ var/n = 0
+ valve_open = !valve_open
+ if(valve_open)
+ SSair.start_processing_machine(src)
+ logmsg = "Valve was opened by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], starting a transfer into \the [holding || "air"].
"
+ if(!holding)
+ var/list/gaseslog = list() //list for logging all gases in canister
+ for(var/id in air_contents.gases)
+ var/gas = air_contents.gases[id]
+ gaseslog[gas[GAS_META][META_GAS_NAME]] = gas[MOLES] //adds gases to gaseslog
+ if(!gas[GAS_META][META_GAS_DANGER])
+ continue
+ if(gas[MOLES] > (gas[GAS_META][META_GAS_MOLES_VISIBLE] || MOLES_GAS_VISIBLE)) //if moles_visible is undefined, default to default visibility
+ danger = TRUE //at least 1 danger gas
+ logmsg = "[key_name(user)] opened a canister [wire_pulsed ? "via wire pulse" : ""] that contains the following:"
+ admin_msg = "[ADMIN_LOOKUPFLW(user)] opened a canister [wire_pulsed ? "via wire pulse" : ""] that contains the following at [ADMIN_VERBOSEJMP(src)]:"
+ for(var/name in gaseslog)
+ n = n + 1
+ logmsg += "\n[name]: [gaseslog[name]] moles."
+ if(n <= 5) //the first five gases added
+ admin_msg += "\n[name]: [gaseslog[name]] moles."
+ if(n == 5 && length(gaseslog) > 5) //message added if more than 5 gases
+ admin_msg += "\nToo many gases to log. Check investigate log."
+ if(danger) //sent to admin's chat if contains dangerous gases
+ message_admins(admin_msg)
+ else
+ logmsg = "valve was closed by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], stopping the transfer into \the [holding || "air"].
"
+ investigate_log(logmsg, INVESTIGATE_ATMOS)
+ release_log += logmsg
+
+/// Turns canister shielding on or off
+/obj/machinery/portable_atmospherics/canister/proc/toggle_shielding(mob/user, wire_pulsed = FALSE)
+ shielding_powered = !shielding_powered
+ SSair.start_processing_machine(src)
+ message_admins("[ADMIN_LOOKUPFLW(user)] turned [shielding_powered ? "on" : "off"] [wire_pulsed ? "via wire pulse" : ""] the [src] powered shielding.")
+ user.investigate_log("turned [shielding_powered ? "on" : "off"] [wire_pulsed ? "via wire pulse" : ""] the [src] powered shielding.")
+ update_appearance()
+
+/// Ejects tank from canister, if any
+/obj/machinery/portable_atmospherics/canister/proc/eject_tank(mob/user, wire_pulsed = FALSE)
+ if(holding)
+ if(valve_open)
+ message_admins("[ADMIN_LOOKUPFLW(user)] removed [holding] from [src] with valve still open [wire_pulsed ? "via wire pulse" : ""] at [ADMIN_VERBOSEJMP(src)] releasing contents into the [span_boldannounce("air")].")
+ user.investigate_log("removed the [holding] [wire_pulsed ? "via wire pulse" : ""], leaving the valve open and transferring into the [span_boldannounce("air")].", INVESTIGATE_ATMOS)
+ replace_tank(user, FALSE)
+ return TRUE
+
+/// Turns hyper-noblium crystal reaction suppression in the canister on or off
+/obj/machinery/portable_atmospherics/canister/proc/toggle_reaction_suppression(mob/user, wire_pulsed = FALSE)
+ if(!nob_crystal_inserted)
+ if(!wire_pulsed)
+ stack_trace("[user] tried to toggle reaction suppression on a canister without a noblium crystal inside and without pulsing wires, possible href exploit attempt.")
+ return
+ suppress_reactions = !suppress_reactions
+ SSair.start_processing_machine(src)
+ message_admins("[ADMIN_LOOKUPFLW(user)] turned [suppress_reactions ? "on" : "off"] [wire_pulsed ? "via wire pulse" : ""] the [src] reaction suppression.")
+ user.investigate_log("turned [suppress_reactions ? "on" : "off"] [wire_pulsed ? "via wire pulse" : ""] the [src] reaction suppression.")
+
/obj/machinery/portable_atmospherics/canister/proc/recolor(datum/greyscale_modify_menu/menu)
set_greyscale(menu.split_colors, menu.config.type)
diff --git a/tgstation.dme b/tgstation.dme
index 9065b3f5208..57300cb18b0 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1811,6 +1811,7 @@
#include "code\datums\wires\airlock.dm"
#include "code\datums\wires\apc.dm"
#include "code\datums\wires\autolathe.dm"
+#include "code\datums\wires\canister.dm"
#include "code\datums\wires\conveyor.dm"
#include "code\datums\wires\ecto_sniffer.dm"
#include "code\datums\wires\emitter.dm"