Files
0f9343bbe3 Adds a notification to silicon/drones being watched (#90471)
## About The Pull Request

Cyborgs now have an alert that shows when someone is watching their
internal camera through a security camera console. SyndEye is excluded
from this as it's spying instead.
Made as an alternative to
https://github.com/tgstation/tgstation/pull/90410 but they also aren't
mutually exclusive, though I have the same fix for borg camera cutting
in this one too.


https://github.com/user-attachments/assets/5dfe92f8-8948-4a80-bb64-cbeaca4f80e0

##### This is part of the same code bounty as
https://github.com/tgstation/tgstation/pull/90410

## Why It's Good For The Game

Pretty much the same reason this PR is an alternative to, it's hard for
borgs to get away with doing antagonist stuff when people are able to
silently watch them at any point from any console or laptop laying
around the station, of which there are many. This hopefully lets borgs
know if someone is watching, to avoid doing anything in front of the
cameras, and to encourage them to get said cameras cut if they plan on
doing anything.

## Changelog

🆑
add: Drones now have internal cameras that shows up on camera consoles
(like Cyborgs)
balance: Cyborgs (and drones) now get a notification when someone is
watching them through a security camera console.
fix: Cutting a Cyborg's camera wire now properly disables it, and
mending it now properly re-enables it.
/🆑

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
2025-04-29 17:45:29 -06:00

231 lines
7.6 KiB
Plaintext

#define DEFAULT_MAP_SIZE 15
/obj/machinery/computer/security
name = "security camera console"
desc = "Used to access the various cameras on the station."
icon_screen = "cameras"
icon_keyboard = "security_key"
circuit = /obj/item/circuitboard/computer/security
light_color = COLOR_SOFT_RED
interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON|INTERACT_MACHINE_REQUIRES_SIGHT
var/list/network = list(CAMERANET_NETWORK_SS13)
var/obj/machinery/camera/active_camera
/// The turf where the camera was last updated.
var/turf/last_camera_turf
var/list/concurrent_users = list()
// Stuff needed to render the map
var/atom/movable/screen/map_view/camera/cam_screen
/obj/machinery/computer/security/Initialize(mapload)
. = ..()
// Map name has to start and end with an A-Z character,
// and definitely NOT with a square bracket or even a number.
// I wasted 6 hours on this. :agony:
var/map_name = "camera_console_[REF(src)]_map"
// Convert networks to lowercase
for(var/i in network)
network -= i
network += LOWER_TEXT(i)
// Initialize map objects
cam_screen = new
cam_screen.generate_view(map_name)
/obj/machinery/computer/security/Destroy()
QDEL_NULL(cam_screen)
return ..()
/obj/machinery/computer/security/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
for(var/i in network)
network -= i
network += "[port.shuttle_id]_[i]"
/obj/machinery/computer/security/ui_interact(mob/user, datum/tgui/ui)
. = ..()
if(!user.client) //prevents errors by trying to pass clients that don't exist.
return
// Update UI
ui = SStgui.try_update_ui(user, src, ui)
// Update the camera, showing static if necessary and updating data if the location has moved.
update_active_camera_screen()
if(!ui)
var/user_ref = REF(user)
var/is_living = isliving(user)
// Ghosts shouldn't count towards concurrent users, which produces
// an audible terminal_on click.
if(is_living)
concurrent_users += user_ref
// Turn on the console
if(length(concurrent_users) == 1 && is_living)
playsound(src, 'sound/machines/terminal/terminal_on.ogg', 25, FALSE)
use_energy(active_power_usage)
// Open UI
ui = new(user, src, "CameraConsole", name)
ui.open()
// Register map objects
cam_screen.display_to(user, ui.window)
/obj/machinery/computer/security/ui_status(mob/user, datum/ui_state/state)
. = ..()
if(. == UI_DISABLED)
return UI_CLOSE
return .
/obj/machinery/computer/security/ui_data()
var/list/data = list()
data["activeCamera"] = null
if(active_camera)
data["activeCamera"] = list(
name = active_camera.c_tag,
ref = REF(active_camera),
status = active_camera.camera_enabled,
)
return data
/obj/machinery/computer/security/ui_static_data()
var/list/data = list()
data["network"] = network
data["mapRef"] = cam_screen.assigned_map
data["cameras"] = GLOB.cameranet.get_available_cameras_data(network)
return data
/obj/machinery/computer/security/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
if(action == "switch_camera")
active_camera?.on_stop_watching(src)
var/obj/machinery/camera/selected_camera = locate(params["camera"]) in GLOB.cameranet.cameras
active_camera = selected_camera
if(isnull(active_camera))
return TRUE
active_camera.on_start_watching(src)
update_active_camera_screen()
return TRUE
/obj/machinery/computer/security/proc/update_active_camera_screen()
// Show static if can't use the camera
if(!active_camera?.can_use())
cam_screen.show_camera_static()
return
var/list/visible_turfs = list()
// Get the camera's turf to correctly gather what's visible from its turf, in case it's located in a moving object (borgs / mechs)
var/new_cam_turf = get_turf(active_camera)
// If we're not forcing an update for some reason and the cameras are in the same location,
// we don't need to update anything.
// Most security cameras will end here as they're not moving.
if(last_camera_turf == new_cam_turf)
return
// Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
last_camera_turf = new_cam_turf
//Here we gather what's visible from the camera's POV based on its view_range and xray modifier if present
var/list/visible_things = active_camera.isXRay(ignore_malf_upgrades = TRUE) ? range(active_camera.view_range, new_cam_turf) : view(active_camera.view_range, new_cam_turf)
for(var/turf/visible_turf in visible_things)
visible_turfs += visible_turf
//Get coordinates for a rectangle area that contains the turfs we see so we can then clear away the static in the resulting rectangle area
var/list/bbox = get_bbox_of_atoms(visible_turfs)
var/size_x = bbox[3] - bbox[1] + 1
var/size_y = bbox[4] - bbox[2] + 1
cam_screen.show_camera(visible_turfs, size_x, size_y)
/obj/machinery/computer/security/ui_close(mob/user)
. = ..()
var/user_ref = REF(user)
var/is_living = isliving(user)
// Living creature or not, we remove you anyway.
concurrent_users -= user_ref
// Unregister map objects
cam_screen?.hide_from(user)
// Turn off the console
if(length(concurrent_users) == 0 && is_living)
active_camera?.on_stop_watching(src)
active_camera = null
last_camera_turf = null
playsound(src, 'sound/machines/terminal/terminal_off.ogg', 25, FALSE)
/atom/movable/screen/map_view/camera
/// All the plane masters that need to be applied.
var/atom/movable/screen/background/cam_background
/atom/movable/screen/map_view/camera/Destroy()
QDEL_NULL(cam_background)
return ..()
/atom/movable/screen/map_view/camera/generate_view(map_key)
. = ..()
cam_background = new
cam_background.del_on_map_removal = FALSE
cam_background.assigned_map = assigned_map
/atom/movable/screen/map_view/camera/display_to_client(client/show_to)
show_to.register_map_obj(cam_background)
. = ..()
/atom/movable/screen/map_view/camera/proc/show_camera(list/visible_turfs, size_x, size_y)
vis_contents = visible_turfs
cam_background.icon_state = "clear"
cam_background.fill_rect(1, 1, size_x, size_y)
/atom/movable/screen/map_view/camera/proc/show_camera_static()
vis_contents.Cut()
cam_background.icon_state = "scanline2"
cam_background.fill_rect(1, 1, DEFAULT_MAP_SIZE, DEFAULT_MAP_SIZE)
// SECURITY MONITORS
/obj/machinery/computer/security/wooden_tv
name = "security camera monitor"
desc = "An old TV hooked into the station's camera network."
icon_state = "television"
icon_keyboard = null
icon_screen = "detective_tv"
pass_flags = PASSTABLE
/obj/machinery/computer/security/mining
name = "outpost camera console"
desc = "Used to access the various cameras on the outpost."
icon_screen = "mining"
icon_keyboard = "mining_key"
network = list(CAMERANET_NETWORK_MINE, CAMERANET_NETWORK_AUXBASE)
circuit = /obj/item/circuitboard/computer/mining
/obj/machinery/computer/security/research
name = "research camera console"
desc = "Used to access the various cameras in science."
network = list(CAMERANET_NETWORK_RD)
circuit = /obj/item/circuitboard/computer/research
/obj/machinery/computer/security/hos
name = "\improper Head of Security's camera console"
desc = "A custom security console with added access to the labor camp network."
network = list(CAMERANET_NETWORK_SS13, CAMERANET_NETWORK_LABOR)
circuit = null
/obj/machinery/computer/security/labor
name = "labor camp monitoring"
desc = "Used to access the various cameras on the labor camp."
network = list(CAMERANET_NETWORK_LABOR)
circuit = null
/obj/machinery/computer/security/qm
name = "\improper Quartermaster's camera console"
desc = "A console with access to the mining, auxiliary base and vault camera networks."
network = list(CAMERANET_NETWORK_MINE, CAMERANET_NETWORK_AUXBASE, CAMERANET_NETWORK_VAULT)
circuit = null
#undef DEFAULT_MAP_SIZE