mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 13:10:02 +01:00
## 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.
605 lines
24 KiB
Plaintext
605 lines
24 KiB
Plaintext
GLOBAL_LIST_INIT(department_radio_prefixes, list(":", "."))
|
||
|
||
GLOBAL_LIST_INIT(department_radio_keys, list(
|
||
// Location
|
||
MODE_KEY_R_HAND = MODE_R_HAND,
|
||
MODE_KEY_L_HAND = MODE_L_HAND,
|
||
MODE_KEY_INTERCOM = MODE_INTERCOM,
|
||
|
||
// Department
|
||
MODE_KEY_DEPARTMENT = MODE_DEPARTMENT,
|
||
RADIO_KEY_COMMAND = RADIO_CHANNEL_COMMAND,
|
||
RADIO_KEY_SCIENCE = RADIO_CHANNEL_SCIENCE,
|
||
RADIO_KEY_MEDICAL = RADIO_CHANNEL_MEDICAL,
|
||
RADIO_KEY_ENGINEERING = RADIO_CHANNEL_ENGINEERING,
|
||
RADIO_KEY_SECURITY = RADIO_CHANNEL_SECURITY,
|
||
RADIO_KEY_SUPPLY = RADIO_CHANNEL_SUPPLY,
|
||
RADIO_KEY_SERVICE = RADIO_CHANNEL_SERVICE,
|
||
|
||
// Faction
|
||
RADIO_KEY_SYNDICATE = RADIO_CHANNEL_SYNDICATE,
|
||
RADIO_KEY_UPLINK = RADIO_CHANNEL_UPLINK,
|
||
RADIO_KEY_CENTCOM = RADIO_CHANNEL_CENTCOM,
|
||
RADIO_KEY_FACTION = RADIO_CHANNEL_FACTION, //SKYRAT EDIT ADDITION - FACTION
|
||
RADIO_KEY_CYBERSUN = RADIO_CHANNEL_CYBERSUN, //SKYRAT EDIT ADDITION - MAPPING
|
||
RADIO_KEY_INTERDYNE = RADIO_CHANNEL_INTERDYNE, //SKYRAT EDIT ADDITION - MAPPING
|
||
RADIO_KEY_GUILD = RADIO_CHANNEL_GUILD, //SKYRAT EDIT ADDITION - MAPPING
|
||
RADIO_KEY_TARKON = RADIO_CHANNEL_TARKON, //SKYRAT EDIT ADDITION - MAPPING
|
||
|
||
// Admin
|
||
MODE_KEY_ADMIN = MODE_ADMIN,
|
||
MODE_KEY_DEADMIN = MODE_DEADMIN,
|
||
MODE_KEY_PUPPET = MODE_PUPPET,
|
||
|
||
// Misc
|
||
RADIO_KEY_AI_PRIVATE = RADIO_CHANNEL_AI_PRIVATE, // AI Upload channel
|
||
RADIO_KEY_ENTERTAINMENT = RADIO_CHANNEL_ENTERTAINMENT, // Entertainment monitors
|
||
|
||
|
||
//kinda localization -- rastaf0
|
||
//same keys as above, but on russian keyboard layout.
|
||
// Location
|
||
"к" = MODE_R_HAND,
|
||
"л" = MODE_L_HAND,
|
||
"ш" = MODE_INTERCOM,
|
||
|
||
// Department
|
||
"р" = MODE_DEPARTMENT,
|
||
"с" = RADIO_CHANNEL_COMMAND,
|
||
"т" = RADIO_CHANNEL_SCIENCE,
|
||
"ь" = RADIO_CHANNEL_MEDICAL,
|
||
"у" = RADIO_CHANNEL_ENGINEERING,
|
||
"ы" = RADIO_CHANNEL_SECURITY,
|
||
"г" = RADIO_CHANNEL_SUPPLY,
|
||
"м" = RADIO_CHANNEL_SERVICE,
|
||
|
||
// Faction
|
||
"е" = RADIO_CHANNEL_SYNDICATE,
|
||
"н" = RADIO_CHANNEL_CENTCOM,
|
||
|
||
// Admin
|
||
"з" = MODE_ADMIN,
|
||
"в" = MODE_KEY_DEADMIN,
|
||
|
||
// Misc
|
||
"щ" = RADIO_CHANNEL_AI_PRIVATE,
|
||
"з" = RADIO_CHANNEL_ENTERTAINMENT,
|
||
))
|
||
|
||
/**
|
||
* Whitelist of saymodes or radio extensions that can be spoken through even if not fully conscious.
|
||
* Associated values are their maximum allowed mob stats.
|
||
*/
|
||
GLOBAL_LIST_INIT(message_modes_stat_limits, list(
|
||
MODE_INTERCOM = HARD_CRIT,
|
||
MODE_CHANGELING = HARD_CRIT,
|
||
MODE_ALIEN = HARD_CRIT,
|
||
MODE_BINARY = HARD_CRIT, //extra stat check on human/binarycheck()
|
||
MODE_MONKEY = HARD_CRIT,
|
||
MODE_MAFIA = HARD_CRIT
|
||
))
|
||
|
||
/mob/living/proc/Ellipsis(original_msg, chance = 50, keep_words)
|
||
if(chance <= 0)
|
||
return "..."
|
||
if(chance >= 100)
|
||
return original_msg
|
||
|
||
var/list/words = splittext(original_msg," ")
|
||
var/list/new_words = list()
|
||
|
||
var/new_msg = ""
|
||
|
||
for(var/w in words)
|
||
if(prob(chance))
|
||
new_words += "..."
|
||
if(!keep_words)
|
||
continue
|
||
new_words += w
|
||
|
||
new_msg = jointext(new_words," ")
|
||
|
||
return new_msg
|
||
|
||
/mob/living/say(
|
||
message,
|
||
bubble_type,
|
||
list/spans = list(),
|
||
sanitize = TRUE,
|
||
datum/language/language,
|
||
ignore_spam = FALSE,
|
||
forced,
|
||
filterproof = FALSE,
|
||
message_range = 7,
|
||
datum/saymode/saymode,
|
||
list/message_mods = list(),
|
||
)
|
||
if(sanitize)
|
||
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
|
||
if(!message || message == "")
|
||
return
|
||
|
||
var/original_message = message
|
||
message = get_message_mods(message, message_mods)
|
||
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)
|
||
return
|
||
|
||
if(message_mods[RADIO_EXTENSION] == MODE_ADMIN)
|
||
SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/cmd_admin_say, message)
|
||
return
|
||
|
||
if(message_mods[RADIO_EXTENSION] == MODE_DEADMIN)
|
||
SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/dsay, message)
|
||
return
|
||
|
||
// dead is the only state you can never emote
|
||
if(stat != DEAD && check_emote(original_message, forced))
|
||
return
|
||
|
||
// Checks if the saymode or channel extension can be used even if not totally conscious.
|
||
var/say_radio_or_mode = saymode || message_mods[RADIO_EXTENSION]
|
||
if(say_radio_or_mode)
|
||
var/mob_stat_limit = GLOB.message_modes_stat_limits[say_radio_or_mode]
|
||
if(stat > (isnull(mob_stat_limit) ? CONSCIOUS : mob_stat_limit))
|
||
saymode = null
|
||
message_mods -= RADIO_EXTENSION
|
||
|
||
switch(stat)
|
||
if(SOFT_CRIT)
|
||
message_mods[WHISPER_MODE] = MODE_WHISPER
|
||
if(UNCONSCIOUS)
|
||
return
|
||
if(HARD_CRIT)
|
||
if(!message_mods[WHISPER_MODE])
|
||
return
|
||
if(DEAD)
|
||
say_dead(original_message, message_mods[MANNEQUIN_CONTROLLED])
|
||
return
|
||
|
||
if(HAS_TRAIT(src, TRAIT_SOFTSPOKEN) && !HAS_TRAIT(src, TRAIT_SIGN_LANG)) // softspoken trait only applies to spoken languages
|
||
message_mods[WHISPER_MODE] = MODE_WHISPER
|
||
|
||
if(client && SSlag_switch.measures[SLOWMODE_SAY] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES) && !forced && src == usr)
|
||
if(!COOLDOWN_FINISHED(client, say_slowmode))
|
||
to_chat(src, span_warning("Message not sent due to slowmode. Please wait [SSlag_switch.slowmode_cooldown/10] seconds between messages.\n\"[message]\""))
|
||
return
|
||
COOLDOWN_START(client, say_slowmode, SSlag_switch.slowmode_cooldown)
|
||
|
||
if(!try_speak(original_message, ignore_spam, forced, filterproof))
|
||
return
|
||
|
||
language ||= message_mods[LANGUAGE_EXTENSION] || get_selected_language()
|
||
|
||
var/succumbed = FALSE
|
||
|
||
// 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
|
||
if(stat == HARD_CRIT)
|
||
var/health_diff = round(-HEALTH_THRESHOLD_DEAD + health)
|
||
// If we cut our message short, abruptly end it with a-..
|
||
var/message_len = length_char(message)
|
||
message = copytext_char(message, 1, health_diff) + "[message_len > health_diff ? "-.." : "..."]"
|
||
message = Ellipsis(message, 10, 1)
|
||
last_words = message
|
||
message_mods[WHISPER_MODE] = MODE_WHISPER_CRIT
|
||
succumbed = TRUE
|
||
|
||
log_sayverb_talk(message, message_mods, forced_by = forced)
|
||
|
||
#ifdef UNIT_TESTS
|
||
// Saves a ref() to our arglist specifically.
|
||
// We do this because we need to check that COMSIG_MOB_SAY is getting EXACTLY this list.
|
||
last_say_args_ref = REF(args)
|
||
#endif
|
||
|
||
// Make sure the arglist is passed exactly - don't pass a copy of it. Say signal handlers will modify some of the parameters.
|
||
var/sigreturn = SEND_SIGNAL(src, COMSIG_MOB_SAY, args)
|
||
if(sigreturn & COMPONENT_UPPERCASE_SPEECH)
|
||
message = uppertext(message)
|
||
|
||
var/list/message_data = treat_message(message) // unfortunately we still need this
|
||
message = message_data["message"]
|
||
var/tts_message = message_data["tts_message"]
|
||
var/list/tts_filter = message_data["tts_filter"]
|
||
|
||
spans |= speech_span
|
||
|
||
var/datum/language/spoken_lang = GLOB.language_datum_instances[language]
|
||
if(LAZYLEN(spoken_lang?.spans))
|
||
spans |= spoken_lang.spans
|
||
|
||
if(message_mods[MODE_SING])
|
||
var/randomnote = pick("\u2669", "\u266A", "\u266B")
|
||
message = "[randomnote] [message] [randomnote]"
|
||
spans |= SPAN_SINGING
|
||
|
||
if(message_mods[WHISPER_MODE]) // whisper away
|
||
spans |= SPAN_ITALICS
|
||
|
||
if(!message)
|
||
if(succumbed)
|
||
succumb()
|
||
return
|
||
|
||
//Get which verb is prefixed to the message before radio but after most modifications
|
||
message_mods[SAY_MOD_VERB] = say_mod(message, message_mods)
|
||
|
||
// SKYRAT EDIT ADDITION START: autopunctuation
|
||
//ensure EOL punctuation exists and that word-bounded 'i' are capitalized before we do anything else
|
||
if(client?.autopunctuation) //BUBBER EDIT ADDITION: AUTOPUNCTUATION PREFERENCE CHECK
|
||
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, 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
|
||
if(radio_return & NOPASS)
|
||
return TRUE
|
||
|
||
if(radio_return & ITALICS)
|
||
spans |= SPAN_ITALICS
|
||
if(radio_return & REDUCE_RANGE)
|
||
message_range = 1
|
||
if(!message_mods[WHISPER_MODE])
|
||
message_mods[WHISPER_MODE] = MODE_WHISPER
|
||
message_mods[SAY_MOD_VERB] = say_mod(message, message_mods)
|
||
|
||
//No screams in space, unless you're next to someone.
|
||
var/turf/T = get_turf(src)
|
||
var/datum/gas_mixture/environment = T.return_air()
|
||
var/pressure = (environment)? environment.return_pressure() : 0
|
||
if(pressure < SOUND_MINIMUM_PRESSURE && !HAS_TRAIT(src, TRAIT_SIGN_LANG))
|
||
message_range = 1
|
||
|
||
if(pressure < ONE_ATMOSPHERE * (HAS_TRAIT(src, TRAIT_SPEECH_BOOSTER) ? 0.1 : 0.4)) //Thin air, let's italicise the message unless we have a loud low pressure speech trait and not in vacuum
|
||
spans |= SPAN_ITALICS
|
||
|
||
send_speech(message, message_range, src, bubble_type, spans, language, message_mods, forced = forced, tts_message = tts_message, tts_filter = tts_filter)//roughly 58% of living/say()'s total cost
|
||
if(succumbed)
|
||
succumb(TRUE)
|
||
to_chat(src, compose_message(src, language, message, null, null, null, spans, message_mods))
|
||
|
||
return TRUE
|
||
|
||
|
||
/mob/living/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), message_range=0)
|
||
if((SEND_SIGNAL(src, COMSIG_MOVABLE_PRE_HEAR, args) & COMSIG_MOVABLE_CANCEL_HEARING) || !GET_CLIENT(src))
|
||
return FALSE
|
||
|
||
var/deaf_message
|
||
var/deaf_type
|
||
|
||
if(speaker != src)
|
||
deaf_type = !radio_freq ? MSG_VISUAL : null
|
||
else
|
||
deaf_type = MSG_AUDIBLE
|
||
|
||
var/atom/movable/virtualspeaker/holopad_speaker = speaker
|
||
var/avoid_highlight = src == (istype(holopad_speaker) ? holopad_speaker.source : speaker)
|
||
|
||
var/is_custom_emote = message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]
|
||
var/understood = TRUE
|
||
if(!is_custom_emote) // we do not translate emotes
|
||
var/untranslated_raw_message = raw_message
|
||
raw_message = translate_language(speaker, message_language, raw_message, spans, message_mods) // translate
|
||
if(raw_message != untranslated_raw_message)
|
||
understood = FALSE
|
||
|
||
var/speaker_is_signing = HAS_TRAIT(speaker, TRAIT_SIGN_LANG)
|
||
var/use_runechat = client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)
|
||
if (stat == UNCONSCIOUS || stat == HARD_CRIT)
|
||
use_runechat = FALSE
|
||
else if (!ismob(speaker) && !client?.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs))
|
||
use_runechat = FALSE
|
||
|
||
// if someone is whispering we make an extra type of message that is obfuscated for people out of range
|
||
// Less than or equal to 0 means normal hearing. More than 0 and less than or equal to EAVESDROP_EXTRA_RANGE means
|
||
// partial hearing. More than EAVESDROP_EXTRA_RANGE means no hearing. Exception for GOOD_HEARING trait
|
||
var/dist = get_dist(speaker, src) - message_range
|
||
if(dist > 0 && dist <= EAVESDROP_EXTRA_RANGE && !HAS_TRAIT(src, TRAIT_GOOD_HEARING) && !isobserver(src)) // ghosts can hear all messages clearly
|
||
raw_message = stars(raw_message)
|
||
if(message_range != INFINITY && dist > EAVESDROP_EXTRA_RANGE && !HAS_TRAIT(src, TRAIT_GOOD_HEARING) && !isobserver(src))
|
||
// Too far away and don't have good hearing, you can't hear anything
|
||
if(is_blind() || HAS_TRAIT(speaker, TRAIT_INVISIBLE_MAN)) // Can't see them speak either
|
||
return FALSE
|
||
if(!isturf(speaker.loc)) // If they're inside of something, probably can't see them speak
|
||
return FALSE
|
||
|
||
// But we can still see them speak
|
||
if(speaker_is_signing)
|
||
deaf_message = "[span_name("[speaker]")] [speaker.get_default_say_verb()] something, but the motions are too subtle to make out from afar."
|
||
else if(can_hear()) // If we can't hear we want to continue to the default deaf message
|
||
if(isliving(speaker))
|
||
var/mob/living/living_speaker = speaker
|
||
var/mouth_hidden = living_speaker.is_mouth_covered() || HAS_TRAIT(living_speaker, TRAIT_FACE_COVERED)
|
||
if(!HAS_TRAIT(src, TRAIT_EMPATH) && mouth_hidden) // Can't see them speak if their mouth is covered or hidden, unless we're an empath
|
||
return FALSE
|
||
|
||
deaf_message = "[span_name("[speaker]")] [speaker.verb_whisper] something, but you are too far away to hear [speaker.p_them()]."
|
||
|
||
if(deaf_message)
|
||
deaf_type = MSG_VISUAL
|
||
message = deaf_message
|
||
show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight)
|
||
return FALSE
|
||
|
||
|
||
// we need to send this signal before compose_message() is used since other signals need to modify
|
||
// the raw_message first. After the raw_message is passed through the various signals, it's ready to be formatted
|
||
// by compose_message() to be displayed in chat boxes for to_chat or runechat
|
||
SEND_SIGNAL(src, COMSIG_MOVABLE_HEAR, args)
|
||
|
||
if(speaker_is_signing) //Checks if speaker is using sign language
|
||
deaf_message = compose_message(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods, TRUE)
|
||
|
||
if(speaker != src)
|
||
if(!radio_freq) //I'm about 90% sure there's a way to make this less cluttered
|
||
deaf_type = MSG_VISUAL
|
||
else
|
||
deaf_type = MSG_AUDIBLE
|
||
|
||
// Create map text prior to modifying message for goonchat, sign lang edition
|
||
if (use_runechat && !is_blind())
|
||
if (message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
|
||
create_chat_message(speaker, null, message_mods[MODE_CUSTOM_SAY_EMOTE], spans, EMOTE_MESSAGE)
|
||
else
|
||
create_chat_message(speaker, message_language, raw_message, spans)
|
||
|
||
if(is_blind())
|
||
return FALSE
|
||
|
||
message = deaf_message
|
||
|
||
var/show_message_success = show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight)
|
||
return understood && show_message_success
|
||
|
||
if(speaker != src)
|
||
if(!radio_freq) //These checks have to be separate, else people talking on the radio will make "You can't hear yourself!" appear when hearing people over the radio while deaf.
|
||
deaf_message = "[span_name("[speaker]")] [speaker.get_default_say_verb()] something but you cannot hear [speaker.p_them()]."
|
||
deaf_type = MSG_VISUAL
|
||
else
|
||
deaf_message = span_notice("You can't hear yourself!")
|
||
deaf_type = MSG_AUDIBLE // Since you should be able to hear yourself without looking
|
||
|
||
// Create map text prior to modifying message for goonchat
|
||
if (use_runechat && can_hear())
|
||
if (message_mods[MODE_CUSTOM_SAY_ERASE_INPUT])
|
||
create_chat_message(speaker, null, message_mods[MODE_CUSTOM_SAY_EMOTE], spans, EMOTE_MESSAGE)
|
||
else
|
||
create_chat_message(speaker, message_language, raw_message, spans)
|
||
|
||
// Recompose message for AI hrefs, language incomprehension.
|
||
message = compose_message(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods)
|
||
var/show_message_success = show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight)
|
||
return understood && show_message_success
|
||
|
||
/mob/living/send_speech(message_raw, message_range = 6, obj/source = src, bubble_type = bubble_icon, list/spans, datum/language/message_language = null, list/message_mods = list(), forced = null, tts_message, list/tts_filter)
|
||
var/whisper_range = 0
|
||
var/is_speaker_whispering = FALSE
|
||
if(message_mods[WHISPER_MODE]) //If we're whispering
|
||
// Needed for good hearing trait. The actual filtering for whispers happens at the /mob/living/Hear proc
|
||
whisper_range = MESSAGE_RANGE - WHISPER_RANGE
|
||
is_speaker_whispering = TRUE
|
||
|
||
var/list/in_view = get_hearers_in_view(message_range + whisper_range, source)
|
||
var/list/listening = get_hearers_in_range(message_range + whisper_range, source)
|
||
|
||
// Pre-process listeners to account for line-of-sight
|
||
for(var/atom/movable/listening_movable as anything in listening)
|
||
if(!(listening_movable in in_view) && !HAS_TRAIT(listening_movable, TRAIT_XRAY_HEARING))
|
||
listening.Remove(listening_movable)
|
||
|
||
if(imaginary_group)
|
||
listening |= imaginary_group
|
||
|
||
if(client) //client is so that ghosts don't have to listen to mice
|
||
for(var/mob/player_mob as anything in GLOB.player_list)
|
||
if(QDELETED(player_mob)) //Some times nulls and deleteds stay in this list. This is a workaround to prevent ic chat breaking for everyone when they do.
|
||
continue //Remove if underlying cause (likely byond issue) is fixed. See TG PR #49004.
|
||
if(player_mob.stat != DEAD) //not dead, not important
|
||
continue
|
||
if(player_mob.z != z || get_dist(player_mob, src) > 7) //they're out of range of normal hearing
|
||
if(is_speaker_whispering)
|
||
if(!(get_chat_toggles(player_mob.client) & CHAT_GHOSTWHISPER)) //they're whispering and we have hearing whispers at any range off
|
||
continue
|
||
else if(!(get_chat_toggles(player_mob.client) & CHAT_GHOSTEARS)) //they're talking normally and we have hearing at any range off
|
||
continue
|
||
listening |= player_mob
|
||
|
||
// this signal ignores whispers or language translations (only used by beetlejuice component)
|
||
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_LIVING_SAY_SPECIAL, src, message_raw)
|
||
|
||
var/list/listened = list()
|
||
for(var/atom/movable/listening_movable as anything in listening)
|
||
if(!listening_movable)
|
||
stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!")
|
||
continue
|
||
|
||
if(listening_movable.Hear(null, src, message_language, message_raw, null, null, null, spans, message_mods, message_range))
|
||
listened += listening_movable
|
||
|
||
//speech bubble
|
||
var/list/speech_bubble_recipients = list()
|
||
var/found_client = FALSE
|
||
var/talk_icon_state = say_test(message_raw)
|
||
for(var/mob/M in listening)
|
||
if(M.client)
|
||
if(!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES)))
|
||
speech_bubble_recipients.Add(M.client)
|
||
found_client = TRUE
|
||
if(SStts.tts_enabled && voice && found_client && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN))
|
||
var/tts_message_to_use = tts_message
|
||
if(!tts_message_to_use)
|
||
tts_message_to_use = message_raw
|
||
|
||
var/list/filter = list()
|
||
var/list/special_filter = list()
|
||
if(length(voice_filter) > 0)
|
||
filter += voice_filter
|
||
|
||
if(length(tts_filter) > 0)
|
||
filter += tts_filter.Join(",")
|
||
|
||
var/voice_to_use = get_tts_voice(filter, special_filter)
|
||
if (!CONFIG_GET(flag/tts_no_whisper) || (CONFIG_GET(flag/tts_no_whisper) && !message_mods[WHISPER_MODE]))
|
||
INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice_to_use, filter.Join(","), listened, message_range = message_range, pitch = pitch, special_filters = special_filter.Join("|"))
|
||
|
||
var/image/say_popup = image('icons/mob/effects/talk.dmi', src, "[bubble_type][talk_icon_state]", FLY_LAYER)
|
||
SET_PLANE_EXPLICIT(say_popup, ABOVE_GAME_PLANE, src)
|
||
say_popup.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
|
||
INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(flick_overlay_global), say_popup, speech_bubble_recipients, 3 SECONDS)
|
||
LAZYADD(update_on_z, say_popup)
|
||
addtimer(CALLBACK(src, PROC_REF(clear_saypopup), say_popup), 3.5 SECONDS)
|
||
|
||
/mob/living/proc/get_tts_voice(list/filter, list/special_filter)
|
||
. = voice
|
||
var/obj/item/clothing/mask/mask = get_item_by_slot(ITEM_SLOT_MASK)
|
||
if(!istype(mask) || mask.up)
|
||
return
|
||
if(mask.voice_override)
|
||
. = mask.voice_override
|
||
if(mask.voice_filter)
|
||
filter += mask.voice_filter
|
||
if(mask.use_radio_beeps_tts)
|
||
special_filter |= TTS_FILTER_RADIO
|
||
|
||
/mob/living/silicon/get_tts_voice(list/filter, list/special_filter)
|
||
. = ..()
|
||
special_filter |= TTS_FILTER_SILICON
|
||
|
||
/mob/living/proc/clear_saypopup(image/say_popup)
|
||
LAZYREMOVE(update_on_z, say_popup)
|
||
|
||
/mob/proc/binarycheck()
|
||
return FALSE
|
||
|
||
/**
|
||
* Treats the passed message with things that may modify speech (stuttering, slurring etc).
|
||
*
|
||
* message - The message to treat.
|
||
* capitalize_message - Whether we run capitalize() on the message after we're done.
|
||
*
|
||
* Returns a list, which is a packet of information corresponding to the message that has been treated, which
|
||
* contains the new message, as well as text-to-speech information.
|
||
*/
|
||
/mob/living/proc/treat_message(message, tts_message, tts_filter, capitalize_message = TRUE)
|
||
RETURN_TYPE(/list)
|
||
|
||
if(HAS_TRAIT(src, TRAIT_UNINTELLIGIBLE_SPEECH))
|
||
message = unintelligize(message)
|
||
|
||
tts_filter = list()
|
||
var/list/data = list(message, tts_message, tts_filter, capitalize_message)
|
||
SEND_SIGNAL(src, COMSIG_LIVING_TREAT_MESSAGE, data)
|
||
message = data[TREAT_MESSAGE_ARG]
|
||
tts_message = data[TREAT_TTS_MESSAGE_ARG]
|
||
tts_filter = data[TREAT_TTS_FILTER_ARG]
|
||
capitalize_message = data[TREAT_CAPITALIZE_MESSAGE]
|
||
|
||
if(!tts_message)
|
||
tts_message = message
|
||
|
||
if(capitalize_message)
|
||
message = capitalize(message)
|
||
tts_message = capitalize(tts_message)
|
||
|
||
///caps the length of individual letters to 3: ex: heeeeeeyy -> heeeyy
|
||
/// prevents TTS from choking on unrealistic text while keeping emphasis
|
||
var/static/regex/length_regex = regex(@"(.+)\1\1\1", "gi")
|
||
while(length_regex.Find(tts_message))
|
||
var/replacement = tts_message[length_regex.index]+tts_message[length_regex.index]+tts_message[length_regex.index]
|
||
tts_message = replacetext(tts_message, length_regex.match, replacement, length_regex.index)
|
||
|
||
// removes repeated consonants at the start of a word: ex: sss
|
||
var/static/regex/word_start_regex = regex(@"\b([^aeiou\L])\1", "gi")
|
||
while(word_start_regex.Find(tts_message))
|
||
var/replacement = tts_message[word_start_regex.index]
|
||
tts_message = replacetext(tts_message, word_start_regex.match, replacement, word_start_regex.index)
|
||
|
||
return list("message" = message, "tts_message" = tts_message, "tts_filter" = tts_filter)
|
||
|
||
/mob/living/proc/radio(message, list/message_mods = list(), list/spans, language)
|
||
//SKYRAT EDIT ADDITION BEGIN
|
||
if((message_mods[MODE_HEADSET] || message_mods[RADIO_EXTENSION]) && !(mobility_flags & MOBILITY_USE) && !isAI(src) && !ispAI(src) && !ismecha(loc)) // If can't use items, you can't press the button
|
||
to_chat(src, span_warning("You can't use the radio right now as you can't reach the button!"))
|
||
return ITALICS | REDUCE_RANGE
|
||
//SKYRAT EDIT END
|
||
var/obj/item/implant/radio/imp = locate() in src
|
||
if(imp?.radio.is_on())
|
||
if(message_mods[MODE_HEADSET])
|
||
imp.radio.talk_into(src, message, , spans, language, message_mods)
|
||
return ITALICS | REDUCE_RANGE
|
||
if(message_mods[RADIO_EXTENSION] == MODE_DEPARTMENT || (message_mods[RADIO_EXTENSION] in imp.radio.channels))
|
||
imp.radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods)
|
||
return ITALICS | REDUCE_RANGE
|
||
switch(message_mods[RADIO_EXTENSION])
|
||
if(MODE_R_HAND)
|
||
for(var/obj/item/r_hand in get_held_items_for_side(RIGHT_HANDS, all = TRUE))
|
||
if (r_hand)
|
||
return r_hand.talk_into(src, message, , spans, language, message_mods)
|
||
return ITALICS | REDUCE_RANGE
|
||
if(MODE_L_HAND)
|
||
for(var/obj/item/l_hand in get_held_items_for_side(LEFT_HANDS, all = TRUE))
|
||
if (l_hand)
|
||
return l_hand.talk_into(src, message, , spans, language, message_mods)
|
||
return ITALICS | REDUCE_RANGE
|
||
|
||
if(MODE_INTERCOM)
|
||
for (var/obj/item/radio/intercom/I in view(MODE_RANGE_INTERCOM, null))
|
||
I.talk_into(src, message, , spans, language, message_mods)
|
||
return ITALICS | REDUCE_RANGE
|
||
|
||
return NONE
|
||
|
||
/mob/living/say_mod(input, list/message_mods = list())
|
||
if(message_mods[WHISPER_MODE] == MODE_WHISPER)
|
||
. = verb_whisper
|
||
else if(message_mods[WHISPER_MODE] == MODE_WHISPER_CRIT && !HAS_TRAIT(src, TRAIT_SUCCUMB_OVERRIDE))
|
||
. = "[verb_whisper] in [p_their()] last breath"
|
||
else if(message_mods[MODE_SING])
|
||
. = verb_sing
|
||
// Any subtype of slurring in our status effects make us "slur"
|
||
else if(locate(/datum/status_effect/speech/slurring) in status_effects)
|
||
if (HAS_TRAIT(src, TRAIT_SIGN_LANG))
|
||
. = "loosely signs"
|
||
else
|
||
. = "slurs"
|
||
else if(has_status_effect(/datum/status_effect/speech/stutter))
|
||
if(HAS_TRAIT(src, TRAIT_SIGN_LANG))
|
||
. = "shakily signs"
|
||
else
|
||
. = "stammers"
|
||
else if(has_status_effect(/datum/status_effect/speech/stutter/derpspeech))
|
||
if(HAS_TRAIT(src, TRAIT_SIGN_LANG))
|
||
. = "incoherently signs"
|
||
else
|
||
. = "gibbers"
|
||
else
|
||
. = ..()
|
||
|
||
/**
|
||
* Living level whisper.
|
||
*
|
||
* Living mobs which whisper have their message only appear to people very close.
|
||
*
|
||
* message - the message to display
|
||
* bubble_type - the type of speech bubble that shows up when they speak (currently does nothing)
|
||
* spans - a list of spans to apply around the message
|
||
* sanitize - whether we sanitize the message
|
||
* language - typepath language to force them to speak / whisper in
|
||
* ignore_spam - whether we ignore the spam filter
|
||
* forced - string source of what forced this speech to happen, also bypasses spam filter / mutes if supplied
|
||
* filterproof - whether we ignore the word filter
|
||
*/
|
||
/mob/living/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof)
|
||
if(!message)
|
||
return
|
||
say("#[message]", bubble_type, spans, sanitize, language, ignore_spam, forced, filterproof)
|