diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm
index 393ffce4fde..d690dcd6274 100644
--- a/code/__DEFINES/atom_hud.dm
+++ b/code/__DEFINES/atom_hud.dm
@@ -20,8 +20,9 @@
#define DIAG_PATH_HUD "16"//Bot path indicators
#define GLAND_HUD "17"//Gland indicators for abductors
#define SENTIENT_DISEASE_HUD "18"
+#define AI_DETECT_HUD "19"
//for antag huds. these are used at the /mob level
-#define ANTAG_HUD "19"
+#define ANTAG_HUD "20"
//by default everything in the hud_list of an atom is an image
//a value in hud_list with one of these will change that behavior
@@ -37,22 +38,23 @@
#define DATA_HUD_DIAGNOSTIC_ADVANCED 6
#define DATA_HUD_ABDUCTOR 7
#define DATA_HUD_SENTIENT_DISEASE 8
+#define DATA_HUD_AI_DETECT 9
//antag HUD defines
-#define ANTAG_HUD_CULT 9
-#define ANTAG_HUD_REV 10
-#define ANTAG_HUD_OPS 11
-#define ANTAG_HUD_WIZ 12
-#define ANTAG_HUD_SHADOW 13
-#define ANTAG_HUD_TRAITOR 14
-#define ANTAG_HUD_NINJA 15
-#define ANTAG_HUD_CHANGELING 16
-#define ANTAG_HUD_ABDUCTOR 17
-#define ANTAG_HUD_DEVIL 18
-#define ANTAG_HUD_SINTOUCHED 19
-#define ANTAG_HUD_SOULLESS 20
-#define ANTAG_HUD_CLOCKWORK 21
-#define ANTAG_HUD_BROTHER 22
+#define ANTAG_HUD_CULT 10
+#define ANTAG_HUD_REV 11
+#define ANTAG_HUD_OPS 12
+#define ANTAG_HUD_WIZ 13
+#define ANTAG_HUD_SHADOW 14
+#define ANTAG_HUD_TRAITOR 15
+#define ANTAG_HUD_NINJA 16
+#define ANTAG_HUD_CHANGELING 17
+#define ANTAG_HUD_ABDUCTOR 18
+#define ANTAG_HUD_DEVIL 19
+#define ANTAG_HUD_SINTOUCHED 20
+#define ANTAG_HUD_SOULLESS 21
+#define ANTAG_HUD_CLOCKWORK 22
+#define ANTAG_HUD_BROTHER 23
// Notification action types
#define NOTIFY_JUMP "jump"
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 9f0fdc27b22..8891c06f750 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -85,20 +85,22 @@
#define BYOND_LIGHTING_PLANE 17
#define BYOND_LIGHTING_LAYER 17
+#define CAMERA_STATIC_PLANE 18
+#define CAMERA_STATIC_LAYER 18
//HUD layer defines
-#define FULLSCREEN_PLANE 18
-#define FLASH_LAYER 18
-#define FULLSCREEN_LAYER 18.1
-#define UI_DAMAGE_LAYER 18.2
-#define BLIND_LAYER 18.3
-#define CRIT_LAYER 18.4
-#define CURSE_LAYER 18.5
+#define FULLSCREEN_PLANE 19
+#define FLASH_LAYER 19
+#define FULLSCREEN_LAYER 19.1
+#define UI_DAMAGE_LAYER 19.2
+#define BLIND_LAYER 19.3
+#define CRIT_LAYER 19.4
+#define CURSE_LAYER 19.5
-#define HUD_PLANE 19
-#define HUD_LAYER 19
-#define ABOVE_HUD_PLANE 20
-#define ABOVE_HUD_LAYER 20
+#define HUD_PLANE 20
+#define HUD_LAYER 20
+#define ABOVE_HUD_PLANE 21
+#define ABOVE_HUD_LAYER 21
-#define SPLASHSCREEN_LAYER 21
-#define SPLASHSCREEN_PLANE 21
+#define SPLASHSCREEN_LAYER 22
+#define SPLASHSCREEN_PLANE 22
diff --git a/code/__DEFINES/sight.dm b/code/__DEFINES/sight.dm
index bf00ed0cf32..6ab3092d1d0 100644
--- a/code/__DEFINES/sight.dm
+++ b/code/__DEFINES/sight.dm
@@ -28,3 +28,8 @@
#define VISOR_VISIONFLAGS (1<<2) //all following flags only matter for glasses
#define VISOR_DARKNESSVIEW (1<<3)
#define VISOR_INVISVIEW (1<<4)
+
+//for whether AI eyes see static, and whether it is mouse-opaque or not
+#define USE_STATIC_NONE 0
+#define USE_STATIC_TRANSPARENT 1
+#define USE_STATIC_OPAQUE 2
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index 07513105ae3..1010aa7676d 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -26,6 +26,7 @@ GLOBAL_LIST_INIT(simple_animals, list(list(),list(),list(),list())) // One for e
GLOBAL_LIST_EMPTY(spidermobs) //all sentient spider mobs
GLOBAL_LIST_EMPTY(bots_list)
GLOBAL_LIST_EMPTY(living_cameras)
+GLOBAL_LIST_EMPTY(aiEyes)
GLOBAL_LIST_EMPTY(language_datum_instances)
GLOBAL_LIST_EMPTY(all_languages)
diff --git a/code/_onclick/hud/picture_in_picture.dm b/code/_onclick/hud/picture_in_picture.dm
index aea87827a6a..e028212e96b 100644
--- a/code/_onclick/hud/picture_in_picture.dm
+++ b/code/_onclick/hud/picture_in_picture.dm
@@ -124,14 +124,16 @@
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)
+ viewing_turfs = get_visible_turfs()
vis_contents += viewing_turfs
+/obj/screen/movable/pic_in_pic/proc/get_visible_turfs()
+ var/turf/T = get_turf(center)
+ if(!T)
+ return list()
+ 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)
+ return block(lowerleft, upperright)
/obj/screen/movable/pic_in_pic/proc/show_to(client/C)
if(C)
diff --git a/code/_onclick/hud/plane_master.dm b/code/_onclick/hud/plane_master.dm
index d4559ed66dc..5bdf61f75d3 100644
--- a/code/_onclick/hud/plane_master.dm
+++ b/code/_onclick/hud/plane_master.dm
@@ -52,3 +52,9 @@
/obj/screen/plane_master/lighting/backdrop(mob/mymob)
mymob.overlay_fullscreen("lighting_backdrop_lit", /obj/screen/fullscreen/lighting_backdrop/lit)
mymob.overlay_fullscreen("lighting_backdrop_unlit", /obj/screen/fullscreen/lighting_backdrop/unlit)
+
+/obj/screen/plane_master/camera_static
+ name = "camera static plane master"
+ plane = CAMERA_STATIC_PLANE
+ appearance_flags = PLANE_MASTER
+ blend_mode = BLEND_OVERLAY
diff --git a/code/datums/holocall.dm b/code/datums/holocall.dm
index 9d4b86b5ef3..4923609db80 100644
--- a/code/datums/holocall.dm
+++ b/code/datums/holocall.dm
@@ -60,7 +60,6 @@
user.remote_control = null
if(!QDELETED(eye))
- eye.RemoveImages()
QDEL_NULL(eye)
if(connected_holopad && !QDELETED(hologram))
diff --git a/code/datums/hud.dm b/code/datums/hud.dm
index 54b28deeee8..32bce89f4d1 100644
--- a/code/datums/hud.dm
+++ b/code/datums/hud.dm
@@ -12,6 +12,7 @@ GLOBAL_LIST_INIT(huds, list(
DATA_HUD_DIAGNOSTIC_ADVANCED = new/datum/atom_hud/data/diagnostic/advanced(),
DATA_HUD_ABDUCTOR = new/datum/atom_hud/abductor(),
DATA_HUD_SENTIENT_DISEASE = new/datum/atom_hud/sentient_disease(),
+ DATA_HUD_AI_DETECT = new/datum/atom_hud/ai_detector(),
ANTAG_HUD_CULT = new/datum/atom_hud/antag(),
ANTAG_HUD_REV = new/datum/atom_hud/antag(),
ANTAG_HUD_OPS = new/datum/atom_hud/antag(),
@@ -100,7 +101,8 @@ GLOBAL_LIST_INIT(huds, list(
return FALSE
hudatoms |= A
for(var/mob/M in hudusers)
- add_to_single_hud(M, A)
+ if(!queued_to_see[M])
+ add_to_single_hud(M, A)
return TRUE
/datum/atom_hud/proc/add_to_single_hud(mob/M, atom/A) //unsafe, no sanity apart from client
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index 9a60fdef31e..bc40dd8d424 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -66,6 +66,16 @@
/datum/atom_hud/sentient_disease
hud_icons = list(SENTIENT_DISEASE_HUD)
+/datum/atom_hud/ai_detector
+ hud_icons = list(AI_DETECT_HUD)
+
+/datum/atom_hud/ai_detector/add_hud_to(mob/M)
+ ..()
+ if(M && (hudusers.len == 1))
+ for(var/V in GLOB.aiEyes)
+ var/mob/camera/aiEye/E = V
+ E.update_ai_detect_hud()
+
/* MED/SEC/DIAG HUD HOOKS */
/*
diff --git a/code/game/machinery/computer/camera_advanced.dm b/code/game/machinery/computer/camera_advanced.dm
index 772bfc16043..86fcefb3407 100644
--- a/code/game/machinery/computer/camera_advanced.dm
+++ b/code/game/machinery/computer/camera_advanced.dm
@@ -59,7 +59,8 @@
C.remove(eyeobj)
if(user.client)
user.reset_perspective(null)
- eyeobj.RemoveImages()
+ if(eyeobj.visible_icon && user.client)
+ user.client.images -= eyeobj.user_image
eyeobj.eye_user = null
user.remote_control = null
@@ -108,7 +109,7 @@
if(!eyeobj.eye_initialized)
var/camera_location
var/turf/myturf = get_turf(src)
- if(eyeobj.use_static)
+ if(eyeobj.use_static != USE_STATIC_NONE)
if((!z_lock.len || (myturf.z in z_lock)) && GLOB.cameranet.checkTurfVis(myturf))
camera_location = myturf
else
@@ -151,6 +152,7 @@
/mob/camera/aiEye/remote
name = "Inactive Camera Eye"
+ ai_detector_visible = FALSE
var/sprint = 10
var/cooldown = 0
var/acceleration = 1
@@ -166,14 +168,9 @@
user.see_in_dark = 2
return 1
-/mob/camera/aiEye/remote/RemoveImages()
- ..()
- if(visible_icon)
- var/client/C = GetViewerClient()
- if(C)
- C.images -= user_image
-
/mob/camera/aiEye/remote/Destroy()
+ if(origin && eye_user)
+ origin.remove_eye_control(eye_user)
origin = null
. = ..()
eye_user = null
@@ -185,15 +182,14 @@
/mob/camera/aiEye/remote/setLoc(T)
if(eye_user)
- if(!isturf(eye_user.loc))
- return
T = get_turf(T)
if (T)
forceMove(T)
else
moveToNullspace()
- if(use_static)
- GLOB.cameranet.visibility(src, GetViewerClient())
+ update_ai_detect_hud()
+ if(use_static != USE_STATIC_NONE)
+ GLOB.cameranet.visibility(src, GetViewerClient(), null, use_static)
if(visible_icon)
if(eye_user.client)
eye_user.client.images -= user_image
diff --git a/code/game/objects/items/devices/multitool.dm b/code/game/objects/items/devices/multitool.dm
index 43c2b06d440..23749d4481e 100644
--- a/code/game/objects/items/devices/multitool.dm
+++ b/code/game/objects/items/devices/multitool.dm
@@ -15,6 +15,7 @@
desc = "Used for pulsing wires to test which to cut. Not recommended by doctors."
icon = 'icons/obj/device.dmi'
icon_state = "multitool"
+ item_state = "multitool"
lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
force = 5
@@ -40,8 +41,6 @@
if(selected_io)
selected_io = null
to_chat(user, "You clear the wired connection from the multitool.")
- else
- ..()
update_icon()
/obj/item/multitool/update_icon()
@@ -98,30 +97,89 @@
// Syndicate device disguised as a multitool; it will turn red when an AI camera is nearby.
-
/obj/item/multitool/ai_detect
var/track_cooldown = 0
var/track_delay = 10 //How often it checks for proximity
var/detect_state = PROXIMITY_NONE
var/rangealert = 8 //Glows red when inside
var/rangewarning = 20 //Glows yellow when inside
+ var/hud_type = DATA_HUD_AI_DETECT
+ var/hud_on = FALSE
+ var/mob/camera/aiEye/remote/ai_detector/eye
+ var/datum/action/item_action/toggle_multitool/toggle_action
-/obj/item/multitool/ai_detect/New()
- ..()
+/obj/item/multitool/ai_detect/Initialize()
+ . = ..()
START_PROCESSING(SSobj, src)
+ eye = new /mob/camera/aiEye/remote/ai_detector()
+ toggle_action = new /datum/action/item_action/toggle_multitool(src)
/obj/item/multitool/ai_detect/Destroy()
STOP_PROCESSING(SSobj, src)
+ if(hud_on && ismob(loc))
+ remove_hud(loc)
+ QDEL_NULL(toggle_action)
+ QDEL_NULL(eye)
return ..()
+/obj/item/multitool/ai_detect/ui_action_click()
+ return
+
+/obj/item/multitool/ai_detect/update_icon()
+ if(selected_io)
+ icon_state = "multitool_red"
+ else
+ icon_state = "[initial(icon_state)][detect_state]"
+
+/obj/item/multitool/ai_detect/equipped(mob/living/carbon/human/user, slot)
+ ..()
+ if(hud_on)
+ show_hud(user)
+
+/obj/item/multitool/ai_detect/dropped(mob/living/carbon/human/user)
+ ..()
+ if(hud_on)
+ remove_hud(user)
+
/obj/item/multitool/ai_detect/process()
if(track_cooldown > world.time)
return
detect_state = PROXIMITY_NONE
+ if(eye.eye_user)
+ eye.setLoc(get_turf(src))
multitool_detect()
- icon_state = "[initial(icon_state)][detect_state]"
+ update_icon()
track_cooldown = world.time + track_delay
+/obj/item/multitool/ai_detect/proc/toggle_hud(mob/user)
+ hud_on = !hud_on
+ if(user)
+ to_chat(user, "You toggle the ai detection HUD on [src] [hud_on ? "on" : "off"].")
+ if(hud_on)
+ show_hud(user)
+ else
+ remove_hud(user)
+
+/obj/item/multitool/ai_detect/proc/show_hud(mob/user)
+ if(user && hud_type)
+ var/obj/screen/plane_master/camera_static/PM = user.hud_used.plane_masters["[CAMERA_STATIC_PLANE]"]
+ PM.alpha = 150
+ var/datum/atom_hud/H = GLOB.huds[hud_type]
+ if(!H.hudusers[user])
+ H.add_hud_to(user)
+ eye.eye_user = user
+ eye.setLoc(get_turf(src))
+
+/obj/item/multitool/ai_detect/proc/remove_hud(mob/user)
+ if(user && hud_type)
+ var/obj/screen/plane_master/camera_static/PM = user.hud_used.plane_masters["[CAMERA_STATIC_PLANE]"]
+ PM.alpha = 255
+ var/datum/atom_hud/H = GLOB.huds[hud_type]
+ H.remove_hud_from(user)
+ if(eye)
+ eye.setLoc(null)
+ eye.eye_user = null
+
/obj/item/multitool/ai_detect/proc/multitool_detect()
var/turf/our_turf = get_turf(src)
for(var/mob/living/silicon/ai/AI in GLOB.ai_list)
@@ -134,6 +192,8 @@
var/datum/camerachunk/chunk = GLOB.cameranet.chunkGenerated(our_turf.x, our_turf.y, our_turf.z)
if(chunk && chunk.seenby.len)
for(var/mob/camera/aiEye/A in chunk.seenby)
+ if(!A.ai_detector_visible)
+ continue
var/turf/detect_turf = get_turf(A)
if(get_dist(our_turf, detect_turf) < rangealert)
detect_state = PROXIMITY_ON_SCREEN
@@ -142,6 +202,24 @@
detect_state = PROXIMITY_NEAR
break
+/mob/camera/aiEye/remote/ai_detector
+ name = "AI detector eye"
+ ai_detector_visible = FALSE
+ use_static = USE_STATIC_TRANSPARENT
+ visible_icon = FALSE
+
+/datum/action/item_action/toggle_multitool
+ name = "Toggle AI detector HUD"
+ check_flags = NONE
+
+/datum/action/item_action/toggle_multitool/Trigger()
+ if(!..())
+ return 0
+ if(target)
+ var/obj/item/multitool/ai_detect/M = target
+ M.toggle_hud(owner)
+ return 1
+
/obj/item/multitool/cyborg
name = "multitool"
desc = "Optimised and stripped-down version of a regular multitool."
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 490a6dcaf72..c223cbfcc86 100755
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -331,9 +331,9 @@
var/datum/camerachunk/C = GLOB.cameranet.chunkGenerated(x, y, z)
if(C)
if(C.obscuredTurfs[src])
- vis_contents += GLOB.cameranet.vis_contents
+ vis_contents += GLOB.cameranet.vis_contents_objects
else
- vis_contents -= GLOB.cameranet.vis_contents
+ vis_contents -= GLOB.cameranet.vis_contents_objects
/turf/proc/burn_tile()
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index c1ec315d92a..d9095ce462d 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -1003,7 +1003,7 @@
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)
+ GLOB.cameranet.visibility(moved_eye, client, all_eyes, USE_STATIC_OPAQUE)
/mob/living/silicon/ai/forceMove(atom/destination)
. = ..()
diff --git a/code/modules/mob/living/silicon/ai/freelook/cameranet.dm b/code/modules/mob/living/silicon/ai/freelook/cameranet.dm
index 36ae19b478f..8d5590def65 100644
--- a/code/modules/mob/living/silicon/ai/freelook/cameranet.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/cameranet.dm
@@ -18,15 +18,24 @@ GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new)
// The object used for the clickable stat() button.
var/obj/effect/statclick/statclick
- // The object used in vis_contents of obscured turfs
- var/vis_contents
+ // The objects used in vis_contents of obscured turfs
+ var/list/vis_contents_objects
+ var/obj/effect/overlay/camera_static/vis_contents_opaque
+ var/obj/effect/overlay/camera_static/vis_contents_transparent
// The image given to the effect in vis_contents on AI clients
var/image/obscured
+ var/image/obscured_transparent
/datum/cameranet/New()
- vis_contents = new /obj/effect/overlay/camera_static()
- obscured = new('icons/effects/cameravis.dmi', vis_contents, null, BYOND_LIGHTING_LAYER + 0.1)
- obscured.plane = BYOND_LIGHTING_PLANE + 1
+ vis_contents_opaque = new /obj/effect/overlay/camera_static()
+ vis_contents_transparent = new /obj/effect/overlay/camera_static/transparent()
+ vis_contents_objects = list(vis_contents_opaque, vis_contents_transparent)
+
+ obscured = new('icons/effects/cameravis.dmi', vis_contents_opaque, null, CAMERA_STATIC_LAYER)
+ obscured.plane = CAMERA_STATIC_PLANE
+
+ obscured_transparent = new('icons/effects/cameravis.dmi', vis_contents_transparent, null, CAMERA_STATIC_LAYER)
+ obscured_transparent.plane = CAMERA_STATIC_PLANE
// Checks if a chunk has been Generated in x, y, z.
/datum/cameranet/proc/chunkGenerated(x, y, z)
@@ -46,7 +55,7 @@ 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(list/moved_eyes, client/C, list/other_eyes)
+/datum/cameranet/proc/visibility(list/moved_eyes, client/C, list/other_eyes, use_static = USE_STATIC_OPAQUE)
if(!islist(moved_eyes))
moved_eyes = moved_eyes ? list(moved_eyes) : list()
if(islist(other_eyes))
@@ -54,19 +63,28 @@ GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new)
else
other_eyes = list()
+ if(C)
+ switch(use_static)
+ if(USE_STATIC_TRANSPARENT)
+ C.images += obscured_transparent
+ if(USE_STATIC_OPAQUE)
+ C.images += obscured
+
for(var/V in moved_eyes)
var/mob/camera/aiEye/eye = V
- 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/visibleChunks = list()
+ if(eye.loc)
+ // 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)
- 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/x = x1; x <= x2; x += CHUNK_SIZE)
+ for(var/y = y1; y <= y2; y += CHUNK_SIZE)
+ visibleChunks |= getCameraChunk(x, y, eye.z)
var/list/remove = eye.visibleCameraChunks - visibleChunks
var/list/add = visibleChunks - eye.visibleCameraChunks
@@ -79,9 +97,6 @@ GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new)
var/datum/camerachunk/c = chunk
c.add(eye)
- if(C)
- C.images += obscured
-
// Updates the chunks that the turf is located in. Use this when obstacles are destroyed or when doors open.
/datum/cameranet/proc/updateVisibility(atom/A, opacity_check = 1)
@@ -173,5 +188,8 @@ GLOBAL_DATUM_INIT(cameranet, /datum/cameranet, new)
mouse_opacity = MOUSE_OPACITY_ICON
invisibility = INVISIBILITY_ABSTRACT
- layer = BYOND_LIGHTING_LAYER + 0.1
- plane = BYOND_LIGHTING_PLANE + 1
+ layer = CAMERA_STATIC_LAYER
+ plane = CAMERA_STATIC_PLANE
+
+/obj/effect/overlay/camera_static/transparent
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
diff --git a/code/modules/mob/living/silicon/ai/freelook/chunk.dm b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
index 9a84616fbf5..d83292f7670 100644
--- a/code/modules/mob/living/silicon/ai/freelook/chunk.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/chunk.dm
@@ -29,6 +29,14 @@
/datum/camerachunk/proc/remove(mob/camera/aiEye/eye)
eye.visibleCameraChunks -= src
seenby -= eye
+ if(!eye.visibleCameraChunks.len)
+ var/client/client = eye.GetViewerClient()
+ if(client)
+ switch(eye.use_static)
+ if(USE_STATIC_TRANSPARENT)
+ client.images -= GLOB.cameranet.obscured_transparent
+ if(USE_STATIC_OPAQUE)
+ client.images -= GLOB.cameranet.obscured
// Called when a chunk has changed. I.E: A wall was deleted.
@@ -81,12 +89,12 @@
for(var/turf in visAdded)
var/turf/t = turf
- t.vis_contents -= GLOB.cameranet.vis_contents
+ t.vis_contents -= GLOB.cameranet.vis_contents_objects
for(var/turf in visRemoved)
var/turf/t = turf
if(obscuredTurfs[t] && !istype(t, /turf/open/ai_visible))
- t.vis_contents += GLOB.cameranet.vis_contents
+ t.vis_contents += GLOB.cameranet.vis_contents_objects
changed = 0
@@ -128,7 +136,7 @@
for(var/turf in obscuredTurfs)
var/turf/t = turf
- t.vis_contents += GLOB.cameranet.vis_contents
+ t.vis_contents += GLOB.cameranet.vis_contents_objects
#undef UPDATE_BUFFER
#undef CHUNK_SIZE
diff --git a/code/modules/mob/living/silicon/ai/freelook/eye.dm b/code/modules/mob/living/silicon/ai/freelook/eye.dm
index c2c395cabbb..bedb9638edb 100644
--- a/code/modules/mob/living/silicon/ai/freelook/eye.dm
+++ b/code/modules/mob/living/silicon/ai/freelook/eye.dm
@@ -7,28 +7,81 @@
name = "Inactive AI Eye"
invisibility = INVISIBILITY_MAXIMUM
+ hud_possible = list(ANTAG_HUD, AI_DETECT_HUD = HUD_LIST_LIST)
var/list/visibleCameraChunks = list()
var/mob/living/silicon/ai/ai = null
var/relay_speech = FALSE
- var/use_static = TRUE
+ var/use_static = USE_STATIC_OPAQUE
var/static_visibility_range = 16
+ var/ai_detector_visible = TRUE
+ var/ai_detector_color = COLOR_RED
+
+/mob/camera/aiEye/Initialize()
+ . = ..()
+ GLOB.aiEyes += src
+ update_ai_detect_hud()
+ setLoc(loc, TRUE)
+
+/mob/camera/aiEye/proc/update_ai_detect_hud()
+ var/datum/atom_hud/ai_detector/hud = GLOB.huds[DATA_HUD_AI_DETECT]
+ var/list/old_images = hud_list[AI_DETECT_HUD]
+ if(!ai_detector_visible)
+ hud.remove_from_hud(src)
+ QDEL_LIST(old_images)
+ return
+
+ if(!hud.hudusers.len)
+ //no one is watching, do not bother updating anything
+ return
+ hud.remove_from_hud(src)
+
+ var/static/list/vis_contents_objects = list()
+ var/obj/effect/overlay/ai_detect_hud/hud_obj = vis_contents_objects[ai_detector_color]
+ if(!hud_obj)
+ hud_obj = new /obj/effect/overlay/ai_detect_hud()
+ hud_obj.color = ai_detector_color
+ vis_contents_objects[ai_detector_color] = hud_obj
+
+ var/list/new_images = list()
+ var/list/turfs = get_visible_turfs()
+ for(var/T in turfs)
+ var/image/I = (old_images.len > new_images.len) ? old_images[new_images.len + 1] : image(null, T)
+ I.loc = T
+ I.vis_contents += hud_obj
+ new_images += I
+ for(var/i in (new_images.len + 1) to old_images.len)
+ qdel(old_images[i])
+ hud_list[AI_DETECT_HUD] = new_images
+ hud.add_to_hud(src)
+
+/mob/camera/aiEye/proc/get_visible_turfs()
+ if(!isturf(loc))
+ return list()
+ var/client/C = GetViewerClient()
+ var/view = C ? getviewsize(C.view) : getviewsize(world.view)
+ var/turf/lowerleft = locate(max(1, x - (view[1] - 1)/2), max(1, y - (view[2] - 1)/2), z)
+ var/turf/upperright = locate(min(world.maxx, lowerleft.x + (view[1] - 1)), min(world.maxy, lowerleft.y + (view[2] - 1)), lowerleft.z)
+ return block(lowerleft, upperright)
// Use this when setting the aiEye's location.
// It will also stream the chunk that the new loc is in.
-/mob/camera/aiEye/proc/setLoc(T)
+/mob/camera/aiEye/proc/setLoc(T, force_update = FALSE)
if(ai)
if(!isturf(ai.loc))
return
T = get_turf(T)
+ if(!force_update && (T == get_turf(src)) )
+ return //we are already here!
if (T)
forceMove(T)
else
- moveToNullspace() // ????
- if(use_static)
+ moveToNullspace()
+ if(use_static != USE_STATIC_NONE)
ai.camera_visibility(src)
if(ai.client && !ai.multicam_on)
ai.client.eye = src
+ update_ai_detect_hud()
update_parallax_contents()
//Holopad
if(istype(ai.current, /obj/machinery/holopad))
@@ -47,11 +100,6 @@
return ai.client
return null
-/mob/camera/aiEye/proc/RemoveImages()
- var/client/C = GetViewerClient()
- if(C && use_static)
- C.images -= GLOB.cameranet.obscured
-
/mob/camera/aiEye/Destroy()
if(ai)
ai.all_eyes -= src
@@ -59,6 +107,12 @@
for(var/V in visibleCameraChunks)
var/datum/camerachunk/c = V
c.remove(src)
+ GLOB.aiEyes -= src
+ if(ai_detector_visible)
+ var/datum/atom_hud/ai_detector/hud = GLOB.huds[DATA_HUD_AI_DETECT]
+ hud.remove_from_hud(src)
+ var/list/L = hud_list[AI_DETECT_HUD]
+ QDEL_LIST(L)
return ..()
/atom/proc/move_camera_by_click()
@@ -131,3 +185,12 @@
/mob/camera/aiEye/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, message_mode)
if(relay_speech && speaker && ai && !radio_freq && speaker != ai && near_camera(speaker))
ai.relay_speech(message, speaker, message_language, raw_message, radio_freq, spans, message_mode)
+
+/obj/effect/overlay/ai_detect_hud
+ name = ""
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ icon = 'icons/effects/alphacolors.dmi'
+ icon_state = ""
+ alpha = 100
+ layer = ABOVE_ALL_MOB_LAYER
+ plane = GAME_PLANE
diff --git a/code/modules/mob/living/silicon/ai/multicam.dm b/code/modules/mob/living/silicon/ai/multicam.dm
index d77e7f8a408..20b5f962429 100644
--- a/code/modules/mob/living/silicon/ai/multicam.dm
+++ b/code/modules/mob/living/silicon/ai/multicam.dm
@@ -124,6 +124,7 @@ GLOBAL_DATUM(ai_camera_room_landmark, /obj/effect/landmark/ai_multicam_room)
var/list/cameras_telegraphed = list()
var/telegraph_cameras = TRUE
var/telegraph_range = 7
+ ai_detector_color = COLOR_ORANGE
/mob/camera/aiEye/pic_in_pic/GetViewerClient()
if(screen && screen.ai)
@@ -139,6 +140,10 @@ GLOBAL_DATUM(ai_camera_room_landmark, /obj/effect/landmark/ai_multicam_room)
else
GLOB.cameranet.visibility(src)
update_camera_telegraphing()
+ update_ai_detect_hud()
+
+/mob/camera/aiEye/pic_in_pic/get_visible_turfs()
+ return screen ? screen.get_visible_turfs() : list()
/mob/camera/aiEye/pic_in_pic/proc/update_camera_telegraphing()
if(!telegraph_cameras)
diff --git a/code/modules/shuttle/navigation_computer.dm b/code/modules/shuttle/navigation_computer.dm
index c58b3eee7be..e8106766cd8 100644
--- a/code/modules/shuttle/navigation_computer.dm
+++ b/code/modules/shuttle/navigation_computer.dm
@@ -249,7 +249,7 @@
/mob/camera/aiEye/remote/shuttle_docker
visible_icon = FALSE
- use_static = FALSE
+ use_static = USE_STATIC_NONE
var/list/placement_images = list()
var/list/placed_images = list()
diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm
index a9f2c63e133..8f63efb2933 100644
--- a/code/modules/uplink/uplink_items.dm
+++ b/code/modules/uplink/uplink_items.dm
@@ -1002,8 +1002,10 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
/datum/uplink_item/device_tools/ai_detector
name = "Artificial Intelligence Detector"
- desc = "A functional multitool that turns red when it detects an artificial intelligence watching it or its \
- holder. Knowing when an artificial intelligence is watching you is useful for knowing when to maintain cover."
+ desc = "A functional multitool that turns red when it detects an artificial intelligence watching it, and can be \
+ activated to display their exact viewing location and nearby security camera blind spots. Knowing when \
+ an artificial intelligence is watching you is useful for knowing when to maintain cover, and finding nearby \
+ blind spots can help you identify escape routes."
item = /obj/item/multitool/ai_detect
cost = 1