From 646d4492d543447d740d8c0be6094dbf8a2fc00d Mon Sep 17 00:00:00 2001
From: NovaBot <154629622+NovaBot13@users.noreply.github.com>
Date: Sat, 8 Jun 2024 10:21:43 -0400
Subject: [PATCH] [MIRROR] Add circuit component cameras [MDB IGNORE] (#2926)
* Add circuit component cameras (#83578)
## About The Pull Request
This PR introduces a limited set of camera components that can be used
by surveillance security consoles and the PDA/laptop camera app.
There is four camera components, each limited to a specified shell
circuit type.
Additionally, drone circuit shells can now use the recharge stations
too, much like how mobs with BCIs can recharge.
### New Components
* Drone Camera
This camera component captures the surrounding area. It has an option to
set the camera range (near 5x5/far 14x14).
* BCI Camera
This camera component uses the active user's eyes as a camera function.
If the user's sights are damaged, the range will be forced to the near
setting. If the user is unconscious/dead/blinded or has no eyes, the
stream will be cut off.
It has an option to set the camera range (near 5x5/far 14x14).
* Polaroid Camera Add-On
This camera component streams to a camera network. The camera range is
hardcoded to the near setting (5x5).
* Airlock Camera
This camera component streams to a camera network. The camera range is
hardcoded to the near setting (5x5).
### Features
* The cameras can be EMP'd and will be disabled for 90 seconds if
successful
* When the cameras are active, they will actively drain the cell's power
per second (near range uses 3kJ & far range uses 8kJ)
* Advance camera console/AIs can use these cameras, however the camera
light is disabled (they will be useless in dark areas)
### Screenshots In Action
This is the drone camera viewed on a security camera console
This is the same drone, now set to the far range setting
## Why It's Good For The Game
This promotes emergent gameplay and improves the overall usefulness for
drones as they can be 100% used remotely.
## Changelog
:cl:
add: Added new circuit camera components
qol: Circuit drones can now recharge at recharge stations
/:cl:
---------
Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
* Add circuit component cameras
---------
Co-authored-by: tmyqlfpir <80724828+tmyqlfpir@users.noreply.github.com>
Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
Co-authored-by: NovaBot13
---
code/game/machinery/rechargestation.dm | 2 +-
code/modules/photography/camera/camera.dm | 2 +-
.../research/designs/wiremod_designs.dm | 5 +
code/modules/research/techweb/all_nodes.dm | 1 +
.../wiremod/components/atom/remotecam.dm | 438 ++++++++++++++++++
code/modules/wiremod/shell/airlock.dm | 2 +-
code/modules/wiremod/shell/drone.dm | 3 +-
tgstation.dme | 1 +
8 files changed, 450 insertions(+), 4 deletions(-)
create mode 100644 code/modules/wiremod/components/atom/remotecam.dm
diff --git a/code/game/machinery/rechargestation.dm b/code/game/machinery/rechargestation.dm
index 2a2f219e618..4e16a1946c7 100644
--- a/code/game/machinery/rechargestation.dm
+++ b/code/game/machinery/rechargestation.dm
@@ -8,7 +8,7 @@
req_access = list(ACCESS_ROBOTICS)
state_open = TRUE
circuit = /obj/item/circuitboard/machine/cyborgrecharger
- occupant_typecache = list(/mob/living/silicon/robot, /mob/living/carbon/human)
+ occupant_typecache = list(/mob/living/silicon/robot, /mob/living/carbon/human, /mob/living/circuit_drone)
processing_flags = NONE
var/recharge_speed
var/repairs
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index c8159de9b34..00cb86446bb 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -45,7 +45,7 @@
/obj/item/camera/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/shell, list(new /obj/item/circuit_component/camera), SHELL_CAPACITY_SMALL)
+ AddComponent(/datum/component/shell, list(new /obj/item/circuit_component/camera, new /obj/item/circuit_component/remotecam/polaroid), SHELL_CAPACITY_SMALL)
/obj/item/camera/attack_self(mob/user)
if(!disk)
diff --git a/code/modules/research/designs/wiremod_designs.dm b/code/modules/research/designs/wiremod_designs.dm
index 204ecaa289b..dbcd7416594 100644
--- a/code/modules/research/designs/wiremod_designs.dm
+++ b/code/modules/research/designs/wiremod_designs.dm
@@ -467,6 +467,11 @@
id = "comp_assoc_list_pick"
build_path = /obj/item/circuit_component/list_pick/assoc
+/datum/design/component/bci/bci_camera
+ name = "BCI Camera"
+ id = "comp_camera_bci"
+ build_path = /obj/item/circuit_component/remotecam/bci
+
/datum/design/compact_remote_shell
name = "Compact Remote Shell"
desc = "A handheld shell with one big button."
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index e678147e8c1..cb1ec6bb1cc 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -894,6 +894,7 @@
"bci_implanter",
"bci_shell",
"comp_bar_overlay",
+ "comp_camera_bci",
"comp_counter_overlay",
"comp_install_detector",
"comp_object_overlay",
diff --git a/code/modules/wiremod/components/atom/remotecam.dm b/code/modules/wiremod/components/atom/remotecam.dm
new file mode 100644
index 00000000000..3d5afa2381c
--- /dev/null
+++ b/code/modules/wiremod/components/atom/remotecam.dm
@@ -0,0 +1,438 @@
+#define REMOTECAM_RANGE_FAR 7
+#define REMOTECAM_RANGE_NEAR 2
+#define REMOTECAM_ENERGY_USAGE_NEAR 0.003 * STANDARD_CELL_CHARGE //Normal components have 0.001 * STANDARD_CELL_CHARGE, this is expensive to livestream footage
+#define REMOTECAM_ENERGY_USAGE_FAR 0.008 * STANDARD_CELL_CHARGE //Far range vision should be expensive, crank this up 8 times
+#define REMOTECAM_EMP_RESET 90 SECONDS
+
+/**
+ * # Remote Camera Component
+ *
+ * Attaches a camera for surveillance-on-the-go.
+ */
+/obj/item/circuit_component/remotecam
+ display_name = "Camera Abstract Type"
+ desc = "This is the abstract parent type - do not use this directly!"
+ category = "Entity"
+ circuit_flags = CIRCUIT_NO_DUPLICATES
+
+ /// Starts the cameraa
+ var/datum/port/input/start
+ /// Stops the program.
+ var/datum/port/input/stop
+ /// Camera range flag (near/far)
+ var/datum/port/input/camera_range
+ /// The network to use
+ var/datum/port/input/network
+
+ /// Allow camera range to be set or not
+ var/camera_range_settable = TRUE
+ /// Used only for the BCI shell type, as the COMSIG_MOVABLE_MOVED signal need to be assigned to the user mob, not the shell circuit
+ var/camera_signal_move_override = FALSE
+
+ /// Camera object
+ var/obj/machinery/camera/shell_camera = null
+ /// The shell storing the parent circuit
+ var/atom/movable/shell_parent = null
+ /// The shell's type (used for prefix naming)
+ var/camera_prefix = "Camera"
+ /// Camera random ID
+ var/c_tag_random = 0
+
+ /// Used to store the current process state
+ var/current_camera_state = FALSE
+ /// Used to store the current cameranet state
+ var/current_cameranet_state = TRUE
+ /// Used to store the camera emp state
+ var/current_camera_emp = FALSE
+ /// Used to store the camera emp timer id
+ var/current_camera_emp_timer_id
+ /// Used to store the last string used for the camera name
+ var/current_camera_name = ""
+ /// Used to store the current camera range setting (near/far)
+ var/current_camera_range = 0
+ /// Used to store the last string used for the camera network
+ var/current_camera_network = ""
+
+/obj/item/circuit_component/remotecam/get_ui_notices()
+ . = ..()
+ if(camera_range_settable)
+ . += create_ui_notice("Energy Usage For Near (0) Range: [display_energy(REMOTECAM_ENERGY_USAGE_NEAR)] Per [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")
+ . += create_ui_notice("Energy Usage For Far (1) Range: [display_energy(REMOTECAM_ENERGY_USAGE_FAR)] Per [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")
+ else
+ . += create_ui_notice("Energy Usage While Active: [display_energy(current_camera_range > 0 ? REMOTECAM_ENERGY_USAGE_FAR : REMOTECAM_ENERGY_USAGE_NEAR)] Per [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")
+
+/obj/item/circuit_component/remotecam/populate_ports()
+ start = add_input_port("Start", PORT_TYPE_SIGNAL)
+ stop = add_input_port("Stop", PORT_TYPE_SIGNAL)
+ if(camera_range_settable)
+ camera_range = add_input_port("Camera Range", PORT_TYPE_NUMBER, default = 0)
+ network = add_input_port("Network", PORT_TYPE_STRING, default = "ss13")
+
+ if(camera_range_settable)
+ current_camera_range = camera_range.value
+ c_tag_random = rand(1, 999)
+
+/obj/item/circuit_component/remotecam/register_shell(atom/movable/shell)
+ shell_parent = shell
+ stop_process()
+
+/obj/item/circuit_component/remotecam/unregister_shell(atom/movable/shell)
+ stop_process()
+ remove_camera()
+ shell_parent = null
+
+/obj/item/circuit_component/remotecam/Destroy()
+ stop_process()
+ remove_camera()
+ shell_parent = null
+ return ..()
+
+/obj/item/circuit_component/remotecam/input_received(datum/port/input/port)
+ if(!shell_parent || !shell_camera)
+ return
+ update_camera_name_network()
+ if(COMPONENT_TRIGGERED_BY(start, port))
+ start_process()
+ cameranet_add()
+ current_camera_state = TRUE
+ else if(COMPONENT_TRIGGERED_BY(stop, port))
+ stop_process()
+ close_camera() //Instantly turn off the camera
+ current_camera_state = FALSE
+
+/**
+ * Initializes the camera
+ */
+/obj/item/circuit_component/remotecam/proc/init_camera()
+ shell_camera.desc = "This camera belongs in a circuit. If you see this, tell a coder!"
+ shell_camera.AddElement(/datum/element/empprotection, EMP_PROTECT_ALL)
+ shell_camera.use_power = NO_POWER_USE
+ shell_camera.start_active = TRUE
+ shell_camera.internal_light = FALSE
+ current_camera_name = ""
+ if(camera_range_settable)
+ current_camera_range = camera_range.value
+ current_cameranet_state = TRUE
+ current_camera_emp = FALSE
+ current_camera_network = ""
+ close_camera()
+ update_camera_range()
+ update_camera_name_network()
+ if(current_camera_state)
+ start_process()
+ update_camera_location()
+ else
+ cameranet_remove() //Remove camera from global cameranet until user activates the camera first
+ if(!camera_signal_move_override)
+ RegisterSignal(shell_parent, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+ RegisterSignal(shell_parent, COMSIG_ATOM_EMP_ACT, PROC_REF(set_camera_emp))
+
+/**
+ * Remove the camera
+ */
+/obj/item/circuit_component/remotecam/proc/remove_camera()
+ if(!shell_camera)
+ return
+ if(!camera_signal_move_override)
+ UnregisterSignal(shell_parent, COMSIG_MOVABLE_MOVED)
+ UnregisterSignal(shell_parent, COMSIG_ATOM_EMP_ACT)
+ if(current_camera_emp)
+ deltimer(current_camera_emp_timer_id)
+ current_camera_emp = FALSE
+ cameranet_add() //Readd camera to cameranet before deleting camera
+ QDEL_NULL(shell_camera)
+
+/**
+ * Close the camera state (only if it's already active)
+ */
+/obj/item/circuit_component/remotecam/proc/close_camera()
+ if(shell_camera?.camera_enabled)
+ shell_camera.toggle_cam(null, 0)
+
+/**
+ * Set the camera range
+ */
+/obj/item/circuit_component/remotecam/proc/update_camera_range()
+ shell_camera.setViewRange(current_camera_range > 0 ? REMOTECAM_RANGE_FAR : REMOTECAM_RANGE_NEAR)
+
+/**
+ * Updates the camera name and network
+ */
+/obj/item/circuit_component/remotecam/proc/update_camera_name_network()
+ if(!parent || !parent.display_name || parent.display_name == "")
+ shell_camera.c_tag = "[camera_prefix]: unspecified #[c_tag_random]"
+ current_camera_name = ""
+ else if(current_camera_name != parent.display_name)
+ current_camera_name = parent.display_name
+ var/new_cam_name = reject_bad_name(current_camera_name, allow_numbers = TRUE, ascii_only = FALSE, strict = TRUE, cap_after_symbols = FALSE)
+ //Set camera name using parent circuit name
+ if(new_cam_name)
+ shell_camera.c_tag = "[camera_prefix]: [new_cam_name] #[c_tag_random]"
+ else
+ shell_camera.c_tag = "[camera_prefix]: unspecified #[c_tag_random]"
+
+ if(!network.value || network.value == "")
+ shell_camera.network = list("ss13")
+ current_camera_network = ""
+ else if(current_camera_network != network.value)
+ current_camera_network = network.value
+ var/new_net_name = LOWER_TEXT(sanitize(current_camera_network))
+ //Set camera network string
+ if(new_net_name)
+ shell_camera.network = list("[new_net_name]")
+ else
+ shell_camera.network = list("ss13")
+
+/**
+ * Update the chunk for the camera (if enabled)
+ */
+/obj/item/circuit_component/remotecam/proc/update_camera_location(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ SIGNAL_HANDLER
+ if(current_camera_state && current_cameranet_state)
+ GLOB.cameranet.updatePortableCamera(shell_camera, 0.5 SECONDS)
+
+/**
+ * Add camera from global cameranet
+ */
+/obj/item/circuit_component/remotecam/proc/cameranet_add()
+ if(current_cameranet_state)
+ return
+ GLOB.cameranet.cameras += shell_camera
+ GLOB.cameranet.addCamera(shell_camera)
+ current_cameranet_state = TRUE
+
+/**
+ * Remove camera from global cameranet
+ */
+/obj/item/circuit_component/remotecam/proc/cameranet_remove()
+ if(!current_cameranet_state)
+ return
+ GLOB.cameranet.removeCamera(shell_camera)
+ GLOB.cameranet.cameras -= shell_camera
+ current_cameranet_state = FALSE
+
+/**
+ * Set the camera as emp'd
+ */
+/obj/item/circuit_component/remotecam/proc/set_camera_emp(datum/source, severity, protection)
+ SIGNAL_HANDLER
+ if(current_camera_emp)
+ return
+ if(!prob(150 / severity))
+ return
+ current_camera_emp = TRUE
+ close_camera()
+ current_camera_emp_timer_id = addtimer(CALLBACK(src, PROC_REF(remove_camera_emp)), REMOTECAM_EMP_RESET, TIMER_STOPPABLE)
+ for(var/mob/M as anything in GLOB.player_list)
+ if (M.client?.eye == shell_camera)
+ M.reset_perspective(null)
+ to_chat(M, span_warning("The screen bursts into static!"))
+
+/**
+ * Restore emp'd camera
+ */
+/obj/item/circuit_component/remotecam/proc/remove_camera_emp()
+ current_camera_emp = FALSE
+
+/**
+ * Adds the component to the SSclock_component process list
+ *
+ * Starts draining cell per second while camera is active
+ */
+/obj/item/circuit_component/remotecam/proc/start_process()
+ START_PROCESSING(SSclock_component, src)
+
+/**
+ * Removes the component to the SSclock_component process list
+ *
+ * Stops draining cell per second
+ */
+/obj/item/circuit_component/remotecam/proc/stop_process()
+ STOP_PROCESSING(SSclock_component, src)
+
+/**
+ * Handle power usage and camera state updating
+ *
+ * This is the generic abstract proc - subtypes with specialized logic should use their own copy of process()
+ */
+/obj/item/circuit_component/remotecam/process(seconds_per_tick)
+ if(!shell_parent || !shell_camera)
+ return PROCESS_KILL
+ //Camera is currently emp'd
+ if (current_camera_emp)
+ close_camera()
+ return
+ var/obj/item/stock_parts/cell/cell = parent.get_cell()
+ //If cell doesn't exist, or we ran out of power
+ if(!cell?.use(current_camera_range > 0 ? REMOTECAM_ENERGY_USAGE_FAR : REMOTECAM_ENERGY_USAGE_NEAR))
+ close_camera()
+ return
+ if(camera_range_settable)
+ //If the camera range has changed, update camera range
+ if(!camera_range.value != !current_camera_range)
+ current_camera_range = camera_range.value
+ update_camera_range()
+ //Set the camera state (if state has been changed)
+ if(current_camera_state ^ shell_camera.camera_enabled)
+ shell_camera.toggle_cam(null, 0)
+
+/obj/item/circuit_component/remotecam/bci
+ display_name = "BCI Camera"
+ desc = "Digitizes user's sight for surveillance-on-the-go. User must have fully functional eyes for digitizer to work. Camera range input is either 0 (near) or 1 (far). Network field is used for camera network."
+ category = "BCI"
+ camera_prefix = "BCI"
+ required_shells = list(/obj/item/organ/internal/cyberimp/bci)
+
+ /// BCIs are organs, and thus the signal must be assigned ONLY when the shell has been installed in a mob - otherwise the camera will never update position
+ camera_signal_move_override = TRUE
+
+ /// Store the BCI owner as a variable, so we can remove the move signal if the user was gibbed/destroyed while the BCI is still installed
+ var/mob/living/carbon/bciuser = null
+
+/obj/item/circuit_component/remotecam/drone
+ display_name = "Remote Camera"
+ desc = "Capture the surrounding environment for surveillance-on-the-go. Camera range input is either 0 (near) or 1 (far). Network field is used for camera network."
+ camera_prefix = "Drone"
+
+/obj/item/circuit_component/remotecam/airlock
+ display_name = "Peephole Camera"
+ desc = "A peephole camera that captures both sides of the airlock. Network field is used for camera network."
+ camera_prefix = "Airlock"
+
+ /// Hardcode camera to near range
+ camera_range_settable = FALSE
+ current_camera_range = 0
+
+/obj/item/circuit_component/remotecam/polaroid
+ display_name = "Camera Stream Add-On"
+ desc = "Relays a polaroid camera's feed as a digital stream for surveillance-on-the-go. The camera stream will not work if stored inside of a container like a backpack/box. Network field is used for camera network."
+ camera_prefix = "Polaroid"
+
+ /// Hardcode camera to near range
+ camera_range_settable = FALSE
+ current_camera_range = 0
+
+/obj/item/circuit_component/remotecam/bci/register_shell(atom/movable/shell)
+ . = ..()
+ if(!istype(shell_parent, /obj/item/organ/internal/cyberimp/bci))
+ return
+ shell_camera = new /obj/machinery/camera (shell_parent)
+ init_camera()
+ RegisterSignal(shell_parent, COMSIG_ORGAN_IMPLANTED, PROC_REF(on_organ_implanted))
+ RegisterSignal(shell_parent, COMSIG_ORGAN_REMOVED, PROC_REF(on_organ_removed))
+ var/obj/item/organ/internal/cyberimp/bci/bci = shell_parent
+ if(bci.owner) //If somehow the camera was added while shell is already installed inside a mob, assign signals
+ if(bciuser) //This should never happen... But if it does, unassign move signal from old mob
+ UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+ bciuser = bci.owner
+ RegisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+
+/obj/item/circuit_component/remotecam/bci/unregister_shell(atom/movable/shell)
+ if(shell_camera)
+ if(bciuser)
+ UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+ bciuser = null
+ UnregisterSignal(shell_parent, list(COMSIG_ORGAN_IMPLANTED, COMSIG_ORGAN_REMOVED))
+ return ..()
+
+/obj/item/circuit_component/remotecam/bci/Destroy()
+ if(shell_camera)
+ if(bciuser)
+ UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+ bciuser = null
+ UnregisterSignal(shell_parent, list(COMSIG_ORGAN_IMPLANTED, COMSIG_ORGAN_REMOVED))
+ return ..()
+
+/obj/item/circuit_component/remotecam/bci/proc/on_organ_implanted(datum/source, mob/living/carbon/owner)
+ SIGNAL_HANDLER
+ if(bciuser)
+ return
+ bciuser = owner
+ RegisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+
+/obj/item/circuit_component/remotecam/bci/proc/on_organ_removed(datum/source, mob/living/carbon/owner)
+ SIGNAL_HANDLER
+ if(!bciuser)
+ return
+ UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
+ bciuser = null
+
+/obj/item/circuit_component/remotecam/drone/register_shell(atom/movable/shell)
+ . = ..()
+ if(!istype(shell_parent, /mob/living/circuit_drone))
+ return
+ current_camera_state = FALSE //Always reset camera state for built-in shell components
+ shell_camera = new /obj/machinery/camera (shell_parent)
+ init_camera()
+
+/obj/item/circuit_component/remotecam/airlock/register_shell(atom/movable/shell)
+ . = ..()
+ if(!istype(shell_parent, /obj/machinery/door/airlock))
+ return
+ current_camera_state = FALSE //Always reset camera state for built-in shell components
+ shell_camera = new /obj/machinery/camera (shell_parent)
+ init_camera()
+
+/obj/item/circuit_component/remotecam/polaroid/register_shell(atom/movable/shell)
+ . = ..()
+ if(!istype(shell_parent, /obj/item/camera))
+ return
+ current_camera_state = FALSE //Always reset camera state for built-in shell components
+ shell_camera = new /obj/machinery/camera (shell_parent)
+ init_camera()
+
+/obj/item/circuit_component/remotecam/bci/process(seconds_per_tick)
+ if(!shell_parent || !shell_camera)
+ return PROCESS_KILL
+ //Camera is currently emp'd
+ if (current_camera_emp)
+ close_camera()
+ return
+ var/obj/item/organ/internal/cyberimp/bci/bci = shell_parent
+ //If shell is not currently inside a head, or user is currently blind, or user is dead
+ if(!bci.owner || bci.owner.is_blind() || bci.owner.stat >= UNCONSCIOUS)
+ close_camera()
+ return
+ var/obj/item/stock_parts/cell/cell = parent.get_cell()
+ //If cell doesn't exist, or we ran out of power
+ if(!cell?.use(current_camera_range > 0 ? REMOTECAM_ENERGY_USAGE_FAR : REMOTECAM_ENERGY_USAGE_NEAR))
+ close_camera()
+ return
+ //If owner is nearsighted, set camera range to short (if it wasn't already)
+ if(bci.owner.is_nearsighted_currently())
+ if(current_camera_range)
+ current_camera_range = 0
+ update_camera_range()
+ //Else if the camera range has changed, update camera range
+ else if(!camera_range.value != !current_camera_range)
+ current_camera_range = camera_range.value
+ update_camera_range()
+ //Set the camera state (if state has been changed)
+ if(current_camera_state ^ shell_camera.camera_enabled)
+ shell_camera.toggle_cam(null, 0)
+
+/obj/item/circuit_component/remotecam/polaroid/process(seconds_per_tick)
+ if(!shell_parent || !shell_camera)
+ return PROCESS_KILL
+ //Camera is currently emp'd
+ if (current_camera_emp)
+ close_camera()
+ return
+ //If camera is stored inside of bag or something, turn it off
+ if(shell_parent.loc.atom_storage)
+ close_camera()
+ return
+ var/obj/item/stock_parts/cell/cell = parent.get_cell()
+ //If cell doesn't exist, or we ran out of power
+ if(!cell?.use(REMOTECAM_ENERGY_USAGE_NEAR))
+ close_camera()
+ return
+ //Set the camera state (if state has been changed)
+ if(current_camera_state ^ shell_camera.camera_enabled)
+ shell_camera.toggle_cam(null, 0)
+
+#undef REMOTECAM_RANGE_FAR
+#undef REMOTECAM_RANGE_NEAR
+#undef REMOTECAM_ENERGY_USAGE_NEAR
+#undef REMOTECAM_ENERGY_USAGE_FAR
+#undef REMOTECAM_EMP_RESET
diff --git a/code/modules/wiremod/shell/airlock.dm b/code/modules/wiremod/shell/airlock.dm
index 6c5cd11df7d..165949529c4 100644
--- a/code/modules/wiremod/shell/airlock.dm
+++ b/code/modules/wiremod/shell/airlock.dm
@@ -16,7 +16,7 @@
. = ..()
AddComponent( \
/datum/component/shell, \
- unremovable_circuit_components = list(new /obj/item/circuit_component/airlock, new /obj/item/circuit_component/airlock_access_event), \
+ unremovable_circuit_components = list(new /obj/item/circuit_component/airlock, new /obj/item/circuit_component/airlock_access_event, new /obj/item/circuit_component/remotecam/airlock), \
capacity = SHELL_CAPACITY_LARGE, \
shell_flags = SHELL_FLAG_ALLOW_FAILURE_ACTION|SHELL_FLAG_REQUIRE_ANCHOR \
)
diff --git a/code/modules/wiremod/shell/drone.dm b/code/modules/wiremod/shell/drone.dm
index aaeafb6b551..4627a3cf326 100644
--- a/code/modules/wiremod/shell/drone.dm
+++ b/code/modules/wiremod/shell/drone.dm
@@ -16,7 +16,8 @@
/mob/living/circuit_drone/Initialize(mapload)
. = ..()
AddComponent(/datum/component/shell, list(
- new /obj/item/circuit_component/bot_circuit()
+ new /obj/item/circuit_component/bot_circuit(),
+ new /obj/item/circuit_component/remotecam/drone()
), SHELL_CAPACITY_LARGE)
/mob/living/circuit_drone/examine(mob/user)
diff --git a/tgstation.dme b/tgstation.dme
index 62af7ed933a..5211a7b7c78 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -6224,6 +6224,7 @@
#include "code\modules\wiremod\components\atom\matscanner.dm"
#include "code\modules\wiremod\components\atom\pinpointer.dm"
#include "code\modules\wiremod\components\atom\reagentscanner.dm"
+#include "code\modules\wiremod\components\atom\remotecam.dm"
#include "code\modules\wiremod\components\atom\self.dm"
#include "code\modules\wiremod\components\atom\species.dm"
#include "code\modules\wiremod\components\bci\install_detector.dm"