From be5931cf8ed9026de02626efa2c9cb2bf0d66575 Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Tue, 24 Feb 2026 19:53:19 -0600
Subject: [PATCH] Adds USB to vitals monitor (#95079)
## About The Pull Request
Vitals monitors have a USB port, allowing you to read patient status
Collects patient stats every 4 seconds, with a 1 second delay before its
initial reading is recorded.
`Status update` triggers every time stats are collected, so every 4
seconds. Basically a clock provided for convenience
`Patient change` triggers when a mob is bucked or unbuckled.
`Defib status` checks if the patient can be defibbed and outputs
"Possible" if they are, or a reason if they aren't.
The rest of the outputs are self explanatory.
## Why It's Good For The Game
Someone talked about this before the original PR was merged and I
thought it was a fun idea.
Allows for stuff like "Trigger a bomb when a certain patient is revived"
or "creating a BCI for a blind doctor".
## Changelog
:cl: Melbert
add: Vitals monitors have USB ports, allowing circuits to read patient
status
/:cl:
---
code/__DEFINES/dcs/signals/signals_object.dm | 3 +
code/game/machinery/wall_vitals.dm | 234 ++++++++++++++----
code/modules/mob/living/carbon/carbon.dm | 1 +
.../modules/wiremod/components/atom/health.dm | 3 +-
4 files changed, 187 insertions(+), 54 deletions(-)
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 91c637a9492..62573bad430 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -639,6 +639,9 @@
/// Sent from /obj/item/mob_holder/purple_raptor/proc/toggle_wings() : (mob/living/carbon/human/user)
#define COMSIG_RAPTOR_WINGS_CLOSED "raptor_wings_closed"
+/// Sent from /obj/machinery/vitals_monitor/proc/set_patient() : (mob/living/old_patient, mob/living/new_patient)
+#define COMSIG_VITALS_SET_PATIENT "vitals_set_patient"
+
/// Sent from /obj/item/bodypart/limb/proc/apply_item(): (obj/item/bodypart/limb)
#define COMSIG_ITEM_APPLIED_TO_LIMB "item_applied_to_limb"
/// Sent from /obj/item/bodypart/limb/proc/Exited(): (obj/item/bodypart/limb)
diff --git a/code/game/machinery/wall_vitals.dm b/code/game/machinery/wall_vitals.dm
index 51790a1e7bc..beb8ecc1d7d 100644
--- a/code/game/machinery/wall_vitals.dm
+++ b/code/game/machinery/wall_vitals.dm
@@ -95,6 +95,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
/obj/machinery/vitals_reader/Initialize(mapload, obj/item/circuitboard/C)
. = ..()
+ AddComponent(/datum/component/usb_port, typecacheof(list(/obj/item/circuit_component/vitals_monitor), only_root_path = TRUE))
register_context()
return INITIALIZE_HINT_LATELOAD
@@ -105,16 +106,13 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
set_light_on(TRUE)
/obj/machinery/vitals_reader/Destroy(force)
- unset_connection()
- unset_patient() // unset connection also unsets patient, but just in case
+ set_connection(null)
+ set_patient(null) // unset connection also unsets patient, but just in case
return ..()
-/obj/machinery/vitals_reader/proc/unset_connected(...)
+/obj/machinery/vitals_reader/proc/comsig_unset_connected(...)
SIGNAL_HANDLER
- UnregisterSignal(connected, COMSIG_QDELETING)
- UnregisterSignal(connected, list(COMSIG_OPERATING_TABLE_SET_PATIENT, COMSIG_MACHINERY_SET_OCCUPANT))
- UnregisterSignal(connected, COMSIG_MOVABLE_MOVED)
- connected = null
+ set_connection(null)
/obj/machinery/vitals_reader/proc/connected_occupant_changed(datum/source, mob/living/new_patient)
SIGNAL_HANDLER
@@ -124,26 +122,25 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
/obj/machinery/vitals_reader/proc/connected_moved(...)
SIGNAL_HANDLER
if(get_dist(src, connected) > connection_range)
- unset_connected()
+ set_connection(null)
-/obj/machinery/vitals_reader/proc/unset_connection(...)
- SIGNAL_HANDLER
- if(isnull(connected))
+/// Sets the machine as connected to this vitals reader
+/// If there is already a connection, it will be unset first.
+/obj/machinery/vitals_reader/proc/set_connection(obj/new_connected)
+ if(connected == new_connected)
+ return
+ if(!isnull(connected))
+ UnregisterSignal(connected, COMSIG_QDELETING)
+ UnregisterSignal(connected, list(COMSIG_OPERATING_TABLE_SET_PATIENT, COMSIG_MACHINERY_SET_OCCUPANT))
+ UnregisterSignal(connected, COMSIG_MOVABLE_MOVED)
+ connected = null
+ set_patient(null)
+ update_use_power(NO_POWER_USE)
+ if(isnull(new_connected))
return
- UnregisterSignal(connected, COMSIG_QDELETING)
- UnregisterSignal(connected, list(COMSIG_OPERATING_TABLE_SET_PATIENT, COMSIG_MACHINERY_SET_OCCUPANT))
- UnregisterSignal(connected, COMSIG_MOVABLE_MOVED)
- connected = null
- unset_patient()
- update_use_power(NO_POWER_USE)
-
-/obj/machinery/vitals_reader/proc/set_connection(obj/new_connected)
- if(!isnull(connected))
- unset_connected()
-
connected = new_connected
- RegisterSignal(connected, COMSIG_QDELETING, PROC_REF(unset_connected))
+ RegisterSignal(connected, COMSIG_QDELETING, PROC_REF(comsig_unset_connected))
RegisterSignals(connected, list(COMSIG_OPERATING_TABLE_SET_PATIENT, COMSIG_MACHINERY_SET_OCCUPANT), PROC_REF(connected_occupant_changed))
RegisterSignal(connected, COMSIG_MOVABLE_MOVED, PROC_REF(connected_moved))
update_use_power(IDLE_POWER_USE)
@@ -178,7 +175,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
return ITEM_INTERACT_BLOCKING
balloon_alert(user, "disconnected from [connected.name]")
- unset_connection()
+ set_connection(null)
return ITEM_INTERACT_SUCCESS
/// Find and connects to a nearby machine
@@ -217,14 +214,14 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
Use a [EXAMINE_HINT("multitool")] to connect it to a neighboring machine.")
return
- if(user.is_blind())
+ if(isnull(patient) || user.is_blind())
return
if(machine_stat & EMPED)
. += span_warning("The display is flickering erratically!")
return
- if(!issilicon(user) && !isobserver(user) && get_dist(patient, user) > 2)
+ if(!issilicon(user) && !isobserver(user) && get_dist(src, user) > 2)
. += span_notice("You are too far away to read the display.")
else if(HAS_TRAIT(user, TRAIT_DUMB) || !user.can_read(src, reading_check_flags = READING_CHECK_LITERACY, silent = TRUE))
. += span_warning("You try to comprehend the display, but it's too complex for you to understand.")
@@ -468,15 +465,28 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
/obj/machinery/vitals_reader/proc/set_patient(mob/living/new_patient)
if(patient == new_patient)
return
- if(isnull(new_patient))
- unset_patient()
- return
+ SEND_SIGNAL(src, COMSIG_VITALS_SET_PATIENT, patient, new_patient)
+ if(!isnull(patient))
+ UnregisterSignal(patient, list(
+ COMSIG_QDELETING,
+ COMSIG_CARBON_POST_REMOVE_LIMB,
+ COMSIG_CARBON_POST_ATTACH_LIMB,
+ COMSIG_LIVING_HEALTH_UPDATE,
+ COMSIG_LIVING_UPDATE_BLOOD_STATUS,
+ ))
- if(isnull(patient))
- unset_patient()
+ patient = null
+ end_processing()
+ if(!QDELING(src))
+ update_use_power(IDLE_POWER_USE)
+ update_appearance()
+ if(isnull(new_patient))
+ return
+ if(QDELING(src))
+ CRASH("[src] is qdeleting but tried to set a new patient ([new_patient])!")
patient = new_patient
- RegisterSignal(patient, COMSIG_QDELETING, PROC_REF(unset_patient))
+ RegisterSignal(patient, COMSIG_QDELETING, PROC_REF(comsig_unset_patient))
RegisterSignals(patient, list(
COMSIG_CARBON_POST_REMOVE_LIMB,
COMSIG_CARBON_POST_ATTACH_LIMB,
@@ -488,27 +498,9 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
update_use_power(ACTIVE_POWER_USE)
last_reported_stat = null
-/// Unset the current patient.
-/obj/machinery/vitals_reader/proc/unset_patient(...)
+/obj/machinery/vitals_reader/proc/comsig_unset_patient(...)
SIGNAL_HANDLER
- if(isnull(patient))
- return
-
- UnregisterSignal(patient, list(
- COMSIG_QDELETING,
- COMSIG_CARBON_POST_REMOVE_LIMB,
- COMSIG_CARBON_POST_ATTACH_LIMB,
- COMSIG_LIVING_HEALTH_UPDATE,
- COMSIG_LIVING_UPDATE_BLOOD_STATUS,
- ))
-
- patient = null
- end_processing()
- update_use_power(IDLE_POWER_USE)
- if(QDELING(src))
- return
-
- update_appearance()
+ set_patient(null)
/// Signal proc to update the display when a signal is received.
/obj/machinery/vitals_reader/proc/update_overlay_on_signal(...)
@@ -536,3 +528,141 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/vitals_reader/advanced, 32)
playsound(src, 'sound/effects/glass/glasshit.ogg', 75, TRUE)
if(BURN)
playsound(src, 'sound/items/tools/welder.ogg', 100, TRUE)
+
+/obj/item/circuit_component/vitals_monitor
+ display_name = "Vitals Monitor"
+ desc = "Allows you to interface with a vitals monitor to read a patient's status."
+
+ /// Reference to the linked vitals monitor
+ var/obj/machinery/vitals_reader/linked_monitor
+
+ /// Cooldown length between patient stat reads
+ var/read_cooldown_period = 4 SECONDS
+ /// Cooldown for reading patient stats
+ COOLDOWN_DECLARE(read_cooldown)
+
+ /// Outputs a signal when the vitals have been updated
+ var/datum/port/output/status_updated
+ /// Outputs a signal when the patient being monitored has changed
+ var/datum/port/output/patient_changed
+
+ /// Outputs the patient being monitored
+ var/datum/port/output/patient_entity
+ /// Outputs a string corresponding to how defibbing would go
+ var/datum/port/output/defib_state
+
+ /// Outputs total brute damage
+ var/datum/port/output/patient_brute_damage
+ /// Outputs total burn damage
+ var/datum/port/output/patient_burn_damage
+ /// Outputs total oxyloss damage
+ var/datum/port/output/patient_oxy_loss
+ /// Outputs total toxin damage
+ var/datum/port/output/patient_tox_loss
+ /// Outputs overall health
+ var/datum/port/output/patient_overall_health
+ /// Outputs brain damage
+ var/datum/port/output/patient_brain_damage
+ /// Outputs blood level
+ var/datum/port/output/patient_blood_level
+
+
+/obj/item/circuit_component/vitals_monitor/Destroy()
+ STOP_PROCESSING(SSclock_component, src)
+ return ..()
+
+/obj/item/circuit_component/vitals_monitor/populate_ports()
+ status_updated = add_output_port("Status Updated", PORT_TYPE_SIGNAL)
+ patient_changed = add_output_port("Patient Changed", PORT_TYPE_SIGNAL)
+
+ patient_entity = add_output_port("Patient", PORT_TYPE_ATOM)
+ defib_state = add_output_port("Defib Status", PORT_TYPE_STRING)
+
+ patient_overall_health = add_output_port("Overall Health", PORT_TYPE_NUMBER)
+ patient_brute_damage = add_output_port("Brute Damage", PORT_TYPE_NUMBER)
+ patient_burn_damage = add_output_port("Burn Damage", PORT_TYPE_NUMBER)
+ patient_oxy_loss = add_output_port("Suffocation Damage", PORT_TYPE_NUMBER)
+ patient_tox_loss = add_output_port("Toxin Damage", PORT_TYPE_NUMBER)
+ patient_brain_damage = add_output_port("Brain Damage", PORT_TYPE_NUMBER)
+ patient_blood_level = add_output_port("Blood Level", PORT_TYPE_NUMBER)
+
+/obj/item/circuit_component/vitals_monitor/register_usb_parent(atom/movable/shell)
+ . = ..()
+ linked_monitor = shell
+ RegisterSignal(shell, COMSIG_VITALS_SET_PATIENT, PROC_REF(handle_patient_change))
+
+ var/obj/machinery/vitals_reader/monitor = shell
+ if(!isnull(monitor.patient))
+ START_PROCESSING(SSclock_component, src)
+
+/obj/item/circuit_component/vitals_monitor/unregister_usb_parent(atom/movable/shell)
+ . = ..()
+ linked_monitor = null
+ UnregisterSignal(shell, COMSIG_VITALS_SET_PATIENT)
+ STOP_PROCESSING(SSclock_component, src)
+
+/obj/item/circuit_component/vitals_monitor/get_ui_notices()
+ . = ..()
+ . += create_ui_notice("Initial Delay: [DisplayTimeText(1 SECONDS)]", "orange", "clock")
+ . += create_ui_notice("Update Interval: [DisplayTimeText(read_cooldown_period)]", "orange", "clock")
+
+/obj/item/circuit_component/vitals_monitor/proc/handle_patient_change(datum/source, mob/living/old_patient, mob/living/new_patient)
+ SIGNAL_HANDLER
+ if(isnull(new_patient))
+ STOP_PROCESSING(SSclock_component, src)
+ else
+ START_PROCESSING(SSclock_component, src)
+ patient_entity.set_output(new_patient)
+ patient_changed.set_output(COMPONENT_SIGNAL)
+ // wait a small amount (arbitrary) to provide initial data
+ COOLDOWN_START(src, read_cooldown, 1 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(delayed_reading)), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME)
+
+/// Reads a patient value and applies a random amount of error if the monitor is emped or emagged
+#define GET_PATIENT_VALUE(monitor, value_to_get) \
+ round(monitor.patient.##value_to_get * ((monitor.machine_stat & (EMPED|EMAGGED)) ? rand(0, 100) * 0.01 : 1), DAMAGE_PRECISION)
+
+/obj/item/circuit_component/vitals_monitor/process(seconds_per_tick)
+ if(isnull(linked_monitor?.patient))
+ return PROCESS_KILL
+ if(!COOLDOWN_FINISHED(src, read_cooldown))
+ return
+ update_output_stats()
+
+/obj/item/circuit_component/vitals_monitor/proc/delayed_reading()
+ if(isnull(linked_monitor?.patient))
+ return
+ update_output_stats()
+
+/obj/item/circuit_component/vitals_monitor/proc/update_output_stats()
+ COOLDOWN_START(src, read_cooldown, read_cooldown_period)
+ status_updated.set_output(COMPONENT_SIGNAL)
+
+ patient_overall_health.set_output(GET_PATIENT_VALUE(linked_monitor, health))
+ patient_brute_damage.set_output(GET_PATIENT_VALUE(linked_monitor, get_brute_loss()))
+ patient_burn_damage.set_output(GET_PATIENT_VALUE(linked_monitor, get_fire_loss()))
+ patient_oxy_loss.set_output(GET_PATIENT_VALUE(linked_monitor, get_oxy_loss()))
+ patient_tox_loss.set_output(GET_PATIENT_VALUE(linked_monitor, get_tox_loss()))
+ patient_brain_damage.set_output(GET_PATIENT_VALUE(linked_monitor, get_organ_loss(ORGAN_SLOT_BRAIN)))
+ patient_blood_level.set_output(GET_PATIENT_VALUE(linked_monitor, get_blood_volume(TRUE)))
+
+ var/is_defib_possible = DEFIB_POSSIBLE
+ if(iscarbon(linked_monitor.patient))
+ var/mob/living/carbon/carbo_patient = linked_monitor.patient
+ is_defib_possible = carbo_patient.can_defib()
+
+ switch(is_defib_possible)
+ if(DEFIB_POSSIBLE)
+ defib_state.set_output("Possible")
+ if(DEFIB_FAIL_SUICIDE, DEFIB_FAIL_BLACKLISTED, DEFIB_FAIL_NO_INTELLIGENCE, DEFIB_FAIL_GOLEM)
+ defib_state.set_output("Impossible")
+ if(DEFIB_FAIL_HUSK)
+ defib_state.set_output("Husked")
+ if(DEFIB_FAIL_TISSUE_DAMAGE)
+ defib_state.set_output("Damaged")
+ if(DEFIB_FAIL_FAILING_HEART, DEFIB_FAIL_NO_HEART)
+ defib_state.set_output("Heart Failure")
+ if(DEFIB_FAIL_FAILING_BRAIN, DEFIB_FAIL_NO_BRAIN)
+ defib_state.set_output("Brain Failure")
+
+#undef GET_PATIENT_VALUE
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 334bdce8d92..d1ee541429f 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -803,6 +803,7 @@
return ..()
/mob/living/carbon/proc/can_defib()
+ SHOULD_BE_PURE(TRUE)
if (HAS_TRAIT(src, TRAIT_SUICIDED))
return DEFIB_FAIL_SUICIDE
diff --git a/code/modules/wiremod/components/atom/health.dm b/code/modules/wiremod/components/atom/health.dm
index 5f071d2758f..8a614066af6 100644
--- a/code/modules/wiremod/components/atom/health.dm
+++ b/code/modules/wiremod/components/atom/health.dm
@@ -37,7 +37,7 @@
burn = add_output_port("Burn Damage", PORT_TYPE_NUMBER)
toxin = add_output_port("Toxin Damage", PORT_TYPE_NUMBER)
oxy = add_output_port("Suffocation Damage", PORT_TYPE_NUMBER)
- health = add_output_port("Health", PORT_TYPE_NUMBER)
+ health = add_output_port("Overall Health", PORT_TYPE_NUMBER)
/obj/item/circuit_component/health/input_received(datum/port/input/port)
@@ -57,4 +57,3 @@
toxin.set_output(organism.get_tox_loss())
oxy.set_output(organism.get_oxy_loss())
health.set_output(organism.health)
-