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.
This commit is contained in:
_0Steven
2025-08-12 23:24:50 +02:00
committed by nevimer
parent afbd48d86f
commit edb85b2d0c
20 changed files with 259 additions and 131 deletions

View File

@@ -105,7 +105,7 @@
/// Return to prevent the movable from talking into the radio. /// Return to prevent the movable from talking into the radio.
#define COMPONENT_CANNOT_USE_RADIO (1<<0) #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" #define COMSIG_MOVABLE_SAY_QUOTE "movable_say_quote"
// Used to access COMSIG_MOVABLE_SAY_QUOTE argslist // Used to access COMSIG_MOVABLE_SAY_QUOTE argslist
/// The index of args that corresponds to the actual message /// The index of args that corresponds to the actual message

View File

@@ -52,7 +52,10 @@
#define MODE_KEY_PUPPET "j" #define MODE_KEY_PUPPET "j"
#define MODE_ALIEN "alientalk" #define MODE_ALIEN "alientalk"
#define MODE_KEY_ALIEN "a"
#define MODE_HOLOPAD "holopad" #define MODE_HOLOPAD "holopad"
#define MODE_KEY_HOLOPAD "h"
#define MODE_CHANGELING "changeling" #define MODE_CHANGELING "changeling"
#define MODE_KEY_CHANGELING "g" #define MODE_KEY_CHANGELING "g"
@@ -125,6 +128,9 @@
#define MSG_AUDIBLE (1<<1) #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 // Used in visible_message_flags, audible_message_flags and runechat_flags
/// Automatically applies emote related spans/fonts/formatting to the message /// Automatically applies emote related spans/fonts/formatting to the message

View File

@@ -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. /// 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) /proc/relay_to_list_and_observers(message, list/mob_list, source, message_type = null)
for(var/mob/creature as anything in mob_list) 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) send_to_observers(message, source)

View File

@@ -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) * Helper for logging chat messages or other logs with arbitrary inputs (e.g. announcements)
* *

View File

@@ -11,6 +11,15 @@ SUBSYSTEM_DEF(radio)
saymodes[SM.key] = SM saymodes[SM.key] = SM
return ..() 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) /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/f_text = num2text(new_frequency)
var/datum/radio_frequency/frequency = frequencies[f_text] var/datum/radio_frequency/frequency = frequencies[f_text]

View File

@@ -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) /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 = get_message_mods(message, message_mods)
message = capitalize(message)
if(message_mods[RADIO_EXTENSION] == MODE_ADMIN) if(message_mods[RADIO_EXTENSION] == MODE_ADMIN)
SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/cmd_admin_say, message) SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/cmd_admin_say, message)
@@ -236,6 +235,9 @@
if(check_emote(message, forced)) if(check_emote(message, forced))
return return
message = check_for_custom_say_emote(message, message_mods)
message = capitalize(message)
if(message_mods[MODE_SING]) if(message_mods[MODE_SING])
var/randomnote = pick("♩", "♪", "♫") var/randomnote = pick("♩", "♪", "♫")
message = "[randomnote] [capitalize(message)] [randomnote]" message = "[randomnote] [capitalize(message)] [randomnote]"
@@ -246,21 +248,17 @@
var/eavesdrop_range = 0 var/eavesdrop_range = 0
if (message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]) 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[WHISPER_MODE] == MODE_WHISPER) 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 spans |= SPAN_ITALICS
eavesdrop_range = EAVESDROP_EXTRA_RANGE eavesdrop_range = EAVESDROP_EXTRA_RANGE
range = WHISPER_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) log_sayverb_talk(message, message_mods, tag = "imaginary friend", forced_by = forced)
var/rendered = "[span_name("[name]")] [quoted_message]"
var/dead_rendered = "[span_name("[name] (Imaginary friend of [owner])")] [quoted_message]" 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() 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 Hear(rendered, src, language, message, null, null, null, spans, message_mods) // We always hear what we say

View File

