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.
#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

View File

@@ -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

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.
/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)

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)
*

View File

@@ -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]

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)
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

View File

@@ -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("<b>\[Blob Telepathy\] [minion.real_name]</b> [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)

View File

@@ -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("<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)
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

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)
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)

View File

@@ -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 = "</span>"
//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 = " <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]"
@@ -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, "<span class='red'>meme</span>"` part of the `Grey Tider says, "meme"`.
* 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"`,
* 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 = "..."

View File

@@ -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("<b>\[Blob Telepathy\] [name](<font color=\"[blobstrain.color]\">[blobstrain.name]</font>)</b> [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("<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)
/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]
*/
/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("<i>Drone Chat: [span_name("[name]")] <span class='message'>[say_quote(msg)]</span></i>", 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("<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)
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)

View File

@@ -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

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())
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

View File

@@ -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

View File

@@ -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, \
<a href='byond://?src=[REF(M)];track=[html_encode(namepart)]'>[span_name("[namepart] ([designation])")]</a> \
<span class='message'>[quoted_message]</span>\
<a href='byond://?src=[REF(hearing_mob)];track=[html_encode(namepart)]'>[span_name("[namepart] ([designation])")]</a> \
<span class='message'>[messagepart]</span>\
"),
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]")] <span class='message'>[quoted_message]</span>\
[span_name("[namepart]")] <span class='message'>[messagepart]</span>\
"),
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]")] <span class='message'>[quoted_message]</span>\
[span_name("[namepart]")] <span class='message'>[messagepart]</span>\
"),
type = MESSAGE_TYPE_RADIO,
avoid_highlighting = src == M
avoid_highlighting = (src == hearing_mob)
)
/mob/living/silicon/binarycheck()

View File

@@ -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 = "<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>"
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.
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

View File

@@ -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