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