Skip to content

Commit

Permalink
Fire/atmosphere alarm reasons (#6773)
Browse files Browse the repository at this point in the history
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request

Fire and atmosphere alarms will now report the reason for the alarm in
the alarm monitoring consoles/programs.

Fire alarms can either be manual or from the panel detecting a nearby
fire.

Air alarms can be manual (in which case no reason is listed), from the
alarm wire being cut (in which case "CIRCUIT FAULT" will be listed,
which also masks any other alarm reasons as the air alarm can't read its
sensors), or from any of its thresholds being exceeded. The air alarm
will, of course, respect any threshold changes and will update the alarm
reason periodically in case conditions change.

To ensure the air alarms would report every reason for the alarm, the
short-circuiting has been commented out from their threshold checks. If
this turns out to be a performance issue, they can always be
uncommented, but the alarm will then only report the first exceeded
threshold it finds.

Example video:



https://github.com/user-attachments/assets/a47146b6-3361-4ff1-954d-12f7005ea83b



## Why It's Good For The Game

This makes it much easier for engineering to determine the cause for an
alarm and bring the appropriate equipment. It's wacky seeing a random
air alarm in maintenance and bringing an air pump and breach control bag
when it turns out someone just left the heater on, etc.etc.

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

:cl:
add: Air and fire alarms will now report the reason for the alarm in the
alarm monitoring consoles/programs.
/:cl:

<!-- Both :cl:'s are required for the changelog to work! You can put
your name to the right of the first :cl: if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->
  • Loading branch information
Shadowtail117 authored Oct 3, 2024
1 parent 72cfcde commit 84812a4
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 33 deletions.
1 change: 1 addition & 0 deletions code/__DEFINES/atmospherics/machinery/alarm.dm
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
AIR_ALARM_RAISE_DANGER : ((val < list[AIR_ALARM_TLV_WARNING_LOW] || val > list[AIR_ALARM_TLV_WARNING_HIGH]+0.001)? \
AIR_ALARM_RAISE_WARNING : AIR_ALARM_RAISE_OKAY) \
)
#define AIR_ALARM_GET_REASON_TLV(val, list, name) (val < list[AIR_ALARM_TLV_WARNING_LOW]) ? "Low [name]" : (val > list[AIR_ALARM_TLV_WARNING_HIGH]) ? "High [name]" : null
2 changes: 1 addition & 1 deletion code/datums/wires/alarm.dm
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
A.apply_mode()

if(WIRE_AALARM)
if(A.alarm_area.atmosalert(2, A))
if(A.alarm_area.atmosalert(2, A, list("CIRCUIT FAULT")))
A.post_alert(2)
A.update_icon()
..()
Expand Down
6 changes: 3 additions & 3 deletions code/game/area/area.dm
Original file line number Diff line number Diff line change
Expand Up @@ -297,15 +297,15 @@
cameras += C
return cameras

/area/proc/atmosalert(danger_level, var/alarm_source)
/area/proc/atmosalert(danger_level, var/alarm_source, var/list/why)
if (danger_level == 0)
atmosphere_alarm.clearAlarm(src, alarm_source)
else
var/obj/machinery/air_alarm/atmosalarm = alarm_source //maybe other things can trigger these, who knows
if(istype(atmosalarm))
atmosphere_alarm.triggerAlarm(src, alarm_source, severity = danger_level, hidden = atmosalarm.alarms_hidden)
atmosphere_alarm.triggerAlarm(src, alarm_source, severity = danger_level, hidden = atmosalarm.alarms_hidden, reasons = why)
else
atmosphere_alarm.triggerAlarm(src, alarm_source, severity = danger_level)
atmosphere_alarm.triggerAlarm(src, alarm_source, severity = danger_level, reasons = why)

//Check all the alarms before lowering atmosalm. Raising is perfectly fine.
for (var/obj/machinery/air_alarm/AA as anything in GLOB.air_alarms)
Expand Down
13 changes: 7 additions & 6 deletions code/game/machinery/fire_alarm.dm
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/fire_alarm/alarms_hidden, 21)

/obj/machinery/fire_alarm/on_bullet_act(obj/projectile/proj, impact_flags, list/bullet_act_args)
. = ..()
alarm()
alarm(manual = TRUE)

/obj/machinery/fire_alarm/emp_act(severity)
if(prob(50 / severity))
Expand Down Expand Up @@ -151,7 +151,7 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/fire_alarm/alarms_hidden, 21)
SPAN_NOTICE("You have disconnected [src]'s detecting unit."))
return

alarm()
alarm(manual = TRUE)

/obj/machinery/fire_alarm/process()//Note: this processing was mostly phased out due to other code, and only runs when needed
if(machine_stat & (NOPOWER|BROKEN))
Expand All @@ -161,7 +161,7 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/fire_alarm/alarms_hidden, 21)
if(time > 0)
time = time - ((world.timeofday - last_process) / 10)
else
alarm()
alarm(manual = TRUE)
time = 0
timing = 0
STOP_PROCESSING(SSobj, src)
Expand Down Expand Up @@ -229,7 +229,7 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/fire_alarm/alarms_hidden, 21)
if(href_list["reset"])
reset()
else if(href_list["alarm"])
alarm()
alarm(manual = TRUE)
else if(href_list["time"])
timing = text2num(href_list["time"])
last_process = world.timeofday
Expand All @@ -256,12 +256,13 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/fire_alarm/alarms_hidden, 21)
update_icon()
return

