From edb85b2d0c752fa10fa41f96df65f07766571fbf Mon Sep 17 00:00:00 2001
From: _0Steven <42909981+00-Steven@users.noreply.github.com>
Date: Tue, 12 Aug 2025 23:24:50 +0200
Subject: [PATCH] Refactors say modes and custom say verbs. Extends custom say
verbs to more situations, forwards more spans. (#92127)
## About The Pull Request
Oh man, so this entire pr started because of two things:
1. A kinda hacky fix to #92123 that got closed a good while ago.
2. A borg I know mentioning you can't do custom say verbs over robotic
talk.
Which subsequently led me down this rabbit hole of say modes and custom
say verbs.
So! The most wide-reaching thing this does is merge the custom say
verb/radio emote logic that used to be specialcased in
`compose_message(...)` into `say_quote(...)`, renaming this to
`generate_messagepart(...)` with its new functionality. This means
things that don't use the exact same chain as living things talking
normally can still generate custom say verbs if given that message
modifier.
Then, we split up say modes into a "can we do this" and "try to do this"
check to reduce conflicts (like #92123), and forward more of our data to
the latter. This allows us to then edit the say modes to actually make
use of that data, and with the previous addition of
`generate_messagepart(...)` allow for custom say verbs to be used.
In doing this I realized the logging was kind of awkward and all over
the place, so we create the new logging helper `log_sayverb_talk(...)`
which handles selecting how we should log things based on the given
message modifiers.
For better or worse I forgot about this pr for a few weeks, so I don't
perfectly remember all the details, but those are the big key parts.
## Why It's Good For The Game
Fixes #92123.
I think custom say verbs are some of the best flavour we have for
talking over radio, and any situation benefits from that being possible.
It's great to be able to tap your microphone, and it's hilarious for an
AI to be able to emote beaming an image directly into the heads of their
borgs over robotic talk.
The rest is mostly cleanup.
---
.../signals_atom/signals_atom_movable.dm | 2 +-
code/__DEFINES/say.dm | 6 +
code/__HELPERS/chat.dm | 7 +-
code/__HELPERS/logging/talk.dm | 25 ++++
code/controllers/subsystem/networks/radio.dm | 9 ++
code/datums/brain_damage/imaginary_friend.dm | 20 ++-
code/datums/components/blob_minion.dm | 12 +-
code/datums/saymode.dm | 141 +++++++++++++-----
code/game/machinery/hologram.dm | 2 +-
code/game/say.dm | 26 ++--
code/modules/antagonists/blob/overmind.dm | 9 +-
.../mob/living/basic/drone/drone_say.dm | 28 ++--
.../mob/living/carbon/alien/alien_say.dm | 10 +-
code/modules/mob/living/living_say.dm | 15 +-
code/modules/mob/living/silicon/ai/ai.dm | 2 +-
code/modules/mob/living/silicon/ai/ai_say.dm | 23 ++-
.../modules/mob/living/silicon/silicon_say.dm | 41 ++---
code/modules/mob/mob_say.dm | 2 +-
code/modules/mob/say_readme.md | 6 +-
code/modules/mod/mod_link.dm | 4 +-
20 files changed, 259 insertions(+), 131 deletions(-)
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
index 1df2f39cc1b..378a86aceeb 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
@@ -105,7 +105,7 @@
/// Return to prevent the movable from talking into the radio.
#define COMPONENT_CANNOT_USE_RADIO (1<<0)
-/// Sent from /atom/movable/proc/say_quote() after say verb is chosen and before spans are applied.
+/// Sent from /atom/movable/proc/generate_messagepart() generating a quoted message, after say verb is chosen and before spans are applied.
#define COMSIG_MOVABLE_SAY_QUOTE "movable_say_quote"
// Used to access COMSIG_MOVABLE_SAY_QUOTE argslist
/// The index of args that corresponds to the actual message
diff --git a/code/__DEFINES/say.dm b/code/__DEFINES/say.dm
index 61c773b0b7c..1a3f3834eb0 100644
--- a/code/__DEFINES/say.dm
+++ b/code/__DEFINES/say.dm
@@ -52,7 +52,10 @@
#define MODE_KEY_PUPPET "j"
#define MODE_ALIEN "alientalk"
+#define MODE_KEY_ALIEN "a"
+
#define MODE_HOLOPAD "holopad"
+#define MODE_KEY_HOLOPAD "h"
#define MODE_CHANGELING "changeling"
#define MODE_KEY_CHANGELING "g"
@@ -125,6 +128,9 @@
#define MSG_AUDIBLE (1<<1)
+// Say mode message handling return flags, exist for readability.
+/// Say mode has handled the message.
+#define SAYMODE_MESSAGE_HANDLED (1<<0)
// Used in visible_message_flags, audible_message_flags and runechat_flags
/// Automatically applies emote related spans/fonts/formatting to the message
diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm
index 669a5f38fc4..5be492d9ef0 100644
--- a/code/__HELPERS/chat.dm
+++ b/code/__HELPERS/chat.dm
@@ -95,5 +95,10 @@ it will be sent to all connected chats.
/// Sends a message to everyone within the list, as well as all observers.
/proc/relay_to_list_and_observers(message, list/mob_list, source, message_type = null)
for(var/mob/creature as anything in mob_list)
- to_chat(creature, message, type = message_type)
+ to_chat(
+ creature,
+ message,
+ type = message_type,
+ avoid_highlighting = (creature == source),
+ )
send_to_observers(message, source)
diff --git a/code/__HELPERS/logging/talk.dm b/code/__HELPERS/logging/talk.dm
index 6d9c534287f..da5bebc17fc 100644
--- a/code/__HELPERS/logging/talk.dm
+++ b/code/__HELPERS/logging/talk.dm
@@ -1,3 +1,28 @@
+/**
+ * Helper for logging chat messages that may or may not have a custom say verb,
+ * or be a pure radio emote outright.
+ *
+ * This proc reads the `message_mods` to determine
+ * in what ways the given message should be logged,
+ * and forwards it to other logging procs as such.
+ * Arguments:
+ * * message - The message being logged
+ * * message_mods - A list of message modifiers, i.e. whispering/singing.
+ * * tag - tag that indicates the type of text(announcement, telepathy, etc)
+ * * log_globally - boolean checking whether or not we write this log to the log file
+ * * forced_by - source that forced the dialogue if any
+ */
+/atom/proc/log_sayverb_talk(message, list/message_mods = list(), tag = null, log_globally = TRUE, forced_by = null)
+ // If it's just the custom say verb, log it to emotes.
+ if(message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
+ log_talk(message_mods[MODE_CUSTOM_SAY_EMOTE], LOG_RADIO_EMOTE, tag, log_globally, forced_by)
+ return
+
+ if(message_mods[WHISPER_MODE])
+ log_talk(message, LOG_WHISPER, tag, log_globally, forced_by, message_mods[MODE_CUSTOM_SAY_EMOTE])
+ else
+ log_talk(message, LOG_SAY, tag, log_globally, forced_by, message_mods[MODE_CUSTOM_SAY_EMOTE])
+
/**
* Helper for logging chat messages or other logs with arbitrary inputs (e.g. announcements)
*
diff --git a/code/controllers/subsystem/networks/radio.dm b/code/controllers/subsystem/networks/radio.dm
index 578b69b0d0e..6d880cedeb3 100644
--- a/code/controllers/subsystem/networks/radio.dm
+++ b/code/controllers/subsystem/networks/radio.dm
@@ -11,6 +11,15 @@ SUBSYSTEM_DEF(radio)
saymodes[SM.key] = SM
return ..()
+/// Gets the say mode associated with the given key, if available to the given user.
+/datum/controller/subsystem/radio/proc/get_available_say_mode(mob/living/user, key)
+ var/datum/saymode/selected_saymode = SSradio.saymodes[key]
+ if(isnull(selected_saymode))
+ return
+ if(!selected_saymode.can_be_used_by(user))
+ return
+ return selected_saymode
+
/datum/controller/subsystem/radio/proc/add_object(obj/device, new_frequency as num, filter = null as text|null)
var/f_text = num2text(new_frequency)
var/datum/radio_frequency/frequency = frequencies[f_text]
diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm
index 449b0ec0657..617f590e5fb 100644
--- a/code/datums/brain_damage/imaginary_friend.dm
+++ b/code/datums/brain_damage/imaginary_friend.dm
@@ -223,7 +223,6 @@
/mob/eye/imaginary_friend/send_speech(message, range = IMAGINARY_FRIEND_SPEECH_RANGE, obj/source = src, bubble_type = bubble_icon, list/spans = list(), datum/language/message_language = null, list/message_mods = list(), forced = null)
message = get_message_mods(message, message_mods)
- message = capitalize(message)
if(message_mods[RADIO_EXTENSION] == MODE_ADMIN)
SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/cmd_admin_say, message)
@@ -236,6 +235,9 @@
if(check_emote(message, forced))
return
+ message = check_for_custom_say_emote(message, message_mods)
+ message = capitalize(message)
+
if(message_mods[MODE_SING])
var/randomnote = pick("♩", "♪", "♫")
message = "[randomnote] [capitalize(message)] [randomnote]"
@@ -246,21 +248,17 @@
var/eavesdrop_range = 0
- if (message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
- message = message_mods[MODE_CUSTOM_SAY_EMOTE]
- log_message(message, LOG_RADIO_EMOTE)
- else
+ if(!(message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]))
if(message_mods[WHISPER_MODE] == MODE_WHISPER)
- log_talk(message, LOG_WHISPER, tag="imaginary friend", forced_by = forced, custom_say_emote = message_mods[MODE_CUSTOM_SAY_EMOTE])
spans |= SPAN_ITALICS
eavesdrop_range = EAVESDROP_EXTRA_RANGE
range = WHISPER_RANGE
- else
- log_talk(message, LOG_SAY, tag="imaginary friend", forced_by = forced, custom_say_emote = message_mods[MODE_CUSTOM_SAY_EMOTE])
- var/quoted_message = say_quote(apply_message_emphasis(message), spans, message_mods)
- var/rendered = "[span_name("[name]")] [quoted_message]"
- var/dead_rendered = "[span_name("[name] (Imaginary friend of [owner])")] [quoted_message]"
+ log_sayverb_talk(message, message_mods, tag = "imaginary friend", forced_by = forced)
+
+ var/messagepart = generate_messagepart(message, spans, message_mods)
+ var/rendered = "[span_name("[name]")] [messagepart]"
+ var/dead_rendered = "[span_name("[name] (Imaginary friend of [owner])")] [messagepart]"
var/language = message_language || owner.get_selected_language()
Hear(rendered, src, language, message, null, null, null, spans, message_mods) // We always hear what we say
diff --git a/code/datums/components/blob_minion.dm b/code/datums/components/blob_minion.dm
index a17fd4b4a15..75eee8410d2 100644
--- a/code/datums/components/blob_minion.dm
+++ b/code/datums/components/blob_minion.dm
@@ -172,11 +172,17 @@
/// We only speak telepathically to blobs
/datum/component/blob_minion/proc/on_try_speech(mob/living/minion, message, ignore_spam, forced)
SIGNAL_HANDLER
- minion.log_talk(message, LOG_SAY, tag = "blob hivemind telepathy")
- var/spanned_message = minion.say_quote(message)
+ INVOKE_ASYNC(src, PROC_REF(send_blob_telepathy), minion, message)
+ return COMPONENT_CANNOT_SPEAK
+
+/datum/component/blob_minion/proc/send_blob_telepathy(mob/living/minion, message)
+ var/list/message_mods = list()
+ // Note: check_for_custom_say_emote can sleep.
+ var/adjusted_message = minion.check_for_custom_say_emote(message, message_mods)
+ minion.log_sayverb_talk(message, message_mods, tag = "blob hivemind telepathy")
+ var/spanned_message = minion.generate_messagepart(adjusted_message, message_mods = message_mods)
var/rendered = span_blob("\[Blob Telepathy\] [minion.real_name] [spanned_message]")
relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, minion, MESSAGE_TYPE_RADIO)
- return COMPONENT_CANNOT_SPEAK
/// Called when a blob minion is transformed into something else, hopefully a spore into a zombie
/datum/component/blob_minion/proc/on_transformed(mob/living/minion, mob/living/replacement)
diff --git a/code/datums/saymode.dm b/code/datums/saymode.dm
index 5e67d960b42..951c805f0af 100644
--- a/code/datums/saymode.dm
+++ b/code/datums/saymode.dm
@@ -1,20 +1,40 @@
/datum/saymode
+ /// The symbol key used to enable this say mode.
var/key
+ /// The corresponding say mode string.
var/mode
+ /// Whether this say mode works with custom say emotes.
+ var/allows_custom_say_emotes = FALSE
-//Return FALSE if you have handled the message. Otherwise, return TRUE and saycode will continue doing saycode things.
-//user = whoever said the message
-//message = the message
-//language = the language.
-/datum/saymode/proc/handle_message(mob/living/user, message, datum/language/language)
+/// Checks whether this saymode can be used by the given user. May send feedback.
+/datum/saymode/proc/can_be_used_by(mob/living/user)
return TRUE
+/**
+ * Handles actually modifying or forwarding our message.
+ * Returns `SAYMODE_[X]` flags.
+ *
+ * user - The living speaking using this say mode.
+ * message - The message to be said.
+ * spans - A list of spans to attach to the message.
+ * language - The language the message was said in.
+ * message_mods - A list of message modifiers, i.e. whispering/singing.
+ */
+/datum/saymode/proc/handle_message(
+ mob/living/user,
+ message,
+ list/spans = list(),
+ datum/language/language,
+ list/message_mods = list()
+)
+ return NONE
+
+
/datum/saymode/changeling
key = MODE_KEY_CHANGELING
mode = MODE_CHANGELING
-/datum/saymode/changeling/handle_message(mob/living/user, message, datum/language/language)
- //we can send the message
+/datum/saymode/changeling/can_be_used_by(mob/living/user)
if(!user.mind)
return FALSE
if(user.mind.has_antag_datum(/datum/antagonist/fallen_changeling))
@@ -26,11 +46,20 @@
if(HAS_TRAIT(user, TRAIT_CHANGELING_HIVEMIND_MUTE))
to_chat(user, span_warning("The poison in the air hinders our ability to interact with the hivemind."))
return FALSE
+ return TRUE
- user.log_talk(message, LOG_SAY, tag="changeling [ling_sender.changelingID]")
+/datum/saymode/changeling/handle_message/handle_message(
+ mob/living/user,
+ message,
+ list/spans = list(),
+ datum/language/language,
+ list/message_mods = list()
+)
+ var/datum/antagonist/changeling/ling_sender = IS_CHANGELING(user)
+ user.log_talk(message, LOG_SAY, tag = "changeling [ling_sender.changelingID]")
var/msg = span_changeling("[ling_sender.changelingID]: [message]")
- //the recipients can receive the message
+ // Send the message to our other changelings.
for(var/datum/antagonist/changeling/ling_receiver in GLOB.antagonists)
if(!ling_receiver.owner)
continue
@@ -45,54 +74,96 @@
for(var/mob/dead/ghost as anything in GLOB.dead_mob_list)
to_chat(ghost, "[FOLLOW_LINK(ghost, user)] [msg]", type = MESSAGE_TYPE_RADIO)
- return FALSE
+ return SAYMODE_MESSAGE_HANDLED
+
/datum/saymode/xeno
- key = "a"
+ key = MODE_KEY_ALIEN
mode = MODE_ALIEN
+ allows_custom_say_emotes = TRUE
-/datum/saymode/xeno/handle_message(mob/living/user, message, datum/language/language)
- if(user.hivecheck())
- user.alien_talk(message)
- return FALSE
+/datum/saymode/xeno/can_be_used_by(mob/living/user)
+ if(!user.hivecheck())
+ return FALSE
+ return TRUE
+
+/datum/saymode/xeno/handle_message/handle_message(
+ mob/living/user,
+ message,
+ list/spans = list(),
+ datum/language/language,
+ list/message_mods = list()
+)
+ user.alien_talk(message, spans, message_mods)
+ return SAYMODE_MESSAGE_HANDLED
/datum/saymode/vocalcords
key = MODE_KEY_VOCALCORDS
mode = MODE_VOCALCORDS
-/datum/saymode/vocalcords/handle_message(mob/living/user, message, datum/language/language)
- if(iscarbon(user))
- var/mob/living/carbon/C = user
- var/obj/item/organ/vocal_cords/V = C.get_organ_slot(ORGAN_SLOT_VOICE)
- if(V?.can_speak_with())
- V.handle_speech(message) //message
- V.speak_with(message) //action
- return FALSE
+/datum/saymode/vocalcords/can_be_used_by(mob/living/user)
+ if(!iscarbon(user))
+ return FALSE
+ return TRUE
+
+/datum/saymode/vocalcords/handle_message/handle_message(
+ mob/living/user,
+ message,
+ list/spans = list(),
+ datum/language/language,
+ list/message_mods = list()
+)
+ var/mob/living/carbon/carbon_user = user
+ var/obj/item/organ/vocal_cords/our_vocal_cords = carbon_user.get_organ_slot(ORGAN_SLOT_VOICE)
+ if(our_vocal_cords?.can_speak_with())
+ our_vocal_cords.handle_speech(message) //message
+ our_vocal_cords.speak_with(message) //action
+ return SAYMODE_MESSAGE_HANDLED
/datum/saymode/binary //everything that uses .b (silicons, drones)
key = MODE_KEY_BINARY
mode = MODE_BINARY
+ allows_custom_say_emotes = TRUE
-/datum/saymode/binary/handle_message(mob/living/user, message, datum/language/language)
+/datum/saymode/binary/can_be_used_by(mob/living/user)
+ if(!isdrone(user) && !user.binarycheck())
+ return FALSE
+ return TRUE
+
+/datum/saymode/binary/handle_message/handle_message(
+ mob/living/user,
+ message,
+ list/spans = list(),
+ datum/language/language,
+ list/message_mods = list()
+)
if(isdrone(user))
var/mob/living/basic/drone/drone_user = user
- drone_user.drone_chat(message)
- return FALSE
- if(user.binarycheck())
- user.robot_talk(message)
- return FALSE
- return FALSE
+ drone_user.drone_chat(message, spans, message_mods)
+ else if(user.binarycheck())
+ user.robot_talk(message, spans, message_mods)
+ return SAYMODE_MESSAGE_HANDLED
/datum/saymode/holopad
- key = "h"
+ key = MODE_KEY_HOLOPAD
mode = MODE_HOLOPAD
+ allows_custom_say_emotes = TRUE
-/datum/saymode/holopad/handle_message(mob/living/user, message, datum/language/language)
- if(isAI(user))
- var/mob/living/silicon/ai/AI = user
- AI.holopad_talk(message, language)
+/datum/saymode/holopad/can_be_used_by(mob/living/user)
+ if(!isAI(user))
return FALSE
return TRUE
+
+/datum/saymode/holopad/handle_message/handle_message(
+ mob/living/user,
+ message,
+ list/spans = list(),
+ datum/language/language,
+ list/message_mods = list()
+)
+ var/mob/living/silicon/ai/ai_user = user
+ ai_user.holopad_talk(message, spans, language, message_mods)
+ return SAYMODE_MESSAGE_HANDLED
diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm
index 99ad58a5f49..b7cc52ca076 100644
--- a/code/game/machinery/hologram.dm
+++ b/code/game/machinery/hologram.dm
@@ -592,7 +592,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/
holocall_to_update.user.Hear(message, speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods, message_range = INFINITY)
if(outgoing_call?.hologram && speaker == outgoing_call.user)
- outgoing_call.hologram.say(raw_message, sanitize = FALSE)
+ outgoing_call.hologram.say(raw_message, spans = spans, sanitize = FALSE, language = message_language, message_mods = message_mods)
if(record_mode && speaker == record_user)
record_message(speaker, raw_message, message_language)
diff --git a/code/game/say.dm b/code/game/say.dm
index a9554944715..d5b42d3905f 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -68,7 +68,8 @@ GLOBAL_LIST_INIT(freqtospan, list(
return
spans |= speech_span
language ||= get_selected_language()
- message_mods[SAY_MOD_VERB] = say_mod(message, message_mods)
+ if(!message_mods[SAY_MOD_VERB])
+ message_mods[SAY_MOD_VERB] = say_mod(message, message_mods)
send_speech(message, message_range, src, bubble_type, spans, language, message_mods, forced = forced)
/// Called when this movable hears a message from a source.
@@ -173,18 +174,15 @@ GLOBAL_LIST_INIT(freqtospan, list(
//End name span.
var/endspanpart = ""
- //Message
- var/messagepart
+ // Language icon.
var/languageicon = ""
- if(message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
- messagepart = message_mods[MODE_CUSTOM_SAY_EMOTE]
- else
- messagepart = speaker.say_quote(raw_message, spans, message_mods)
-
+ if(!message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
var/datum/language/dialect = GLOB.language_datum_instances[message_language]
if(istype(dialect) && dialect.display_icon(src))
languageicon = "[dialect.get_icon()] "
+ // The actual message part.
+ var/messagepart = speaker.generate_messagepart(raw_message, spans, message_mods)
messagepart = " [messagepart]"
return "[spanpart1][spanpart2][freqpart][languageicon][compose_track_href(speaker, namepart)][namepart][compose_job(speaker, message_language, raw_message, radio_freq)][endspanpart][messagepart]"
@@ -225,14 +223,20 @@ GLOBAL_LIST_INIT(freqtospan, list(
return verb_say
/**
- * This prock is used to generate a message for chat
- * Generates the `says, "meme"` part of the `Grey Tider says, "meme"`.
+ * This proc is used to generate the 'message' part of a chat message.
+ * Generates the `says, "meme"` part of the `Grey Tider says, "meme"`,
+ * or the `taps their microphone.` part of `Grey Tider taps their microphone.`.
*
* input - The message to be said
* spans - A list of spans to attach to the message. Includes the atom's speech span by default
* message_mods - A list of message modifiers, i.e. whispering/singing
*/
-/atom/movable/proc/say_quote(input, list/spans = list(speech_span), list/message_mods = list())
+/atom/movable/proc/generate_messagepart(input, list/spans = list(speech_span), list/message_mods = list())
+ // If we only care about the emote part, early return.
+ if(message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
+ return apply_message_emphasis(message_mods[MODE_CUSTOM_SAY_EMOTE])
+
+ // Otherwise, we format our full quoted message.
if(!input)
input = "..."
diff --git a/code/modules/antagonists/blob/overmind.dm b/code/modules/antagonists/blob/overmind.dm
index 2761077d86a..037c2169972 100644
--- a/code/modules/antagonists/blob/overmind.dm
+++ b/code/modules/antagonists/blob/overmind.dm
@@ -321,10 +321,11 @@ GLOBAL_LIST_EMPTY(blob_nodes)
if (!message)
return
- src.log_talk(message, LOG_SAY)
-
- var/message_a = say_quote(message)
- var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]"))
+ var/list/message_mods = list()
+ var/adjusted_message = check_for_custom_say_emote(message, message_mods)
+ log_sayverb_talk(message, message_mods, tag = "blob hivemind telepathy")
+ var/messagepart = generate_messagepart(adjusted_message, message_mods = message_mods)
+ var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [messagepart]"))
relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, src, MESSAGE_TYPE_RADIO)
/mob/eye/blob/blob_act(obj/structure/blob/B)
diff --git a/code/modules/mob/living/basic/drone/drone_say.dm b/code/modules/mob/living/basic/drone/drone_say.dm
index af0bef41bb1..3a006d8d1b2 100644
--- a/code/modules/mob/living/basic/drone/drone_say.dm
+++ b/code/modules/mob/living/basic/drone/drone_say.dm
@@ -9,18 +9,24 @@
* * exact_faction_match - Passed to [/mob/proc/faction_check_atom]
*/
/proc/_alert_drones(msg, dead_can_hear = FALSE, atom/source, mob/living/faction_checked_mob, exact_faction_match)
- if (dead_can_hear && source)
- for (var/mob/dead_mob in GLOB.dead_mob_list)
+ if(dead_can_hear && source)
+ for(var/mob/dead_mob in GLOB.dead_mob_list)
var/link = FOLLOW_LINK(dead_mob, source)
to_chat(dead_mob, "[link] [msg]")
for(var/global_drone in GLOB.drones_list)
var/mob/living/basic/drone/drone = global_drone
- if(istype(drone) && drone.stat != DEAD)
- if(faction_checked_mob)
- if(drone.faction_check_atom(faction_checked_mob, exact_faction_match))
- to_chat(drone, msg)
- else
- to_chat(drone, msg)
+ if(!istype(drone))
+ continue
+ if(drone.stat == DEAD)
+ continue
+ if(faction_checked_mob && !drone.faction_check_atom(faction_checked_mob, exact_faction_match))
+ continue
+ to_chat(
+ drone,
+ msg,
+ type = MESSAGE_TYPE_RADIO,
+ avoid_highlighting = (drone == source),
+ )
@@ -39,5 +45,7 @@
*
* Shares the same radio code with binary
*/
-/mob/living/basic/drone/proc/drone_chat(msg)
- alert_drones("Drone Chat: [span_name("[name]")] [say_quote(msg)]", TRUE)
+/mob/living/basic/drone/proc/drone_chat(message, list/spans = list(), list/message_mods = list())
+ log_sayverb_talk(message, message_mods, tag = "drone chat")
+ var/message_part = generate_messagepart(message, spans, message_mods)
+ alert_drones("Drone Chat: [span_name("[name]")] [message_part]", TRUE)
diff --git a/code/modules/mob/living/carbon/alien/alien_say.dm b/code/modules/mob/living/carbon/alien/alien_say.dm
index 43317ff59e7..21655bfa162 100644
--- a/code/modules/mob/living/carbon/alien/alien_say.dm
+++ b/code/modules/mob/living/carbon/alien/alien_say.dm
@@ -1,10 +1,10 @@
-/mob/living/proc/alien_talk(message, shown_name = real_name, big_voice = FALSE)
- src.log_talk(message, LOG_SAY)
+/mob/living/proc/alien_talk(message, list/spans = list(), list/message_mods = list(), shown_name = real_name, big_voice = FALSE)
+ log_sayverb_talk(message, message_mods, tag = "alien hivemind")
message = trim(message)
if(!message)
return
- var/message_a = say_quote(message)
+ var/message_a = generate_messagepart(message, spans, message_mods)
var/hivemind_spans = "alien"
if(big_voice)
hivemind_spans += " big"
@@ -16,8 +16,8 @@
var/link = FOLLOW_LINK(player, src)
to_chat(player, "[link] [rendered]", type = MESSAGE_TYPE_RADIO)
-/mob/living/carbon/alien/adult/royal/queen/alien_talk(message, shown_name = name)
- ..(message, shown_name, TRUE)
+/mob/living/carbon/alien/adult/royal/queen/alien_talk(message, list/spans = list(), list/message_mods = list(), shown_name = name, big_voice = TRUE)
+ ..(message, spans, message_mods, shown_name, TRUE)
/mob/living/carbon/hivecheck()
var/obj/item/organ/alien/hivenode/N = get_organ_by_type(/obj/item/organ/alien/hivenode)
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index edb2081bf6b..12f97e9a4ce 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -121,8 +121,8 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
var/original_message = message
message = get_message_mods(message, message_mods)
- saymode = SSradio.saymodes[message_mods[RADIO_KEY]]
- if (!forced && !saymode)
+ saymode = SSradio.get_available_say_mode(src, message_mods[RADIO_KEY])
+ if(!forced && (isnull(saymode) || saymode.allows_custom_say_emotes))
message = check_for_custom_say_emote(message, message_mods)
if(!message)
@@ -176,15 +176,10 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
var/succumbed = FALSE
- // If there's a custom say emote it gets logged differently.
- if(message_mods[MODE_CUSTOM_SAY_EMOTE])
- log_message(message_mods[MODE_CUSTOM_SAY_EMOTE], LOG_RADIO_EMOTE)
-
// If it's not erasing the input portion, then something is being said and this isn't a pure custom say emote.
if(!message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
if(message_mods[WHISPER_MODE] == MODE_WHISPER)
message_range = 1
- log_talk(message, LOG_WHISPER, forced_by = forced, custom_say_emote = message_mods[MODE_CUSTOM_SAY_EMOTE])
if(stat == HARD_CRIT)
var/health_diff = round(-HEALTH_THRESHOLD_DEAD + health)
// If we cut our message short, abruptly end it with a-..
@@ -194,8 +189,8 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
last_words = message
message_mods[WHISPER_MODE] = MODE_WHISPER_CRIT
succumbed = TRUE
- else
- log_talk(message, LOG_SAY, forced_by = forced, custom_say_emote = message_mods[MODE_CUSTOM_SAY_EMOTE])
+
+ log_sayverb_talk(message, message_mods, forced_by = forced)
#ifdef UNIT_TESTS
// Saves a ref() to our arglist specifically.
@@ -241,7 +236,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
message = autopunct_bare(message)
// SKYRAT EDIT ADDITION END
//This is before anything that sends say a radio message, and after all important message type modifications, so you can scumb in alien chat or something
- if(saymode && !saymode.handle_message(src, message, language))
+ if(saymode && (saymode.handle_message(src, message, spans, language, message_mods) & SAYMODE_MESSAGE_HANDLED))
return
var/radio_return = radio(message, message_mods, spans, language)//roughly 27% of living/say()'s total cost
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 00cff3fe3ae..1cdfa46c511 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -798,7 +798,7 @@
/mob/living/silicon/ai/proc/relay_speech(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list())
var/raw_translation = translate_language(speaker, message_language, raw_message, spans, message_mods)
var/atom/movable/source = speaker.GetSource() || speaker // is the speaker virtual/radio
- var/treated_message = source.say_quote(raw_translation, spans, message_mods)
+ var/treated_message = source.generate_messagepart(raw_translation, spans, message_mods)
var/start = "Relayed Speech: "
var/namepart
diff --git a/code/modules/mob/living/silicon/ai/ai_say.dm b/code/modules/mob/living/silicon/ai/ai_say.dm
index 8e673bbe722..ca59835483b 100644
--- a/code/modules/mob/living/silicon/ai/ai_say.dm
+++ b/code/modules/mob/living/silicon/ai/ai_say.dm
@@ -42,25 +42,24 @@
return FALSE
//For holopads only. Usable by AI.
-/mob/living/silicon/ai/proc/holopad_talk(message, language)
+/mob/living/silicon/ai/proc/holopad_talk(message, list/spans = list(), language, list/message_mods = list())
message = trim(message)
if (!message)
return
var/obj/machinery/holopad/active_pad = current
- if(istype(active_pad) && active_pad.masters[src])//If there is a hologram and its master is the user.
- var/obj/effect/overlay/holo_pad_hologram/ai_holo = active_pad.masters[src]
- var/turf/padturf = get_turf(active_pad)
- var/padloc
- if(padturf)
- padloc = AREACOORD(padturf)
- else
- padloc = "(UNKNOWN)"
- src.log_talk(message, LOG_SAY, tag="HOLOPAD in [padloc]")
- ai_holo.say(message, sanitize = FALSE, language = language)
- else
+ // Only continue if there is a hologram and its master is the user.
+ if(!istype(active_pad) || !active_pad.masters[src])
to_chat(src, span_alert("No holopad connected."))
+ return
+
+ var/obj/effect/overlay/holo_pad_hologram/ai_holo = active_pad.masters[src]
+ var/turf/pad_turf = get_turf(active_pad)
+ var/pad_loc = pad_turf ? AREACOORD(pad_turf) : "(UNKNOWN)"
+
+ log_sayverb_talk(message, message_mods, tag = "HOLOPAD in [pad_loc]")
+ ai_holo.say(message, spans = spans, sanitize = FALSE, language = language, message_mods = message_mods)
/* SKYRAT EDIT REMOVAL - MOVED TO: MODULAR_SKYRAT/MODULES/ALT_VOX/CODE/VOX_PROCS.DM
// Make sure that the code compiles with AI_VOX undefined
diff --git a/code/modules/mob/living/silicon/silicon_say.dm b/code/modules/mob/living/silicon/silicon_say.dm
index d2b83890171..7e806bbdf8c 100644
--- a/code/modules/mob/living/silicon/silicon_say.dm
+++ b/code/modules/mob/living/silicon/silicon_say.dm
@@ -1,8 +1,8 @@
-/mob/living/proc/robot_talk(message)
- log_talk(message, LOG_SAY, tag="binary")
+/mob/living/proc/robot_talk(message, list/spans = list(), list/message_mods = list())
+ log_sayverb_talk(message, message_mods, tag="binary")
var/designation = "Default Cyborg"
- var/spans = list(SPAN_ROBOT)
+ spans |= SPAN_ROBOT
if(issilicon(src))
var/mob/living/silicon/player = src
@@ -15,9 +15,10 @@
// AIs are loud and ugly
spans |= SPAN_COMMAND
- var/quoted_message = say_quote(
+ var/messagepart = generate_messagepart(
message,
- spans
+ spans,
+ message_mods,
)
var/namepart = name
@@ -31,31 +32,31 @@
namepart = brain.mainframe.name
designation = brain.mainframe.job
- for(var/mob/M in GLOB.player_list)
- if(M.binarycheck())
- if(isAI(M))
+ for(var/mob/hearing_mob in GLOB.player_list)
+ if(hearing_mob.binarycheck())
+ if(isAI(hearing_mob))
to_chat(
- M,
+ hearing_mob,
span_binarysay("\
Robotic Talk, \
- [span_name("[namepart] ([designation])")] \
- [quoted_message]\
+ [span_name("[namepart] ([designation])")] \
+ [messagepart]\
"),
type = MESSAGE_TYPE_RADIO,
- avoid_highlighting = src == M
+ avoid_highlighting = (src == hearing_mob)
)
else
to_chat(
- M,
+ hearing_mob,
span_binarysay("\
Robotic Talk, \
- [span_name("[namepart]")] [quoted_message]\
+ [span_name("[namepart]")] [messagepart]\
"),
type = MESSAGE_TYPE_RADIO,
- avoid_highlighting = src == M
+ avoid_highlighting = (src == hearing_mob)
)
- if(isobserver(M))
+ if(isobserver(hearing_mob))
var/following = src
// If the AI talks on binary chat, we still want to follow
@@ -65,17 +66,17 @@
var/mob/living/silicon/ai/ai = src
following = ai.eyeobj
- var/follow_link = FOLLOW_LINK(M, following)
+ var/follow_link = FOLLOW_LINK(hearing_mob, following)
to_chat(
- M,
+ hearing_mob,
span_binarysay("\
[follow_link] \
Robotic Talk, \
- [span_name("[namepart]")] [quoted_message]\
+ [span_name("[namepart]")] [messagepart]\
"),
type = MESSAGE_TYPE_RADIO,
- avoid_highlighting = src == M
+ avoid_highlighting = (src == hearing_mob)
)
/mob/living/silicon/binarycheck()
diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm
index 8dce7302089..90f84cfe7da 100644
--- a/code/modules/mob/mob_say.dm
+++ b/code/modules/mob/mob_say.dm
@@ -162,7 +162,7 @@
if(name != real_name)
alt_name = " (died as [real_name])"
- var/spanned = say_quote(apply_message_emphasis(message))
+ var/spanned = generate_messagepart(message)
var/source = "DEAD: [name][alt_name]"
var/rendered = " [emoji_parse(spanned)]"
log_talk(message, LOG_SAY, tag="DEAD")
diff --git a/code/modules/mob/say_readme.md b/code/modules/mob/say_readme.md
index 5284012de29..e39dfb9fbcf 100644
--- a/code/modules/mob/say_readme.md
+++ b/code/modules/mob/say_readme.md
@@ -53,7 +53,7 @@ global procs
languages live either in datum/languages_holder or in the mind.
verb_say/verb_ask/verb_exclaim/verb_yell/verb_sing
- These determine what the verb is for their respective action. Used in say_quote().
+ These determine what the verb is for their respective action. Used in generate_messagepart().
say(message, bubble_type, var/list/spans, sanitize, datum/language/language, ignore_spam, forced)
Say() is the "mother-proc". It calls all the other procs required for speaking, but does little itself.
@@ -82,8 +82,8 @@ global procs
Modifies the message by comparing the languages of the speaker with the languages of the hearer.
Called on the hearer.
- say_quote(input, spans, list/message_mods)
- Adds a verb and quotes to a message. Also attaches span classes to a message.
+ generate_messagepart(input, spans, list/message_mods)
+ Either adds a lone custom say verb, or a verb and quotes to a message. Also attaches span classes to a message.
Verbs are determined by verb_say/verb_ask/verb_yell/verb_sing variables. Called on the speaker.
/mob
diff --git a/code/modules/mod/mod_link.dm b/code/modules/mod/mod_link.dm
index 85ff92cf2e7..e44c923403e 100644
--- a/code/modules/mod/mod_link.dm
+++ b/code/modules/mod/mod_link.dm
@@ -122,7 +122,7 @@
. = ..()
if(speaker != wearer && speaker != ai_assistant)
return
- mod_link.visual.say(raw_message, sanitize = FALSE, message_range = 2)
+ mod_link.visual.say(raw_message, spans = spans, sanitize = FALSE, language = message_language, message_range = 2, message_mods = message_mods)
/obj/item/mod/control/proc/on_overlay_change(atom/source, cache_index, overlay)
SIGNAL_HANDLER
@@ -306,7 +306,7 @@
. = ..()
if(speaker != loc)
return
- mod_link.visual.say(raw_message, sanitize = FALSE, message_range = 3)
+ mod_link.visual.say(raw_message, spans = spans, sanitize = FALSE, language = message_language, message_range = 3, message_mods = message_mods)
/obj/item/clothing/neck/link_scryer/proc/on_overlay_change(atom/source, cache_index, overlay)
SIGNAL_HANDLER