@@ -172,11 +172,17 @@
/// We only speak telepathically to blobs /// We only speak telepathically to blobs
/datum/component/blob_minion/proc/on_try_speech(mob/living/minion, message, ignore_spam, forced) /datum/component/blob_minion/proc/on_try_speech(mob/living/minion, message, ignore_spam, forced)
SIGNAL_HANDLER SIGNAL_HANDLER
minion.log_talk(message, LOG_SAY, tag = "blob hivemind telepathy") INVOKE_ASYNC(src, PROC_REF(send_blob_telepathy), minion, message)
var/spanned_message = minion.say_quote(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("<b>\[Blob Telepathy\] [minion.real_name]</b> [spanned_message]") var/rendered = span_blob("<b>\[Blob Telepathy\] [minion.real_name]</b> [spanned_message]")
relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, minion, MESSAGE_TYPE_RADIO) 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 /// 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) /datum/component/blob_minion/proc/on_transformed(mob/living/minion, mob/living/replacement)

View File

@@ -1,20 +1,40 @@
/datum/saymode /datum/saymode
/// The symbol key used to enable this say mode.
var/key var/key
/// The corresponding say mode string.
var/mode 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. /// Checks whether this saymode can be used by the given user. May send feedback.
//user = whoever said the message /datum/saymode/proc/can_be_used_by(mob/living/user)
//message = the message
//language = the language.
/datum/saymode/proc/handle_message(mob/living/user, message, datum/language/language)
return TRUE 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 /datum/saymode/changeling
key = MODE_KEY_CHANGELING key = MODE_KEY_CHANGELING
mode = MODE_CHANGELING mode = MODE_CHANGELING
/datum/saymode/changeling/handle_message(mob/living/user, message, datum/language/language) /datum/saymode/changeling/can_be_used_by(mob/living/user)
//we can send the message
if(!user.mind) if(!user.mind)
return FALSE return FALSE
if(user.mind.has_antag_datum(/datum/antagonist/fallen_changeling)) if(user.mind.has_antag_datum(/datum/antagonist/fallen_changeling))
@@ -26,11 +46,20 @@
if(HAS_TRAIT(user, TRAIT_CHANGELING_HIVEMIND_MUTE)) 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.")) to_chat(user, span_warning("The poison in the air hinders our ability to interact with the hivemind."))
return FALSE 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("<b>[ling_sender.changelingID]:</b> [message]") var/msg = span_changeling("<b>[ling_sender.changelingID]:</b> [message]")
//the recipients can receive the message // Send the message to our other changelings.
for(var/datum/antagonist/changeling/ling_receiver in GLOB.antagonists) for(var/datum/antagonist/changeling/ling_receiver in GLOB.antagonists)
if(!ling_receiver.owner) if(!ling_receiver.owner)
continue continue
@@ -45,54 +74,96 @@
for(var/mob/dead/ghost as anything in GLOB.dead_mob_list) for(var/mob/dead/ghost as anything in GLOB.dead_mob_list)
to_chat(ghost, "[FOLLOW_LINK(ghost, user)] [msg]", type = MESSAGE_TYPE_RADIO) to_chat(ghost, "[FOLLOW_LINK(ghost, user)] [msg]", type = MESSAGE_TYPE_RADIO)
return FALSE return SAYMODE_MESSAGE_HANDLED
/datum/saymode/xeno /datum/saymode/xeno
key = "a" key = MODE_KEY_ALIEN
mode = MODE_ALIEN mode = MODE_ALIEN
allows_custom_say_emotes = TRUE
/datum/saymode/xeno/handle_message(mob/living/user, message, datum/language/language) /datum/saymode/xeno/can_be_used_by(mob/living/user)
if(user.hivecheck()) if(!user.hivecheck())
user.alien_talk(message) return FALSE
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 /datum/saymode/vocalcords
key = MODE_KEY_VOCALCORDS key = MODE_KEY_VOCALCORDS
mode = MODE_VOCALCORDS mode = MODE_VOCALCORDS
/datum/saymode/vocalcords/handle_message(mob/living/user, message, datum/language/language) /datum/saymode/vocalcords/can_be_used_by(mob/living/user)
if(iscarbon(user)) if(!iscarbon(user))
var/mob/living/carbon/C = user return FALSE
var/obj/item/organ/vocal_cords/V = C.get_organ_slot(ORGAN_SLOT_VOICE) return TRUE
if(V?.can_speak_with())
V.handle_speech(message) //message /datum/saymode/vocalcords/handle_message/handle_message(
V.speak_with(message) //action mob/living/user,
return FALSE 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) /datum/saymode/binary //everything that uses .b (silicons, drones)
key = MODE_KEY_BINARY key = MODE_KEY_BINARY
mode = MODE_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)) if(isdrone(user))
var/mob/living/basic/drone/drone_user = user var/mob/living/basic/drone/drone_user = user
drone_user.drone_chat(message) drone_user.drone_chat(message, spans, message_mods)
return FALSE else if(user.binarycheck())
if(user.binarycheck()) user.robot_talk(message, spans, message_mods)
user.robot_talk(message) return SAYMODE_MESSAGE_HANDLED
return FALSE
return FALSE
/datum/saymode/holopad /datum/saymode/holopad
key = "h" key = MODE_KEY_HOLOPAD
mode = MODE_HOLOPAD mode = MODE_HOLOPAD
allows_custom_say_emotes = TRUE
/datum/saymode/holopad/handle_message(mob/living/user, message, datum/language/language) /datum/saymode/holopad/can_be_used_by(mob/living/user)
if(isAI(user)) if(!isAI(user))
var/mob/living/silicon/ai/AI = user
AI.holopad_talk(message, language)
return FALSE return FALSE
return TRUE 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