/obj/machinery/fire_alarm/proc/alarm(var/duration = 0)
/obj/machinery/fire_alarm/proc/alarm(var/duration = 0, var/manual = FALSE)
if(!(working))
return
var/area/area = get_area(src)
for(var/obj/machinery/fire_alarm/FA in area)
fire_alarm.triggerAlarm(loc, FA, duration, hidden = alarms_hidden)
var/msg = manual ? "Manual" : "Fire Detected"
fire_alarm.triggerAlarm(loc, FA, duration, hidden = alarms_hidden, reasons = list(msg))
update_icon()
playsound(src.loc, 'sound/machines/airalarm.ogg', 25, 0, 4)

Expand Down
20 changes: 11 additions & 9 deletions code/modules/alarm/alarm.dm
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/// How long will the alarm/trigger remain active once origin/source has been found to be gone?
#define ALARM_RESET_DELAY 100
/datum/alarm_source
var/source = null // The source trigger
var/source_name = "" // The name of the source should it be lost (for example a destroyed camera)
var/duration = 0 // How long this source will be alarming, 0 for indefinetely.
var/severity = 1 // How severe the alarm from this source is.
var/start_time = 0 // When this source began alarming.
var/end_time = 0 // Use to set when this trigger should clear, in case the source is lost.
var/source = null // The source trigger
var/source_name = "" // The name of the source should it be lost (for example a destroyed camera)
var/duration = 0 // How long this source will be alarming, 0 for indefinetely.
var/severity = 1 // How severe the alarm from this source is.
var/start_time = 0 // When this source began alarming.
var/end_time = 0 // Use to set when this trigger should clear, in case the source is lost.
var/list/reasons = new() // The reason(s) for the alarm (for fire and atmosphere alarms).

/datum/alarm_source/New(var/atom/source)
src.source = source
Expand All @@ -24,11 +25,11 @@
var/end_time //Used to set when this alarm should clear, in case the origin is lost.
var/hidden = FALSE //If this alarm can be seen from consoles or other things.

/datum/alarm/New(var/atom/origin, var/atom/source, var/duration, var/severity, var/hidden)
/datum/alarm/New(var/atom/origin, var/atom/source, var/duration, var/severity, var/hidden, var/list/reasons)
src.origin = origin

cameras() // Sets up both cameras and last alarm area.
set_source_data(source, duration, severity, hidden)
set_source_data(source, duration, severity, hidden, reasons)

/datum/alarm/process(delta_time)
// Has origin gone missing?
Expand All @@ -43,7 +44,7 @@
AS.duration = 0
AS.end_time = world.time + ALARM_RESET_DELAY

/datum/alarm/proc/set_source_data(var/atom/source, var/duration, var/severity, var/hidden)
/datum/alarm/proc/set_source_data(var/atom/source, var/duration, var/severity, var/hidden, var/list/reasons)
var/datum/alarm_source/AS = sources_assoc[source]
if(!AS)
AS = new/datum/alarm_source(source)
Expand All @@ -56,6 +57,7 @@
AS.duration = duration
AS.severity = severity
src.hidden = min(src.hidden, hidden)
AS.reasons = reasons

/datum/alarm/proc/clear(var/source)
var/datum/alarm_source/AS = sources_assoc[source]
Expand Down
6 changes: 3 additions & 3 deletions code/modules/alarm/alarm_handler.dm
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
A.process()
check_alarm_cleared(A)

/datum/alarm_handler/proc/triggerAlarm(var/atom/origin, var/atom/source, var/duration = 0, var/severity = 1, var/hidden = 0)
/datum/alarm_handler/proc/triggerAlarm(var/atom/origin, var/atom/source, var/duration = 0, var/severity = 1, var/hidden = 0, var/list/reasons)
var/new_alarm
//Proper origin and source mandatory
if(!(origin && source))
Expand All @@ -23,9 +23,9 @@
//see if there is already an alarm of this origin
var/datum/alarm/existing = alarms_assoc[origin]
if(existing)
existing.set_source_data(source, duration, severity, hidden)
existing.set_source_data(source, duration, severity, hidden, reasons)
else
existing = new/datum/alarm(origin, source, duration, severity, hidden)
existing = new/datum/alarm(origin, source, duration, severity, hidden, reasons)
new_alarm = 1

alarms |= existing
Expand Down
42 changes: 32 additions & 10 deletions code/modules/atmospherics/machinery/air_alarm.dm
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/air_alarm, 26)
///If the alarms from this machine are visible on consoles
var/alarms_hidden = FALSE

var/list/alarmreasons = new()

/obj/machinery/air_alarm/Initialize(mapload)
. = ..()
GLOB.air_alarms += src
Expand Down Expand Up @@ -152,6 +154,10 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/air_alarm, 26)
if (danger_level)
handle_sounds()

