diff --git a/aurorastation.dme b/aurorastation.dme index 96147b23f3c..ad06d52c1e1 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -393,7 +393,6 @@ #include "code\controllers\subsystems\radio.dm" #include "code\controllers\subsystems\records.dm" #include "code\controllers\subsystems\responseteam.dm" -#include "code\controllers\subsystems\runechat.dm" #include "code\controllers\subsystems\sentry.dm" #include "code\controllers\subsystems\skills.dm" #include "code\controllers\subsystems\skybox.dm" @@ -2713,7 +2712,6 @@ #include "code\modules\mob\emote.dm" #include "code\modules\mob\examinations.dm" #include "code\modules\mob\gender.dm" -#include "code\modules\mob\hear_say.dm" #include "code\modules\mob\holder.dm" #include "code\modules\mob\inventory.dm" #include "code\modules\mob\login.dm" @@ -3049,6 +3047,9 @@ #include "code\modules\mob\living\simple_animal\hostile\retaliate\retaliate.dm" #include "code\modules\mob\living\simple_animal\hostile\toy\mech.dm" #include "code\modules\mob\living\simple_animal\mechanical\mechanical.dm" +#include "code\modules\mob\say_message\hear_message.dm" +#include "code\modules\mob\say_message\say_message.dm" +#include "code\modules\mob\say_message\say_segment.dm" #include "code\modules\modular_computers\_description.dm" #include "code\modules\modular_computers\laptop_vendor.dm" #include "code\modules\modular_computers\computers\modular_computer\core.dm" diff --git a/code/__DEFINES/species_languages.dm b/code/__DEFINES/species_languages.dm index 52da8480ae0..e2236cc519b 100644 --- a/code/__DEFINES/species_languages.dm +++ b/code/__DEFINES/species_languages.dm @@ -102,6 +102,20 @@ #define PRESSUREPROOF BITFLAG(11) // Pressure doesn't affect hearing #define PASSLISTENOBJ BITFLAG(12) // Listening Objs can't hear this language +// These language flags do not support multiple languages in one message. +#define LANG_NO_MULTILANG (SIGNLANG | HIVEMIND | PRESSUREPROOF | KNOWONLYHEAR) + +// Clarity levels: how clearly one listener perceives one message (the acoustic/sight axis). +#define CLARITY_NONE 0 // not perceived (deaf / vacuum / blind-sign) +#define CLARITY_FAINT 1 // body stars()-ed (radio static, whisper eavesdrop) +#define CLARITY_DROWSY 2 // word-sampled "you almost hear..." (asleep) +#define CLARITY_CLEAR 3 // verbatim (subject to comprehension only) + +// Delivery mode: how the message reached the listener. Selects the envelope. +#define SAYMODE_SPOKEN 1 +#define SAYMODE_RADIO 2 +#define SAYMODE_SIGN 3 + // Autohiss #define AUTOHISS_OFF 0 #define AUTOHISS_BASIC 1 diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 9a3bb4d4ce8..cc1287c9d02 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -280,7 +280,6 @@ #define FIRE_PRIORITY_TGUI 110 #define FIRE_PRIORITY_STATPANEL 390 #define FIRE_PRIORITY_CHAT 400 -#define FIRE_PRIORITY_RUNECHAT 410 #define FIRE_PRIORITY_TIMER 700 #define FIRE_PRIORITY_SOUND_LOOPS 800 #define FIRE_PRIORITY_SPEECH_CONTROLLER 900 diff --git a/code/controllers/subsystems/runechat.dm b/code/controllers/subsystems/runechat.dm deleted file mode 100644 index 8b99a635326..00000000000 --- a/code/controllers/subsystems/runechat.dm +++ /dev/null @@ -1,15 +0,0 @@ -TIMER_SUBSYSTEM_DEF(runechat) - name = "Runechat" - priority = FIRE_PRIORITY_RUNECHAT - - var/list/datum/callback/message_queue = list() - -/datum/controller/subsystem/timer/runechat/fire(resumed) - CAN_BE_REDEFINED(TRUE) - . = ..() //poggers - while(message_queue.len) - var/datum/callback/queued_message = message_queue[message_queue.len] - queued_message.Invoke() - message_queue.len-- - if(MC_TICK_CHECK) - return diff --git a/code/datums/langchat/langchat.dm b/code/datums/langchat/langchat.dm index 12e231581af..45ddd09b561 100644 --- a/code/datums/langchat/langchat.dm +++ b/code/datums/langchat/langchat.dm @@ -1,285 +1,183 @@ -/atom/var/langchat_height = 32 // abovetile usually -/atom/var/langchat_color = "#FFFFFF" -/atom/var/langchat_styles = "" +/// Returns whether langchat is enabled for a given mob. +#define LANGCHAT_CLIENT_ENABLED(M) (M && M.client && M.client.prefs && (M.client.prefs.toggles_secondary & FLOATING_MESSAGES)) #define LANGCHAT_LONGEST_TEXT 64 #define LANGCHAT_WIDTH 96 #define LANGCHAT_MAX_ALPHA 196 -//pop defines -#define LANGCHAT_DEFAULT_POP 0 //normal message -#define LANGCHAT_PANIC_POP 1 //this causes shaking -#define LANGCHAT_FAST_POP 2 //this just makes it go away faster +// Types of message pop +#define LANGCHAT_DEFAULT_POP 0 // Normal messages. +#define LANGCHAT_PANIC_POP 1 // Fast, shaking messages. +#define LANGCHAT_FAST_POP 2 // Fast messages. -// params for default pop +// Parameters for LANGCHAT_DEFAULT_POP #define LANGCHAT_MESSAGE_POP_TIME 3 #define LANGCHAT_MESSAGE_POP_Y_SINK 8 -// params for panic pop +// Parameters for LANGCHAT_PANIC_POP #define LANGCHAT_MESSAGE_PANIC_POP_TIME 1 #define LANGCHAT_MESSAGE_PANIC_POP_Y_SINK 8 #define LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE 6 #define LANGCHAT_MESSAGE_PANIC_SHAKE_TIMES 6 #define LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN 1 -// params for fast pop + +// Parameters for LANGCHAT_FAST_POP #define LANGCHAT_MESSAGE_FAST_POP_TIME 1 #define LANGCHAT_MESSAGE_FAST_POP_Y_SINK 8 -#define langchat_client_enabled(M) (M && M.client && M.client.prefs && (M.client.prefs.toggles_secondary & FLOATING_MESSAGES)) +/atom/var/langchat_height = 32 // Abovetile usually +/atom/var/langchat_color = "#FFFFFF" +/atom/var/langchat_styles = "" -/// Hides the images, if they exist. Do not null the langchat images; they are rotated when the mob is buckled or proned to maintain text orientation. +/// A single overhead bubble storing the text image, and the listeners it was shown to. +/datum/langchat_bubble + var/image/bubble + var/list/mob/listeners + +/// Initializes a new bubble from a text image and a list of listeners. +/datum/langchat_bubble/New(image/bubble, list/mob/listeners) + . = ..() + src.bubble = bubble + src.listeners = listeners + +/// The floating bubbles currently shown for this atom. +/atom/var/list/langchat_images + +/// Drops all active bubbles for this atom. /atom/proc/langchat_drop_images() - if(langchat_listeners) - for(var/mob/M in langchat_listeners) - if(M.client) - M.client.images -= langchat_image - for(var/mob/M in langchat_listeners_untranslated) - if(M.client) - M.client.images -= langchat_image_untranslated - langchat_listeners = null - langchat_listeners_untranslated = null + for(var/datum/langchat_bubble/entry as anything in langchat_images) + for(var/mob/listener as anything in entry.listeners) + if(listener.client) + listener.client.images -= entry.bubble + langchat_images = null -/atom/proc/get_maxptext_x_offset(image/maptext_image) +/atom/proc/get_maptext_x_offset(image/maptext_image) return (world.icon_size / 2) - (maptext_image.maptext_width / 2) -/atom/movable/get_maxptext_x_offset(image/maptext_image) +/atom/movable/get_maptext_x_offset(image/maptext_image) return (bound_width / 2) - (maptext_image.maptext_width / 2) -/mob/get_maxptext_x_offset(image/maptext_image) +/mob/get_maptext_x_offset(image/maptext_image) return (icon_size / 2) - (maptext_image.maptext_width / 2) -/// Creates the image if it doesn't exist, resets settings that are modified by speech procs. -/atom/proc/langchat_make_image(override_color = null) - if(!langchat_image) - langchat_image = image(null, src) - langchat_image.layer = 20 - langchat_image.plane = RUNECHAT_PLANE - langchat_image.appearance_flags = NO_CLIENT_COLOR|KEEP_APART|RESET_COLOR|RESET_TRANSFORM - langchat_image.maptext_y = langchat_height - langchat_image.maptext_height = 64 - langchat_image.maptext_y -= LANGCHAT_MESSAGE_POP_Y_SINK - langchat_image.maptext_x = get_maxptext_x_offset(langchat_image) +/// Constructs a single floating langchat bubble showing maptext_string to the given listeners. +/atom/proc/langchat_build_image(maptext_string, list/mob/listeners, override_color, animation_style = LANGCHAT_DEFAULT_POP, list/additional_styles = list("langchat")) + if(!length(listeners)) + return - langchat_image.pixel_y = 0 - langchat_image.alpha = 0 - langchat_image.color = override_color ? override_color : langchat_color + var/image/bubble = image(null, src) + bubble.layer = 20 + bubble.plane = RUNECHAT_PLANE + bubble.appearance_flags = NO_CLIENT_COLOR|KEEP_APART|RESET_COLOR|RESET_TRANSFORM + bubble.maptext_y = langchat_height - LANGCHAT_MESSAGE_POP_Y_SINK + bubble.maptext_height = 64 + bubble.alpha = 0 + bubble.color = override_color ? override_color : langchat_color if(appearance_flags & PIXEL_SCALE) - langchat_image.appearance_flags |= PIXEL_SCALE + bubble.appearance_flags |= PIXEL_SCALE + langchat_configure_bubble(bubble, override_color) -/mob/langchat_make_image(override_color = null) - var/new_image = FALSE - if(!langchat_image) - new_image = TRUE - . = ..() - // Recenter for icons more than 32 wide - if(new_image) - langchat_image.maptext_x += (icon_size - 32) / 2 + bubble.maptext = generate_text_image(maptext_string, additional_styles) + bubble.maptext_width = LANGCHAT_WIDTH + bubble.maptext_x = get_maptext_x_offset(bubble) + bubble.loc = isturf(loc) ? src : recursive_holder_check(src) -/// Creates the (untranslated) image if it doesn't exist, resets settings that are modified by speech procs. -/atom/proc/langchat_make_image_untranslated(override_color = null) - if(!langchat_image_untranslated) - langchat_image_untranslated = image(null, src) - langchat_image_untranslated.layer = 20 - langchat_image_untranslated.plane = RUNECHAT_PLANE - langchat_image_untranslated.appearance_flags = NO_CLIENT_COLOR|KEEP_APART|RESET_COLOR|RESET_TRANSFORM - langchat_image_untranslated.maptext_y = langchat_height - langchat_image_untranslated.maptext_height = 64 - langchat_image_untranslated.maptext_y -= LANGCHAT_MESSAGE_POP_Y_SINK - langchat_image_untranslated.maptext_x = get_maxptext_x_offset(langchat_image_untranslated) + for(var/mob/listener as anything in listeners) + listener.client.images += bubble - langchat_image_untranslated.pixel_y = 0 - langchat_image_untranslated.alpha = 0 - langchat_image_untranslated.color = override_color ? override_color : langchat_color - if(appearance_flags & PIXEL_SCALE) - langchat_image_untranslated.appearance_flags |= PIXEL_SCALE + animate_style(bubble, animation_style) + LAZYADD(langchat_images, new /datum/langchat_bubble(bubble, listeners)) -/mob/langchat_make_image_untranslated(override_color = null) - var/new_image = FALSE - if(!langchat_image_untranslated) - new_image = TRUE - . = ..() - // Recenter for icons more than 32 wide - if(new_image) - langchat_image_untranslated.maptext_x += (icon_size - 32) / 2 +/// Per-type appearance tweaks applied to a freshly built bubble. +/atom/proc/langchat_configure_bubble(image/bubble, override_color) + return -/mob/abstract/ghost/langchat_make_image(override_color = null) +/mob/abstract/ghost/langchat_configure_bubble(image/bubble, override_color) if(!override_color) - override_color = "#c51fb7" - . = ..() - langchat_image.appearance_flags |= RESET_ALPHA + bubble.color = "#c51fb7" + bubble.appearance_flags |= RESET_ALPHA -/atom/proc/langchat_speech(message, list/listeners, datum/language/language, override_color, skip_language_check = FALSE, animation_style = LANGCHAT_DEFAULT_POP, list/additional_styles = list("langchat")) +/// Floats flat text to everybody who can see it. +/atom/proc/langchat_speech(message, list/mob/listeners, override_color, animation_style = LANGCHAT_DEFAULT_POP, list/additional_styles = list("langchat")) langchat_drop_images() - langchat_make_image(override_color) - langchat_make_image_untranslated(override_color) - langchat_listeners = listeners - langchat_listeners_untranslated = list() - var/mob/listener - // Listener list management. - for(listener in langchat_listeners) - // Remove those who have the langchat_client disabled or who are deaf. - if(!langchat_client_enabled(listener) || listener.ear_deaf) - langchat_listeners -= listener + var/list/mob/shown = list() + for(var/mob/listener in listeners) + if(!LANGCHAT_CLIENT_ENABLED(listener) || listener.ear_deaf) continue - // Handle listeners who don't understand the language being spoken. - if(!skip_language_check && !listener.say_understands(src, language)) - langchat_listeners_untranslated += listener - langchat_listeners -= listener + shown += listener + if(!length(shown)) + return - // Generate the translated langchat_image. - langchat_image.maptext = generate_text_image(message, additional_styles = additional_styles) - langchat_image.maptext_width = LANGCHAT_WIDTH - langchat_image.maptext_x = get_maxptext_x_offset(langchat_image) - for(var/mob/comprehending_listener in langchat_listeners) - comprehending_listener.client.images += langchat_image - - // Generate the untranslated langchat_image. Note that we have to loop through confused listeners first here, as some - // might know languages that confer partial comprehension, and that would result in a unique langchat image. - for(var/mob/confused_listener in langchat_listeners_untranslated) - langchat_image_untranslated.maptext = generate_text_image(message, language, additional_styles, confused_listener.languages) - langchat_image_untranslated.maptext_width = LANGCHAT_WIDTH - langchat_image_untranslated.maptext_x = get_maxptext_x_offset(langchat_image_untranslated) - confused_listener.client.images += langchat_image_untranslated + langchat_build_image(message, shown, override_color, animation_style, additional_styles) var/timer = (length(message) / LANGCHAT_LONGEST_TEXT) * 4 SECONDS + 2 SECONDS + addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_drop_images)), timer, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_NO_HASH_WAIT) - if(isturf(loc)) - langchat_image.loc = src - if(langchat_image_untranslated) - langchat_image_untranslated.loc = src - else - langchat_image.loc = recursive_holder_check(src) - if(langchat_image_untranslated) - langchat_image_untranslated.loc = recursive_holder_check(src) - - animate_style(langchat_image, animation_style) - if(langchat_listeners_untranslated) - animate_style(langchat_image_untranslated, animation_style) - - addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_drop_images), language), timer, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_NO_HASH_WAIT) - -/atom/proc/langchat_long_speech(message, list/listeners, datum/language/language, override_color, skip_language_check = FALSE) +/// Floats a say message to every listener in view. Each listener receives their own perceived text based on +/// their comprehension of the spoken languages. Identical text is grouped into a single bubble for performance. +/atom/proc/langchat_say_message(datum/say_message/msg, list/mob/listeners, override_color, animation_style = LANGCHAT_DEFAULT_POP, list/additional_styles = list("langchat")) langchat_drop_images() - langchat_make_image() - langchat_make_image_untranslated(override_color) - var/text_left = null - var/truncated_message = message + // Group listeners by exact text perceived. + var/list/groups = list() + for(var/mob/listener in listeners) + if(!LANGCHAT_CLIENT_ENABLED(listener) || listener.ear_deaf) + continue + var/perceived = msg.plain_text_for(listener) + if(!length(perceived)) + continue + var/list/group = groups[perceived] + if(!group) + group = list() + groups[perceived] = group + group += listener + if(!length(groups)) + return - langchat_listeners = listeners - var/mob/listener - // Listener list management. - for(listener in langchat_listeners) - // Remove those who have the langchat_client disabled or who are deaf. - if(!langchat_client_enabled(listener) || listener.ear_deaf) - langchat_listeners -= listener - // Handle listeners who don't understand the language being spoken. - if(!skip_language_check && !listener.say_understands(src, language)) - langchat_listeners_untranslated += listener - langchat_listeners -= listener + for(var/perceived in groups) + langchat_build_image(perceived, groups[perceived], override_color, animation_style, additional_styles) - if(length(message) > LANGCHAT_LONGEST_TEXT) - truncated_message = copytext_char(message, 1, LANGCHAT_LONGEST_TEXT - 5) + "..." - text_left = "..." + copytext_char(message, LANGCHAT_LONGEST_TEXT - 5) - var/timer = 6 SECONDS - if(text_left) - timer = 4 SECONDS - truncated_message = "[truncated_message]" + // One-off timer for duration. Disappears for all listeners at the same time. + var/timer = (length(msg.to_string()) / LANGCHAT_LONGEST_TEXT) * 4 SECONDS + 2 SECONDS + addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_drop_images)), timer, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_NO_HASH_WAIT) - // Generate the translated langchat_image. - langchat_image.maptext = generate_text_image(truncated_message) - langchat_image.maptext_width = LANGCHAT_WIDTH - langchat_image.maptext_x = get_maxptext_x_offset(langchat_image) - for(var/mob/comprehending_listener in langchat_listeners) - comprehending_listener.client.images += langchat_image - - // Generate the untranslated langchat_image. Note that we have to loop through confused listeners first here, as some - // might know languages that confer partial comprehension, and that would result in a unique langchat image. - for(var/mob/confused_listener in langchat_listeners_untranslated) - langchat_image_untranslated.maptext = confused_listener.generate_text_image(truncated_message, src) - langchat_image_untranslated.maptext_width = LANGCHAT_WIDTH - langchat_image_untranslated.maptext_x = get_maxptext_x_offset(langchat_image_untranslated) - confused_listener.client.images += langchat_image_untranslated - - if(isturf(loc)) - langchat_image.loc = src - if(langchat_image_untranslated) - langchat_image_untranslated.loc = src - else - langchat_image.loc = recursive_holder_check(src) - if(langchat_image_untranslated) - langchat_image_untranslated.loc = recursive_holder_check(src) - - animate_style(langchat_image) - if(langchat_listeners_untranslated) - animate_style(langchat_image_untranslated) - - if(text_left) - addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_long_speech), text_left, listeners, language), timer, TIMER_OVERRIDE|TIMER_UNIQUE|TIMER_NO_HASH_WAIT) - else - addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_drop_images), language), timer, TIMER_OVERRIDE|TIMER_UNIQUE|TIMER_NO_HASH_WAIT) - -/** - * Generates the maptext_image, translated or untranslated, for a given message. - */ -/atom/proc/generate_text_image(message, datum/language/language = null, list/additional_styles = list("langchat"), list/languages = null) +/// Builds the maptext span for a floating message. +/atom/proc/generate_text_image(message, list/additional_styles = list("langchat")) + var/list/styles = additional_styles ? additional_styles.Copy() : list() var/text_to_display = message var/use_mob_style = TRUE - var/image/r_icon - - if(language) - message = language.scramble(message, languages) - text_to_display = message if(length(text_to_display) > LANGCHAT_LONGEST_TEXT) text_to_display = copytext_char(text_to_display, 1, LANGCHAT_LONGEST_TEXT + 1) + "..." - if(additional_styles.Find("emote")) - additional_styles.Remove("emote") + if(styles.Find("emote")) + styles.Remove("emote") use_mob_style = FALSE - r_icon = image('icons/mob/chat_icons.dmi', icon_state = "emote") - else if(additional_styles.Find("virtual-speaker")) - additional_styles.Remove("virtual-speaker") - r_icon = image('icons/mob/chat_icons.dmi', icon_state = "radio") - if(r_icon) + var/image/r_icon = image('icons/mob/chat_icons.dmi', icon_state = "emote") text_to_display = "\icon[r_icon]&zwsp;[text_to_display]" - text_to_display = "[text_to_display]" - return text_to_display + return "[text_to_display]" -/** - * Animate the given maptext_image. - */ -/atom/proc/animate_style(var/image/langchat_image, animation_style = LANGCHAT_DEFAULT_POP) - if(langchat_image) +/// Animates the given maptext image. +/atom/proc/animate_style(var/image/bubble, animation_style = LANGCHAT_DEFAULT_POP) + if(bubble) switch(animation_style) if(LANGCHAT_DEFAULT_POP) - langchat_image.alpha = 0 - animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_POP_TIME) + bubble.alpha = 0 + animate(bubble, pixel_y = bubble.pixel_y + LANGCHAT_MESSAGE_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_POP_TIME) if(LANGCHAT_PANIC_POP) - langchat_image.alpha = LANGCHAT_MAX_ALPHA - animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_PANIC_POP_Y_SINK, time = LANGCHAT_MESSAGE_PANIC_POP_TIME) - animate(pixel_x = langchat_image.pixel_x - LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) + bubble.alpha = LANGCHAT_MAX_ALPHA + animate(bubble, pixel_y = bubble.pixel_y + LANGCHAT_MESSAGE_PANIC_POP_Y_SINK, time = LANGCHAT_MESSAGE_PANIC_POP_TIME) + animate(pixel_x = bubble.pixel_x - LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) for(var/i = 1 to LANGCHAT_MESSAGE_PANIC_SHAKE_TIMES) - animate(pixel_x = langchat_image.pixel_x + 2*LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = 2*LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) - animate(pixel_x = langchat_image.pixel_x - 2*LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) - animate(pixel_x = langchat_image.pixel_x + LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) + animate(pixel_x = bubble.pixel_x + 2*LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = 2*LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) + animate(pixel_x = bubble.pixel_x - 2*LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) + animate(pixel_x = bubble.pixel_x + LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING) if(LANGCHAT_FAST_POP) - langchat_image.alpha = 0 - animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_FAST_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_FAST_POP_TIME) + bubble.alpha = 0 + animate(bubble, pixel_y = bubble.pixel_y + LANGCHAT_MESSAGE_FAST_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_FAST_POP_TIME) -/** Displays image to a single listener after it was built above eg. for chaining different game logic than speech code -This does just that, doesn't check deafness or language! Do what you will in that regard **/ -/atom/proc/langchat_display_image(mob/M) - if(langchat_image) - if(!langchat_client_enabled(M)) - return - if(!langchat_listeners) // shouldn't happen - langchat_listeners = list() - langchat_listeners |= M - M.client.images += langchat_image - -/// Will attempt to find what's holding this item if it's being contained by something, ie if it's in a satchel held by a human, this'll return the human +/// Attempts to return what's holding this item if being contained by something. e.g. Cortical borer returns their host. /proc/recursive_holder_check(obj/item/held_item, recursion_limit = 3) if(recursion_limit <= 0) return held_item diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm index a5237effb0f..f859c948744 100644 --- a/code/game/atom/_atom.dm +++ b/code/game/atom/_atom.dm @@ -141,17 +141,6 @@ /// The mob currently interacting with the atom during a `do_after` timer. Used to validate `DO_TARGET_UNIQUE_ACT` flag checks. var/mob/do_unique_target_user - /* - * Duplicate vars and logic created for untranslated images for the sake of getting an untranslated langchat to display for listeners who do not understand - * the language being spoken. Someone could certainly think of cleaner ways to do this, but for want of a better solution right now, it has been implemented - * in this rote manner to make it easier to strip out in future if it needs replaced. - */ - - var/image/langchat_image - var/image/langchat_image_untranslated - var/list/mob/langchat_listeners - var/list/mob/langchat_listeners_untranslated - /atom/Destroy(force) if(opacity) updateVisibility(src) @@ -180,10 +169,7 @@ // The component is attached to us normaly and will be deleted elsewhere orbiters = null do_unique_target_user = null - QDEL_NULL(langchat_image) - QDEL_NULL(langchat_image_untranslated) - langchat_listeners?.Cut() - langchat_listeners_untranslated?.Cut() + langchat_drop_images() return ..() /atom/proc/handle_ricochet(obj/projectile/ricocheting_projectile) diff --git a/code/game/gamemodes/changeling/implements/hivemind.dm b/code/game/gamemodes/changeling/implements/hivemind.dm index 703fa5117aa..472a3f94d91 100644 --- a/code/game/gamemodes/changeling/implements/hivemind.dm +++ b/code/game/gamemodes/changeling/implements/hivemind.dm @@ -39,18 +39,18 @@ to_chat(src, SPAN_DANGER("You can use 'say' to speak with them and the rest of the hivemind.")) to_chat(src, SPAN_DANGER("What you say can only be heard by [ling] and the other members of their local hivemind.")) -/mob/abstract/hivemind/say(message) - message = sanitize_text(message) +/mob/abstract/hivemind/say(text) + text = sanitize_text(text) - if(!message) + if(!text) return - log_say("[changeling_mob] Hivemind/[src.key] : [message]") + log_say("[changeling_mob] Hivemind/[src.key] : [text]") if(src.client?.prefs.muted & (MUTE_DEADCHAT|MUTE_IC)) to_chat(src, SPAN_WARNING("You cannot talk. (Admin Muted)")) return - relay_hivemind(changeling_message_process(message), changeling_mob) + relay_hivemind(changeling_message_process(text), changeling_mob) /mob/abstract/hivemind/emote() to_chat(src, SPAN_WARNING("You cannot emote.")) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index a7eec14b0ab..664ad28c2d4 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -938,7 +938,7 @@ GLOBAL_LIST_INIT(slot_flags_enumeration, list( /obj/item/proc/showoff(mob/user) var/list/viewers = get_hearers_in_view(world.view, src) - user.langchat_speech("holds up [src].", viewers, GLOB.all_languages, skip_language_check = TRUE, animation_style = LANGCHAT_FAST_POP, additional_styles = list("langchat_small", "emote")) + user.langchat_speech("holds up [src].", viewers, animation_style = LANGCHAT_FAST_POP, additional_styles = list("langchat_small", "emote")) for (var/mob/M in viewers) if(!user.is_invisible_to(M)) M.show_message("[user] holds up [icon2html(src, M)] [src]. Take a closer look.",1) diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index a7cb03e57a8..c1312df8383 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -442,7 +442,7 @@ var/global/list/default_interrogation_channels = list( // If we were to send to a channel we don't have, drop it. return null -/obj/item/radio/talk_into(mob/living/M, message, channel, var/say_verb = "says", var/datum/language/speaking = null, var/ignore_restrained) +/obj/item/radio/talk_into(mob/living/M, message, channel, var/say_verb = "says", var/datum/language/speaking = null, var/ignore_restrained, var/datum/say_message/say_message = null) if(!on) return FALSE if(!M || !message) @@ -487,7 +487,7 @@ var/global/list/default_interrogation_channels = list( // Determine the identify information attached to the signal var/datum/weakref/speaker_weakref = WEAKREF(M) - var/datum/signal/subspace/vocal/signal = new(src, connection.frequency, speaker_weakref, speaking, message, say_verb) + var/datum/signal/subspace/vocal/signal = new(src, connection.frequency, speaker_weakref, speaking, message, say_verb, say_message) // All radios attempt to use the subspace system . = signal.send_to_receivers() @@ -594,7 +594,7 @@ var/global/list/default_interrogation_channels = list( /obj/item/radio/borg/list_channels(var/mob/user) return list_secure_channels(user) -/obj/item/radio/borg/talk_into(mob/living/M, message, channel, verb, datum/language/speaking, var/ignore_restrained) +/obj/item/radio/borg/talk_into(mob/living/M, message, channel, verb, datum/language/speaking, var/ignore_restrained, var/datum/say_message/say_message = null) . = ..() if (isrobot(src.loc)) var/mob/living/silicon/robot/R = src.loc diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 73dbb9594dc..ab491137d9c 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -394,7 +394,7 @@ if(mob.client) clients_in_hearers += mob.client if(length(clients_in_hearers)) - langchat_speech(message, hearers, GLOB.all_languages, skip_language_check = TRUE) + langchat_speech(message, hearers) /// Override this to customize the effects an activated signaler has. /obj/proc/do_signaler() diff --git a/code/game/objects/structures/crates_lockers/closets/statue.dm b/code/game/objects/structures/crates_lockers/closets/statue.dm index 4c33224650a..7b34a5fb37b 100644 --- a/code/game/objects/structures/crates_lockers/closets/statue.dm +++ b/code/game/objects/structures/crates_lockers/closets/statue.dm @@ -5,7 +5,7 @@ /mob/statue_mob/send_emote() to_chat(src, "You are unable to move while trapped as a statue.") -/mob/statue_mob/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) +/mob/statue_mob/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) to_chat(src, "You are unable to speak while trapped as a statue.") /obj/structure/closet/statue diff --git a/code/game/objects/structures/machinery/telecomms/broadcasting.dm b/code/game/objects/structures/machinery/telecomms/broadcasting.dm index 271aaff621a..1529dbb950d 100644 --- a/code/game/objects/structures/machinery/telecomms/broadcasting.dm +++ b/code/game/objects/structures/machinery/telecomms/broadcasting.dm @@ -97,12 +97,15 @@ /datum/signal/subspace/vocal var/datum/weakref/speaker var/datum/language/language + /// The speaker's say_message, if any. Null for the announcer, etc. + var/datum/say_message/say_message -/datum/signal/subspace/vocal/New(obj/source, frequency, datum/weakref/speaker, datum/language/language, message, say_verb) +/datum/signal/subspace/vocal/New(obj/source, frequency, datum/weakref/speaker, datum/language/language, message, say_verb, datum/say_message/say_message) src.source = source src.frequency = frequency src.language = language src.speaker = speaker + src.say_message = say_message var/turf/T = get_turf(source) if(isturf(T)) @@ -127,7 +130,7 @@ ) /datum/signal/subspace/vocal/copy() - var/datum/signal/subspace/vocal/copy = new(source, frequency, speaker, language) + var/datum/signal/subspace/vocal/copy = new(source, frequency, speaker, language, say_message = say_message) copy.original = src copy.data = data.Copy() copy.levels = levels @@ -214,12 +217,25 @@ var/compression = data["compression"] + // Carry the original message if untouched in transit, else build one from the flat text. + var/datum/say_message/radio_msg + if(say_message && say_message.to_string() == data["message"]) + radio_msg = say_message.copy() + else + radio_msg = new + radio_msg.speaker = M + radio_msg.verb = data["say_verb"] + radio_msg.collapse_to(language, message) + radio_msg.say_mode = SAYMODE_RADIO + radio_msg.base_clarity = compression ? CLARITY_FAINT : CLARITY_CLEAR + radio_msg.radio_parts = list(part_a, part_b, part_c) + for (var/mob/hearer in receive) if(!hearer) crash_with("null found in the hearers list returned by the spatial grid") continue - hearer.hear_radio(message, data["say_verb"], language, part_a, part_b, part_c, M, !!compression) + hearer.hear_message(radio_msg) // --- This following recording is intended for research and feedback in the use of department radio channels --- diff --git a/code/modules/emotes/emote_mob.dm b/code/modules/emotes/emote_mob.dm index 11909b383b7..173ac37737e 100644 --- a/code/modules/emotes/emote_mob.dm +++ b/code/modules/emotes/emote_mob.dm @@ -170,7 +170,7 @@ audible_message(message, ghost_hearing = do_show_observers) emote_viewers = get_hearers_in_LOS(world.view, src) - langchat_speech(langchat_message, emote_viewers, GLOB.all_languages, skip_language_check = TRUE, additional_styles = list("emote", "langchat_small")) + langchat_speech(langchat_message, emote_viewers, additional_styles = list("emote", "langchat_small")) // Specific mob type exceptions below. /mob/living/silicon/ai/emote(var/act, var/type, var/message) diff --git a/code/modules/hallucinations/mob.dm b/code/modules/hallucinations/mob.dm index cdcefd5cd83..3c1415beafd 100644 --- a/code/modules/hallucinations/mob.dm +++ b/code/modules/hallucinations/mob.dm @@ -1,17 +1,13 @@ -//Hallucinated Hearing -/mob/living/carbon/hear_say(var/message, var/verb = "says", var/datum/language/language, var/alt_name = "",var/italics = 0, var/mob/speaker, var/sound/speech_sound, var/sound_vol) - if(hallucination >= 60 && prob(1)) - var/orig_message = message - message = pick(SShallucinations.hallucinated_phrases) - log_say("Hallucination level changed [orig_message] by [speaker] to [message] for [key_name(src)].") - return ..() +//Hallucinated hearing: a chance to mishear the body entirely +/mob/proc/hallucinate_heard(text, mob/speaker) + return text -/mob/living/carbon/hear_radio(var/message, var/verb="says", var/datum/language/language, var/part_a, var/part_b, var/part_c, var/mob/speaker, var/hard_to_hear = 0, var/vname ="") +/mob/living/carbon/hallucinate_heard(text, mob/speaker) if(hallucination >= 60 && prob(1)) - var/orig_message = message - message = pick(SShallucinations.hallucinated_phrases) - log_say("Hallucination level changed [orig_message] by [speaker] to [message] for [key_name(src)].") - ..() + var/orig_message = text + text = pick(SShallucinations.hallucinated_phrases) + log_say("Hallucination level changed [orig_message] by [speaker] to [text] for [key_name(src)].") + return text //Main handling proc, called in life() /mob/living/carbon/proc/handle_hallucinations() diff --git a/code/modules/heavy_vehicle/mech_interaction.dm b/code/modules/heavy_vehicle/mech_interaction.dm index 304dd00e963..010ef2ffe00 100644 --- a/code/modules/heavy_vehicle/mech_interaction.dm +++ b/code/modules/heavy_vehicle/mech_interaction.dm @@ -699,10 +699,9 @@ GLOBAL_DATUM_INIT(mech_state, /datum/ui_state/default, new()) /mob/living/heavy_vehicle/get_floating_chat_y_offset() return 20 -/mob/living/heavy_vehicle/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "", var/italics = 0, var/mob/speaker = null, var/sound/speech_sound, var/sound_vol) +/mob/living/heavy_vehicle/react_to_message(datum/say_message/msg) if(can_listen()) - addtimer(CALLBACK(src, PROC_REF(handle_hear_say), speaker, message), 0.5 SECONDS) - return ..() + addtimer(CALLBACK(src, PROC_REF(handle_hear_say), msg.speaker, msg.to_string()), 0.5 SECONDS) // heavily commented so it doesn't look like one fat chunk of code, which it still does - Geeves /mob/living/heavy_vehicle/proc/handle_hear_say(var/mob/speaker, var/text) diff --git a/code/modules/mob/abstract/ghost/observer/say.dm b/code/modules/mob/abstract/ghost/observer/say.dm index 986307b3a0a..646a092cee6 100644 --- a/code/modules/mob/abstract/ghost/observer/say.dm +++ b/code/modules/mob/abstract/ghost/observer/say.dm @@ -1,12 +1,12 @@ -/mob/abstract/ghost/observer/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) - if (!message) +/mob/abstract/ghost/observer/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) + if (!text) return - log_say("Ghost/[src.key] : [message]") + log_say("Ghost/[src.key] : [text]") if (src.client) if(src.client.prefs.muted & (MUTE_DEADCHAT|MUTE_IC)) to_chat(src, SPAN_WARNING("You cannot talk in deadchat (muted).")) return - . = src.say_dead(message) + . = src.say_dead(text) diff --git a/code/modules/mob/abstract/ghost/storyteller/storyteller.dm b/code/modules/mob/abstract/ghost/storyteller/storyteller.dm index 1926bf58498..5618b515e68 100644 --- a/code/modules/mob/abstract/ghost/storyteller/storyteller.dm +++ b/code/modules/mob/abstract/ghost/storyteller/storyteller.dm @@ -45,11 +45,11 @@ if(!can_reenter_corpse) SSodyssey.remove_storyteller(src) -/mob/abstract/ghost/storyteller/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) - if (!message) +/mob/abstract/ghost/storyteller/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) + if (!text) return - var/msg = sanitize(message) + var/msg = sanitize(text) if(!msg) return diff --git a/code/modules/mob/abstract/new_player/new_player.dm b/code/modules/mob/abstract/new_player/new_player.dm index b66667ea8c6..d5dd84e8298 100644 --- a/code/modules/mob/abstract/new_player/new_player.dm +++ b/code/modules/mob/abstract/new_player/new_player.dm @@ -431,6 +431,9 @@ INITIALIZE_IMMEDIATE(/mob/abstract/new_player) /mob/abstract/new_player/Move() return TRUE +/mob/abstract/new_player/hear_message(datum/say_message/msg) + return FALSE + /mob/abstract/new_player/proc/close_spawn_windows() src << browse(null, "window=playersetup") //closes the player setup window @@ -465,12 +468,6 @@ INITIALIZE_IMMEDIATE(/mob/abstract/new_player) /mob/abstract/new_player/is_ready() return ready && ..() -/mob/abstract/new_player/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "",var/italics = 0, var/mob/speaker = null) - return - -/mob/abstract/new_player/hear_radio(var/message, var/verb="says", var/datum/language/language=null, var/part_a, var/part_b, var/part_c, var/mob/speaker = null, var/hard_to_hear = 0) - return - /mob/abstract/new_player/MayRespawn() return 1 diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm deleted file mode 100644 index 9b353df7e4e..00000000000 --- a/code/modules/mob/hear_say.dm +++ /dev/null @@ -1,313 +0,0 @@ -// At minimum every mob has a hear_say proc. - -/** - * hear_say's return value determines whether or not the mob in question also receives a langchat image. - */ -/mob/proc/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "",var/italics = 0, var/mob/speaker = null, var/sound/speech_sound, var/sound_vol, var/font_size = null) - if(!istype(src, /mob/living/test) && cant_hear()) - return FALSE - - if(speaker && !istype(speaker, /mob/living/test) && (!speaker.client && istype(src,/mob/abstract/ghost/observer) && client.prefs.toggles & CHAT_GHOSTEARS && !(speaker in view(src)))) - //Does the speaker have a client? It's either random stuff that observers won't care about (Experiment 97B says, 'EHEHEHEHEHEHEHE') - //Or someone snoring. So we make it where they won't hear it. - return FALSE - - //make sure the air can transmit speech - hearer's side - var/turf/T = get_turf(src) - var/vacuum_proof = ((language && (language.flags & PRESSUREPROOF)) || isghost(src)) - var/speaker_name = speaker.name - - if(ishuman(speaker)) - var/mob/living/carbon/human/H = speaker - speaker_name = H.GetVoice() - - if(T && !vacuum_proof) //Ghosts can hear even in vacuum. - var/datum/gas_mixture/environment = T.return_air() - var/pressure = SAFE_XGM_PRESSURE(environment) - var/distance_to_speaker = get_dist(speaker, src) - if(pressure < SOUND_MINIMUM_PRESSURE && distance_to_speaker > 1) - // Yeah, this isn't quite realistic to be able to see if someone is talking through, say, an opaque mask, but for gameplay purposes it should help indicate that you're not bugged. - if(distance_to_speaker <= 4 && !italics) - to_chat(src, SPAN_NOTICE("[speaker_name] talks, but you're in a vacuum. Maybe if you were close enough for the sound to transmit through touch...")) - return FALSE - - if (pressure < ONE_ATMOSPHERE*0.4) //sound distortion pressure, to help clue people in that the air is thin, even if it isn't a vacuum yet - italics = 1 - sound_vol *= 0.5 //muffle the sound a bit, so it's like we're actually talking through contact - - if((language && (language.flags & KNOWONLYHEAR)) && !say_understands(speaker, language)) - return TRUE - - if(!vr_mob && (sleeping || stat == UNCONSCIOUS)) - hear_sleep(message) - return FALSE - - //non-verbal languages are garbled if you can't see the speaker. Yes, this includes if they are inside a closet. - if (language && (language.flags & NONVERBAL)) - if((!speaker || (src.sdisabilities & BLIND || src.blinded) || !(speaker in view(src))) && !isghost(src)) - message = stars(message) - - if(!(language && (language.flags & INNATE)) && !say_understands(speaker, language)) // skip understanding checks for INNATE languages - message = language ? language.scramble(message, languages) : stars(message) - - var/accent_icon = speaker.get_accent_icon(language, src) - - if(italics) - message = "[message]" - - var/track = null - if(isghost(src)) - if(speaker_name != speaker.real_name && speaker.real_name) - speaker_name = "[speaker.real_name] ([speaker_name])" - track = "[ghost_follow_link(speaker, src)] " - if((client.prefs.toggles & CHAT_GHOSTEARS) && (get_turf(speaker) in view(src))) - message = "[message]" - - if(isdeaf(src)) - if(!language || !(language.flags & INNATE)) // INNATE is the flag for audible-emote-language, so we don't want to show an "x talks but you cannot hear them" message if it's set - if(speaker == src) - to_chat(src, SPAN_WARNING("You cannot hear yourself speak!")) - return FALSE - else - to_chat(src, "[speaker_name][alt_name] talks but you cannot hear them.") - return FALSE - else - if(language) - if(font_size) - on_hear_say("[track][accent_icon ? accent_icon + " " : ""][speaker_name][alt_name] [language.format_message(message, verb)]") - else - on_hear_say("[track][accent_icon ? accent_icon + " " : ""][speaker_name][alt_name] [language.format_message(message, verb)]") - else - if(font_size) - on_hear_say("[track][accent_icon ? accent_icon + " " : ""][speaker_name][alt_name] [verb], \"[message]\"") - else - on_hear_say("[track][accent_icon ? accent_icon + " " : ""][speaker_name][alt_name] [verb], \"[message]\"") - if (speech_sound && (get_dist(speaker, src) <= world.view && src.z == speaker.z)) - var/turf/source = speaker? get_turf(speaker) : get_turf(src) - playsound(source, speech_sound, sound_vol, vary = TRUE) - return TRUE - -/mob/proc/cant_hear() - if(!client && !vr_mob) - return TRUE - return FALSE - -/mob/living/carbon/cant_hear() - . = ..() - if(.) - var/datum/dionastats/DS = get_dionastats() - if(DS?.nym) - return FALSE - -/mob/living/carbon/alien/diona/cant_hear() - . = ..() - if(. && detached && gestalt) - return FALSE - -/mob/proc/on_hear_say(var/message) - to_chat(src, message) - if(vr_mob) - to_chat(vr_mob, message) - -/mob/living/carbon/on_hear_say(var/message) - ..() - var/datum/dionastats/DS = get_dionastats() - if(DS?.nym) - var/mob/living/carbon/alien/diona/D = DS.nym.resolve() - if(D) - to_chat(D, message) - -/mob/living/carbon/alien/diona/on_hear_say(var/message) - to_chat(src, message) - if(detached && gestalt) - to_chat(gestalt, message) - -/mob/living/silicon/on_hear_say(var/message) - var/time = say_timestamp() - to_chat(src, "[time] [message]") - if(vr_mob) - to_chat(vr_mob, "[time] [message]") - -/mob/proc/hear_radio(var/message, var/verb="says", var/datum/language/language=null, var/part_a, var/part_b, var/part_c, var/mob/speaker = null, var/hard_to_hear = 0) - if(!client && !vr_mob) - return - - if(!vr_mob && (sleeping || stat == UNCONSCIOUS)) - hear_sleep(message) - return - - var/track = null - - //non-verbal languages are garbled if you can't see the speaker. Yes, this includes if they are inside a closet. - if (language && (language.flags & NONVERBAL)) - if (!speaker || (src.sdisabilities & BLIND || src.blinded) || !(speaker in view(src))) - message = stars(message) - - if(!(language && (language.flags & INNATE))) // skip understanding checks for INNATE languages - if(!say_understands(speaker,language)) - if(istype(speaker,/mob/living/simple_animal)) - var/mob/living/simple_animal/S = speaker - if(S.speak && S.speak.len) - message = pick(S.speak) - else - return - else - if(language) - message = language.scramble(message, languages) - else - message = stars(message) - - if(hard_to_hear) - message = stars(message) - - var/speaker_name - if(speaker != null) - speaker_name = speaker.name - else - speaker_name = "Unknown" - - if(ishuman(speaker)) - speaker_name = speaker.GetVoice() - - if(hard_to_hear) - speaker_name = "Unknown" - - var/changed_voice - - var/accent_icon = speaker.get_accent_icon(language, src) - accent_icon = accent_icon ? accent_icon + " " : "" - part_a = replacetext(part_a, "%ACCENT%", accent_icon) - - if(istype(src, /mob/living/silicon/ai) && !hard_to_hear) - var/jobname // the mob's "job" - var/mob/living/carbon/human/impersonating //The crew member being impersonated, if any. - - if (ishuman(speaker)) - var/mob/living/carbon/human/H = speaker - - if(H.wear_mask && istype(H.wear_mask,/obj/item/clothing/mask/gas/voice)) - changed_voice = 1 - var/list/impersonated = list() - var/mob/living/carbon/human/I = impersonated[speaker_name] - if(!I) - for(var/mob/living/carbon/human/M in GLOB.mob_list) - if(M.real_name == speaker_name) - I = M - impersonated[speaker_name] = I - break - - // If I's display name is currently different from the voice name and using an agent ID then don't impersonate - // as this would allow the AI to track I and realize the mismatch. - if(I && !(I.name != speaker_name && I.wear_id && istype(I.wear_id,/obj/item/card/id/syndicate))) - impersonating = I - jobname = impersonating.get_assignment() - else - jobname = "Unknown" - else - jobname = H.get_assignment() - - else if (iscarbon(speaker)) // Nonhuman carbon mob - jobname = "No id" - else if (isAI(speaker)) - jobname = "AI" - else if (isrobot(speaker)) - jobname = "Cyborg" - else if (istype(speaker, /mob/living/silicon/pai)) - jobname = "Personal AI" - else - jobname = "Unknown" - - if(changed_voice) - if(impersonating) - track = "[speaker_name] ([jobname])" - else - track = "[speaker_name] ([jobname])" - else - track = "[speaker_name] ([jobname])" - - if(isghost(src)) - if(speaker != null) - if(speaker_name != speaker.real_name && !isAI(speaker)) //Announce computer and various stuff that broadcasts doesn't use it's real name but AI's can't pretend to be other mobs. - speaker_name = "[speaker.real_name] ([speaker_name])" - track = "[ghost_follow_link(speaker, src)] " - - var/formatted - if(language) - formatted = language.format_message_radio(message, verb) - else - formatted = "[verb], \"[message]\"" - formatted += part_c - if(isdeaf(src)) - if(prob(20)) - to_chat(src, SPAN_WARNING("You feel your headset vibrate but can hear nothing from it!")) - else - on_hear_radio(part_a, speaker_name, track, part_b, formatted, accent_icon) - -/proc/say_timestamp() - return "\[[worldtime2text()]\]" - -/mob/proc/on_hear_radio(part_a, speaker_name, track, part_b, formatted, accent_icon) - to_chat(src, "[part_a][speaker_name][part_b][formatted]") - if(vr_mob) - to_chat(vr_mob, "[part_a][speaker_name][part_b][formatted]") - -/mob/abstract/ghost/observer/on_hear_radio(part_a, speaker_name, track, part_b, formatted) - to_chat(src, "[track][part_a][speaker_name][part_b][formatted]") - -/mob/living/silicon/on_hear_radio(part_a, speaker_name, track, part_b, formatted) - var/time = say_timestamp() - to_chat(src, "[time][part_a][speaker_name][part_b][formatted]") - if(vr_mob) - to_chat(vr_mob, "[time][part_a][speaker_name][part_b][formatted]") - -/mob/living/silicon/ai/on_hear_radio(part_a, speaker_name, track, part_b, formatted) - var/time = say_timestamp() - to_chat(src, "[time][part_a][track][part_b][formatted]") - if(vr_mob) - to_chat(vr_mob, "[time][part_a][track][part_b][formatted]") - -/mob/proc/hear_signlang(var/message, var/verb = "gestures", var/datum/language/language, var/mob/speaker = null, var/list/sign_adv_length = null) - if(!client || !speaker) - return - - if(say_understands(speaker, language)) - message = "[speaker] [verb], \"[message]\"" - else - if (length(sign_adv_length) <= 4) - sign_adv_length = list(" briefly", " a short message", " a message", " a lengthy message", " a very lengthy message") - var/adverb - var/length = length(message) * pick(0.8, 0.9, 1.0, 1.1, 1.2) //Inserts a little fuzziness. - switch(length) - if(0 to 12) adverb = sign_adv_length[1] - if(12 to 30) adverb = sign_adv_length[2] - if(30 to 48) adverb = sign_adv_length[3] - if(48 to 90) adverb = sign_adv_length[4] - else adverb = sign_adv_length[5] - message = "[speaker] [verb][adverb]." - - if(src.status_flags & PASSEMOTES) - for(var/obj/item/holder/H in src.contents) - H.show_message(message) - for(var/mob/living/M in src.contents) - M.show_message(message) - src.show_message(message) - -/mob/proc/hear_sleep(var/message) - if (isdeaf(src)) - return - var/heard = "" - if(prob(15)) - var/list/punctuation = list(",", "!", ".", ";", "?") - var/list/messages = text2list(message, " ") - var/R = rand(1, messages.len) - var/heardword = messages[R] - if(copytext(heardword,1, 1) in punctuation) - heardword = copytext(heardword,2) - if(copytext(heardword,-1) in punctuation) - heardword = copytext(heardword,1,length(heardword)) - heard = "...You hear something about...[heardword]" - - else - heard = "...You almost hear someone talking..." - - to_chat(src, heard) diff --git a/code/modules/mob/language/generic.dm b/code/modules/mob/language/generic.dm index 80e71e190e7..2521693510f 100644 --- a/code/modules/mob/language/generic.dm +++ b/code/modules/mob/language/generic.dm @@ -2,19 +2,16 @@ /datum/language/noise name = LANGUAGE_NOISE desc = "Noises." - key = "" - flags = RESTRICTED | NONGLOBAL | INNATE | NO_TALK_MSG | NO_STUTTER | TCOMSSIM + key = "e" + flags = RESTRICTED | INNATE | NO_TALK_MSG | NO_STUTTER | TCOMSSIM allow_accents = TRUE /datum/language/noise/format_message(message, verb) - return span("message", "[message]") + return span("message", colourize(message)) /datum/language/noise/format_message_plain(message, verb) return message -/datum/language/noise/format_message_radio(message, verb, accent) - return "[message]" - /datum/language/noise/get_talkinto_msg_range(message) // if you make a loud noise (screams etc), you'll be heard from 4 tiles over instead of two return (copytext(message, length(message)) == "!") ? 4 : 2 diff --git a/code/modules/mob/language/language.dm b/code/modules/mob/language/language.dm index 33a2ee7ae5c..3ab0ab85093 100644 --- a/code/modules/mob/language/language.dm +++ b/code/modules/mob/language/language.dm @@ -202,13 +202,14 @@ return input_size /datum/language/proc/format_message(message, verb) - return "[verb], \"[capitalize(message)]\"" + return "[verb], [colourize("\"[capitalize(message)]\"")]" /datum/language/proc/format_message_plain(message, verb) return "[verb], \"[capitalize(message)]\"" -/datum/language/proc/format_message_radio(message, verb) - return "[verb], \"[capitalize(message)]\"" +/// Wraps text in this language's colour span. +/datum/language/proc/colourize(message) + return "[message]" /datum/language/proc/get_talkinto_msg_range(message) // if you yell, you'll be heard from two tiles over instead of one @@ -376,6 +377,22 @@ return prefix in GLOB.config.language_prefixes +/// Caches the trigger regex per prefix-set so it isn't rebuilt each message. +GLOBAL_LIST_EMPTY(language_trigger_regex_cache) + +/// Returns a cached regex matching a language prefix at message start or after whitespace. +/mob/proc/get_language_trigger_regex() + var/list/prefixes = (client?.prefs?.language_prefixes?.len) ? client.prefs.language_prefixes : GLOB.config.language_prefixes + var/cache_key = jointext(prefixes, "") + . = GLOB.language_trigger_regex_cache[cache_key] + if(.) + return + var/list/escaped = list() + for(var/prefix in prefixes) + escaped += "\\[prefix]" + . = regex("(^|\\s)(\[[jointext(escaped, "")]\])", "g") + GLOB.language_trigger_regex_cache[cache_key] = . + /mob/verb/check_languages() set name = "Check Known Languages" set category = "IC.Language" diff --git a/code/modules/mob/living/bot/bot.dm b/code/modules/mob/living/bot/bot.dm index fab90b9aaa9..83d047f4fa9 100644 --- a/code/modules/mob/living/bot/bot.dm +++ b/code/modules/mob/living/bot/bot.dm @@ -163,8 +163,8 @@ return return attack_hand(user) -/mob/living/bot/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) - ..(message, null, "beeps") +/mob/living/bot/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) + ..(text, null, "beeps") /mob/living/bot/Collide(atom/A) if(on && botcard && istype(A, /obj/structure/machinery/door)) diff --git a/code/modules/mob/living/carbon/brain/say.dm b/code/modules/mob/living/carbon/brain/say.dm index 4de289de48a..c713e63e958 100644 --- a/code/modules/mob/living/carbon/brain/say.dm +++ b/code/modules/mob/living/carbon/brain/say.dm @@ -1,16 +1,16 @@ //TODO: Convert this over for languages. -/mob/living/carbon/brain/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/carbon/brain/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if (silent) return if(!(container && istype(container, /obj/item/mmi))) return //No MMI, can't speak, bucko./N else - speaking = parse_language(message) + speaking = parse_language(text) if(speaking) - message = copytext(message, 2+length(speaking.key)) - var/ending = copytext(message, length(message)) - var/pre_ending = copytext(message, length(message) - 1, length(message)) + text = copytext(text, 2+length(speaking.key)) + var/ending = copytext(text, length(text)) + var/pre_ending = copytext(text, length(text) - 1, length(text)) if (speaking) verb = speaking.get_spoken_verb(ending, pre_ending) else @@ -23,14 +23,14 @@ if(prob(10))//10% chane to drop the message entirely return else - message = Gibberish(message, (emp_damage*6))//scrambles the message, gets worse when emp_damage is higher + text = Gibberish(text, (emp_damage*6))//scrambles the message, gets worse when emp_damage is higher if(speaking && speaking.flags & HIVEMIND) - speaking.broadcast(src,trim(message)) + speaking.broadcast(src,trim(text)) return if(istype(container, /obj/item/mmi/radio_enabled)) var/obj/item/mmi/radio_enabled/R = container if(R.radio) - R.radio.hear_talk(src, sanitize(message), verb, speaking) - ..(trim(message), speaking, verb) + R.radio.hear_talk(src, sanitize(text), verb, speaking) + ..(trim(text), speaking, verb) diff --git a/code/modules/mob/living/carbon/human/say.dm b/code/modules/mob/living/carbon/human/say.dm index 759b6deed2a..4b013759ce5 100644 --- a/code/modules/mob/living/carbon/human/say.dm +++ b/code/modules/mob/living/carbon/human/say.dm @@ -65,9 +65,9 @@ return ..() -/mob/living/carbon/human/say(message, datum/language/speaking, verb, alt_name, ghost_hearing, whisper, skip_edit) +/mob/living/carbon/human/say(text, datum/language/speaking, verb, alt_name, ghost_hearing, whisper, skip_edit) // Messaging the user is handled by the species proc. - if(!species.can_speak(src, speaking, message)) + if(!species.can_speak(src, speaking, text)) return FALSE . = ..() @@ -116,7 +116,7 @@ /* ***Deprecated*** - let this be handled at the hear_say or hear_radio proc + let this be handled at the hear_message proc This is left in for robot speaking when humans gain binary channel access until I get around to rewriting robot_talk() proc. There is no language handling build into it however there is at the /mob level so we accept the call @@ -195,21 +195,23 @@ return headsets[headsets[1]] return null -/mob/living/carbon/human/handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper, var/is_singing = FALSE) - if(!whisper && (paralysis || InStasis())) - whisper(message, speaking) +/mob/living/carbon/human/handle_message_mode(datum/say_message/msg, datum/language/primary, list/used_radios) + var/text = msg.to_string() + var/message_mode = msg.message_mode + if(!msg.whisper && (paralysis || InStasis())) + whisper(text, primary, say_verb = TRUE, msg = msg) return TRUE switch(message_mode) if("intercom") for(var/obj/item/radio/intercom/I in view(1)) I.add_fingerprint(src) used_radios += I - I.talk_into(src, message, null, verb, speaking) + I.talk_into(src, text, null, msg.verb, primary, say_message = msg) if("headset") var/obj/item/radio/R = get_radio() if(R) used_radios += R - R.talk_into(src, message, null, verb, speaking) + R.talk_into(src, text, null, msg.verb, primary, say_message = msg) if("right ear") var/obj/item/radio/R var/has_radio = FALSE @@ -221,7 +223,7 @@ has_radio = TRUE if(has_radio) used_radios += R - R.talk_into(src,message,null,verb,speaking) + R.talk_into(src, text, null, msg.verb, primary, say_message = msg) if("left ear") var/obj/item/radio/R var/has_radio = FALSE @@ -233,7 +235,7 @@ has_radio = TRUE if(has_radio) used_radios += R - R.talk_into(src,message,null,verb,speaking) + R.talk_into(src, text, null, msg.verb, primary, say_message = msg) if("wrist") var/obj/item/radio/R var/has_radio = FALSE @@ -245,16 +247,16 @@ has_radio = TRUE if(has_radio) used_radios += R - R.talk_into(src,message,null,verb,speaking) + R.talk_into(src, text, null, msg.verb, primary, say_message = msg) if("whisper") - whisper(message, speaking, is_singing, say_verb = TRUE) + whisper(text, primary, msg.singing, say_verb = TRUE, msg = msg) return TRUE else if(message_mode) var/obj/item/radio/R = get_radio() if(R) used_radios += R - R.talk_into(src, message, message_mode, verb, speaking) + R.talk_into(src, text, message_mode, msg.verb, primary, say_message = msg) /mob/living/carbon/human/handle_speech_sound() var/list/returns = ..() diff --git a/code/modules/mob/living/carbon/slime/say.dm b/code/modules/mob/living/carbon/slime/say.dm index eaefd6206e9..e5bd37a0888 100644 --- a/code/modules/mob/living/carbon/slime/say.dm +++ b/code/modules/mob/living/carbon/slime/say.dm @@ -1,10 +1,10 @@ -/mob/living/carbon/slime/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) - verb = say_quote(message) +/mob/living/carbon/slime/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) + verb = say_quote(text) - if(copytext(message,1,2) == "*") - return emote(copytext(message,2)) + if(copytext(text,1,2) == "*") + return emote(copytext(text,2)) - return ..(message, null, verb) + return ..(text, null, verb) /mob/living/carbon/slime/say_quote(var/text) var/ending = copytext(text, length(text)) @@ -21,16 +21,8 @@ return TRUE return ..() -/mob/living/carbon/slime/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "", var/italics = 0, var/mob/speaker = null, var/sound/speech_sound, var/sound_vol) - if(is_friend(speaker)) +/mob/living/carbon/slime/react_to_message(datum/say_message/msg) + if(is_friend(msg.speaker)) speech_buffer = list() - speech_buffer.Add(speaker) - speech_buffer.Add(lowertext(html_decode(message))) - return ..() - -/mob/living/carbon/slime/hear_radio(var/message, var/verb="says", var/datum/language/language=null, var/part_a, var/part_b, var/part_c, var/mob/speaker = null, var/hard_to_hear = 0, var/vname ="") - if(is_friend(speaker)) - speech_buffer = list() - speech_buffer.Add(speaker) - speech_buffer.Add(lowertext(html_decode(message))) - return ..() + speech_buffer.Add(msg.speaker) + speech_buffer.Add(lowertext(html_decode(msg.to_string()))) diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index caf2a24e2ba..1f2fa8a625e 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -145,14 +145,16 @@ var/list/channel_to_radio_key = new /mob/living/proc/get_stutter_verbs() return list("stammers", "stutters") -/mob/living/proc/handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper) +/mob/living/proc/handle_message_mode(datum/say_message/msg, datum/language/primary, list/used_radios) + var/text = msg.to_string() + var/message_mode = msg.message_mode if(message_mode == "intercom") for(var/obj/item/radio/intercom/I in view(1, src)) used_radios += I - I.talk_into(src, message, verb, speaking) + I.talk_into(src, text, msg.verb, primary, say_message = msg) - if(message_mode == "whisper" && !whisper) - whisper(message, speaking, say_verb = TRUE) + if(message_mode == "whisper" && !msg.whisper) + whisper(text, primary, say_verb = TRUE, msg = msg) return TRUE return FALSE @@ -187,150 +189,179 @@ var/list/channel_to_radio_key = new return "statement" -/mob/living/say(var/message, var/datum/language/speaking = null, var/verb, var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/// Applies final edits to the message like autohiss, then adds markup. Returns false if msg ends empty. +/mob/living/proc/finalize_say_message(datum/say_message/msg, skip_edit = FALSE) + if(!skip_edit) + for(var/datum/say_segment/segment as anything in msg.segments) + if(segment.language && (segment.language.flags & NO_STUTTER)) + continue + segment.text = handle_autohiss(segment.text, segment.language) + var/list/hsp_params = handle_speech_problems(segment.text, msg.verb, msg.message_mode, msg.message_range) + if(hsp_params) + segment.text = hsp_params[HSP_MSG] || segment.text + msg.verb = hsp_params[HSP_VERB] || msg.verb + msg.message_mode = hsp_params[HSP_MSGMODE] || msg.message_mode + msg.message_range = hsp_params[HSP_MSGRANGE] || msg.message_range + for(var/datum/say_segment/segment as anything in msg.segments) + segment.text = process_chat_markup(segment.text, list("~", "_")) + return length(msg.to_string()) ? TRUE : FALSE + +/// Delivers message to each listener. Returns clients that heard it. +/// Removes delivered mobs from the listeners list. +/mob/living/proc/deliver_say_message(datum/say_message/msg, list/listeners) + var/list/client/heard = list() + for(var/mob/M in listeners) + if(M.hear_message(msg) && M.client) + heard += M.client + listeners -= M + return heard + +/// Builds and animates the speech bubble over the speaker for the given clients. +/mob/living/proc/show_speech_bubble(datum/say_message/msg, message, list/client/show_to) + var/speech_bubble_state = check_speech_punctuation_state(message) + var/speech_state_modifier = get_speech_bubble_state_modifier() + if(speech_bubble_state && speech_state_modifier) + speech_bubble_state = "[speech_state_modifier]_[speech_bubble_state]" + if(!speech_bubble_state) + return + var/image/speech_bubble = image('icons/mob/talk.dmi', src, speech_bubble_state) + adjust_typing_indicator_offsets(speech_bubble) + speech_bubble.layer = layer + speech_bubble.plane = plane + speech_bubble.appearance_flags = RESET_COLOR|RESET_ALPHA + INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(animate_speechbubble), speech_bubble, show_to, 30) + +/mob/living/say(var/text, var/datum/language/speaking = null, var/verb, var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(stat) if(stat == DEAD) - return say_dead(message) + return say_dead(text) return if(silent) to_chat(src, SPAN_WARNING("You try to speak, but nothing comes out!")) return - var/message_mode = parse_message_mode(message, "headset") + var/message_mode = parse_message_mode(text, "headset") var/regex/emote = regex("^(\[\\*^\])\[^*\]+$") - if(emote.Find(message)) - if(emote.group[1] == "*") return client_emote(copytext(message, 2)) - if(emote.group[1] == "^") return custom_emote(VISIBLE_MESSAGE, copytext(message,2)) + if(emote.Find(text)) + if(emote.group[1] == "*") return client_emote(copytext(text, 2)) + if(emote.group[1] == "^") return custom_emote(VISIBLE_MESSAGE, copytext(text,2)) //parse the radio code and consume it if (message_mode) if (message_mode == "headset") - message = copytext(message,2) //it would be really nice if the parse procs could do this for us. + text = copytext(text,2) //it would be really nice if the parse procs could do this for us. else - message = copytext(message,3) + text = copytext(text,3) - message = trim(message) - message = formalize_text(message) + text = trim(text) + text = formalize_text(text) - var/had_speaking = !!speaking - //parse the language code and consume it - if(!speaking || speaking.always_parse_language) - speaking = parse_language(message) - if(!had_speaking) - if(speaking) - message = copytext(message,2+length(speaking.key)) - else - speaking = get_default_language() + var/datum/language/forced = (speaking && !speaking.always_parse_language) ? speaking : null + var/datum/say_message/msg = build_say_message(text, forced) - if(speaking) - var/list/speech_mod = speaking.handle_message_mode(message_mode) - speaking = speech_mod[1] + if(!length(msg.to_string())) + return FALSE + + // the first segment's language is the message's primary + var/datum/language/primary = msg.segments[1].language + var/list/speech_mod = primary?.handle_message_mode(message_mode) + if(speech_mod) + primary = speech_mod[1] message_mode = speech_mod[2] - var/is_singing = FALSE - if(length(message) >= 1 && copytext(message, 1, 2) == "%") - message = copytext(message, 2) - if(speaking?.sing_verb) - is_singing = TRUE + if(copytext(msg.segments[1].text, 1, 2) == "%") + msg.segments[1].text = copytext(msg.segments[1].text, 2) + if(primary?.sing_verb) + msg.singing = TRUE + msg.sing_note = pick("\u2669", "\u266A", "\u266B") - // This is broadcast to all mobs with the language, - // irrespective of distance or anything else. - if(speaking && (speaking.flags & HIVEMIND)) - if(speaking.name == LANGUAGE_VAURCA && within_jamming_range(src)) + // hivemind languages reach all speakers regardless of distance + if(msg.single_language?.flags & HIVEMIND) + if(msg.single_language.name == LANGUAGE_VAURCA && within_jamming_range(src)) to_chat(src, SPAN_WARNING("Your head buzzes as your message is blocked with jamming signals.")) return - speaking.broadcast(src,trim(message)) + msg.single_language.broadcast(src, trim(msg.to_string())) return TRUE - if(!verb) - verb = say_quote(message, speaking, is_singing, whisper) + msg.verb = verb || say_quote(msg.to_string(), primary, msg.singing, whisper) var/is_shouting = FALSE - if(speaking) - for(var/verb_to_check in speaking.shout_verb) - if(verb_to_check == verb) + if(primary) + for(var/verb_to_check in primary.shout_verb) + if(verb_to_check == msg.verb) is_shouting = TRUE - continue + break if(is_muzzled()) to_chat(src, SPAN_DANGER("You're muzzled and cannot speak!")) return - message = trim_left(message) - var/message_range = world.view - if(!skip_edit && !(speaking && (speaking.flags & NO_STUTTER))) - message = handle_autohiss(message, speaking) - var/list/hsp_params = handle_speech_problems(message, verb, message_mode, message_range) - if(hsp_params) - message = hsp_params[HSP_MSG] || message - verb = hsp_params[HSP_VERB] || verb - message_mode = hsp_params[HSP_MSGMODE] || message_mode - message_range = hsp_params[HSP_MSGRANGE] || message_range + msg.message_mode = message_mode + msg.message_range = world.view + msg.whisper = whisper + msg.alt_name = alt_name + msg.say_mode = SAYMODE_SPOKEN - if(!message || message == "") + if(!finalize_say_message(msg, skip_edit)) return FALSE - message = process_chat_markup(message, list("~", "_")) - if(is_singing) - var/randomnote = pick("\u2669", "\u266A", "\u266B") - message = "[randomnote] [message] [randomnote]" + if(msg.single_language?.flags & SIGNLANG) + msg.say_mode = SAYMODE_SIGN + msg.verb = pick(msg.single_language.signlang_verb) + return say_signlang(msg) + if(primary && (primary.flags & NONVERBAL) && prob(30)) + custom_emote(VISIBLE_MESSAGE, "[pick(primary.signlang_verb)].") - //handle nonverbal and sign languages here - if (speaking) - if (speaking.flags & NONVERBAL) - if (prob(30)) - src.custom_emote(VISIBLE_MESSAGE, "[pick(speaking.signlang_verb)].") - - if (speaking.flags & SIGNLANG) - return say_signlang(message, pick(speaking.signlang_verb), speaking, speaking.sign_adv_length) + text = msg.to_string() var/list/obj/item/used_radios = new - if(handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper, is_singing)) + if(handle_message_mode(msg, primary, used_radios)) return TRUE var/list/handle_v = handle_speech_sound() - var/sound/speech_sound = handle_v[1] - var/sound_vol = handle_v[2] - var/italics = handle_v[3] + msg.speech_sound = handle_v[1] + msg.sound_vol = handle_v[2] + msg.italics = handle_v[3] + msg.font_size = get_font_size_modifier() //speaking into radios if(length(used_radios)) - italics = TRUE - message_range = 1 - if(speaking) - message_range = speaking.get_talkinto_msg_range(message) - if(!speaking || !(speaking.flags & NO_TALK_MSG)) - var/msg = SPAN_NOTICE("\The [src] talks into \the [used_radios[1]].") + msg.italics = TRUE + msg.message_range = 1 + if(primary) + msg.message_range = primary.get_talkinto_msg_range(text) + if(!primary || !(primary.flags & NO_TALK_MSG)) + var/talk_msg = SPAN_NOTICE("\The [src] talks into \the [used_radios[1]].") for (var/mob/living/L in get_hearers_in_view(5, src) - src) - L.show_message(msg) - if(speech_sound) - sound_vol *= 0.5 + L.show_message(talk_msg) + if(msg.speech_sound) + msg.sound_vol *= 0.5 var/list/listening = list() var/turf/T = get_turf(src) if(whisper) - message_range = 1 - italics = TRUE + msg.message_range = 1 + msg.italics = TRUE if(T) - if(!speaking || !(speaking.flags & PRESSUREPROOF)) + if(!msg.single_language || !(msg.single_language.flags & PRESSUREPROOF)) //make sure the air can transmit speech - speaker's side var/datum/gas_mixture/environment = T.return_air() var/pressure = SAFE_XGM_PRESSURE(environment) if(pressure < SOUND_MINIMUM_PRESSURE) - message_range = 1 + msg.message_range = 1 if (pressure < ONE_ATMOSPHERE*0.4) //sound distortion pressure, to help clue people in that the air is thin, even if it isn't a vacuum yet - italics = TRUE - sound_vol *= 0.5 //muffle the sound a bit, so it's like we're actually talking through contact + msg.italics = TRUE + msg.sound_vol *= 0.5 //muffle the sound a bit, so it's like we're actually talking through contact - listening = get_hearers_in_view(message_range, src) + listening = get_hearers_in_view(msg.message_range, src) - var/sensitive_listener = get_intent_listeners(src, message_range + 1) + var/sensitive_listener = get_intent_listeners(src, msg.message_range + 1) listening = mergelists(listening, sensitive_listener, TRUE) if(client) @@ -342,51 +373,33 @@ var/list/channel_to_radio_key = new if(player_mob.client?.prefs.toggles & CHAT_GHOSTEARS) listening |= player_mob - var/list/hear_clients = list() - for(var/mob/M in listening) - var/heard_say = M.hear_say(message, verb, speaking, alt_name, italics, src, speech_sound, sound_vol, get_font_size_modifier()) - if(heard_say && M.client) - hear_clients += M.client - listening -= M + var/list/hear_clients = deliver_say_message(msg, listening) - var/speech_bubble_state = check_speech_punctuation_state(message) - var/speech_state_modifier = get_speech_bubble_state_modifier() - if(speech_bubble_state && speech_state_modifier) - speech_bubble_state = "[speech_state_modifier]_[speech_bubble_state]" - - var/image/speech_bubble - if(speech_bubble_state) - speech_bubble = image('icons/mob/talk.dmi', src, speech_bubble_state) - adjust_typing_indicator_offsets(speech_bubble) - speech_bubble.layer = layer - speech_bubble.plane = plane - - speech_bubble.appearance_flags = RESET_COLOR|RESET_ALPHA - INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(animate_speechbubble), speech_bubble, hear_clients, 30) + show_speech_bubble(msg, text, hear_clients) var/list/langchat_styles = list() - if(istype(speaking, /datum/language/noise)) + if(istype(primary, /datum/language/noise)) langchat_styles = list("emote", "langchat_small") if(whisper) langchat_styles = list("langchat_italic") if(is_shouting) langchat_styles = list("langchat_yell") - langchat_speech(message, get_hearers_in_view(message_range, src), speaking, additional_styles = langchat_styles) + langchat_say_message(msg, get_hearers_in_view(msg.message_range, src), additional_styles = langchat_styles) - var/bypass_listen_obj = (speaking && (speaking.flags & PASSLISTENOBJ)) + var/bypass_listen_obj = (msg.single_language && (msg.single_language.flags & PASSLISTENOBJ)) if(!bypass_listen_obj) for(var/obj/O as anything in listening) if(O) //It's possible that it could be deleted in the meantime. - INVOKE_ASYNC(O, TYPE_PROC_REF(/obj, hear_talk), src, message, verb, speaking) + INVOKE_ASYNC(O, TYPE_PROC_REF(/obj, hear_talk), src, text, msg.verb, primary) if(mind) - mind.last_words = message + mind.last_words = text if(whisper) - log_whisper("[key_name(src)] : ([get_lang_name(speaking)]) [message]") + log_whisper("[key_name(src)] : [msg.raw_message]") else - log_say("[key_name(src)] : ([get_lang_name(speaking)]) [message]") + log_say("[key_name(src)] : [msg.raw_message]") return TRUE @@ -404,12 +417,12 @@ var/list/channel_to_radio_key = new for(var/client/C in show_to) C.images -= I -/mob/living/proc/say_signlang(var/message, var/verb="gestures", var/datum/language/language, var/list/sign_adv_length) - log_say("[key_name(src)] : ([get_lang_name(language)]) [message]") +/mob/living/proc/say_signlang(datum/say_message/msg) + log_say("[key_name(src)] : ([get_lang_name(msg.single_language)]) [msg.raw_message]") - for (var/mob/O in viewers(src, null)) - O.hear_signlang(message, verb, language, src, sign_adv_length) - return 1 + for(var/mob/O in viewers(src, null)) + O.hear_message(msg) + return TRUE /obj/effect/speech_bubble var/mob/parent diff --git a/code/modules/mob/living/silicon/pai/say.dm b/code/modules/mob/living/silicon/pai/say.dm index 4fce1281981..09de779f4b4 100644 --- a/code/modules/mob/living/silicon/pai/say.dm +++ b/code/modules/mob/living/silicon/pai/say.dm @@ -1,4 +1,4 @@ -/mob/living/silicon/pai/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/silicon/pai/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(silence_time) to_chat(src, "Communication circuits remain uninitialized.") return diff --git a/code/modules/mob/living/silicon/say.dm b/code/modules/mob/living/silicon/say.dm index f8a1e22c218..1fb8d277195 100644 --- a/code/modules/mob/living/silicon/say.dm +++ b/code/modules/mob/living/silicon/say.dm @@ -14,9 +14,11 @@ . = TRUE message = Gibberish(message, damaged - 10) -/mob/living/silicon/robot/handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper) - if(message_mode == "whisper" && !whisper) - whisper(message, speaking, say_verb = TRUE) +/mob/living/silicon/robot/handle_message_mode(datum/say_message/msg, datum/language/primary, list/used_radios) + var/text = msg.to_string() + var/message_mode = msg.message_mode + if(message_mode == "whisper" && !msg.whisper) + whisper(text, primary, say_verb = TRUE, msg = msg) return TRUE if(message_mode) if(!is_component_functioning("radio")) @@ -24,37 +26,40 @@ return 0 if(message_mode == "general") message_mode = null - log_say("[key_name(src)] : [message]") - return common_radio.talk_into(src, message, message_mode, verb, speaking) + log_say("[key_name(src)] : [text]") + return common_radio.talk_into(src, text, message_mode, msg.verb, primary, say_message = msg) /mob/living/silicon/robot/drone/handle_message_mode() return null -/mob/living/silicon/ai/handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper) - if(message_mode == "whisper" && !whisper) - whisper(message, speaking, say_verb = TRUE) +/mob/living/silicon/ai/handle_message_mode(datum/say_message/msg, datum/language/primary, list/used_radios) + var/text = msg.to_string() + var/message_mode = msg.message_mode + if(message_mode == "whisper" && !msg.whisper) + whisper(text, primary, say_verb = TRUE, msg = msg) return TRUE if(message_mode == "department") - log_say("[key_name(src)] : [message]") - return holopad_talk(message, verb, speaking) + return holopad_talk(msg) else if(message_mode) if(ai_radio.disabledAi || ai_restore_power_routine || stat) to_chat(src, SPAN_DANGER("System Error - Transceiver Disabled.")) return FALSE if(message_mode == "general") message_mode = null - log_say("[key_name(src)] : [message]") - return ai_radio.talk_into(src, message, message_mode, verb, speaking) + log_say("[key_name(src)] : [text]") + return ai_radio.talk_into(src, text, message_mode, msg.verb, primary, say_message = msg) -/mob/living/silicon/pai/handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper) +/mob/living/silicon/pai/handle_message_mode(datum/say_message/msg, datum/language/primary, list/used_radios) + var/text = msg.to_string() + var/message_mode = msg.message_mode if(message_mode) - if(message_mode == "whisper" && !whisper) - whisper(message, speaking, say_verb = TRUE) + if(message_mode == "whisper" && !msg.whisper) + whisper(text, primary, say_verb = TRUE, msg = msg) return TRUE if(message_mode == "general") message_mode = null - log_say("[key_name(src)] : [message]") - return radio.talk_into(src, message, message_mode, verb, speaking) + log_say("[key_name(src)] : [text]") + return radio.talk_into(src, text, message_mode, msg.verb, primary, say_message = msg) /mob/living/silicon/say_quote(var/text, var/datum/language/speaking = null, var/singing = FALSE, var/whisper = FALSE) if(singing) @@ -93,19 +98,15 @@ return ..() //For holopads only. Usable by AI. -/mob/living/silicon/ai/proc/holopad_talk(var/message, verb, datum/language/speaking) - log_say("[key_name(src)] : [message]") - message = trim(message) - if(!message) +/mob/living/silicon/ai/proc/holopad_talk(datum/say_message/msg) + log_say("[key_name(src)] : [msg.raw_message]") + if(!length(msg.to_string())) return var/obj/structure/machinery/hologram/holopad/H = src.holo if(H?.active_holograms[src])//If there is a hologram and its master is the user. - // AI can hear their own message, this formats it for them. - if(speaking) - to_chat(src, "Holopad transmitted, [real_name] [speaking.format_message(message, verb)]") - else - to_chat(src, "Holopad transmitted, [real_name] [verb], \"[message]\"") + // AI hears its own message. text_for keeps each language. + to_chat(src, "Holopad transmitted, [real_name] [msg.verb], \"[msg.text_for(src)]\"") //This is so pAI's and people inside lockers/boxes,etc can hear the AI Holopad, the alternative being recursion through contents. //This is much faster. @@ -129,13 +130,12 @@ hearturfs += get_turf(O) listening_obj |= O - for(var/mob/M in GLOB.player_list) if(M.stat == DEAD && M.client?.prefs.toggles & CHAT_GHOSTEARS) - M.hear_say(message, verb, speaking, null, null, src) + M.hear_message(msg) continue if(M.loc && (get_turf(M) in hearturfs)) - M.hear_say(message, verb, speaking, null, null, src) + M.hear_message(msg) else to_chat(src, SPAN_WARNING("No holopad connected.")) return FALSE diff --git a/code/modules/mob/living/simple_animal/borer/borer_captive.dm b/code/modules/mob/living/simple_animal/borer/borer_captive.dm index 367d8694529..776692eecf0 100644 --- a/code/modules/mob/living/simple_animal/borer/borer_captive.dm +++ b/code/modules/mob/living/simple_animal/borer/borer_captive.dm @@ -6,23 +6,23 @@ var/datum/progressbar/resist_bar var/resist_start_time = 0 -/mob/living/captive_brain/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/captive_brain/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(istype(src.loc,/mob/living/simple_animal/borer)) - if(!message) + if(!text) return - log_say("[key_name(src)] : [message]") + log_say("[key_name(src)] : [text]") if(stat == DEAD) - return say_dead(message) + return say_dead(text) var/mob/living/simple_animal/borer/B = src.loc - to_chat(src, "You whisper silently, \"[message]\"") - to_chat(B.host, "The captive mind of [src] whispers, \"[message]\"") + to_chat(src, "You whisper silently, \"[text]\"") + to_chat(B.host, "The captive mind of [src] whispers, \"[text]\"") for(var/mob/M in GLOB.player_list) if(istype(M, /mob/abstract/new_player)) continue else if(M.stat == DEAD && M.client.prefs.toggles & CHAT_GHOSTEARS) - to_chat(M, "The captive mind of [src] whispers, \"[message]\"") + to_chat(M, "The captive mind of [src] whispers, \"[text]\"") /mob/living/captive_brain/Destroy() QDEL_NULL(resist_bar) diff --git a/code/modules/mob/living/simple_animal/borer/say.dm b/code/modules/mob/living/simple_animal/borer/say.dm index d345f6c82e2..dcee8303dcb 100644 --- a/code/modules/mob/living/simple_animal/borer/say.dm +++ b/code/modules/mob/living/simple_animal/borer/say.dm @@ -1,23 +1,23 @@ -/mob/living/simple_animal/borer/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) - message = capitalize(message) +/mob/living/simple_animal/borer/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) + text = capitalize(text) - if(!message) + if(!text) return if(stat == DEAD) - return say_dead(message) + return say_dead(text) if(stat) return - message = formalize_text(message) + text = formalize_text(text) - if(copytext(message, 1, 2) == "*") - return emote(copytext(message, 2)) + if(copytext(text, 1, 2) == "*") + return emote(copytext(text, 2)) - var/datum/language/L = parse_language(message) + var/datum/language/L = parse_language(text) if(L?.flags & HIVEMIND) - L.broadcast(src, trim(copytext(message,3)), src.truename) + L.broadcast(src, trim(copytext(text,3)), src.truename) return if(!host) @@ -30,14 +30,14 @@ to_chat(src, SPAN_NOTICE("There are no viable hosts to speak with.")) else var/mob/living/carbon/human/chosen_sayer = pick(viable_sayers) - log_say("[key_name(src)] : (forcing [key_name(chosen_sayer)]) [message]") - chosen_sayer.say(message) + log_say("[key_name(src)] : (forcing [key_name(chosen_sayer)]) [text]") + chosen_sayer.say(text) return - to_chat(src, "You drop words into [host]'s mind: \"[message]\"") - to_chat(host, "Your own thoughts speak: \"[message]\"") - log_say("[key_name(src)] : (borer whisper -> [key_name(host)]) [message]") + to_chat(src, "You drop words into [host]'s mind: \"[text]\"") + to_chat(host, "Your own thoughts speak: \"[text]\"") + log_say("[key_name(src)] : (borer whisper -> [key_name(host)]) [text]") for(var/mob/M in GLOB.mob_list) if(M.client && M.stat == DEAD && !isnewplayer(M) && (M.client.prefs.toggles & CHAT_GHOSTEARS)) - to_chat(M, "[src.truename] whispers to [host], \"[message]\"") + to_chat(M, "[src.truename] whispers to [host], \"[text]\"") diff --git a/code/modules/mob/living/simple_animal/friendly/spiderbot.dm b/code/modules/mob/living/simple_animal/friendly/spiderbot.dm index fee86ea65df..1fbb14f411f 100644 --- a/code/modules/mob/living/simple_animal/friendly/spiderbot.dm +++ b/code/modules/mob/living/simple_animal/friendly/spiderbot.dm @@ -343,22 +343,24 @@ /mob/living/simple_animal/spiderbot/get_bullet_impact_effect_type(var/def_zone) return BULLET_IMPACT_METAL -/mob/living/simple_animal/spiderbot/handle_message_mode(message_mode, message, verb, speaking, used_radios, alt_name, whisper) +/mob/living/simple_animal/spiderbot/handle_message_mode(datum/say_message/msg, datum/language/primary, list/used_radios) + var/text = msg.to_string() + var/message_mode = msg.message_mode switch(message_mode) if("whisper") - if(!whisper) - whisper(message, speaking, say_verb = TRUE) + if(!msg.whisper) + whisper(text, primary, say_verb = TRUE, msg = msg) return TRUE if("headset") - radio.talk_into(src, message, null, verb, speaking) + radio.talk_into(src, text, null, msg.verb, primary, say_message = msg) used_radios += radio if("intercom") var/turf/T = get_turf(src) for(var/obj/item/radio/intercom/I in view(1, T)) - I.talk_into(src, message, null, verb, speaking) + I.talk_into(src, text, null, msg.verb, primary, say_message = msg) used_radios += I if(message_mode) - radio.talk_into(src, message, message_mode, verb, speaking) + radio.talk_into(src, text, message_mode, msg.verb, primary, say_message = msg) used_radios += radio /mob/living/simple_animal/spiderbot/proc/control_integrated_radio() diff --git a/code/modules/mob/living/simple_animal/hostile/commanded/commanded.dm b/code/modules/mob/living/simple_animal/hostile/commanded/commanded.dm index b83e0ce0f70..b279bdf53cd 100644 --- a/code/modules/mob/living/simple_animal/hostile/commanded/commanded.dm +++ b/code/modules/mob/living/simple_animal/hostile/commanded/commanded.dm @@ -24,17 +24,10 @@ ABSTRACT_TYPE(/mob/living/simple_animal/hostile/commanded) if(!short_name) short_name = name -/mob/living/simple_animal/hostile/commanded/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "", var/italics = 0, var/mob/speaker = null, var/sound/speech_sound, var/sound_vol) - if(thinking_enabled && !stat && ((speaker in friends) || speaker == master)) - command_buffer.Add(speaker) - command_buffer.Add(lowertext(html_decode(message))) - return 0 - -/mob/living/simple_animal/hostile/commanded/hear_radio(var/message, var/verb="says", var/datum/language/language=null, var/part_a, var/part_b, var/part_c, var/mob/speaker = null, var/hard_to_hear = 0) - if(thinking_enabled && !stat && ((speaker in friends) || speaker == master)) - command_buffer.Add(speaker) - command_buffer.Add(lowertext(html_decode(message))) - return 0 +/mob/living/simple_animal/hostile/commanded/react_to_message(datum/say_message/msg) + if(thinking_enabled && !stat && ((msg.speaker in friends) || msg.speaker == master)) + command_buffer.Add(msg.speaker) + command_buffer.Add(lowertext(html_decode(msg.to_string()))) /mob/living/simple_animal/hostile/commanded/can_name(var/mob/living/M) if(master && (M != master)) diff --git a/code/modules/mob/living/simple_animal/hostile/toy/mech.dm b/code/modules/mob/living/simple_animal/hostile/toy/mech.dm index 26282399d89..86bbf2aabb4 100644 --- a/code/modules/mob/living/simple_animal/hostile/toy/mech.dm +++ b/code/modules/mob/living/simple_animal/hostile/toy/mech.dm @@ -80,10 +80,9 @@ return return ..() -/mob/living/simple_animal/hostile/mech/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "", var/italics = 0, var/mob/speaker = null, var/sound/speech_sound, var/sound_vol) +/mob/living/simple_animal/hostile/mech/react_to_message(datum/say_message/msg) if(!seeking_targets) - addtimer(CALLBACK(src, PROC_REF(handle_hear_say), speaker, message), 0.5 SECONDS) - return ..() + addtimer(CALLBACK(src, PROC_REF(handle_hear_say), msg.speaker, msg.to_string()), 0.5 SECONDS) /mob/living/simple_animal/hostile/mech/proc/handle_hear_say(var/mob/speaker, var/text) if(seeking_targets) diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index c08243d1235..cd8c9028535 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -681,7 +681,7 @@ to_chat(src, SPAN_WARNING("There is no perch nearby to sit on.")) return -/mob/living/simple_animal/parrot/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/simple_animal/parrot/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(stat) return @@ -691,40 +691,35 @@ var/message_mode="" - if(copytext(message,1,2) == ";") + if(copytext(text,1,2) == ";") message_mode = "headset" - message = copytext(message,2) + text = copytext(text,2) - if(length(message) >= 2) - var/channel_prefix = copytext(message, 1 ,3) + if(length(text) >= 2) + var/channel_prefix = copytext(text, 1 ,3) message_mode = department_radio_keys[channel_prefix] - if(copytext(message,1,2) == ":") + if(copytext(text,1,2) == ":") var/positioncut = 3 - message = trim(copytext(message,positioncut)) + text = trim(copytext(text,positioncut)) - message = capitalize(trim_left(message)) + text = capitalize(trim_left(text)) if(message_mode) if(message_mode in radiochannels) if(ears && istype(ears,/obj/item/radio)) - ears.talk_into(src,sanitize(message), message_mode, verb, null) + ears.talk_into(src,sanitize(text), message_mode, verb, null) - ..(message) + ..(text) -/mob/living/simple_animal/parrot/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "",var/italics = 0, var/mob/speaker = null) +/mob/living/simple_animal/parrot/react_to_message(datum/say_message/msg) if(prob(50)) - parrot_hear(message) - ..(message,verb,language,alt_name,italics,speaker) - - - -/mob/living/simple_animal/parrot/hear_radio(var/message, var/verb="says", var/datum/language/language=null, var/part_a, var/part_b, var/part_c, var/mob/speaker = null, var/hard_to_hear = 0) - if(prob(50)) - parrot_hear("[pick(available_channels)] [message]") - ..(message,verb,language,part_a,part_b,speaker,hard_to_hear) + if(msg.say_mode == SAYMODE_RADIO) + parrot_hear("[pick(available_channels)] [msg.to_string()]") + else + parrot_hear(msg.to_string()) /mob/living/simple_animal/parrot/proc/parrot_hear(var/message="") diff --git a/code/modules/mob/living/simple_animal/shade.dm b/code/modules/mob/living/simple_animal/shade.dm index c6565006f98..e146f938856 100644 --- a/code/modules/mob/living/simple_animal/shade.dm +++ b/code/modules/mob/living/simple_animal/shade.dm @@ -195,20 +195,19 @@ to_chat(L, SPAN_NOTICE("[pick(nagging_doubts)]")) possession_heard_message = 1 -/mob/living/simple_animal/shade/bluespace/hear_say(var/message, var/verb = "says", var/datum/language/language = null, var/alt_name = "", var/italics = 0, var/mob/speaker = null, var/sound/speech_sound, var/sound_vol) - ..() - if(speaker && speaker != src && !possessive) - last_message_heard = message +/mob/living/simple_animal/shade/bluespace/react_to_message(datum/say_message/msg) + if(msg.speaker && msg.speaker != src && !possessive) + last_message_heard = msg.to_string() message_countdown = min(300, message_countdown + 35) adjustCloneLoss(-2) if(heard_dying_message) heard_dying_message = 0 to_chat(src, SPAN_NOTICE("The soothing echoes of life reinvigorate you.")) -/mob/living/simple_animal/shade/bluespace/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/simple_animal/shade/bluespace/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(!possessive) var/list/words_in_memory = dd_text2List(last_message_heard, " ") - var/list/words_in_message = dd_text2List(message, " ") + var/list/words_in_message = dd_text2List(text, " ") for(var/word1 in words_in_message) var/valid = 0 for(var/word2 in words_in_memory) @@ -218,8 +217,8 @@ valid = 1 break if(!valid) - message = replacetext(message, word1, pick(words_in_memory)) - message = slur(message,15) + text = replacetext(text, word1, pick(words_in_memory)) + text = slur(text,15) ..() else to_chat(src, SPAN_WARNING("You cannot muster a voice when possessing another!")) diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index b1f58f52965..bae3ce58457 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -876,7 +876,7 @@ /mob/living/simple_animal/proc/reset_sound_time() sound_time = TRUE -/mob/living/simple_animal/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/simple_animal/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(speak_emote.len) verb = pick(speak_emote) @@ -887,7 +887,7 @@ make_noise(sound_chance) var/can_ghosts_hear = client ? GHOSTS_ALL_HEAR : ONLY_GHOSTS_IN_VIEW - ..(message, speaking, verb, ghost_hearing = can_ghosts_hear) + ..(text, speaking, verb, ghost_hearing = can_ghosts_hear) /mob/living/simple_animal/get_speech_ending(verb, var/ending) return verb diff --git a/code/modules/mob/living/whisper.dm b/code/modules/mob/living/whisper.dm index cc5e294edf2..39900bb2d7c 100644 --- a/code/modules/mob/living/whisper.dm +++ b/code/modules/mob/living/whisper.dm @@ -14,7 +14,7 @@ whisper(message) -/mob/living/whisper(var/message, var/datum/language/speaking, var/is_singing = FALSE, var/say_verb = FALSE) +/mob/living/whisper(var/text, var/datum/language/speaking, var/is_singing = FALSE, var/say_verb = FALSE, var/datum/say_message/msg = null) if(is_muzzled()) to_chat(src, SPAN_DANGER("You're muzzled and cannot speak!")) return @@ -23,49 +23,90 @@ to_chat(src, SPAN_WARNING("You try to speak, but nothing comes out!")) return - var/had_speaking = !!speaking - speaking = speaking ? speaking : parse_language(message) + // A reroute from say() hands us its already-built say_message. + // Otherwise, build one. + var/from_say = !isnull(msg) + if(!msg) + var/datum/language/forced = (speaking && !speaking.always_parse_language) ? speaking : null + msg = build_say_message(text, forced) - if(!had_speaking) - if(speaking) - message = copytext_char(message, 2 + length(speaking.key)) - else - speaking = get_default_language() + if(!length(msg.to_string())) + return - if(length_char(message) >= 1 && copytext_char(message, 1, 2) == "%") - if(speaking?.sing_verb) - is_singing = TRUE + var/datum/language/primary = msg.segments[1].language - var/message_range = 1 - var/eavesdropping_range = 2 - var/watching_range = 5 + if(!from_say) + if(copytext(msg.segments[1].text, 1, 2) == "%") + msg.segments[1].text = copytext(msg.segments[1].text, 2) + if(primary?.sing_verb) + msg.singing = TRUE + msg.sing_note = pick("\u2669", "\u266A", "\u266B") + if(is_singing && primary?.sing_verb) + msg.singing = TRUE + + // Hivemind has no distance. It always broadcasts. + if(msg.single_language?.flags & HIVEMIND) + if(msg.single_language.name == LANGUAGE_VAURCA && within_jamming_range(src)) + to_chat(src, SPAN_WARNING("Your head buzzes as your message is blocked with jamming signals.")) + return + msg.single_language.broadcast(src, trim(msg.to_string())) + return TRUE var/whisper_text = "whispers" var/not_heard = "[whisper_text] something" - if(speaking) - if(speaking.whisper_verb) - whisper_text = pick(speaking.whisper_verb) + if(primary) + if(primary.whisper_verb) + whisper_text = pick(primary.whisper_verb) not_heard = "[whisper_text] something" else var/adverb = pick("quietly", "softly") - var/speak_text = pick(speaking.speech_verb) - if(is_singing && speaking?.sing_verb) - speak_text = pick(speaking.sing_verb) + var/speak_text = pick(primary.speech_verb) + if(msg.singing && primary.sing_verb) + speak_text = pick(primary.sing_verb) whisper_text = "[speak_text] [adverb]" not_heard = "[speak_text] something [adverb]" - //Check to see who hears the full whisper message, and who just gets the not_heard message + msg.whisper = TRUE + msg.say_mode = SAYMODE_SPOKEN + msg.verb = whisper_text + msg.italics = TRUE + msg.message_range = 1 + msg.alt_name = "" + + if(!from_say && !finalize_say_message(msg, say_verb)) + return + + var/list/handle_v = handle_speech_sound() + msg.speech_sound = handle_v[1] + msg.sound_vol = handle_v[2] + if(handle_v[3]) + msg.italics = TRUE + msg.font_size = get_font_size_modifier() + + // Sign language is visual and whole-message. It routes separately. + if(msg.single_language?.flags & SIGNLANG) + msg.say_mode = SAYMODE_SIGN + msg.verb = pick(msg.single_language.signlang_verb) + return say_signlang(msg) + + var/eavesdropping_range = 2 + var/watching_range = 5 + + // Inner ring is everybody who clearly hears the whisper. + var/list/inner = get_hearers_in_view(msg.message_range, src) + inner = mergelists(inner, get_intent_listeners(src, msg.message_range + 1), TRUE) + + // Outer ring is for those further away. var/list/eavesdropping = list() var/list/watching = list() var/list/observers = list() - var/list/all_in_range = hearers(watching_range, src) - for(var/mob/M as anything in all_in_range) - // If they got sensitive hearing enabled, increase the whisper hearing range by one + for(var/mob/M as anything in hearers(watching_range, src)) + if(M in inner) + continue var/sensitive = astype(M, /mob/living/carbon/human)?.is_listening() ? 1 : 0 - //Preventing duplicate messages to ghostear'd observers if(M.stat == DEAD && M.client && (M.client.prefs.toggles & CHAT_GHOSTEARS)) continue - if(get_dist(src, M) <= (message_range + sensitive)) //In range to hear + if(get_dist(src, M) <= (msg.message_range + sensitive)) continue else if(get_dist(src, M) <= (eavesdropping_range + sensitive)) if(M.stat == DEAD && M.client) @@ -78,18 +119,43 @@ else watching += M - if(length(observers)) //For ghosts who do NOT have ghost ears. They will see the whole message if nearby, no *s or not_heard messages. - for(var/mob/M in observers) - M.hear_say(message, "whispers", speaking, "", TRUE, src) + // Ghost ears hear all speech. They hear it from any distance. + if(client) + for(var/mob/player_mob in GLOB.player_list) + if(!player_mob || player_mob.stat != DEAD || (player_mob in inner)) + continue + if(player_mob.client?.prefs.toggles & CHAT_GHOSTEARS) + inner |= player_mob + msg.base_clarity = CLARITY_CLEAR + var/list/hear_clients = deliver_say_message(msg, inner) + + // Nearby ghosts without ghost ears hear it clearly. + for(var/mob/M in observers) + M.hear_message(msg) + + // Eavesdroppers can only hear the message faintly. if(length(eavesdropping)) - var/new_message = stars(message) + var/datum/say_message/faint = msg.copy() + faint.base_clarity = CLARITY_FAINT for(var/mob/M in eavesdropping) - M.hear_say(new_message, "whispers", speaking, "", TRUE, src) + M.hear_message(faint) if(length(watching)) var/rendered = "[src.name] [not_heard]." - for (var/mob/M in watching) + for(var/mob/M in watching) M.show_message(rendered, 2) - say(message, speaking, whisper_text, whisper = TRUE, skip_edit = say_verb) + show_speech_bubble(msg, msg.to_string(), hear_clients) + langchat_say_message(msg, get_hearers_in_view(msg.message_range, src), additional_styles = list("langchat_italic")) + + if(!(msg.single_language && (msg.single_language.flags & PASSLISTENOBJ))) + for(var/obj/O as anything in inner) + if(O) + INVOKE_ASYNC(O, TYPE_PROC_REF(/obj, hear_talk), src, msg.to_string(), msg.verb, primary) + + if(mind) + mind.last_words = msg.to_string() + + log_whisper("[key_name(src)] : [msg.raw_message]") + return TRUE diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm index 29457b9d15b..f888c7b4574 100644 --- a/code/modules/mob/say.dm +++ b/code/modules/mob/say.dm @@ -1,4 +1,4 @@ -/mob/proc/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) +/mob/proc/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE) return ///what clients use to speak. when you type a message into the chat bar in say mode, this is the first thing that goes off serverside. @@ -88,7 +88,7 @@ /* ***Deprecated*** - let this be handled at the hear_say or hear_radio proc + let this be handled at the hear_message proc This is left in for robot speaking when humans gain binary channel access until I get around to rewriting robot_talk() proc. There is no language handling build into it however there is at the /mob level so we accept the call @@ -187,3 +187,84 @@ return L else return null + +/// Splits a message into per-language /datum/say_segment, switching language at each spoken prefix. +/// A forced language takes the whole message with no prefix parsing. +/mob/living/proc/build_say_message(message, datum/language/forced_language) + RETURN_TYPE(/datum/say_message) + var/datum/say_message/say_message = new + say_message.speaker = src + say_message.raw_message = message + + if(forced_language) + say_message.collapse_to(forced_language, message) + return say_message + + var/datum/language/current = get_default_language() + if(copytext(message, 1, 2) == "!") + current = GLOB.all_languages[LANGUAGE_NOISE] + message = trim_left(copytext(message, 2)) + + var/regex/trigger = get_language_trigger_regex() + trigger.next = 1 + + var/segment_start = 1 + var/list/datum/say_segment/segments = list() + + while(trigger.Find(message)) + var/lead = trigger.group[1] //empty at the start of the message, otherwise the whitespace before the prefix + var/prefix_pos = trigger.index + length(lead) + var/key_pos = prefix_pos + 1 + + var/key_len = 2 + var/datum/language/found = resolve_language_key(copytext(message, key_pos, key_pos + 2)) + if(!found) + key_len = 1 + found = resolve_language_key(copytext(message, key_pos, key_pos + 1)) + if(!found) + continue //not a language we can speak, leave it as literal text and keep scanning + + var/before = copytext(message, segment_start, prefix_pos) + + var/after = key_pos + key_len + if(copytext(message, after, after + 1) == " ") + after++ + + // If LANG_NO_MULTILANG is present, it must be first. If first, + // set the whole message to that language. If not, skip it. + if(found.flags & LANG_NO_MULTILANG) + if(!length(segments) && !length(before)) + say_message.collapse_to(found, copytext(message, after)) + return say_message + continue + + segments += new /datum/say_segment(before, current) + segment_start = after + current = found + trigger.next = after + + segments += new /datum/say_segment(copytext(message, segment_start), current) + say_message.segments = clean_segments(segments) + say_message.single_language = (length(say_message.segments) == 1) ? say_message.segments[1].language : null + return say_message + +/// Returns the language for a prefix key if we can speak it, otherwise null. +/mob/proc/resolve_language_key(key) + RETURN_TYPE(/datum/language) + if(!key) + return null + var/datum/language/found = GLOB.language_keys[lowertext(key)] + return (istype(found) && can_speak(found)) ? found : null + +/// Drops empty segments and merges adjacent ones sharing a language. +/proc/clean_segments(list/datum/say_segment/segments) + var/list/datum/say_segment/cleaned = list() + for(var/datum/say_segment/segment as anything in segments) + if(!length(segment.text)) + continue + var/datum/say_segment/last = cleaned.len ? cleaned[cleaned.len] : null + if(last && last.language == segment.language) + last.text += segment.text + else + cleaned += segment + return cleaned diff --git a/code/modules/mob/say_message/hear_message.dm b/code/modules/mob/say_message/hear_message.dm new file mode 100644 index 00000000000..fa837fd8d55 --- /dev/null +++ b/code/modules/mob/say_message/hear_message.dm @@ -0,0 +1,291 @@ +/// Resolves how clearly this listener perceives the message. +/mob/proc/get_clarity(datum/say_message/msg) + var/mob/speaker = msg.speaker + + if(msg.say_mode == SAYMODE_SIGN) + if(isghost(src)) + return msg.base_clarity + if((src.sdisabilities & BLIND) || src.blinded || !speaker || !(speaker in view(src))) + return CLARITY_NONE + return msg.base_clarity + + if(msg.say_mode != SAYMODE_RADIO && !(msg.single_language?.flags & PRESSUREPROOF) && !isghost(src)) + var/turf/T = get_turf(src) + if(T) + var/datum/gas_mixture/air = T.return_air() + var/pressure = SAFE_XGM_PRESSURE(air) + if(pressure < SOUND_MINIMUM_PRESSURE && get_dist(speaker, src) > 1) + if(get_dist(speaker, src) <= 4 && !msg.italics) + to_chat(src, SPAN_NOTICE("[speaker?.name] talks, but you're in a vacuum. Maybe if you were close enough for the sound to transmit through touch...")) + return CLARITY_NONE + + // vr_mob is a Psionic mob in the dream. They hear clearly even when not conscious. + if(!vr_mob && (sleeping || stat == UNCONSCIOUS)) + return CLARITY_DROWSY + + if(isdeaf(src)) + // INNATE is an audible emote. Gives no 'cannot hear' message. + if(!(msg.single_language?.flags & INNATE)) + if(msg.say_mode == SAYMODE_RADIO) + if(prob(20)) + to_chat(src, SPAN_WARNING("You feel your headset vibrate but can hear nothing from it!")) + else if(speaker == src) + to_chat(src, SPAN_WARNING("You cannot hear yourself speak!")) + else + to_chat(src, "[speaker?.name][msg.alt_name] talks but you cannot hear them.") + return CLARITY_NONE + return msg.base_clarity + + return msg.base_clarity + +/// Whether a message can be shown to this mob at all. +/mob/proc/has_chat_sink() + return client || vr_mob + +/mob/living/carbon/has_chat_sink() + . = ..() + if(.) + return TRUE + var/datum/dionastats/DS = get_dionastats() + return DS?.nym ? TRUE : FALSE + +/mob/living/carbon/alien/diona/has_chat_sink() + . = ..() + if(.) + return TRUE + return (detached && gestalt) ? TRUE : FALSE + +/// Builds the correct envelope around the body depending on SAYMODE. +/mob/proc/format_envelope(datum/say_message/msg, muffled = FALSE, clarity = CLARITY_CLEAR) + switch(msg.say_mode) + if(SAYMODE_SIGN) + return format_envelope_sign(msg, msg.text_for(src, clarity)) + if(SAYMODE_RADIO) + return format_envelope_radio(msg, clarity) + else + return format_envelope_spoken(msg, clarity, muffled) + +/// Wraps a say envelope around the message body. +/mob/proc/format_envelope_spoken(datum/say_message/msg, clarity = CLARITY_CLEAR, muffled = FALSE) + var/list/rendered = msg.render_body(src, clarity) + var/body = rendered[1] + if(!length(body)) + return "" + var/emote_led = rendered[2] + + var/mob/speaker = msg.speaker + var/speaker_name = speaker?.name + if(ishuman(speaker)) + var/mob/living/carbon/human/H = speaker + speaker_name = H.GetVoice() + + var/datum/language/accent_language + for(var/datum/say_segment/segment as anything in msg.segments) + if(segment.language?.allow_accents) + accent_language = segment.language + break + var/accent_icon = accent_language ? speaker?.get_accent_icon(accent_language, src) : null + + if(msg.italics || muffled) + body = "[body]" + + var/track = null + if(isghost(src)) + if(speaker_name != speaker.real_name && speaker.real_name) + speaker_name = "[speaker.real_name] ([speaker_name])" + track = "[ghost_follow_link(speaker, src)] " + if((client.prefs.toggles & CHAT_GHOSTEARS) && (get_turf(speaker) in view(src))) + body = "[body]" + + var/leading = emote_led ? "" : "[msg.verb], " + var/font_open = msg.font_size ? "" : "" + var/font_close = msg.font_size ? "" : "" + return "[track][accent_icon ? accent_icon + " " : ""][font_open][speaker_name][msg.alt_name] [leading][body][font_close]" + +/// Wraps a radio envelope around the message body. +/mob/proc/format_envelope_radio(datum/say_message/msg, clarity = CLARITY_CLEAR) + var/mob/speaker = msg.speaker + var/hard_to_hear = (msg.base_clarity == CLARITY_FAINT) + var/list/radio_parts = msg.radio_parts + var/part_a = LAZYACCESS(radio_parts, 1) + var/part_b = LAZYACCESS(radio_parts, 2) + var/part_c = LAZYACCESS(radio_parts, 3) + + var/speaker_name = speaker ? speaker.name : "Unknown" + if(ishuman(speaker)) + speaker_name = speaker.GetVoice() + if(hard_to_hear) + speaker_name = "Unknown" + + var/datum/language/language = msg.single_language + var/accent_icon = speaker?.get_accent_icon(language, src) + accent_icon = accent_icon ? accent_icon + " " : "" + part_a = replacetext(part_a, "%ACCENT%", accent_icon) + + var/track = null + if(istype(src, /mob/living/silicon/ai) && !hard_to_hear) + var/changed_voice + var/jobname + var/mob/living/carbon/human/impersonating + + if(ishuman(speaker)) + var/mob/living/carbon/human/H = speaker + if(H.wear_mask && istype(H.wear_mask, /obj/item/clothing/mask/gas/voice)) + changed_voice = 1 + var/mob/living/carbon/human/I + for(var/mob/living/carbon/human/M in GLOB.mob_list) + if(M.real_name == speaker_name) + I = M + break + if(I && !(I.name != speaker_name && I.wear_id && istype(I.wear_id, /obj/item/card/id/syndicate))) + impersonating = I + jobname = impersonating.get_assignment() + else + jobname = "Unknown" + else + jobname = H.get_assignment() + else if(iscarbon(speaker)) + jobname = "No id" + else if(isAI(speaker)) + jobname = "AI" + else if(isrobot(speaker)) + jobname = "Cyborg" + else if(istype(speaker, /mob/living/silicon/pai)) + jobname = "Personal AI" + else + jobname = "Unknown" + + if(changed_voice) + if(impersonating) + track = "[speaker_name] ([jobname])" + else + track = "[speaker_name] ([jobname])" + else + track = "[speaker_name] ([jobname])" + + if(isghost(src)) + if(speaker != null) + if(speaker_name != speaker.real_name && !isAI(speaker)) + speaker_name = "[speaker.real_name] ([speaker_name])" + track = "[ghost_follow_link(speaker, src)] " + + var/list/rendered = msg.render_body(src, clarity) + var/body = rendered[1] + if(!length(body)) + return "" + var/leading = rendered[2] ? "" : "[msg.verb], " + var/formatted = "[leading][body][part_c]" + + if(istype(src, /mob/living/silicon/ai) && track && !hard_to_hear) + return "[part_a][track][part_b][formatted]" + if(isghost(src)) + return "[track][part_a][speaker_name][part_b][formatted]" + return "[part_a][speaker_name][part_b][formatted]" + +/mob/proc/format_envelope_sign(datum/say_message/msg, body) + var/mob/speaker = msg.speaker + if(say_understands(speaker, msg.single_language)) + return "[speaker] [msg.verb], \"[body]\"" + var/list/sign_adv_length = msg.single_language?.sign_adv_length + if(length(sign_adv_length) <= 4) + sign_adv_length = list(" briefly", " a short message", " a message", " a lengthy message", " a very lengthy message") + var/adverb + var/length = length(body) * pick(0.8, 0.9, 1.0, 1.1, 1.2) // a little fuzziness + switch(length) + if(0 to 12) adverb = sign_adv_length[1] + if(12 to 30) adverb = sign_adv_length[2] + if(30 to 48) adverb = sign_adv_length[3] + if(48 to 90) adverb = sign_adv_length[4] + else adverb = sign_adv_length[5] + return "[speaker] [msg.verb][adverb]." + +/// Handles a mob receiving a say. +/mob/proc/hear_message(datum/say_message/msg) + var/clarity = get_clarity(msg) + if(clarity == CLARITY_NONE) + return FALSE + . = FALSE + if(has_chat_sink()) + display_message(msg, clarity) + . = TRUE + react_to_message(msg) + +/// Noop by default. Overriden by clientless mobs that react to speech. +/mob/proc/react_to_message(datum/say_message/msg) + return + +/// Renders the body for this listener, then decorates and outputs it. +/mob/proc/display_message(datum/say_message/msg, clarity) + var/mob/speaker = msg.speaker + // Ghosts skip clientless out-of-view chatter. + if(msg.say_mode != SAYMODE_RADIO && isghost(src) && speaker && !speaker.client && (client?.prefs.toggles & CHAT_GHOSTEARS) && !(speaker in view(src))) + return + + if(clarity == CLARITY_DROWSY) + var/drowsy = msg.text_for(src, clarity) + if(length(drowsy)) + on_hear_message(drowsy) + return + + // In thin air, non-pressureproof languages are muffled. + var/muffled = FALSE + var/sound_vol = msg.sound_vol + if(msg.say_mode != SAYMODE_RADIO && !isghost(src) && !(msg.single_language?.flags & PRESSUREPROOF)) + var/turf/T = get_turf(src) + if(T) + var/datum/gas_mixture/air = T.return_air() + if(SAFE_XGM_PRESSURE(air) < ONE_ATMOSPHERE * 0.4) + muffled = TRUE + sound_vol *= 0.5 + + var/envelope = format_envelope(msg, muffled, clarity) + if(!length(envelope)) + return + on_hear_message(envelope) + // This is a special case for internal mobs like cortical borers. + // They see through the eyes of their host so we must relay the message. + if(msg.say_mode == SAYMODE_SIGN && (status_flags & PASSEMOTES)) + for(var/obj/item/holder/H in contents) + H.show_message(envelope) + for(var/mob/living/M in contents) + M.show_message(envelope) + if(msg.say_mode != SAYMODE_RADIO) + play_speech_sound(msg, sound_vol) + +/// Plays the speech sound for a given message. +/mob/proc/play_speech_sound(datum/say_message/msg, sound_vol) + var/mob/speaker = msg.speaker + if(msg.speech_sound && (get_dist(speaker, src) <= world.view && src.z == speaker?.z)) + var/turf/source = speaker ? get_turf(speaker) : get_turf(src) + playsound(source, msg.speech_sound, sound_vol, vary = TRUE) + +/proc/say_timestamp() + return "\[[worldtime2text()]\]" + +/// The output sink for a given message. +/mob/proc/on_hear_message(message) + to_chat(src, message) + if(vr_mob) + to_chat(vr_mob, message) + +/// Dionae require special treatment. Messages should be forwarded to the nymph. +/mob/living/carbon/on_hear_message(message) + ..() + var/datum/dionastats/DS = get_dionastats() + if(DS?.nym) + var/mob/living/carbon/alien/diona/D = DS.nym.resolve() + if(D) + to_chat(D, message) + +/// A detached gestalt should be forwarded heard messages. +/mob/living/carbon/alien/diona/on_hear_message(message) + to_chat(src, message) + if(detached && gestalt) + to_chat(gestalt, message) + +/// Silicons receive a timestamp with heard messages. +/mob/living/silicon/on_hear_message(message) + var/time = say_timestamp() + to_chat(src, "[time] [message]") + if(vr_mob) + to_chat(vr_mob, "[time] [message]") diff --git a/code/modules/mob/say_message/say_message.dm b/code/modules/mob/say_message/say_message.dm new file mode 100644 index 00000000000..2bbf3421d37 --- /dev/null +++ b/code/modules/mob/say_message/say_message.dm @@ -0,0 +1,187 @@ +/// A complete audible message made of a list of /datum/say_segment. +/datum/say_message + /// The mob saying the message. + var/mob/speaker + + /// Name shown instead of the speaker's real name, if any. + var/alt_name = "" + + /// The raw string that was typed. Used in the admin log. + var/raw_message + + /// The list of segments that make up the message body. + var/list/datum/say_segment/segments = list() + + /// The sole language of this message, or null. + var/datum/language/single_language + + /// Which verb to use. Determined by punctuation and language of the first piece. + var/verb = "says" + + /// Is the message being whispered? + var/whisper = FALSE + + /// Is the message being sung? + var/singing = FALSE + + /// The musical note bracketing a sung message. + var/sing_note + + /// Should the message render as italic? + var/italics = FALSE + + /// Optional font size override for the rendered message. + var/font_size + + /// Optional range limit for hearing the message. + var/message_range + + /// The requested mode for this message. + var/message_mode + + /// Sound played to listeners alongside the message, if any. + var/sound/speech_sound + + /// Volume of speech_sound. + var/sound_vol + + /// How the message is reaching the listener. + var/say_mode = SAYMODE_SPOKEN + + /// Initial clarity. Most of the time this is clear. Sometimes the radio will degrade it. + var/base_clarity = CLARITY_CLEAR + + /// Radio envelope parts. This must live here for performance reasons pending a telecomms rewrite. + var/list/radio_parts + +/// Renders each segment as the listener perceives it. Returns list(text, is_emote) per segment. +/datum/say_message/proc/render_pieces(mob/listener, clarity = CLARITY_CLEAR) + var/list/pieces = list() + var/cap_next = TRUE + for(var/datum/say_segment/segment as anything in segments) + var/is_emote = (segment.language?.flags & INNATE) ? TRUE : FALSE + var/rendered = segment.plain_text_for(listener, speaker) + if(clarity == CLARITY_FAINT && !is_emote) + rendered = length(rendered) ? stars(rendered) : "" + rendered = trim(rendered) // Strip whitespace for quoting. We'll re-add it later. + if(!length(rendered)) + continue + if(is_emote) + cap_next = TRUE // emote re-arms caps for the next spoken run + else if(cap_next) + rendered = capitalize(rendered) + cap_next = FALSE + if(segment.language) + rendered = segment.language.colourize(rendered) + pieces += list(list(rendered, is_emote)) + return pieces + +/// Quotes the given text if it's not an emote. +/proc/quote_run(text, emote) + text = trim(text) + if(!length(text)) + return "" + return emote ? text : "\"[text]\"" + +/// Renders the body. Speech is quoted. Returns list(body, leads_with_emote). +/datum/say_message/proc/render_body(mob/listener, clarity = CLARITY_CLEAR) + var/list/pieces = render_pieces(listener, clarity) + if(!length(pieces)) + return list("", FALSE) + + var/leads_with_emote = pieces[1][2] + var/list/runs = list() + var/list/cur = list() + var/cur_emote = pieces[1][2] + for(var/list/piece in pieces) + if(length(cur) && piece[2] != cur_emote) + var/q = quote_run(jointext(cur, " "), cur_emote) + if(length(q)) + runs += q + cur = list() + cur_emote = piece[2] + cur += piece[1] + var/last = quote_run(jointext(cur, " "), cur_emote) + if(length(last)) + runs += last + + var/body = listener.hallucinate_heard(jointext(runs, " "), speaker) + if(singing && length(body)) + body = "[sing_note] [body] [sing_note]" + return list(body, leads_with_emote) + +/// Returns the complete message as the given listener perceives it. +/datum/say_message/proc/text_for(mob/listener, clarity = CLARITY_CLEAR) + if(clarity == CLARITY_DROWSY) + return drowsy_text_for(listener) + + var/list/out = list() + for(var/list/piece in render_pieces(listener, clarity)) + out += piece[1] + if(!length(out)) + return "" + var/body = listener.hallucinate_heard(jointext(out, ""), speaker) + if(singing && length(body)) + body = "[sing_note] [body] [sing_note]" + return body + +/// Returns the message as a bare perceived string. +/datum/say_message/proc/plain_text_for(mob/listener) + var/list/plain = list() + for(var/datum/say_segment/segment as anything in segments) + plain += segment.plain_text_for(listener, speaker) + return jointext(plain, "") + +/// Returns the message as decorated text for drowsy listeners. +/datum/say_message/proc/drowsy_text_for(mob/listener) + if(isdeaf(listener)) + return "" + if(!prob(15)) + return "...You almost hear someone talking..." + + var/list/messages = text2list(plain_text_for(listener), " ") + if(!length(messages)) + return "...You almost hear someone talking..." + + var/list/punctuation = list(",", "!", ".", ";", "?") + var/heardword = messages[rand(1, messages.len)] + if(copytext(heardword, 1, 1) in punctuation) + heardword = copytext(heardword, 2) + if(copytext(heardword, -1) in punctuation) + heardword = copytext(heardword, 1, length(heardword)) + return "...You hear something about...[heardword]" + +/// Returns a flat string with languages stripped out. +/datum/say_message/proc/to_string() + var/list/out = list() + for(var/datum/say_segment/segment as anything in segments) + out += segment.text + return jointext(out, "") + +/// Collapses the whole message into a single segment in the given language. +/datum/say_message/proc/collapse_to(datum/language/language, text) + single_language = language + segments = list(new /datum/say_segment(text, language)) + +/// Returns a shallow copy. +/datum/say_message/proc/copy() + var/datum/say_message/copy = new + copy.speaker = speaker + copy.alt_name = alt_name + copy.raw_message = raw_message + copy.segments = segments + copy.single_language = single_language + copy.verb = verb + copy.whisper = whisper + copy.singing = singing + copy.sing_note = sing_note + copy.italics = italics + copy.font_size = font_size + copy.message_range = message_range + copy.message_mode = message_mode + copy.speech_sound = speech_sound + copy.sound_vol = sound_vol + copy.say_mode = say_mode + copy.base_clarity = base_clarity + copy.radio_parts = radio_parts + return copy diff --git a/code/modules/mob/say_message/say_segment.dm b/code/modules/mob/say_message/say_segment.dm new file mode 100644 index 00000000000..7635e053464 --- /dev/null +++ b/code/modules/mob/say_message/say_segment.dm @@ -0,0 +1,25 @@ +/// A single contiguous segment of spoken text in a single language. +/datum/say_segment + /// The segment's text. + var/text + + /// The language this segment is spoken in. + var/datum/language/language + +/// Constructs a new segment of spoken text in the given language. +/datum/say_segment/New(text, datum/language/language) + . = ..() + src.text = text + src.language = language + +/// Returns the segment as the given listener perceives it. +/datum/say_segment/proc/plain_text_for(mob/listener, mob/speaker) + if(language && (language.flags & KNOWONLYHEAR) && !listener.say_understands(speaker, language)) + return "" + var/rendered = text + if(language && (language.flags & NONVERBAL)) + if((!speaker || (listener.sdisabilities & BLIND) || listener.blinded || !(speaker in view(listener))) && !isghost(listener)) + rendered = stars(rendered) + if(!(language && (language.flags & INNATE)) && !listener.say_understands(speaker, language)) + rendered = language ? language.scramble(rendered, listener.languages) : stars(rendered) + return rendered diff --git a/code/modules/shareddream/brainghost.dm b/code/modules/shareddream/brainghost.dm index 26eea0f9602..6e831839049 100644 --- a/code/modules/shareddream/brainghost.dm +++ b/code/modules/shareddream/brainghost.dm @@ -80,11 +80,11 @@ awaken_impl(TRUE) body.handle_shared_dreaming(TRUE) -/mob/living/brain_ghost/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) +/mob/living/brain_ghost/say(var/text, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/ghost_hearing = GHOSTS_ALL_HEAR, var/whisper = FALSE, var/skip_edit = FALSE) if(!istype(body) || body.stat!=UNCONSCIOUS) return if(prob(20)) // 1/5 chance to mumble out anything you say in the dream. - var/list/words = text2list(replacetext(message, "\[^a-zA-Z]*$", ""), " ") + var/list/words = text2list(replacetext(text, "\[^a-zA-Z]*$", ""), " ") var/word_count = rand(1, words.len) // How many words to mumble out from within the sentance var/words_start = rand(1, words.len - (word_count - 1)) // Where the chunk of said words should start. @@ -97,7 +97,7 @@ body.say(mumble_message) // Mumble in Nral Malic body.set_stat(UNCONSCIOUS) // Toggled before anything else can happen. Ideally. - ..(message, speaking, verb="says", alt_name="") + ..(text, speaking, verb="says", alt_name="") /mob/living/brain_ghost/get_floating_chat_y_offset() diff --git a/code/unit_tests/mob_tests.dm b/code/unit_tests/mob_tests.dm index 1420dd455a8..ea46246e9b5 100644 --- a/code/unit_tests/mob_tests.dm +++ b/code/unit_tests/mob_tests.dm @@ -18,11 +18,14 @@ /mob/living/test var/heard = FALSE -/mob/living/test/on_hear_say(var/message) - . = ..(message) +// Clientless test mobs still need to reach the output sink to record a hearing. +/mob/living/test/has_chat_sink() + return TRUE + +/mob/living/test/on_hear_message(message) + ..() if(message) heard = TRUE - return . /datum/unit_test/mob_hear name = "MOB: Living mobs test for mob's speech" diff --git a/html/changelogs/lewis-saycode.yml b/html/changelogs/lewis-saycode.yml new file mode 100644 index 00000000000..308c68b7623 --- /dev/null +++ b/html/changelogs/lewis-saycode.yml @@ -0,0 +1,13 @@ +author: Llywelwyn + +delete-after: True + +changes: + - rscadd: "Adds code-switching: you can now speak in multiple languages in the same message." + - rscadd: "Adds audible emotes to the language list. They can be triggered with ,e." + - rscdel: "Removed SSrunechat." + - refactor: "Rewrote langchat in order to support multiple languages and partial comprehension." + - refactor: "Rewrote a vast majority of all saycode and the code responsible for displaying saytext to clients." + - bugfix: "Sleeping mobs are no longer able to understand all languages." + - bugfix: "Langchat now correctly shows the appropriate comprehension for all viewers rather than all viewers sharing the comprehension of the last viewer." + - bugfix: "Languages which are supposed to be invisible when not understood no longer appear as scrambled overhead text." diff --git a/maps/away/away_site/quarantined_outpost/quarantined_outpost_holopads.dm b/maps/away/away_site/quarantined_outpost/quarantined_outpost_holopads.dm index 396456db382..e64ee875507 100644 --- a/maps/away/away_site/quarantined_outpost/quarantined_outpost_holopads.dm +++ b/maps/away/away_site/quarantined_outpost/quarantined_outpost_holopads.dm @@ -122,9 +122,13 @@ sleep(loop_sleep_time) continue - hologram.langchat_speech("[I]", get_hearers_in_view(7, src), skip_language_check = TRUE) + hologram.langchat_speech("[I]", get_hearers_in_view(7, src)) + var/datum/say_message/msg = new + msg.speaker = holo_icon + msg.verb = things_to_say[I] + msg.collapse_to(GLOB.all_languages[language], "[I]") for(var/mob/living/L in get_hearers_in_view(7, src)) - L.hear_say("[I]", "[things_to_say[I]]", GLOB.all_languages[language], speaker = holo_icon) + L.hear_message(msg) if(soundblock) playsound(get_turf(src), text2path(soundblock), 60, FALSE) sleep(loop_sleep_time) diff --git a/maps/away/away_site/quarantined_outpost/quarantined_outpost_objects.dm b/maps/away/away_site/quarantined_outpost/quarantined_outpost_objects.dm index 6f942e5f955..e5be5e30c97 100644 --- a/maps/away/away_site/quarantined_outpost/quarantined_outpost_objects.dm +++ b/maps/away/away_site/quarantined_outpost/quarantined_outpost_objects.dm @@ -248,9 +248,13 @@ GLOBAL_LIST_EMPTY(trackables_pool) "AAaargh!" ) var/chosen_sentence = pick(mimic_sentences) - langchat_speech("[chosen_sentence]", get_hearers_in_view(7, src), skip_language_check = TRUE) + langchat_speech("[chosen_sentence]", get_hearers_in_view(7, src)) + var/datum/say_message/msg = new + msg.speaker = src + msg.verb = "says" + msg.collapse_to(GLOB.all_languages[language], "[chosen_sentence]") for(var/mob/living/L in get_hearers_in_view(7, src)) - L.hear_say("[chosen_sentence]", "says", GLOB.all_languages[language], speaker = src) + L.hear_message(msg) /mob/living/simple_animal/hostile/revivable/abomination/on_attack_mob(var/mob/hit_mob, var/obj/item/organ/external/limb) . = ..()