Files
Bubberstation/code/modules/wiremod/components/atom/remotecam.dm
tmyqlfpir f6d28662a3 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.

<img width="366" alt="components"
src="https://github.com/tgstation/tgstation/assets/80724828/0e628863-9998-46d6-8822-e0a44543b4c2">

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

<img width="136" alt="drone camera"
src="https://github.com/tgstation/tgstation/assets/80724828/fd045871-56bf-44a6-bb4f-ebe895d56d3d">

* Drone Camera
This camera component captures the surrounding area. It has an option to
set the camera range (near 5x5/far 14x14).

<img width="136" alt="bci camera"
src="https://github.com/tgstation/tgstation/assets/80724828/16bf2dd1-823b-4d66-8249-5d0f1bb1b779">

* 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).

<img width="136" alt="polaroid camera"
src="https://github.com/tgstation/tgstation/assets/80724828/7c4d53df-b4af-4f7c-8942-a63842510720">

* Polaroid Camera Add-On
This camera component streams to a camera network. The camera range is
hardcoded to the near setting (5x5).

<img width="136" alt="airlock camera"
src="https://github.com/tgstation/tgstation/assets/80724828/5d9e9d55-49fc-45a7-99c8-aaf1ae08f6d1">

* 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
<details>
This is the drone camera viewed on a security camera console<br>
<img width="425" alt="near"
src="https://github.com/tgstation/tgstation/assets/80724828/e5247828-0fee-4552-9e70-5e5ee897c117"><br>
This is the same drone, now set to the far range setting<br>
<img width="425" alt="far"
src="https://github.com/tgstation/tgstation/assets/80724828/e58e3e85-aa90-4f1a-9dff-957c65764b77"><br>
</details>

## 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
🆑
add: Added new circuit camera components
qol: Circuit drones can now recharge at recharge stations
/🆑

---------

Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
2024-06-08 10:49:49 +00:00

439 lines
16 KiB
Plaintext

#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