Adds a camera viewing app, SecurEye, for Laptops and Modular Consoles (#1303)

Co-authored-by: zxaber <37497534+zxaber@users.noreply.github.com>
This commit is contained in:
SkyratBot
2020-10-14 18:44:38 +02:00
committed by GitHub
parent 358419da57
commit f5d76f547b
7 changed files with 310 additions and 33 deletions

View File

@@ -0,0 +1,195 @@
#define DEFAULT_MAP_SIZE 15
/datum/computer_file/program/secureye
filename = "secureye"
filedesc = "SecurEye"
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
size = 5
tgui_id = "NtosSecurEye"
program_icon = "eye"
var/list/network = list("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/map_name
var/obj/screen/map_view/cam_screen
/// All the plane masters that need to be applied.
var/list/cam_plane_masters
var/obj/screen/background/cam_background
/datum/computer_file/program/secureye/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(/obj/screen/plane_master))
var/obj/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/secureye/Destroy()
qdel(cam_screen)
QDEL_LIST(cam_plane_masters)
qdel(cam_background)
return ..()
/datum/computer_file/program/secureye/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/secureye/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/secureye/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/secureye/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/secureye/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/secureye/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/secureye/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/secureye/proc/get_available_cameras()
var/list/L = list()
for (var/obj/machinery/camera/cam in GLOB.cameranet.cameras)
if(!is_station_level(cam.z))//Only show station cameras.
continue
L.Add(cam)
var/list/camlist = list()
for(var/obj/machinery/camera/cam 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

View File

@@ -2578,6 +2578,7 @@
#include "code\modules\modular_computers\file_system\programs\radar.dm" #include "code\modules\modular_computers\file_system\programs\radar.dm"
#include "code\modules\modular_computers\file_system\programs\robocontrol.dm" #include "code\modules\modular_computers\file_system\programs\robocontrol.dm"
#include "code\modules\modular_computers\file_system\programs\robotact.dm" #include "code\modules\modular_computers\file_system\programs\robotact.dm"
#include "code\modules\modular_computers\file_system\programs\secureye.dm"
#include "code\modules\modular_computers\file_system\programs\sm_monitor.dm" #include "code\modules\modular_computers\file_system\programs\sm_monitor.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\contract_uplink.dm" #include "code\modules\modular_computers\file_system\programs\antagonist\contract_uplink.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\dos.dm" #include "code\modules\modular_computers\file_system\programs\antagonist\dos.dm"

View File

@@ -4,14 +4,14 @@ import { classes } from 'common/react';
import { createSearch } from 'common/string'; import { createSearch } from 'common/string';
import { Fragment } from 'inferno'; import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend'; import { useBackend, useLocalState } from '../backend';
import { Button, ByondUi, Input, Section } from '../components'; import { Button, ByondUi, Input, Section, Flex } from '../components';
import { Window } from '../layouts'; import { Window } from '../layouts';
/** /**
* Returns previous and next camera names relative to the currently * Returns previous and next camera names relative to the currently
* active camera. * active camera.
*/ */
const prevNextCamera = (cameras, activeCamera) => { export const prevNextCamera = (cameras, activeCamera) => {
if (!activeCamera) { if (!activeCamera) {
return []; return [];
} }
@@ -29,7 +29,7 @@ const prevNextCamera = (cameras, activeCamera) => {
* *
* Filters cameras, applies search terms and sorts the alphabetically. * Filters cameras, applies search terms and sorts the alphabetically.
*/ */
const selectCameras = (cameras, searchText = '') => { export const selectCameras = (cameras, searchText = '') => {
const testSearch = createSearch(searchText, camera => camera.name); const testSearch = createSearch(searchText, camera => camera.name);
return flow([ return flow([
// Null camera filter // Null camera filter
@@ -100,36 +100,45 @@ export const CameraConsoleContent = (props, context) => {
const { activeCamera } = data; const { activeCamera } = data;
const cameras = selectCameras(data.cameras, searchText); const cameras = selectCameras(data.cameras, searchText);
return ( return (
<Fragment> <Flex
<Input direction={"column"}
autoFocus height="100%">
fluid <Flex.Item>
mb={1} <Input
placeholder="Search for a camera" autoFocus
onInput={(e, value) => setSearchText(value)} /> fluid
<Section> mt={1}
{cameras.map(camera => ( placeholder="Search for a camera"
onInput={(e, value) => setSearchText(value)} />
</Flex.Item>
<Flex.Item
height="100%">
<Section
fill
scrollable>
{cameras.map(camera => (
// We're not using the component here because performance // We're not using the component here because performance
// would be absolutely abysmal (50+ ms for each re-render). // would be absolutely abysmal (50+ ms for each re-render).
<div <div
key={camera.name} key={camera.name}
title={camera.name} title={camera.name}
className={classes([ className={classes([
'Button', 'Button',
'Button--fluid', 'Button--fluid',
'Button--color--transparent', 'Button--color--transparent',
'Button--ellipsis', 'Button--ellipsis',
activeCamera activeCamera
&& camera.name === activeCamera.name && camera.name === activeCamera.name
&& 'Button--selected', && 'Button--selected',
])} ])}
onClick={() => act('switch_camera', { onClick={() => act('switch_camera', {
name: camera.name, name: camera.name,
})}> })}>
{camera.name} {camera.name}
</div> </div>
))} ))}
</Section> </Section>
</Fragment> </Flex.Item>
</Flex>
); );
}; };

View File

@@ -0,0 +1,60 @@
import { filter, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { classes } from 'common/react';
import { createSearch } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Button, ByondUi, Input, Section } from '../components';
import { NtosWindow } from '../layouts';
import { prevNextCamera, selectCameras, CameraConsoleContent } from './CameraConsole';
import { logger } from "../logging";
export const NtosSecurEye = (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 (
<NtosWindow
width={800}
height={600}
theme={PC_device_theme}>
<NtosWindow.Content>
<div className="CameraConsole__left">
<CameraConsoleContent />
</div>
<div className="CameraConsole__right">
<div className="CameraConsole__toolbar">
<b>Camera: </b>
{activeCamera
&& activeCamera.name
|| '—'}
</div>
<div className="CameraConsole__toolbarRight">
<Button
icon="chevron-left"
disabled={!prevCameraName}
onClick={() => act('switch_camera', {
name: prevCameraName,
})} />
<Button
icon="chevron-right"
disabled={!nextCameraName}
onClick={() => act('switch_camera', {
name: nextCameraName,
})} />
</div>
<ByondUi
className="CameraConsole__map"
params={{
id: mapRef,
type: 'map',
}} />
</div>
</NtosWindow.Content>
</NtosWindow>
);
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long