diff --git a/code/__DEFINES/mob_defines.dm b/code/__DEFINES/mob_defines.dm
index 81d6c5d61221e..1de9803499240 100644
--- a/code/__DEFINES/mob_defines.dm
+++ b/code/__DEFINES/mob_defines.dm
@@ -273,7 +273,7 @@
#define ispathanimal(A) (ispath(A, /mob/living/simple_animal))
#define iscameramob(A) (istype((A), /mob/camera))
-#define is_ai_eye(A) (istype((A), /mob/camera/eye))
+#define is_ai_eye(A) (istype((A), /mob/camera/eye/ai))
#define isovermind(A) (istype((A), /mob/camera/blob))
#define isobserver(A) (istype((A), /mob/dead/observer))
diff --git a/code/__DEFINES/sight.dm b/code/__DEFINES/sight.dm
index 842bc77edbe73..3f9e6c40d1917 100644
--- a/code/__DEFINES/sight.dm
+++ b/code/__DEFINES/sight.dm
@@ -48,3 +48,8 @@
#define VISOR_VISIONFLAGS (1<<2) //all following flags only matter for glasses
#define VISOR_DARKNESSVIEW (1<<3)
#define VISOR_INVISVIEW (1<<4)
+
+// Should AI eyes be counted for get_mobs_in_view?
+#define AI_EYE_EXCLUDE 0
+#define AI_EYE_REQUIRE_HEAR 1
+#define AI_EYE_INCLUDE 2
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 771023644467b..b7b2b5fca1edd 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -146,7 +146,7 @@
return turfs
/// Recursively loops through the contents of this atom looking for mobs, optionally requiring them to have a client.
-/proc/collect_nested_mobs(atom/parent, list/mobs, recursion_limit = 3, client_check = TRUE)
+/proc/collect_nested_mobs(atom/parent, list/mobs, recursion_limit = 3, client_check = TRUE, ai_eyes = AI_EYE_EXCLUDE)
var/list/next_layer = list(parent)
for(var/depth in 1 to recursion_limit)
var/list/layer = next_layer
@@ -156,7 +156,16 @@
continue
var/mob/this_mob = thing
if(!client_check || this_mob.client)
- mobs += this_mob
+ if(is_ai(this_mob))
+ // AIs can get messages from their eye as well as themselves, so use |= to make sure they don't get double messages.
+ mobs |= this_mob
+ else
+ // Everything else can only be visited once, so use += for efficiency.
+ mobs += this_mob
+ else if(ai_eyes != AI_EYE_EXCLUDE && is_ai_eye(this_mob))
+ var/mob/camera/eye/ai/eye = this_mob
+ if((ai_eyes == AI_EYE_INCLUDE || eye.relay_speech) && eye.ai && (!client_check || eye.ai.client))
+ mobs |= eye.ai
for(var/mob/dead/observer/ghost in this_mob.observers)
if(!client_check || ghost.client)
mobs += ghost
@@ -166,7 +175,7 @@
// The old system would loop through lists for a total of 5000 per function call, in an empty server.
// This new system will loop at around 1000 in an empty server.
-/proc/get_mobs_in_view(R, atom/source, include_clientless = FALSE)
+/proc/get_mobs_in_view(R, atom/source, include_clientless = FALSE, ai_eyes = AI_EYE_EXCLUDE)
// Returns a list of mobs in range of R from source. Used in radio and say code.
#ifdef GAME_TESTS
// kind of feels cleaner clobbering here than changing the loop?
@@ -181,7 +190,7 @@
for(var/atom/A in hear(R, T))
if(isobj(A) || ismob(A))
- collect_nested_mobs(A, hear, 3, !include_clientless)
+ collect_nested_mobs(A, hear, 3, !include_clientless, ai_eyes)
return hear
diff --git a/code/datums/emote.dm b/code/datums/emote.dm
index bfdc4d593336b..f97f08652b0fe 100644
--- a/code/datums/emote.dm
+++ b/code/datums/emote.dm
@@ -292,7 +292,7 @@
var/runechat_text = text
if(length(text) > 100)
runechat_text = "[copytext(text, 1, 101)]..."
- var/list/can_see = get_mobs_in_view(1, user) //Allows silicon & mmi mobs carried around to see the emotes of the person carrying them around.
+ var/list/can_see = get_mobs_in_view(1, user, ai_eyes=AI_EYE_INCLUDE) //Allows silicon & mmi mobs carried around to see the emotes of the person carrying them around.
can_see |= viewers(user, null)
for(var/mob/O as anything in can_see)
if(O.status_flags & PASSEMOTES)
@@ -304,6 +304,10 @@
if(O.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
O.create_chat_message(user, runechat_text, symbol = RUNECHAT_SYMBOL_EMOTE)
+ if(is_ai_eye(O))
+ var/mob/camera/eye/ai/eye = O
+ if(!(eye.ai in can_see) && eye.ai?.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
+ eye.ai.create_chat_message(user, runechat_text, symbol = RUNECHAT_SYMBOL_EMOTE)
/**
* Check whether or not an emote can be used due to a cooldown.
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 8a888c4f195a0..76a49169c7b6e 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -1170,7 +1170,7 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
if(!message)
return
var/list/speech_bubble_hearers = list()
- for(var/mob/M as anything in get_mobs_in_view(7, src))
+ for(var/mob/M as anything in get_mobs_in_view(7, src, ai_eyes=AI_EYE_REQUIRE_HEAR))
M.show_message("[src] [atom_say_verb], \"[message]\"", 2, null, 1)
if(M.client)
speech_bubble_hearers += M.client
diff --git a/code/game/gamemodes/miniantags/pulsedemon/pulsedemon.dm b/code/game/gamemodes/miniantags/pulsedemon/pulsedemon.dm
index 83e7c32d8b69f..ad8964afbb078 100644
--- a/code/game/gamemodes/miniantags/pulsedemon/pulsedemon.dm
+++ b/code/game/gamemodes/miniantags/pulsedemon/pulsedemon.dm
@@ -578,7 +578,7 @@
else if(istype(loc, /obj/machinery/hologram/holopad))
var/obj/machinery/hologram/holopad/H = loc
name = "[H]"
- for(var/mob/M as anything in get_mobs_in_view(7, H))
+ for(var/mob/M as anything in get_mobs_in_view(7, H, ai_eyes = AI_EYE_REQUIRE_HEAR))
M.hear_say(message_pieces, verb, FALSE, src)
name = real_name
return TRUE
@@ -594,7 +594,7 @@
/mob/living/simple_animal/demon/pulse_demon/visible_message(message, self_message, blind_message, chat_message_type)
// overriden because pulse demon is quite often in non-turf locs, and /mob/visible_message acts differently there
- for(var/mob/M as anything in get_mobs_in_view(7, src))
+ for(var/mob/M as anything in get_mobs_in_view(7, src, ai_eyes = AI_EYE_INCLUDE))
if(M.see_invisible < invisibility)
continue //can't view the invisible
var/msg = message
diff --git a/code/game/objects/items/devices/megaphone.dm b/code/game/objects/items/devices/megaphone.dm
index 12bbbb31bc7b3..86848b4911394 100644
--- a/code/game/objects/items/devices/megaphone.dm
+++ b/code/game/objects/items/devices/megaphone.dm
@@ -83,7 +83,7 @@
for(var/obj/O in view(14, get_turf(src)))
O.hear_talk(user, message_to_multilingual("[message]"))
- for(var/mob/M as anything in get_mobs_in_view(7, src))
+ for(var/mob/M as anything in get_mobs_in_view(7, src, ai_eyes = AI_EYE_REQUIRE_HEAR))
if((M.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT) && M.can_hear())
M.create_chat_message(user, message, FALSE, "big")
diff --git a/code/game/objects/items/devices/radio/radio_objects.dm b/code/game/objects/items/devices/radio/radio_objects.dm
index 98a12cb914520..dda8d996a335b 100644
--- a/code/game/objects/items/devices/radio/radio_objects.dm
+++ b/code/game/objects/items/devices/radio/radio_objects.dm
@@ -547,7 +547,7 @@ GLOBAL_LIST_EMPTY(deadsay_radio_systems)
/obj/item/radio/proc/send_announcement()
if(is_listening())
- return get_mobs_in_view(canhear_range, src)
+ return get_mobs_in_view(canhear_range, src, ai_eyes = AI_EYE_REQUIRE_HEAR)
return null
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 5bc8d953e2a98..bd849b8974fc9 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -150,7 +150,7 @@
/mob/visible_message(message, self_message, blind_message, chat_message_type)
if(!isturf(loc)) // mobs inside objects (such as lockers) shouldn't have their actions visible to those outside the object
- for(var/mob/M as anything in get_mobs_in_view(3, src))
+ for(var/mob/M as anything in get_mobs_in_view(3, src, ai_eyes = AI_EYE_INCLUDE))
if(M.see_invisible < invisibility)
continue //can't view the invisible
var/msg = message
@@ -162,7 +162,7 @@
msg = blind_message
M.show_message(msg, EMOTE_VISIBLE, blind_message, EMOTE_AUDIBLE, chat_message_type)
return
- for(var/mob/M as anything in get_mobs_in_view(7, src))
+ for(var/mob/M as anything in get_mobs_in_view(7, src, ai_eyes = AI_EYE_INCLUDE))
if(M.see_invisible < invisibility)
continue //can't view the invisible
var/msg = message
@@ -175,7 +175,7 @@
// message is output to anyone who can see, e.g. "The [src] does something!"
// blind_message (optional) is what blind people will hear e.g. "You hear something!"
/atom/proc/visible_message(message, blind_message)
- for(var/mob/M as anything in get_mobs_in_view(7, src))
+ for(var/mob/M as anything in get_mobs_in_view(7, src, ai_eyes = AI_EYE_INCLUDE))
if(!M.client)
continue
M.show_message(message, EMOTE_VISIBLE, blind_message, EMOTE_AUDIBLE)
@@ -191,7 +191,7 @@
if(hearing_distance)
range = hearing_distance
var/msg = message
- for(var/mob/M as anything in get_mobs_in_view(range, src))
+ for(var/mob/M as anything in get_mobs_in_view(range, src, ai_eyes = AI_EYE_REQUIRE_HEAR))
M.show_message(msg, EMOTE_AUDIBLE, deaf_message, EMOTE_VISIBLE)
// based on say code
@@ -217,7 +217,7 @@
var/range = 7
if(hearing_distance)
range = hearing_distance
- for(var/mob/M as anything in get_mobs_in_view(range, src))
+ for(var/mob/M as anything in get_mobs_in_view(range, src, ai_eyes = AI_EYE_REQUIRE_HEAR))
M.show_message(message, EMOTE_AUDIBLE, deaf_message, EMOTE_VISIBLE)
/mob/proc/findname(msg)