View File

@@ -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) 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) 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) if(record_mode && speaker == record_user)
record_message(speaker, raw_message, message_language) record_message(speaker, raw_message, message_language)

View File

@@ -68,7 +68,8 @@ GLOBAL_LIST_INIT(freqtospan, list(
return return
spans |= speech_span spans |= speech_span
language ||= get_selected_language() 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) send_speech(message, message_range, src, bubble_type, spans, language, message_mods, forced = forced)
/// Called when this movable hears a message from a source. /// Called when this movable hears a message from a source.
@@ -173,18 +174,15 @@ GLOBAL_LIST_INIT(freqtospan, list(
//End name span. //End name span.
var/endspanpart = "</span>" var/endspanpart = "</span>"
//Message // Language icon.
var/messagepart
var/languageicon = "" var/languageicon = ""
if(message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]) 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)
var/datum/language/dialect = GLOB.language_datum_instances[message_language] var/datum/language/dialect = GLOB.language_datum_instances[message_language]
if(istype(dialect) && dialect.display_icon(src)) if(istype(dialect) && dialect.display_icon(src))
languageicon = "[dialect.get_icon()] " languageicon = "[dialect.get_icon()] "
// The actual message part.
var/messagepart = speaker.generate_messagepart(raw_message, spans, message_mods)
messagepart = " <span class='message'>[messagepart]</span></span>" messagepart = " <span class='message'>[messagepart]</span></span>"
return "[spanpart1][spanpart2][freqpart][languageicon][compose_track_href(speaker, namepart)][namepart][compose_job(speaker, message_language, raw_message, radio_freq)][endspanpart][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 return verb_say
/** /**
* This prock is used to generate a message for chat * This proc is used to generate the 'message' part of a chat message.
* Generates the `says, "<span class='red'>meme</span>"` part of the `Grey Tider says, "meme"`. * Generates the `says, "<span class='red'>meme</span>"` 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 * input - The message to be said
* spans - A list of spans to attach to the message. Includes the atom's speech span by default * 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 * 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) if(!input)
input = "..." input = "..."

View File

@@ -321,10 +321,11 @@ GLOBAL_LIST_EMPTY(blob_nodes)
if (!message) if (!message)
return return
src.log_talk(message, LOG_SAY) var/list/message_mods = list()
var/adjusted_message = check_for_custom_say_emote(message, message_mods)
var/message_a = say_quote(message) log_sayverb_talk(message, message_mods, tag = "blob hivemind telepathy")
var/rendered = span_big(span_blob("<b>\[Blob Telepathy\] [name](<font color=\"[blobstrain.color]\">[blobstrain.name]</font>)</b> [message_a]")) var/messagepart = generate_messagepart(adjusted_message, message_mods = message_mods)
var/rendered = span_big(span_blob("<b>\[Blob Telepathy\] [name](<font color=\"[blobstrain.color]\">[blobstrain.name]</font>)</b> [messagepart]"))
relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, src, MESSAGE_TYPE_RADIO) relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, src, MESSAGE_TYPE_RADIO)
/mob/eye/blob/blob_act(obj/structure/blob/B) /mob/eye/blob/blob_act(obj/structure/blob/B)

View File

@@ -9,18 +9,24 @@
* * exact_faction_match - Passed to [/mob/proc/faction_check_atom] * * 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) /proc/_alert_drones(msg, dead_can_hear = FALSE, atom/source, mob/living/faction_checked_mob, exact_faction_match)
if (dead_can_hear && source) if(dead_can_hear && source)
for (var/mob/dead_mob in GLOB.dead_mob_list) for(var/mob/dead_mob in GLOB.dead_mob_list)
var/link = FOLLOW_LINK(dead_mob, source) var/link = FOLLOW_LINK(dead_mob, source)
to_chat(dead_mob, "[link] [msg]") to_chat(dead_mob, "[link] [msg]")
for(var/global_drone in GLOB.drones_list) for(var/global_drone in GLOB.drones_list)
var/mob/living/basic/drone/drone = global_drone var/mob/living/basic/drone/drone = global_drone
if(istype(drone) && drone.stat != DEAD) if(!istype(drone))
if(faction_checked_mob) continue
if(drone.faction_check_atom(faction_checked_mob, exact_faction_match)) if(drone.stat == DEAD)
to_chat(drone, msg) continue
else if(faction_checked_mob && !drone.faction_check_atom(faction_checked_mob, exact_faction_match))
to_chat(drone, msg) continue
to_chat(
drone,
msg,
type = MESSAGE_TYPE_RADIO,
avoid_highlighting = (drone == source),
)
@@ -39,5 +45,7 @@
* *
* Shares the same radio code with binary * Shares the same radio code with binary
*/ */
/mob/living/basic/drone/proc/drone_chat(msg) /mob/living/basic/drone/proc/drone_chat(message, list/spans = list(), list/message_mods = list())
alert_drones("<i>Drone Chat: [span_name("[name]")] <span class='message'>[say_quote(msg)]</span></i>", TRUE) log_sayverb_talk(message, message_mods, tag = "drone chat")
var/message_part = generate_messagepart(message, spans, message_mods)
alert_drones("<i>Drone Chat: [span_name("[name]")] <span class='message'>[message_part]</span></i>", TRUE)

View File

@@ -1,10 +1,10 @@
/mob/living/proc/alien_talk(message, shown_name = real_name, big_voice = FALSE) /mob/living/proc/alien_talk(message, list/spans = list(), list/message_mods = list(), shown_name = real_name, big_voice = FALSE)
src.log_talk(message, LOG_SAY) log_sayverb_talk(message, message_mods, tag = "alien hivemind")
message = trim(message) message = trim(message)
if(!message) if(!message)
return return
var/message_a = say_quote(message) var/message_a = generate_messagepart(message, spans, message_mods)
var/hivemind_spans = "alien" var/hivemind_spans = "alien"
if(big_voice) if(big_voice)
hivemind_spans += " big" hivemind_spans += " big"
@@ -16,8 +16,8 @@
var/link = FOLLOW_LINK(player, src) var/link = FOLLOW_LINK(player, src)
to_chat(player, "[link] [rendered]", type = MESSAGE_TYPE_RADIO) to_chat(player, "[link] [rendered]", type = MESSAGE_TYPE_RADIO)
/mob/living/carbon/alien/adult/royal/queen/alien_talk(message, shown_name = name) /mob/living/carbon/alien/adult/royal/queen/alien_talk(message, list/spans = list(), list/message_mods = list(), shown_name = name, big_voice = TRUE)
..(message, shown_name, TRUE) ..(message, spans, message_mods, shown_name, TRUE)
/mob/living/carbon/hivecheck() /mob/living/carbon/hivecheck()
var/obj/item/organ/alien/hivenode/N = get_organ_by_type(/obj/item/organ/alien/hivenode) var/obj/item/organ/alien/hivenode/N = get_organ_by_type(/obj/item/organ/alien/hivenode)

View File

@@ -121,8 +121,8 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
var/original_message = message var/original_message = message
message = get_message_mods(message, message_mods) message = get_message_mods(message, message_mods)
saymode = SSradio.saymodes[message_mods[RADIO_KEY]] saymode = SSradio.get_available_say_mode(src, message_mods[RADIO_KEY])
if (!forced && !saymode) if(!forced && (isnull(saymode) || saymode.allows_custom_say_emotes))
message = check_for_custom_say_emote(message, message_mods) message = check_for_custom_say_emote(message, message_mods)
if(!message) if(!message)
@@ -176,15 +176,10 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
var/succumbed = FALSE 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 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[MODE_CUSTOM_SAY_ERASE_INPUT])
if(message_mods[WHISPER_MODE] == MODE_WHISPER) if(message_mods[WHISPER_MODE] == MODE_WHISPER)
message_range = 1 message_range = 1
log_talk(message, LOG_WHISPER, forced_by = forced, custom_say_emote = message_mods[MODE_CUSTOM_SAY_EMOTE])
if(stat == HARD_CRIT) if(stat == HARD_CRIT)
var/health_diff = round(-HEALTH_THRESHOLD_DEAD + health) var/health_diff = round(-HEALTH_THRESHOLD_DEAD + health)
// If we cut our message short, abruptly end it with a-.. // 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 last_words = message
message_mods[WHISPER_MODE] = MODE_WHISPER_CRIT message_mods[WHISPER_MODE] = MODE_WHISPER_CRIT
succumbed = TRUE 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 #ifdef UNIT_TESTS
// Saves a ref() to our arglist specifically. // Saves a ref() to our arglist specifically.
@@ -241,7 +236,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
message = autopunct_bare(message) message = autopunct_bare(message)
// SKYRAT EDIT ADDITION END // 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 //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 return
var/radio_return = radio(message, message_mods, spans, language)//roughly 27% of living/say()'s total cost var/radio_return = radio(message, message_mods, spans, language)//roughly 27% of living/say()'s total cost

