From 6b4c209244cafd9f6acb17193f2c947dc425becd Mon Sep 17 00:00:00 2001
From: zxaber <37497534+zxaber@users.noreply.github.com>
Date: Wed, 14 Oct 2020 03:14:06 -0700
Subject: [PATCH] tgui: SecurEye, for Laptops and Modular Consoles (#54225)
New app for viewing cameras. Runs the same way the stand-alone console does, and requires security access to download. Does not work on tablets, requires a laptop or console.
Gives sec an on-the-go option, at the expense that they do still have to hold the device to view it (rather than stuffing it into the ID slot). Folded laptops can still be stored in your bag, so you can quickly store it after scoping out a bust.
---
.../file_system/programs/secureye.dm | 195 ++++++++++++++++++
tgstation.dme | 1 +
.../packages/tgui/interfaces/CameraConsole.js | 69 ++++---
tgui/packages/tgui/interfaces/NtosSecurEye.js | 60 ++++++
tgui/public/tgui-common.chunk.js | 2 +-
tgui/public/tgui-panel.bundle.js | 2 +-
tgui/public/tgui.bundle.js | 2 +-
7 files changed, 298 insertions(+), 33 deletions(-)
create mode 100644 code/modules/modular_computers/file_system/programs/secureye.dm
create mode 100644 tgui/packages/tgui/interfaces/NtosSecurEye.js
diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm
new file mode 100644
index 00000000000..78e72640edd
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/secureye.dm
@@ -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
diff --git a/tgstation.dme b/tgstation.dme
index 573098d8063..a4ef909bccc 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2565,6 +2565,7 @@
#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\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\antagonist\contract_uplink.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\dos.dm"
diff --git a/tgui/packages/tgui/interfaces/CameraConsole.js b/tgui/packages/tgui/interfaces/CameraConsole.js
index fefa3981db3..39a70f64eca 100644
--- a/tgui/packages/tgui/interfaces/CameraConsole.js
+++ b/tgui/packages/tgui/interfaces/CameraConsole.js
@@ -4,14 +4,14 @@ 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 { Button, ByondUi, Input, Section, Flex } from '../components';
import { Window } from '../layouts';
/**
* Returns previous and next camera names relative to the currently
* active camera.
*/
-const prevNextCamera = (cameras, activeCamera) => {
+export const prevNextCamera = (cameras, activeCamera) => {
if (!activeCamera) {
return [];
}
@@ -29,7 +29,7 @@ const prevNextCamera = (cameras, activeCamera) => {
*
* 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);
return flow([
// Null camera filter
@@ -100,36 +100,45 @@ export const CameraConsoleContent = (props, context) => {
const { activeCamera } = data;
const cameras = selectCameras(data.cameras, searchText);
return (
- "+e+"
/gi,"\n").replace(/<\/?[a-z0-9-_]+[^>]*>/gi,"").replace(/&(nbsp|amp|quot|lt|gt|apos);/g,(function(e,n){return t[n]})).replace(/?([0-9]+);/gi,(function(e,t){var n=parseInt(t,10);return String.fromCharCode(n)})).replace(/?([0-9a-f]+);/gi,(function(e,t){var n=parseInt(t,16);return String.fromCharCode(n)}))};t.buildQueryString=function(e){return Object.keys(e).map((function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])})).join("&")}},function(e,t,n){"use strict";t.__esModule=!0,t.Box=t.computeBoxClassName=t.computeBoxProps=t.halfUnit=t.unit=void 0;var r=n(6),o=n(0),i=n(465),a=n(42);var u=function(e){return"string"==typeof e?e.endsWith("px")&&!Byond.IS_LTE_IE8?parseFloat(e)/12+"rem":e:"number"==typeof e?Byond.IS_LTE_IE8?12*e+"px":e+"rem":void 0};t.unit=u;var c=function(e){return"string"==typeof e?u(e):"number"==typeof e?u(.5*e):void 0};t.halfUnit=c;var s=function(e){return"string"==typeof e&&a.CSS_COLORS.includes(e)},l=function(e){return function(t,n){"number"!=typeof n&&"string"!=typeof n||(t[e]=n)}},f=function(e,t){return function(n,r){"number"!=typeof r&&"string"!=typeof r||(n[e]=t(r))}},d=function(e,t){return function(n,r){r&&(n[e]=t)}},p=function(e,t,n){return function(r,o){if("number"==typeof o||"string"==typeof o)for(var i=0;ic||n!=n?l*Infinity:l*n}},function(e,t,n){"use strict";var r=n(4),o=Math.hypot,i=Math.abs,a=Math.sqrt;r({target:"Math",stat:!0,forced:!!o&&o(Infinity,NaN)!==Infinity},{hypot:function(e,t){for(var n,r,o=0,u=0,c=arguments.length,s=0;u
\n":"'+(n?e:ee(e,!0))+"
\n"}return e}(),t.blockquote=function(){function e(e){return""+(n?e:ee(e,!0))+"\n"+e+"
\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
\n":"
\n"}return e}(),t.list=function(){function e(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+""+r+">\n"}return e}(),t.listitem=function(){function e(e){return"\n\n"+e+"\n"+t+"
\n"}return e}(),t.tablerow=function(){function e(e){return"\n"+e+" \n"}return e}(),t.tablecell=function(){function e(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+""+n+">\n"}return e}(),t.strong=function(){function e(e){return""+e+""}return e}(),t.em=function(){function e(e){return""+e+""}return e}(),t.codespan=function(){function e(e){return""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?"
":"
"}return e}(),t.del=function(){function e(e){return""+e+""}return e}(),t.link=function(){function e(e,t,n){if(null===(e=J(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"+n+""}return e}(),t.image=function(){function e(e,t,n){if(null===(e=J(this.options.sanitize,this.options.baseUrl,e)))return n;var r='":">"}return e}(),t.text=function(){function e(e){return e}return e}(),e}(),ne=function(){function e(){}var t=e.prototype;return t.strong=function(){function e(e){return e}return e}(),t.em=function(){function e(e){return e}return e}(),t.codespan=function(){function e(e){return e}return e}(),t.del=function(){function e(e){return e}return e}(),t.html=function(){function e(e){return e}return e}(),t.text=function(){function e(e){return e}return e}(),t.link=function(){function e(e,t,n){return""+n}return e}(),t.image=function(){function e(e,t,n){return""+n}return e}(),t.br=function(){function e(){return""}return e}(),e}(),re=function(){function e(){this.seen={}}return e.prototype.slug=function(){function e(e){var t=e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t)){var n=t;do{this.seen[n]++,t=n+"-"+this.seen[n]}while(this.seen.hasOwnProperty(t))}return this.seen[t]=0,t}return e}(),e}(),oe=a.defaults,ie=I.unescape,ae=function(){function e(e){this.options=e||oe,this.options.renderer=this.options.renderer||new te,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ne,this.slugger=new re}e.parse=function(){function t(t,n){return new e(n).parse(t)}return t}();var t=e.prototype;return t.parse=function(){function e(e,t){void 0===t&&(t=!0);var n,r,o,i,a,u,c,s,l,f,d,p,h,v,g,m,y,b,x="",w=e.length;for(n=0;n
"+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a