//Putting this check here to overwrite any previous alarm reasons if the wire was cut
if(WIRE_AALARM in wires.cut_wires)
alarmreasons = list("CIRCUIT FAULT")

if(old_level != danger_level)
apply_danger_level(danger_level)

Expand All @@ -176,6 +182,11 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/air_alarm, 26)
if(RCON_YES)
remote_control = 1

//This is extremely snowflakey but it's the simplest way to make sure the reasons constantly update even if there's already an alarm here
//Basically looking to see if there is already an alarm in this area triggered by this air alarm, and updating the reason for it.
var/datum/alarm_source/ouralarm = atmosphere_alarm.alarms_assoc[get_area(src)]?.sources_assoc[src]
if(ouralarm) ouralarm.reasons = alarmreasons

return

/obj/machinery/air_alarm/proc/handle_sounds()
Expand Down Expand Up @@ -248,29 +259,40 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/air_alarm, 26)
/obj/machinery/air_alarm/proc/overall_danger_level(datum/gas_mixture/environment)
var/environment_pressure = environment.return_pressure()
var/partial_pressure_factor = (R_IDEAL_GAS_EQUATION * environment.temperature) / environment.volume
alarmreasons = new()
var/reason

var/dangerlevel = AIR_ALARM_TEST_TLV(environment_pressure, tlv_pressure)
if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
return dangerlevel
reason = AIR_ALARM_GET_REASON_TLV(environment_pressure, tlv_pressure, "Pressure")
if(reason) alarmreasons += reason
// if(dangerlevel >= AIR_ALARM_RAISE_DANGER) These are being commented instead of deleted in case not short-circuiting this causes performance issues.
// return dangerlevel
dangerlevel = max(dangerlevel, AIR_ALARM_TEST_TLV(environment.temperature, tlv_temperature))
if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
return dangerlevel
reason = AIR_ALARM_GET_REASON_TLV(environment.temperature, tlv_temperature, "Temperature")
if(reason) alarmreasons += reason
// if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
// return dangerlevel

// todo: would be faster to iterate once and store the groups we care about...

for(var/id in tlv_ids)
var/list/tlv = tlv_ids[id]
var/partial = environment.gas[id] * partial_pressure_factor
dangerlevel = max(dangerlevel, AIR_ALARM_TEST_TLV(partial, tlv))
if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
return dangerlevel
var/datum/gas/gas_datum = global.gas_data.gases[id]
reason = AIR_ALARM_GET_REASON_TLV(partial, tlv, gas_datum.name)
if(reason) alarmreasons += reason
// if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
// return dangerlevel

for(var/name in tlv_groups)
var/list/tlv = tlv_groups[name]
var/partial = environment.moles_by_group(global.gas_data.gas_group_by_name[name]) * partial_pressure_factor
dangerlevel = max(dangerlevel, AIR_ALARM_TEST_TLV(partial, tlv))
if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
return dangerlevel
reason = AIR_ALARM_GET_REASON_TLV(partial, tlv, name)
if(reason) alarmreasons += reason
// if(dangerlevel >= AIR_ALARM_RAISE_DANGER)
// return dangerlevel

return dangerlevel

Expand Down Expand Up @@ -486,7 +508,7 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/air_alarm, 26)
send_signal(scrubber.id_tag, list("power" = FALSE))

/obj/machinery/air_alarm/proc/apply_danger_level(var/new_danger_level)
if(report_danger_level && alarm_area.atmosalert(new_danger_level, src))
if(report_danger_level && alarm_area.atmosalert(new_danger_level, src, alarmreasons))
post_alert(new_danger_level)

update_icon()
Expand Down Expand Up @@ -700,7 +722,7 @@ CREATE_WALL_MOUNTING_TYPES_SHIFTED(/obj/machinery/air_alarm, 26)
return TRUE
if("alarm")
//! warning: legacy
if(alarm_area.atmosalert(2, src))
if(alarm_area.atmosalert(2, src, list("Manual")))
apply_danger_level(2)
return TRUE
if("reset")
Expand Down
5 changes: 4 additions & 1 deletion code/modules/tgui/modules/alarm.dm
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,19 @@
for(var/datum/alarm/A in AH.visible_alarms(z))
var/cameras[0]
var/lost_sources[0]
var/list/reasons = list()

if(isAI(user))
for(var/obj/machinery/camera/C in A.cameras())
cameras[++cameras.len] = C.ui_structure()
for(var/datum/alarm_source/AS in A.sources)
if(!AS.source)
lost_sources[++lost_sources.len] = AS.source_name
reasons |= AS.reasons //No duplicates
reasons = reasons.Join(", ")

categories[categories.len]["alarms"] += list(list(
"name" = "[A.alarm_name()]" + "[A.max_severity() > 1 ? "(MAJOR)" : ""]",
"name" = "[A.alarm_name()]" + "[A.max_severity() > 1 ? " (MAJOR)" : ""]" + "[reasons ? " ([reasons])" : ""]",
"origin_lost" = A.origin == null,
"has_cameras" = cameras.len,
"cameras" = cameras,
Expand Down

0 comments on commit 84812a4

Please sign in to comment.