View File

@@ -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()) /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/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/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/start = "Relayed Speech: "
var/namepart var/namepart

View File

@@ -42,25 +42,24 @@
return FALSE return FALSE
//For holopads only. Usable by AI. //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) message = trim(message)
if (!message) if (!message)
return return
var/obj/machinery/holopad/active_pad = current 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. // Only continue if there is a hologram and its master is the user.
var/obj/effect/overlay/holo_pad_hologram/ai_holo = active_pad.masters[src] if(!istype(active_pad) || !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
to_chat(src, span_alert("No holopad connected.")) 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 /* SKYRAT EDIT REMOVAL - MOVED TO: MODULAR_SKYRAT/MODULES/ALT_VOX/CODE/VOX_PROCS.DM
// Make sure that the code compiles with AI_VOX undefined // Make sure that the code compiles with AI_VOX undefined

View File

@@ -1,8 +1,8 @@
/mob/living/proc/robot_talk(message) /mob/living/proc/robot_talk(message, list/spans = list(), list/message_mods = list())
log_talk(message, LOG_SAY, tag="binary") log_sayverb_talk(message, message_mods, tag="binary")
var/designation = "Default Cyborg" var/designation = "Default Cyborg"
var/spans = list(SPAN_ROBOT) spans |= SPAN_ROBOT
if(issilicon(src)) if(issilicon(src))
var/mob/living/silicon/player = src var/mob/living/silicon/player = src
@@ -15,9 +15,10 @@
// AIs are loud and ugly // AIs are loud and ugly
spans |= SPAN_COMMAND spans |= SPAN_COMMAND
var/quoted_message = say_quote( var/messagepart = generate_messagepart(
message, message,
spans spans,
message_mods,
) )
var/namepart = name var/namepart = name
@@ -31,31 +32,31 @@
namepart = brain.mainframe.name namepart = brain.mainframe.name
designation = brain.mainframe.job designation = brain.mainframe.job
for(var/mob/M in GLOB.player_list) for(var/mob/hearing_mob in GLOB.player_list)
if(M.binarycheck()) if(hearing_mob.binarycheck())
if(isAI(M)) if(isAI(hearing_mob))
to_chat( to_chat(
M, hearing_mob,
span_binarysay("\ span_binarysay("\
Robotic Talk, \ Robotic Talk, \
<a href='byond://?src=[REF(M)];track=[html_encode(namepart)]'>[span_name("[namepart] ([designation])")]</a> \ <a href='byond://?src=[REF(hearing_mob)];track=[html_encode(namepart)]'>[span_name("[namepart] ([designation])")]</a> \
<span class='message'>[quoted_message]</span>\ <span class='message'>[messagepart]</span>\
"), "),
type = MESSAGE_TYPE_RADIO, type = MESSAGE_TYPE_RADIO,
avoid_highlighting = src == M avoid_highlighting = (src == hearing_mob)
) )
else else
to_chat( to_chat(
M, hearing_mob,
span_binarysay("\ span_binarysay("\
Robotic Talk, \ Robotic Talk, \
[span_name("[namepart]")] <span class='message'>[quoted_message]</span>\ [span_name("[namepart]")] <span class='message'>[messagepart]</span>\
"), "),
type = MESSAGE_TYPE_RADIO, type = MESSAGE_TYPE_RADIO,
avoid_highlighting = src == M avoid_highlighting = (src == hearing_mob)
) )
if(isobserver(M)) if(isobserver(hearing_mob))
var/following = src var/following = src
// If the AI talks on binary chat, we still want to follow // If the AI talks on binary chat, we still want to follow
@@ -65,17 +66,17 @@
var/mob/living/silicon/ai/ai = src var/mob/living/silicon/ai/ai = src
following = ai.eyeobj following = ai.eyeobj
var/follow_link = FOLLOW_LINK(M, following) var/follow_link = FOLLOW_LINK(hearing_mob, following)
to_chat( to_chat(
M, hearing_mob,
span_binarysay("\ span_binarysay("\
[follow_link] \ [follow_link] \
Robotic Talk, \ Robotic Talk, \
[span_name("[namepart]")] <span class='message'>[quoted_message]</span>\ [span_name("[namepart]")] <span class='message'>[messagepart]</span>\
"), "),
type = MESSAGE_TYPE_RADIO, type = MESSAGE_TYPE_RADIO,
avoid_highlighting = src == M avoid_highlighting = (src == hearing_mob)
) )
/mob/living/silicon/binarycheck() /mob/living/silicon/binarycheck()

View File

@@ -162,7 +162,7 @@
if(name != real_name) if(name != real_name)
alt_name = " (died as [real_name])" alt_name = " (died as [real_name])"
var/spanned = say_quote(apply_message_emphasis(message)) var/spanned = generate_messagepart(message)
var/source = "<span class='game'><span class='prefix'>DEAD:</span> <span class='name'>[name]</span>[alt_name]" var/source = "<span class='game'><span class='prefix'>DEAD:</span> <span class='name'>[name]</span>[alt_name]"
var/rendered = " <span class='message'>[emoji_parse(spanned)]</span></span>" var/rendered = " <span class='message'>[emoji_parse(spanned)]</span></span>"
log_talk(message, LOG_SAY, tag="DEAD") log_talk(message, LOG_SAY, tag="DEAD")

View File

@@ -53,7 +53,7 @@ global procs
languages live either in datum/languages_holder or in the mind. languages live either in datum/languages_holder or in the mind.
verb_say/verb_ask/verb_exclaim/verb_yell/verb_sing 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(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. 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. Modifies the message by comparing the languages of the speaker with the languages of the hearer.
Called on the hearer. Called on the hearer.
say_quote(input, spans, list/message_mods) generate_messagepart(input, spans, list/message_mods)
Adds a verb and quotes to a message. Also attaches span classes to a message. 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. Verbs are determined by verb_say/verb_ask/verb_yell/verb_sing variables. Called on the speaker.
/mob /mob

View File

@@ -122,7 +122,7 @@
. = ..() . = ..()
if(speaker != wearer && speaker != ai_assistant) if(speaker != wearer && speaker != ai_assistant)
return 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) /obj/item/mod/control/proc/on_overlay_change(atom/source, cache_index, overlay)
SIGNAL_HANDLER SIGNAL_HANDLER
@@ -306,7 +306,7 @@
. = ..() . = ..()
if(speaker != loc) if(speaker != loc)
return 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) /obj/item/clothing/neck/link_scryer/proc/on_overlay_change(atom/source, cache_index, overlay)
SIGNAL_HANDLER SIGNAL_HANDLER