diff --git a/_maps/map_files/generic/CentCom.dmm b/_maps/map_files/generic/CentCom.dmm
index 0f9857ad73..02c20e6896 100644
--- a/_maps/map_files/generic/CentCom.dmm
+++ b/_maps/map_files/generic/CentCom.dmm
@@ -5699,6 +5699,10 @@
},
/turf/open/floor/plasteel/cafeteria,
/area/centcom/holding)
+"pW" = (
+/obj/effect/landmark/ai_multicam_room,
+/turf/open/ai_visible,
+/area/ai_multicam_room)
"pX" = (
/obj/item/storage/crayons,
/obj/structure/table,
@@ -10234,6 +10238,9 @@
dir = 8
},
/area/centcom/evac)
+"Di" = (
+/turf/closed/indestructible/riveted,
+/area/ai_multicam_room)
"Dj" = (
/obj/machinery/vr_sleeper{
dir = 8
@@ -13624,6 +13631,9 @@
"PY" = (
/turf/open/floor/plasteel/whitegreen,
/area/centcom/holding)
+"Qe" = (
+/turf/open/ai_visible,
+/area/ai_multicam_room)
"Qk" = (
/obj/structure/flora/ausbushes/fullgrass,
/obj/structure/flora/ausbushes/ppflowers,
@@ -30581,29 +30591,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
aa
aa
aa
@@ -30838,29 +30848,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -31095,29 +31105,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -31352,29 +31362,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -31609,29 +31619,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -31866,29 +31876,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -32123,29 +32133,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -32380,29 +32390,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -32637,29 +32647,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -32894,29 +32904,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -33151,29 +33161,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -33408,29 +33418,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+pW
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -33665,29 +33675,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -33922,29 +33932,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -34179,29 +34189,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -34436,29 +34446,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -34693,29 +34703,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -34950,29 +34960,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -35207,29 +35217,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -35464,29 +35474,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -35721,29 +35731,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -35978,29 +35988,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Qe
+Di
aa
aa
aa
@@ -36235,29 +36245,29 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
-aa
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
+Di
aa
aa
aa
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 9ef689de8b..e15d4f035c 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -26,6 +26,14 @@
return
next_click = world.time + 1
+ if(multicam_on)
+ var/turf/T = get_turf(A)
+ if(T)
+ for(var/obj/screen/movable/pic_in_pic/ai/P in T.vis_locs)
+ if(P.ai == src)
+ P.Click(params)
+ break
+
if(check_click_intercept(params,A))
return
diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm
index 3225252520..b558e7c87c 100644
--- a/code/_onclick/hud/_defines.dm
+++ b/code/_onclick/hud/_defines.dm
@@ -134,6 +134,8 @@
#define ui_ai_take_picture "SOUTH:6,WEST+12"
#define ui_ai_view_images "SOUTH:6,WEST+13"
#define ui_ai_sensor "SOUTH:6,WEST+14"
+#define ui_ai_multicam "SOUTH+1:6,WEST+13"
+#define ui_ai_add_multicam "SOUTH+1:6,WEST+14"
//Pop-up inventory
#define ui_shoes "WEST+1:8,SOUTH:5"
diff --git a/code/_onclick/hud/ai.dm b/code/_onclick/hud/ai.dm
index 8d4aca5fbf..463051c6b4 100644
--- a/code/_onclick/hud/ai.dm
+++ b/code/_onclick/hud/ai.dm
@@ -164,6 +164,26 @@
var/mob/living/silicon/S = usr
S.toggle_sensors()
+/obj/screen/ai/multicam
+ name = "Multicamera Mode"
+ icon_state = "multicam"
+
+/obj/screen/ai/multicam/Click()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.toggle_multicam()
+
+/obj/screen/ai/add_multicam
+ name = "New Camera"
+ icon_state = "new_cam"
+
+/obj/screen/ai/add_multicam/Click()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.drop_new_multicam()
+
/datum/hud/ai
ui_style_icon = 'icons/mob/screen_ai.dmi'
@@ -247,12 +267,20 @@
using.screen_loc = ui_ai_view_images
static_inventory += using
-
//Medical/Security sensors
using = new /obj/screen/ai/sensors()
using.screen_loc = ui_ai_sensor
static_inventory += using
+//Multicamera mode
+ using = new /obj/screen/ai/multicam()
+ using.screen_loc = ui_ai_multicam
+ static_inventory += using
+
+//Add multicamera camera
+ using = new /obj/screen/ai/add_multicam()
+ using.screen_loc = ui_ai_add_multicam
+ static_inventory += using
/mob/living/silicon/ai/create_mob_hud()
if(client && !hud_used)
diff --git a/code/_onclick/hud/movable_screen_objects.dm b/code/_onclick/hud/movable_screen_objects.dm
index 4da721012d..f71ced7a9d 100644
--- a/code/_onclick/hud/movable_screen_objects.dm
+++ b/code/_onclick/hud/movable_screen_objects.dm
@@ -12,6 +12,8 @@
var/snap2grid = FALSE
var/moved = FALSE
var/locked = FALSE
+ var/x_off = -16
+ var/y_off = -16
//Snap Screen Object
//Tied to the grid, snaps to the nearest turf
@@ -42,8 +44,8 @@
screen_loc = "[screen_loc_X[1]],[screen_loc_Y[1]]"
else //Normalise Pixel Values (So the object drops at the center of the mouse, not 16 pixels off)
- var/pix_X = text2num(screen_loc_X[2]) - 16
- var/pix_Y = text2num(screen_loc_Y[2]) - 16
+ var/pix_X = text2num(screen_loc_X[2]) + x_off
+ var/pix_Y = text2num(screen_loc_Y[2]) + y_off
screen_loc = "[screen_loc_X[1]]:[pix_X],[screen_loc_Y[1]]:[pix_Y]"
moved = screen_loc
diff --git a/code/_onclick/hud/picture_in_picture.dm b/code/_onclick/hud/picture_in_picture.dm
new file mode 100644
index 0000000000..ce5f2c9884
--- /dev/null
+++ b/code/_onclick/hud/picture_in_picture.dm
@@ -0,0 +1,144 @@
+/obj/screen/movable/pic_in_pic
+ name = "Picture-in-picture"
+ screen_loc = "CENTER"
+ plane = GAME_PLANE
+ var/atom/center
+ var/width = 0
+ var/height = 0
+ var/list/shown_to = list()
+ var/list/viewing_turfs = list()
+ var/obj/screen/component_button/button_x
+ var/obj/screen/component_button/button_expand
+ var/obj/screen/component_button/button_shrink
+
+ var/mutable_appearance/standard_background
+ var/const/max_dimensions = 10
+
+/obj/screen/movable/pic_in_pic/Initialize()
+ . = ..()
+ make_backgrounds()
+
+/obj/screen/movable/pic_in_pic/Destroy()
+ for(var/C in shown_to)
+ unshow_to(C)
+ QDEL_NULL(button_x)
+ QDEL_NULL(button_shrink)
+ QDEL_NULL(button_expand)
+ return ..()
+
+/obj/screen/movable/pic_in_pic/component_click(obj/screen/component_button/component, params)
+ if(component == button_x)
+ qdel(src)
+ else if(component == button_expand)
+ set_view_size(width+1, height+1)
+ else if(component == button_shrink)
+ set_view_size(width-1, height-1)
+
+/obj/screen/movable/pic_in_pic/proc/make_backgrounds()
+ standard_background = new /mutable_appearance()
+ standard_background.icon = 'icons/misc/pic_in_pic.dmi'
+ standard_background.icon_state = "background"
+ standard_background.layer = SPACE_LAYER
+
+/obj/screen/movable/pic_in_pic/proc/add_buttons()
+ var/static/mutable_appearance/move_tab
+ if(!move_tab)
+ move_tab = new /mutable_appearance()
+ //all these properties are always the same, and since adding something to the overlay
+ //list makes a copy, there is no reason to make a new one each call
+ move_tab.icon = 'icons/misc/pic_in_pic.dmi'
+ move_tab.icon_state = "move"
+ move_tab.plane = HUD_PLANE
+ var/matrix/M = matrix()
+ M.Translate(0, (height + 0.25) * world.icon_size)
+ move_tab.transform = M
+ add_overlay(move_tab)
+
+ if(!button_x)
+ button_x = new /obj/screen/component_button(null, src)
+ var/mutable_appearance/MA = new /mutable_appearance()
+ MA.name = "close"
+ MA.icon = 'icons/misc/pic_in_pic.dmi'
+ MA.icon_state = "x"
+ MA.plane = HUD_PLANE
+ button_x.appearance = MA
+ M = matrix()
+ M.Translate((max(4, width) - 0.75) * world.icon_size, (height + 0.25) * world.icon_size)
+ button_x.transform = M
+ vis_contents += button_x
+
+ if(!button_expand)
+ button_expand = new /obj/screen/component_button(null, src)
+ var/mutable_appearance/MA = new /mutable_appearance()
+ MA.name = "expand"
+ MA.icon = 'icons/misc/pic_in_pic.dmi'
+ MA.icon_state = "expand"
+ MA.plane = HUD_PLANE
+ button_expand.appearance = MA
+ M = matrix()
+ M.Translate(world.icon_size, (height + 0.25) * world.icon_size)
+ button_expand.transform = M
+ vis_contents += button_expand
+
+ if(!button_shrink)
+ button_shrink = new /obj/screen/component_button(null, src)
+ var/mutable_appearance/MA = new /mutable_appearance()
+ MA.name = "shrink"
+ MA.icon = 'icons/misc/pic_in_pic.dmi'
+ MA.icon_state = "shrink"
+ MA.plane = HUD_PLANE
+ button_shrink.appearance = MA
+ M = matrix()
+ M.Translate(2 * world.icon_size, (height + 0.25) * world.icon_size)
+ button_shrink.transform = M
+ vis_contents += button_shrink
+
+/obj/screen/movable/pic_in_pic/proc/add_background()
+ if((width > 0) && (height > 0))
+ var/matrix/M = matrix()
+ M.Scale(width + 0.5, height + 0.5)
+ M.Translate((width-1)/2 * world.icon_size, (height-1)/2 * world.icon_size)
+ standard_background.transform = M
+ add_overlay(standard_background)
+
+/obj/screen/movable/pic_in_pic/proc/set_view_size(width, height, do_refresh = TRUE)
+ width = CLAMP(width, 0, max_dimensions)
+ height = CLAMP(height, 0, max_dimensions)
+ src.width = width
+ src.height = height
+
+ y_off = -height * world.icon_size - 16
+
+ cut_overlays()
+ add_background()
+ add_buttons()
+ if(do_refresh)
+ refresh_view()
+
+/obj/screen/movable/pic_in_pic/proc/set_view_center(atom/target, do_refresh = TRUE)
+ center = target
+ if(do_refresh)
+ refresh_view()
+
+/obj/screen/movable/pic_in_pic/proc/refresh_view()
+ vis_contents -= viewing_turfs
+ if(!width || !height)
+ return
+ var/turf/T = get_turf(center)
+ if(!T)
+ return
+ var/turf/lowerleft = locate(max(1, T.x - round(width/2)), max(1, T.y - round(height/2)), T.z)
+ var/turf/upperright = locate(min(world.maxx, lowerleft.x + width - 1), min(world.maxy, lowerleft.y + height - 1), lowerleft.z)
+ viewing_turfs = block(lowerleft, upperright)
+ vis_contents += viewing_turfs
+
+
+/obj/screen/movable/pic_in_pic/proc/show_to(client/C)
+ if(C)
+ shown_to[C] = 1
+ C.screen += src
+
+/obj/screen/movable/pic_in_pic/proc/unshow_to(client/C)
+ if(C)
+ shown_to -= C
+ C.screen -= src
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 02ece81b41..d2ac10a83f 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -30,6 +30,9 @@
/obj/screen/orbit()
return
+/obj/screen/proc/component_click(obj/screen/component_button/component, params)
+ return
+
/obj/screen/text
icon = null
icon_state = null
@@ -604,3 +607,15 @@
holder.screen -= src
holder = null
return ..()
+
+
+/obj/screen/component_button
+ var/obj/screen/parent
+
+/obj/screen/component_button/Initialize(mapload, obj/screen/parent)
+ . = ..()
+ src.parent = parent
+
+/obj/screen/component_button/Click(params)
+ if(parent)
+ parent.component_click(src, params)
diff --git a/code/datums/holocall.dm b/code/datums/holocall.dm
index d19e8be2e1..22dc6a7671 100644
--- a/code/datums/holocall.dm
+++ b/code/datums/holocall.dm
@@ -60,9 +60,6 @@
user.remote_control = null
if(!QDELETED(eye))
- if(user_good && user.client)
- for(var/datum/camerachunk/chunk in eye.visibleCameraChunks)
- chunk.remove(eye)
qdel(eye)
eye = null
@@ -284,7 +281,7 @@
else
var/datum/preset_holoimage/H = new preset_image_type
record.caller_image = H.build_image()
-
+
//These build caller image from outfit and some additional data, for use by mappers for ruin holorecords
/datum/preset_holoimage
var/nonhuman_mobtype //Fill this if you just want something nonhuman
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index 3e8a0bb84d..2ea74637d5 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -36,6 +36,7 @@
var/alarm_on = FALSE
var/busy = FALSE
var/emped = FALSE //Number of consecutive EMP's on this camera
+ var/in_use_lights = 0
// Upgrades bitflag
var/upgrades = 0
@@ -285,7 +286,7 @@
else if (stat & EMPED)
icon_state = "[initial(icon_state)]emp"
else
- icon_state = "[initial(icon_state)]"
+ icon_state = "[initial(icon_state)][in_use_lights ? "_in_use" : ""]"
/obj/machinery/camera/proc/toggle_cam(mob/user, displaymessage = 1)
status = !status
diff --git a/code/game/machinery/computer/camera_advanced.dm b/code/game/machinery/computer/camera_advanced.dm
index 6112a5d760..26a8ac940e 100644
--- a/code/game/machinery/computer/camera_advanced.dm
+++ b/code/game/machinery/computer/camera_advanced.dm
@@ -190,7 +190,7 @@
else
moveToNullspace()
if(use_static)
- GLOB.cameranet.visibility(src)
+ GLOB.cameranet.visibility(src, GetViewerClient())
if(visible_icon)
if(eye_user.client)
eye_user.client.images -= user_image
diff --git a/code/modules/lighting/lighting_object.dm b/code/modules/lighting/lighting_object.dm
index fe25dc9439..c0c5a411e7 100644
--- a/code/modules/lighting/lighting_object.dm
+++ b/code/modules/lighting/lighting_object.dm
@@ -11,8 +11,6 @@
layer = LIGHTING_LAYER
invisibility = INVISIBILITY_LIGHTING
- blend_mode = BLEND_ADD
-
var/needs_update = FALSE
var/turf/myturf
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 8664a111ae..cbc04d11be 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -74,7 +74,7 @@
var/nuking = FALSE
var/obj/machinery/doomsday_device/doomsday_device
- var/mob/camera/aiEye/eyeobj = new
+ var/mob/camera/aiEye/eyeobj
var/sprint = 10
var/cooldown = 0
var/acceleration = 1
@@ -85,6 +85,13 @@
var/datum/action/innate/deploy_last_shell/redeploy_action = new
var/chnotify = 0
+ var/multicam_allowed = FALSE
+ var/multicam_on = FALSE
+ var/obj/screen/movable/pic_in_pic/ai/master_multicam
+ var/list/multicam_screens = list()
+ var/list/all_eyes = list()
+ var/max_multicams = 6
+
/mob/living/silicon/ai/Initialize(mapload, datum/ai_laws/L, mob/target_ai)
. = ..()
if(!target_ai) //If there is no player/brain inside.
@@ -116,8 +123,7 @@
job = "AI"
- eyeobj.ai = src
- eyeobj.forceMove(src.loc)
+ create_eye()
rename_self("ai")
holo_icon = getHologramIcon(icon('icons/mob/ai.dmi',"default"))
@@ -871,9 +877,12 @@
current = A
if(client)
if(ismovableatom(A))
+ if(A != GLOB.ai_camera_room_landmark)
+ end_multicam()
client.perspective = EYE_PERSPECTIVE
client.eye = A
else
+ end_multicam()
if(isturf(loc))
if(eyeobj)
client.eye = eyeobj
@@ -993,3 +1002,11 @@
. = ..()
if(!target_ai)
target_ai = src //cheat! just give... ourselves as the spawned AI, because that's technically correct
+
+/mob/living/silicon/ai/proc/camera_visibility(mob/camera/aiEye/moved_eye)
+ GLOB.cameranet.visibility(moved_eye, client, all_eyes)
+
+/mob/living/silicon/ai/forceMove(atom/destination)
+ . = ..()
+ if(.)
+ end_multicam()
diff --git a/code/modules/mob/living/silicon/ai/freelook/cameranet.dm b/code/modules/mob/living/silicon/ai/freelook/cameranet.dm
index 815ddb64ea..5cc08ff1e0 100644
--- a/code/modules/mob/living/silicon/ai/freelook/cameranet.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/cameranet.dm
@@ -38,29 +38,63 @@ GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new)
// Updates what the aiEye can see. It is recommended you use this when the aiEye moves or it's location is set.
-/datum/cameranet/proc/visibility(mob/camera/aiEye/ai)
- // 0xf = 15
- var/x1 = max(0, ai.x - 16) & ~(CHUNK_SIZE - 1)
- var/y1 = max(0, ai.y - 16) & ~(CHUNK_SIZE - 1)
- var/x2 = min(world.maxx, ai.x + 16) & ~(CHUNK_SIZE - 1)
- var/y2 = min(world.maxy, ai.y + 16) & ~(CHUNK_SIZE - 1)
+/datum/cameranet/proc/visibility(list/moved_eyes, client/C, list/other_eyes)
+ if(!islist(moved_eyes))
+ moved_eyes = moved_eyes ? list(moved_eyes) : list()
+ if(islist(other_eyes))
+ other_eyes = (other_eyes - moved_eyes)
+ else
+ other_eyes = list()
- var/list/visibleChunks = list()
+ var/list/chunks_pre_seen = list()
+ var/list/chunks_post_seen = list()
- for(var/x = x1; x <= x2; x += CHUNK_SIZE)
- for(var/y = y1; y <= y2; y += CHUNK_SIZE)
- visibleChunks |= getCameraChunk(x, y, ai.z)
+ for(var/V in moved_eyes)
+ var/mob/camera/aiEye/eye = V
+ if(C)
+ chunks_pre_seen |= eye.visibleCameraChunks
+ // 0xf = 15
+ var/static_range = eye.static_visibility_range
+ var/x1 = max(0, eye.x - static_range) & ~(CHUNK_SIZE - 1)
+ var/y1 = max(0, eye.y - static_range) & ~(CHUNK_SIZE - 1)
+ var/x2 = min(world.maxx, eye.x + static_range) & ~(CHUNK_SIZE - 1)
+ var/y2 = min(world.maxy, eye.y + static_range) & ~(CHUNK_SIZE - 1)
- var/list/remove = ai.visibleCameraChunks - visibleChunks
- var/list/add = visibleChunks - ai.visibleCameraChunks
+ var/list/visibleChunks = list()
- for(var/chunk in remove)
- var/datum/camerachunk/c = chunk
- c.remove(ai)
+ for(var/x = x1; x <= x2; x += CHUNK_SIZE)
+ for(var/y = y1; y <= y2; y += CHUNK_SIZE)
+ visibleChunks |= getCameraChunk(x, y, eye.z)
- for(var/chunk in add)
- var/datum/camerachunk/c = chunk
- c.add(ai)
+ var/list/remove = eye.visibleCameraChunks - visibleChunks
+ var/list/add = visibleChunks - eye.visibleCameraChunks
+
+ for(var/chunk in remove)
+ var/datum/camerachunk/c = chunk
+ c.remove(eye, FALSE)
+
+ for(var/chunk in add)
+ var/datum/camerachunk/c = chunk
+ c.add(eye, FALSE)
+
+ if(C)
+ chunks_post_seen |= eye.visibleCameraChunks
+
+ if(C)
+ for(var/V in other_eyes)
+ var/mob/camera/aiEye/eye = V
+ chunks_post_seen |= eye.visibleCameraChunks
+
+ var/list/remove = chunks_pre_seen - chunks_post_seen
+ var/list/add = chunks_post_seen - chunks_pre_seen
+
+ for(var/chunk in remove)
+ var/datum/camerachunk/c = chunk
+ C.images -= c.obscured
+
+ for(var/chunk in add)
+ var/datum/camerachunk/c = chunk
+ C.images += c.obscured
// Updates the chunks that the turf is located in. Use this when obstacles are destroyed or when doors open.
diff --git a/code/modules/mob/living/silicon/ai/freelook/chunk.dm b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
index f062c7e010..e03bba3067 100644
--- a/code/modules/mob/living/silicon/ai/freelook/chunk.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
@@ -20,10 +20,11 @@
// Add an AI eye to the chunk, then update if changed.
-/datum/camerachunk/proc/add(mob/camera/aiEye/eye)
- var/client/client = eye.GetViewerClient()
- if(client)
- client.images += obscured
+/datum/camerachunk/proc/add(mob/camera/aiEye/eye, add_images = TRUE)
+ if(add_images)
+ var/client/client = eye.GetViewerClient()
+ if(client)
+ client.images += obscured
eye.visibleCameraChunks += src
visible++
seenby += eye
@@ -32,10 +33,11 @@
// Remove an AI eye from the chunk, then update if changed.
-/datum/camerachunk/proc/remove(mob/camera/aiEye/eye)
- var/client/client = eye.GetViewerClient()
- if(client)
- client.images -= obscured
+/datum/camerachunk/proc/remove(mob/camera/aiEye/eye, remove_images = TRUE)
+ if(remove_images)
+ var/client/client = eye.GetViewerClient()
+ if(client)
+ client.images -= obscured
eye.visibleCameraChunks -= src
seenby -= eye
if(visible > 0)
diff --git a/code/modules/mob/living/silicon/ai/freelook/eye.dm b/code/modules/mob/living/silicon/ai/freelook/eye.dm
index b5b1bd082b..469639d58f 100644
--- a/code/modules/mob/living/silicon/ai/freelook/eye.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/eye.dm
@@ -11,6 +11,7 @@
var/mob/living/silicon/ai/ai = null
var/relay_speech = FALSE
var/use_static = TRUE
+ var/static_visibility_range = 16
// Use this when setting the aiEye's location.
// It will also stream the chunk that the new loc is in.
@@ -25,8 +26,8 @@
else
moveToNullspace() // ????
if(use_static)
- GLOB.cameranet.visibility(src)
- if(ai.client)
+ ai.camera_visibility(src)
+ if(ai.client && !ai.multicam_on)
ai.client.eye = src
update_parallax_contents()
//Holopad
@@ -35,6 +36,8 @@
H.move_hologram(ai, T)
if(ai.camera_light_on)
ai.light_cameras()
+ if(ai.master_multicam)
+ ai.master_multicam.refresh_view()
/mob/camera/aiEye/Move()
return 0
@@ -45,20 +48,27 @@
return null
/mob/camera/aiEye/proc/RemoveImages()
- if(use_static)
- for(var/datum/camerachunk/chunk in visibleCameraChunks)
- chunk.remove(src)
+ var/client/C = GetViewerClient()
+ if(C && use_static)
+ for(var/V in visibleCameraChunks)
+ var/datum/camerachunk/c = V
+ C.images -= c.obscured
/mob/camera/aiEye/Destroy()
- ai = null
+ if(ai)
+ ai.all_eyes -= src
+ ai = null
+ for(var/V in visibleCameraChunks)
+ var/datum/camerachunk/c = V
+ c.remove(src)
return ..()
/atom/proc/move_camera_by_click()
if(isAI(usr))
var/mob/living/silicon/ai/AI = usr
- if(AI.eyeobj && AI.client.eye == AI.eyeobj)
+ if(AI.eyeobj && (AI.multicam_on || (AI.client.eye == AI.eyeobj)) && (AI.eyeobj.z == z))
AI.cameraFollow = null
- if (isturf(src.loc) || isturf(src))
+ if (isturf(loc) || isturf(src))
AI.eyeobj.setLoc(src)
// This will move the AIEye. It will also cause lights near the eye to light up, if toggled.
@@ -95,12 +105,19 @@
if(!eyeobj || !eyeobj.loc || QDELETED(eyeobj))
to_chat(src, "ERROR: Eyeobj not found. Creating new eye...")
- eyeobj = new(loc)
- eyeobj.ai = src
- eyeobj.name = "[src.name] (AI Eye)" // Give it a name
+ create_eye()
eyeobj.setLoc(loc)
+/mob/living/silicon/ai/proc/create_eye()
+ if(eyeobj)
+ return
+ eyeobj = new /mob/camera/aiEye()
+ all_eyes += eyeobj
+ eyeobj.ai = src
+ eyeobj.setLoc(loc)
+ eyeobj.name = "[name] (AI Eye)"
+
/mob/living/silicon/ai/verb/toggle_acceleration()
set category = "AI Commands"
set name = "Toggle Camera Acceleration"
diff --git a/code/modules/mob/living/silicon/ai/life.dm b/code/modules/mob/living/silicon/ai/life.dm
index a060c899d9..0fd33110b6 100644
--- a/code/modules/mob/living/silicon/ai/life.dm
+++ b/code/modules/mob/living/silicon/ai/life.dm
@@ -96,6 +96,7 @@
/mob/living/silicon/ai/proc/start_RestorePowerRoutine()
to_chat(src, "Backup battery online. Scanners, camera, and radio interface offline. Beginning fault-detection.")
+ end_multicam()
sleep(50)
var/turf/T = get_turf(src)
var/area/AIarea = get_area(src)
diff --git a/code/modules/mob/living/silicon/ai/login.dm b/code/modules/mob/living/silicon/ai/login.dm
index c771f60aa2..2dc9a1fb1c 100644
--- a/code/modules/mob/living/silicon/ai/login.dm
+++ b/code/modules/mob/living/silicon/ai/login.dm
@@ -4,4 +4,6 @@
for(var/obj/machinery/ai_status_display/O in GLOB.ai_status_displays) //change status
O.mode = 1
O.emotion = "Neutral"
+ if(multicam_on)
+ end_multicam()
view_core()
diff --git a/code/modules/mob/living/silicon/ai/multicam.dm b/code/modules/mob/living/silicon/ai/multicam.dm
new file mode 100644
index 0000000000..b6f3724fcf
--- /dev/null
+++ b/code/modules/mob/living/silicon/ai/multicam.dm
@@ -0,0 +1,266 @@
+//Picture in picture
+
+/obj/screen/movable/pic_in_pic/ai
+ var/mob/living/silicon/ai/ai
+ var/mutable_appearance/highlighted_background
+ var/highlighted = FALSE
+ var/mob/camera/aiEye/pic_in_pic/aiEye
+
+/obj/screen/movable/pic_in_pic/ai/Initialize()
+ . = ..()
+ aiEye = new /mob/camera/aiEye/pic_in_pic()
+ aiEye.screen = src
+
+/obj/screen/movable/pic_in_pic/ai/Destroy()
+ set_ai(null)
+ QDEL_NULL(aiEye)
+ return ..()
+
+/obj/screen/movable/pic_in_pic/ai/Click()
+ ..()
+ if(ai)
+ ai.select_main_multicam_window(src)
+
+/obj/screen/movable/pic_in_pic/ai/make_backgrounds()
+ ..()
+ highlighted_background = new /mutable_appearance()
+ highlighted_background.icon = 'icons/misc/pic_in_pic.dmi'
+ highlighted_background.icon_state = "background_highlight"
+ highlighted_background.layer = SPACE_LAYER
+
+/obj/screen/movable/pic_in_pic/ai/add_background()
+ if((width > 0) && (height > 0))
+ var/matrix/M = matrix()
+ M.Scale(width + 0.5, height + 0.5)
+ M.Translate((width-1)/2 * world.icon_size, (height-1)/2 * world.icon_size)
+ highlighted_background.transform = M
+ standard_background.transform = M
+ add_overlay(highlighted ? highlighted_background : standard_background)
+
+/obj/screen/movable/pic_in_pic/ai/set_view_size(width, height, do_refresh = TRUE)
+ aiEye.static_visibility_range = (round(max(width, height) / 2) + 1)
+ if(ai)
+ ai.camera_visibility(aiEye)
+ ..()
+
+/obj/screen/movable/pic_in_pic/ai/set_view_center(atom/target, do_refresh = TRUE)
+ ..()
+ aiEye.setLoc(get_turf(target))
+
+/obj/screen/movable/pic_in_pic/ai/refresh_view()
+ ..()
+ aiEye.setLoc(get_turf(center))
+
+/obj/screen/movable/pic_in_pic/ai/proc/highlight()
+ if(highlighted)
+ return
+ highlighted = TRUE
+ cut_overlay(standard_background)
+ add_overlay(highlighted_background)
+
+/obj/screen/movable/pic_in_pic/ai/proc/unhighlight()
+ if(!highlighted)
+ return
+ highlighted = FALSE
+ cut_overlay(highlighted_background)
+ add_overlay(standard_background)
+
+/obj/screen/movable/pic_in_pic/ai/proc/set_ai(mob/living/silicon/ai/new_ai)
+ if(ai)
+ ai.multicam_screens -= src
+ ai.all_eyes -= aiEye
+ if(ai.master_multicam == src)
+ ai.master_multicam = null
+ if(ai.multicam_on)
+ unshow_to(ai.client)
+ ai = new_ai
+ if(new_ai)
+ new_ai.multicam_screens += src
+ ai.all_eyes += aiEye
+ if(new_ai.multicam_on)
+ show_to(new_ai.client)
+
+//Turf, area, and landmark for the viewing room
+
+/turf/open/ai_visible
+ name = ""
+ icon = 'icons/misc/pic_in_pic.dmi'
+ icon_state = "room_background"
+ flags_1 = NOJAUNT_1
+
+/turf/open/ai_visible/Initialize()
+ . = ..()
+ obscured = image(null, src, null)
+
+/area/ai_multicam_room
+ name = "ai_multicam_room"
+ icon_state = "ai_camera_room"
+ dynamic_lighting = DYNAMIC_LIGHTING_DISABLED
+ valid_territory = FALSE
+ ambientsounds = list()
+ blob_allowed = FALSE
+ noteleport = TRUE
+ hidden = TRUE
+ safe = TRUE
+
+GLOBAL_DATUM(ai_camera_room_landmark, /obj/effect/landmark/ai_multicam_room)
+
+/obj/effect/landmark/ai_multicam_room
+ name = "ai camera room"
+ icon = 'icons/mob/landmarks.dmi'
+ icon_state = "x"
+
+/obj/effect/landmark/ai_multicam_room/Initialize()
+ . = ..()
+ qdel(GLOB.ai_camera_room_landmark)
+ GLOB.ai_camera_room_landmark = src
+
+/obj/effect/landmark/ai_multicam_room/Destroy()
+ if(GLOB.ai_camera_room_landmark == src)
+ GLOB.ai_camera_room_landmark = null
+ return ..()
+
+//Dummy camera eyes
+
+/mob/camera/aiEye/pic_in_pic
+ name = "Secondary AI Eye"
+ var/obj/screen/movable/pic_in_pic/ai/screen
+ var/list/cameras_telegraphed = list()
+ var/telegraph_cameras = TRUE
+ var/telegraph_range = 7
+
+/mob/camera/aiEye/pic_in_pic/GetViewerClient()
+ if(screen && screen.ai)
+ return screen.ai.client
+
+/mob/camera/aiEye/pic_in_pic/setLoc(turf/T)
+ if (T)
+ forceMove(T)
+ else
+ moveToNullspace()
+ if(screen && screen.ai)
+ screen.ai.camera_visibility(src)
+ else
+ GLOB.cameranet.visibility(src)
+ update_camera_telegraphing()
+
+/mob/camera/aiEye/pic_in_pic/proc/update_camera_telegraphing()
+ if(!telegraph_cameras)
+ return
+ var/list/obj/machinery/camera/add = list()
+ var/list/obj/machinery/camera/remove = list()
+ var/list/obj/machinery/camera/visible = list()
+ for (var/VV in visibleCameraChunks)
+ var/datum/camerachunk/CC = VV
+ for (var/V in CC.cameras)
+ var/obj/machinery/camera/C = V
+ if (!C.can_use() || (get_dist(C, src) > telegraph_range))
+ continue
+ visible |= C
+
+ add = visible - cameras_telegraphed
+ remove = cameras_telegraphed - visible
+
+ for (var/V in remove)
+ var/obj/machinery/camera/C = V
+ if(QDELETED(C))
+ continue
+ cameras_telegraphed -= C
+ C.in_use_lights--
+ C.update_icon()
+ for (var/V in add)
+ var/obj/machinery/camera/C = V
+ if(QDELETED(C))
+ continue
+ cameras_telegraphed |= C
+ C.in_use_lights++
+ C.update_icon()
+
+/mob/camera/aiEye/pic_in_pic/proc/disable_camera_telegraphing()
+ telegraph_cameras = FALSE
+ for (var/V in cameras_telegraphed)
+ var/obj/machinery/camera/C = V
+ if(QDELETED(C))
+ continue
+ C.in_use_lights--
+ C.update_icon()
+ cameras_telegraphed.Cut()
+
+/mob/camera/aiEye/pic_in_pic/Destroy()
+ disable_camera_telegraphing()
+ return ..()
+
+//AI procs
+
+/mob/living/silicon/ai/proc/drop_new_multicam(silent = FALSE)
+ if(!multicam_allowed)
+ if(!silent)
+ to_chat(src, "This action is currently disabled. Contact an administrator to enable this feature.")
+ return
+ if(!eyeobj)
+ return
+ if(multicam_screens.len >= max_multicams)
+ if(!silent)
+ to_chat(src, "Cannot place more than [max_multicams] multicamera windows.")
+ return
+ var/obj/screen/movable/pic_in_pic/ai/C = new /obj/screen/movable/pic_in_pic/ai()
+ C.set_view_size(3, 3, FALSE)
+ C.set_view_center(get_turf(eyeobj))
+ C.set_ai(src)
+ if(!silent)
+ to_chat(src, "Added new multicamera window.")
+ return C
+
+/mob/living/silicon/ai/proc/toggle_multicam()
+ if(!multicam_allowed)
+ to_chat(src, "This action is currently disabled. Contact an administrator to enable this feature.")
+ return
+ if(multicam_on)
+ end_multicam()
+ else
+ start_multicam()
+
+/mob/living/silicon/ai/proc/start_multicam()
+ if(multicam_on || aiRestorePowerRoutine || !isturf(loc))
+ return
+ if(!GLOB.ai_camera_room_landmark)
+ to_chat(src, "This function is not available at this time.")
+ return
+ multicam_on = TRUE
+ refresh_multicam()
+ to_chat(src, "Multiple-camera viewing mode activated.")
+
+/mob/living/silicon/ai/proc/refresh_multicam()
+ reset_perspective(GLOB.ai_camera_room_landmark)
+ if(client)
+ for(var/V in multicam_screens)
+ var/obj/screen/movable/pic_in_pic/P = V
+ P.show_to(client)
+
+/mob/living/silicon/ai/proc/end_multicam()
+ if(!multicam_on)
+ return
+ multicam_on = FALSE
+ select_main_multicam_window(null)
+ if(client)
+ for(var/V in multicam_screens)
+ var/obj/screen/movable/pic_in_pic/P = V
+ P.unshow_to(client)
+ reset_perspective()
+ to_chat(src, "Multiple-camera viewing mode deactivated.")
+
+
+/mob/living/silicon/ai/proc/select_main_multicam_window(obj/screen/movable/pic_in_pic/ai/P)
+ if(master_multicam == P)
+ return
+
+ if(master_multicam)
+ master_multicam.set_view_center(get_turf(eyeobj), FALSE)
+ master_multicam.unhighlight()
+ master_multicam = null
+
+ if(P)
+ P.highlight()
+ eyeobj.setLoc(get_turf(P.center))
+ P.set_view_center(eyeobj)
+ master_multicam = P
diff --git a/icons/misc/pic_in_pic.dmi b/icons/misc/pic_in_pic.dmi
new file mode 100644
index 0000000000..9c550f3c25
Binary files /dev/null and b/icons/misc/pic_in_pic.dmi differ
diff --git a/icons/mob/screen_ai.dmi b/icons/mob/screen_ai.dmi
index a449cad906..8388ea3f80 100644
Binary files a/icons/mob/screen_ai.dmi and b/icons/mob/screen_ai.dmi differ
diff --git a/icons/obj/machines/camera.dmi b/icons/obj/machines/camera.dmi
index 343a5118e8..1e1789d643 100644
Binary files a/icons/obj/machines/camera.dmi and b/icons/obj/machines/camera.dmi differ
diff --git a/icons/turf/areas.dmi b/icons/turf/areas.dmi
index 4e78e0444b..1fadd875d7 100644
Binary files a/icons/turf/areas.dmi and b/icons/turf/areas.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 909f5fdd99..e0a84f22a5 100755
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -181,6 +181,7 @@
#include "code\_onclick\hud\monkey.dm"
#include "code\_onclick\hud\movable_screen_objects.dm"
#include "code\_onclick\hud\parallax.dm"
+#include "code\_onclick\hud\picture_in_picture.dm"
#include "code\_onclick\hud\plane_master.dm"
#include "code\_onclick\hud\revenanthud.dm"
#include "code\_onclick\hud\robot.dm"
@@ -1949,6 +1950,7 @@
#include "code\modules\mob\living\silicon\ai\life.dm"
#include "code\modules\mob\living\silicon\ai\login.dm"
#include "code\modules\mob\living\silicon\ai\logout.dm"
+#include "code\modules\mob\living\silicon\ai\multicam.dm"
#include "code\modules\mob\living\silicon\ai\say.dm"
#include "code\modules\mob\living\silicon\ai\vox_sounds.dm"
#include "code\modules\mob\living\silicon\ai\freelook\cameranet.dm"