diff --git a/code/modules/modular_computers/file_system/programs/watchdog.dm b/code/modules/modular_computers/file_system/programs/watchdog.dm new file mode 100644 index 000000000000..602f251efecc --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/watchdog.dm @@ -0,0 +1,196 @@ +#define DEFAULT_MAP_SIZE 15 + +/datum/computer_file/program/watchdog + filename = "watchdog" + filedesc = "Watchdog" + ui_header = "borg_mon.gif" + program_icon_state = "generic" + extended_desc = "This program allows access to standard security camera networks." + requires_ntnet = TRUE + transfer_access = ACCESS_SECURITY + usage_flags = PROGRAM_CONSOLE | PROGRAM_LAPTOP | PROGRAM_TABLET + size = 5 + tgui_id = "NtosWatchdog" + program_icon = "eye" + + // Will need to modify the network to the "watchdog" network + var/list/network = list("watchdog") + 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/atom/movable/screen/map_view/cam_screen + /// All the plane masters that need to be applied. + var/list/cam_plane_masters + var/atom/movable/screen/background/cam_background + +/datum/computer_file/program/watchdog/New() + . = ..() + // Map name has to start and end with an A-Z character, + // and definitely NOT with a square bracket or even a number. + map_name = "camera_console_[REF(src)]_map" + // Convert networks to lowercase + for(var/i in network) + network -= i + network += lowertext(i) + // Initialize map objects + cam_screen = new + cam_screen.name = "screen" + cam_screen.assigned_map = map_name + cam_screen.del_on_map_removal = FALSE + cam_screen.screen_loc = "[map_name]:1,1" + 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 + +/datum/computer_file/program/watchdog/Destroy() + qdel(cam_screen) + QDEL_LIST(cam_plane_masters) + qdel(cam_background) + return ..() + +/datum/computer_file/program/watchdog/ui_interact(mob/user, datum/tgui/ui) + // 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 + // Register map objects + user.client.register_map_obj(cam_screen) + for(var/plane in cam_plane_masters) + user.client.register_map_obj(plane) + user.client.register_map_obj(cam_background) + return ..() + +/datum/computer_file/program/watchdog/ui_data() + var/list/data = get_header_data() + data["network"] = network + data["activeCamera"] = null + if(active_camera) + data["activeCamera"] = list( + name = active_camera.c_tag, + status = active_camera.status, + ) + return data + +/datum/computer_file/program/watchdog/ui_static_data() + var/list/data = list() + data["mapRef"] = map_name + var/list/cameras = get_available_cameras() + data["cameras"] = list() + for(var/i in cameras) + var/obj/machinery/camera/C = cameras[i] + data["cameras"] += list(list( + name = C.c_tag, + )) + + return data + +/datum/computer_file/program/watchdog/ui_act(action, params) + . = ..() + if(.) + return + + if(action == "switch_camera") + var/c_tag = params["name"] + var/list/cameras = get_available_cameras() + var/obj/machinery/camera/selected_camera = cameras[c_tag] + active_camera = selected_camera + playsound(src, get_sfx("terminal_type"), 25, FALSE) + + if(!selected_camera) + return TRUE + + update_active_camera_screen() + + return TRUE + +/datum/computer_file/program/watchdog/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) + +/datum/computer_file/program/watchdog/proc/update_active_camera_screen() + // Show static if can't use the camera + if(!active_camera?.can_use()) + show_camera_static() + return + + var/list/visible_turfs = list() + + // 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 = isliving(active_camera.loc) ? active_camera.loc : 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. + var/newturf = get_turf(cam_location) + if(last_camera_turf == newturf) + 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 = active_camera.isXRay() ? range(active_camera.view_range, cam_location) : view(active_camera.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) + +/datum/computer_file/program/watchdog/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 +/datum/computer_file/program/watchdog/proc/get_available_cameras() + var/list/L = list() + for (var/obj/machinery/camera/cam as anything in GLOB.cameranet.cameras) + if(cam.virtual_z() != computer.virtual_z())//Only show cameras on the same level. + continue + L.Add(cam) + var/list/camlist = list() + for(var/obj/machinery/camera/cam as anything in L) + 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) + camlist["[cam.c_tag]"] = cam + return camlist diff --git a/tgui/packages/tgui/interfaces/NtosWatchdog.js b/tgui/packages/tgui/interfaces/NtosWatchdog.js new file mode 100644 index 000000000000..73d0ba5da50f --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosWatchdog.js @@ -0,0 +1,60 @@ +import { useBackend } from '../backend'; +import { Button, ByondUi } from '../components'; +import { NtosWindow } from '../layouts'; +import { + prevNextCamera, + selectCameras, + CameraConsoleContent, +} from './CameraConsole'; + +export const NtosWatchdog = (props, context) => { + const { act, data, config } = useBackend(context); + const { PC_device_theme, mapRef, activeCamera } = data; + const cameras = selectCameras(data.cameras); + const [prevCameraName, nextCameraName] = prevNextCamera( + cameras, + activeCamera + ); + return ( + + +
+ +
+
+
+ Camera: + {(activeCamera && activeCamera.name) || '—'} +
+
+
+ +
+
+
+ ); +};