Skip to content

Commit

Permalink
Camera Console Recalibration and Bodycameras (#3952)
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

This change modifies camera consoles so that they can take a custom
network input. Camera consoles were also modified to get closer in
parity to the SecurEye app. Tablets were given the ability to download
securEye.

| Camera Consoles
|![image](https://github.com/user-attachments/assets/b59c4498-d0c9-4eaa-a051-e3365a4705c8)|
|-----------------|---|
| SecurEye
|![image](https://github.com/user-attachments/assets/320ca69a-5d6c-46b1-a152-2244403415b2)|

Static cameras were modified so that using a multitool on one with its
panel open now presents several new options. One can copy a network that
a camera is set to onto a multitool, transfer a saved network into a
camera, or even set a new network entirely.

![image](https://github.com/user-attachments/assets/3dadb434-6217-493c-a3fd-1a36b2fb06d3)
They were also given a new variable, can_transmit_across_z_levels, which
defaults to false. This variable is for varedits, and a potential future
addition that may enable transmission across Z-levels for some cost.

A new portable type of camera was also added, the body camera. It can be
activated or deactivated by being alt-clicked, and its tag or network
settings can be modified if a multitool is used on it.
They can be purchased from the outpost cargo market at a rate of two
units for 250 credits.

Bodycameras can be worn in your pockets, your coat/armor, security belts
or webbings, or a helmet, but they get obscured when put into a bag, a
box, or your boots.

They have a view range of 5, which is 2 less than camera structures.


https://github.com/user-attachments/assets/9c69cd3a-59c4-40a7-94fd-2be9932ec15f


https://github.com/user-attachments/assets/75655b88-583f-44a6-93ef-e59e159826ce

This PR by Timberpoes proved instrumental in solving a major problem
during the development process:
tgstation/tgstation#52767

The bodycamera sprite is modeled after the handheld radio sprite made by
@rye-rice in this PR:
#2610

![image](https://github.com/user-attachments/assets/c4f3f659-d285-4936-942e-7d11db54a9a9)
I'm not a good pixel artist, so any feedback here is appreciated, on top
of feedback with regards to anything else about this PR.

## Why It's Good For The Game

It would be nice to be able to track your crew more effectively, be it
if you were to send them out to scrap a derelict, or to secure contested
territory. This is something that Command or Foremen that want to
oversee operations might find useful, as it can be kind of boring to not
be able to see your crew when they're at work. This should help that
issue.

## Changelog

:cl: Rye-Rice, Timberpoes
add: Added bodycameras
add: Added the ability to set custom networks on cameras and camera
consoles
code: Made camera consoles and SecurEye have better parity
/: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
zimon9 authored Jan 29, 2025
1 parent 8cd2be1 commit c773321
Show file tree
Hide file tree
Showing 14 changed files with 505 additions and 102 deletions.
1 change: 1 addition & 0 deletions code/datums/components/storage/concrete/pockets.dm
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
/obj/item/clothing/glasses/sunglasses/ballistic,
/obj/item/ammo_casing,
/obj/item/ammo_box/magazine/illestren_a850r,
/obj/item/bodycamera,
))

/datum/component/storage/concrete/pockets/holster
Expand Down
25 changes: 22 additions & 3 deletions code/game/machinery/camera/camera.dm
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
var/busy = FALSE
var/emped = FALSE //Number of consecutive EMP's on this camera
var/in_use_lights = 0
var/can_transmit_across_z_levels = FALSE

// Upgrades bitflag
var/upgrades = 0
Expand Down Expand Up @@ -258,8 +259,26 @@
if(!panel_open)
return

setViewRange((view_range == initial(view_range)) ? short_range : initial(view_range))
to_chat(user, "<span class='notice'>You [(view_range == initial(view_range)) ? "restore" : "mess up"] the camera's focus.</span>")
var/obj/item/multitool/M = I
var/list/choice_list = list("Occlude the camera lens", "Save the network to the multitool buffer", "Transfer the buffered network to the camera", "Change the camera network")
var/choice = tgui_input_list(user, "Select an option", "Camera Settings", choice_list)
switch(choice)
if("Occlude the camera lens")
setViewRange((view_range == initial(view_range)) ? short_range : initial(view_range))
to_chat(user, "<span class='notice'>You [(view_range == initial(view_range)) ? "restore" : "mess up"] the camera's focus.</span>")

if("Save the network to the multitool buffer")
M.buffer = network[1]
to_chat(user, "<span class='notice'>You add network '[network[1]]' to the multitool's buffer.</span>")

if("Transfer the buffered network to the camera")
network[1] = M.buffer
to_chat(user, "<span class='notice'>You tune [src] to transmit across the '[network[1]]' network using the saved data from the multiool's buffer.</span>")

if("Change the camera network")
network[1] = stripped_input(user, "Tune [src] to a specific network. Enter the network name and ensure that it is no bigger than 32 characters long. Network names are not case sensitive.", "Network Tuning", max_length = 32)
to_chat(user, "<span class='notice'>You set [src] to transmit across the '[network[1]]' network.</span>")

return TRUE

/obj/machinery/camera/welder_act(mob/living/user, obj/item/I)
Expand Down Expand Up @@ -508,6 +527,6 @@
user.sight |= (SEE_TURFS|SEE_MOBS|SEE_OBJS)
user.see_in_dark = max(user.see_in_dark, 8)
else
user.sight = 0
user.sight = SEE_BLACKNESS
user.see_in_dark = 2
return 1
241 changes: 183 additions & 58 deletions code/game/machinery/computer/camera.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
light_color = COLOR_SOFT_RED

var/list/network = list("ss13")
var/temp_network = list("")
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/map_name
var/const/default_map_size = 15
var/atom/movable/screen/map_view/cam_screen
var/atom/movable/screen/plane_master/lighting/cam_plane_master
/// All the plane masters that need to be applied.
var/list/cam_plane_masters
var/atom/movable/screen/background/cam_background

/obj/machinery/computer/security/retro
Expand Down Expand Up @@ -43,18 +47,20 @@
cam_screen.assigned_map = map_name
cam_screen.del_on_map_removal = FALSE
cam_screen.screen_loc = "[map_name]:1,1"
cam_plane_master = new
cam_plane_master.name = "plane_master"
cam_plane_master.assigned_map = map_name
cam_plane_master.del_on_map_removal = FALSE
cam_plane_master.screen_loc = "[map_name]:CENTER"
cam_plane_masters = list()
for(var/plane in subtypesof(/atom/movable/screen/plane_master))
var/atom/movable/screen/instance = new plane()
instance.assigned_map = map_name
instance.del_on_map_removal = FALSE
instance.screen_loc = "[map_name]:CENTER"
cam_plane_masters += instance
cam_background = new
cam_background.assigned_map = map_name
cam_background.del_on_map_removal = FALSE

/obj/machinery/computer/security/Destroy()
qdel(cam_screen)
qdel(cam_plane_master)
QDEL_LIST(cam_plane_masters)
qdel(cam_background)
return ..()

Expand All @@ -63,12 +69,21 @@
network -= i
network += "[REF(port)][i]"

/obj/machinery/computer/security/multitool_act(mob/living/user, obj/item/I)
. = ..()
var/obj/item/multitool/M = I
if(M.buffer != null)
network = M.buffer
to_chat(user, "<span class='notice'>You input network '[M.buffer]' from the multitool's buffer into [src].</span>")
return

/obj/machinery/computer/security/ui_interact(mob/user, datum/tgui/ui)
// Update UI
ui = SStgui.try_update_ui(user, src, ui)

// Show static if can't use the camera
if(!active_camera?.can_use())
show_camera_static()
update_active_camera_screen()

if(!ui)
var/user_ref = REF(user)
var/is_living = isliving(user)
Expand All @@ -82,7 +97,8 @@
use_power(active_power_usage)
// Register map objects
user.client.register_map_obj(cam_screen)
user.client.register_map_obj(cam_plane_master)
for(var/plane in cam_plane_masters)
user.client.register_map_obj(plane)
user.client.register_map_obj(cam_background)
// Open UI
ui = new(user, src, "CameraConsole", name)
Expand All @@ -93,16 +109,33 @@
data["network"] = network
data["activeCamera"] = null
if(active_camera)
if(!active_camera?.can_use())
data["activeCamera"] = list(
name = active_camera.c_tag + " (DEACTIVATED)",
status = active_camera.status,
)
else
data["activeCamera"] = list(
name = active_camera.c_tag,
status = active_camera.status,
)
if(istype(active_camera, /obj/machinery/camera))
var/obj/machinery/camera/active_camera_S = active_camera
if(!active_camera_S?.can_use())
data["activeCamera"] = list(
name = active_camera_S.c_tag,
status = active_camera_S.status,
)
else
data["activeCamera"] = list(
name = active_camera_S.c_tag,
status = active_camera_S.status,
)
active_camera = active_camera_S

else if(istype(active_camera, /obj/item/bodycamera))
var/obj/machinery/camera/active_camera_B = active_camera
if(!active_camera_B?.can_use())
data["activeCamera"] = list(
name = active_camera_B.c_tag,
status = active_camera_B.status,
)
else
data["activeCamera"] = list(
name = active_camera_B.c_tag,
status = active_camera_B.status,
)
active_camera = active_camera_B
return data

/obj/machinery/computer/security/ui_static_data()
Expand All @@ -111,38 +144,77 @@
var/list/cameras = get_available_cameras()
data["cameras"] = list()
for(var/i in cameras)
var/obj/machinery/camera/C = cameras[i]
if(!C?.can_use())
var/obj/C = cameras[i]
if(istype(C, /obj/machinery/camera))
var/obj/machinery/camera/C_cam = C
data["cameras"] += list(list(
name = C.c_tag + " (DEACTIVATED)",
name = C_cam.c_tag,
))
else
else if(istype(C, /obj/item/bodycamera))
var/obj/item/bodycamera/C_cam = C
data["cameras"] += list(list(
name = C.c_tag,
name = C_cam.c_tag,
))
return data

/obj/machinery/computer/security/ui_act(action, params)
//This is the only way to refresh the UI, from what I've found
/obj/machinery/computer/security/proc/ui_refresh(mob/user, datum/tgui/ui)
ui.close()
ui_interact(user, ui)
show_camera_static()

/obj/machinery/computer/security/ui_act(action, params, ui)
. = ..()
if(.)
return

if(action == "set_network")
network = temp_network
ui_refresh(usr, ui)

if(action == "set_temp_network")
temp_network = sanitize_filename(params["name"])

if(action == "refresh")
ui_refresh(usr, ui)

if(action == "switch_camera")
var/c_tag = params["name"]
var/list/cameras = get_available_cameras()
var/obj/machinery/camera/C = cameras[c_tag]
var/obj/C = cameras[c_tag]
active_camera = C
playsound(src, get_sfx("terminal_type"), 25, FALSE)

update_active_camera_screen()

return TRUE

/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
user.client.clear_map(map_name)
// Turn off the console
if(length(concurrent_users) == 0 && is_living)
active_camera = null
playsound(src, 'sound/machines/terminal_off.ogg', 25, FALSE)
use_power(0)

/obj/machinery/computer/security/proc/update_active_camera_screen()
if(istype(active_camera, /obj/machinery/camera))
var/obj/machinery/camera/active_camera_S = active_camera

// Show static if can't use the camera
if(!active_camera?.can_use())
if(!active_camera_S?.can_use())
show_camera_static()
return TRUE

var/list/visible_turfs = list()
for(var/turf/T in (C.isXRay() \
? range(C.view_range, C) \
: view(C.view_range, C)))
for(var/turf/T in (active_camera_S.isXRay() \
? range(active_camera_S.view_range, active_camera_S) \
: view(active_camera_S.view_range, active_camera_S)))
visible_turfs += T

var/list/bbox = get_bbox_of_atoms(visible_turfs)
Expand All @@ -153,44 +225,97 @@
cam_background.icon_state = "clear"
cam_background.fill_rect(1, 1, size_x, size_y)

return TRUE
if(istype(active_camera, /obj/item/bodycamera))
var/obj/item/bodycamera/active_camera_B = active_camera

/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
user.client.clear_map(map_name)
// Turn off the console
if(length(concurrent_users) == 0 && is_living)
active_camera = null
playsound(src, 'sound/machines/terminal_off.ogg', 25, FALSE)
use_power(0)
// Show static if can't use the camera
if(!active_camera_B?.can_use())
show_camera_static()
return TRUE

var/list/visible_turfs = list()

if(!active_camera_B.loc)
return

// Derived from https://github.com/tgstation/tgstation/pull/52767
// Is this camera located in or attached to a living thing? If so, assume the camera's loc is the living thing.
var/cam_location = active_camera_B.loc

// Is the camera in the following items? If so, let it transmit an image as normal
if((istype(cam_location, /obj/item/clothing/suit)) || (istype(cam_location, /obj/item/clothing/head/helmet)) || istype(cam_location, /obj/item/storage/belt))
cam_location = active_camera_B.loc.loc

// 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(istype(active_camera, /obj/machinery/camera))
return

// Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
last_camera_turf = get_turf(cam_location)

var/list/visible_things = view(active_camera_B.view_range, cam_location)

for(var/turf/visible_turf in visible_things)
visible_turfs += visible_turf

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.vis_contents = visible_turfs
cam_background.icon_state = "clear"
cam_background.fill_rect(1, 1, size_x, size_y)

/obj/machinery/computer/security/proc/show_camera_static()
cam_screen.vis_contents.Cut()
cam_background.icon_state = "scanline2"
cam_background.fill_rect(1, 1, default_map_size, default_map_size)

// Returns the list of cameras accessible from this computer
/obj/machinery/computer/security/proc/get_available_cameras()
var/list/L = list()
for (var/obj/machinery/camera/C in GLOB.cameranet.cameras)
if((is_away_level(src) || is_away_level(C)) && (C.virtual_z() != virtual_z()))//if on away mission, can only receive feed from same z_level cameras
continue
for (var/obj/C in GLOB.cameranet.cameras)
if(istype(C, /obj/machinery/camera))
var/obj/machinery/camera/cam = C
if(cam.virtual_z() != virtual_z())
if(cam.can_transmit_across_z_levels)
//let them transmit
else
continue
else if(istype(C, /obj/item/bodycamera))
var/obj/item/bodycamera/cam = C
if((cam.virtual_z() != virtual_z()) || (cam.can_transmit_across_z_levels))//if on away mission, can only receive feed from same z_level cameras
if(cam.can_transmit_across_z_levels)
//let them transmit
else
continue
L.Add(C)
var/list/D = list()
for(var/obj/machinery/camera/C in L)
if(!C.network)
stack_trace("Camera in a cameranet has no camera network")
continue
if(!(islist(C.network)))
stack_trace("Camera in a cameranet has a non-list camera network")
continue
var/list/tempnetwork = C.network & network
if(tempnetwork.len)
D["[C.c_tag]"] = C
for(var/obj/C in L)
if(istype(C, /obj/machinery/camera))
var/obj/machinery/camera/cam = C
if(!cam.network)
stack_trace("Camera in a cameranet has no camera network")
continue
if(!(islist(cam.network)))
stack_trace("Camera in a cameranet has a non-list camera network")
continue
var/list/tempnetwork = cam.network & network
if(tempnetwork.len)
D["[cam.c_tag]"] = C

else if(istype(C, /obj/item/bodycamera))
var/obj/item/bodycamera/cam = C
if(!cam.network)
stack_trace("Camera in a cameranet has no camera network")
continue
if(!(islist(cam.network)))
stack_trace("Camera in a cameranet has a non-list camera network")
continue
var/list/tempnetwork = cam.network & network
if(tempnetwork.len)
D["[cam.c_tag]"] = cam
return D

// SECURITY MONITORS
Expand Down
Loading

0 comments on commit c773321

Please sign in to comment.