Multi-Languages: a complete Saycode rewrite (#22637)

Rewrites Saycode and Langchat to add support for multiple languages in
one message, including audible emotes.

<img width="1139" height="338" alt="image"
src="https://github.com/user-attachments/assets/25e26932-7a6e-4c54-ab74-56fffb92ecad"
/>

Here's some samples to explain how to mix languages.

<img width="422" height="26" alt="image"
src="https://github.com/user-attachments/assets/e1b176cc-8625-4dc9-83c8-a053d3f310e6"
/>

`Languages ,2 can be mixed ,3 like this.`

<img width="540" height="21" alt="image"
src="https://github.com/user-attachments/assets/19156c67-4670-4d7a-84d7-26e527de2676"
/>

`!explains, ,2Emotes work too. ,0The text will get auto-quoted.`

<img width="592" height="18" alt="image"
src="https://github.com/user-attachments/assets/cfc31c5c-2383-41c8-82be-b36836339321"
/>

`,3Most languages ,0can be ,2mixed ,1arbitrarily, ,3any number of
,0times.`

<img width="636" height="20" alt="image"
src="https://github.com/user-attachments/assets/388b4f9d-192a-4374-ac31-bbd4e4e5dfe4"
/>

`,2Emotes. ,eAnd he nods. ,3They don't have to come first anymore.`

There are some exceptions. The exceptions are currently anything with
any of the flags `SIGNLANG`, `HIVEMIND`, `PRESSUREPROOF`,
`KNOWONLYHEAR`. Exceptions work the same as current languages do: they
must be the first language in the message. If so, they prevent switching
into any other language mid-message; if they're not first, they just
wont trigger.

They're exceptions currently because there's not really a clean way that
I or the people I asked for help on this one to make them look nice.
`SIGNLANG` for example doesn't scramble text, it shows it's own `
gestures a lengthy message.` text for those that don't understand. Could
we just replace every instance of sign language with that if somebody
doesn't understand? Probably. It would look pretty awful though. e.g.
`Alina Eskelinen says, "Hello." Alina Eskelinen gestures a short
message.`

This definitely needs testmerging because it more or less rewrites the
entire pipeline surrounding `say`. The `say` code itself had to be
rewritten to support the multiple languages, as well as all the existing
plumbing for listeners receiving messages. In return though, it's
significantly more straightforward and hopefully by extension easier for
people to add to in the future.

Primarily, instead of having four different `hear_say`, `hear_radio`,
`hear_sleep`, etc., routes for messages to come through, every single
audible message is received by `hear_message`, which is responsible for
figuring out how clear the message is (is the radio damaged? is it a
whisper we're eavesdropping on?), who needs to receive it in their
chatbox, formatting it correctly for each listener, and finally if any
npc or object within range needs to react to it in some way, like a
parrot or a mech.

changelog:
- 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: "Removes 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."

Forgive me whoever has to review this. Biggest areas that have room for
error is stuff like a borer inside someone's head, and Dionae stuff. Old
langchat had odd exceptions for those and I was forced to rewrite it
entirely, but I think I got it all back to how it was working before.
This commit is contained in:
lew
2026-06-11 10:47:26 +00:00
committed by GitHub
parent a7b05ec4db
commit 11a9a2d644
46 changed files with 1185 additions and 924 deletions
+1 -1
View File
@@ -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)
+8 -12
View File
@@ -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()
@@ -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)
@@ -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)
@@ -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
@@ -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
-313
View File
@@ -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 = "<i>[message]</i>"
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 = "<b>[message]</b>"
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, "<span class='name'>[speaker_name]</span>[alt_name] talks but you cannot hear them.")
return FALSE
else
if(language)
if(font_size)
on_hear_say("[track][accent_icon ? accent_icon + " " : ""]<font size='[font_size]'><span class='game say'><span class='name'>[speaker_name]</span>[alt_name] [language.format_message(message, verb)]</span></font>")
else
on_hear_say("[track][accent_icon ? accent_icon + " " : ""]<span class='game say'><span class='name'>[speaker_name]</span>[alt_name] [language.format_message(message, verb)]</span>")
else
if(font_size)
on_hear_say("[track][accent_icon ? accent_icon + " " : ""]<font size='[font_size]'><span class='game say'><span class='name'>[speaker_name]</span>[alt_name] [verb], <span class='message'><span class='body'>\"[message]\"</span></span></span></font>")
else
on_hear_say("[track][accent_icon ? accent_icon + " " : ""]<span class='game say'><span class='name'>[speaker_name]</span>[alt_name] [verb], <span class='message'><span class='body'>\"[message]\"</span></span></span>")
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 = "<a class='ai_tracking' href='byond://?src=[REF(src)];trackname=[html_encode(speaker_name)];track=[REF(impersonating)]'>[speaker_name] ([jobname])</a>"
else
track = "[speaker_name] ([jobname])"
else
track = "<a class='ai_tracking' href='byond://?src=[REF(src)];trackname=[html_encode(speaker_name)];track=[REF(speaker)]'>[speaker_name] ([jobname])</a>"
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], <span class=\"body\">\"[message]\"</span>"
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 "<span class='say_quote'>\[[worldtime2text()]\]</span>"
/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 = "<B>[speaker]</B> [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 = "<B>[speaker]</B> [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 = "<span class = 'game_say'>...You hear something about...[heardword]</span>"
else
heard = "<span class = 'game_say'>...<i>You almost hear someone talking</i>...</span>"
to_chat(src, heard)
+3 -6
View File
@@ -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", "<span class='[colour]'>[message]</span>")
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 "<span class='[colour]'>[message]</span>"
/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
+20 -3
View File
@@ -202,13 +202,14 @@
return input_size
/datum/language/proc/format_message(message, verb)
return "[verb], <span class='message'><span class='[colour]'>\"[capitalize(message)]\"</span></span>"
return "[verb], <span class='message'>[colourize("\"[capitalize(message)]\"")]</span>"
/datum/language/proc/format_message_plain(message, verb)
return "[verb], \"[capitalize(message)]\""
/datum/language/proc/format_message_radio(message, verb)
return "[verb], <span class='[colour]'>\"[capitalize(message)]\"</span>"
/// Wraps text in this language's colour span.
/datum/language/proc/colourize(message)
return "<span class='[colour]'>[message]</span>"
/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"
+2 -2
View File
@@ -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))
+9 -9
View File
@@ -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)
+15 -13
View File
@@ -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 = ..()
+9 -17
View File
@@ -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())))
+131 -118
View File
@@ -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] <span class='singing'>[message]</span> [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
+1 -1
View File
@@ -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, "<font color=green>Communication circuits remain uninitialized.</font>")
return
+29 -29
View File
@@ -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, "<i><span class='game say'>Holopad transmitted, <span class='name'>[real_name]</span> [speaking.format_message(message, verb)]</span></i>")
else
to_chat(src, "<i><span class='game say'>Holopad transmitted, <span class='name'>[real_name]</span> [verb], <span class='message'><span class='body'>\"[message]\"</span></span></span></i>")
// AI hears its own message. text_for keeps each language.
to_chat(src, "<i><span class='game say'>Holopad transmitted, <span class='name'>[real_name]</span> [msg.verb], <span class='message'><span class='body'>\"[msg.text_for(src)]\"</span></span></span></i>")
//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
@@ -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)
@@ -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, "<b>You drop words into [host]'s mind:</b> \"[message]\"")
to_chat(host, "<b>Your own thoughts speak:</b> \"[message]\"")
log_say("[key_name(src)] : (borer whisper -> [key_name(host)]) [message]")
to_chat(src, "<b>You drop words into [host]'s mind:</b> \"[text]\"")
to_chat(host, "<b>Your own thoughts speak:</b> \"[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, "<b>[src.truename]</b> whispers to <b>[host]</b>, \"[message]\"")
to_chat(M, "<b>[src.truename]</b> whispers to <b>[host]</b>, \"[text]\"")
@@ -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()
@@ -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))
@@ -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)
+15 -20
View File
@@ -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="")
@@ -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!"))
@@ -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
+99 -33
View File
@@ -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 = "<span class='game say'><span class='name'>[src.name]</span> [not_heard].</span>"
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
+83 -2
View File
@@ -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
@@ -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, "<span class='name'>[speaker?.name]</span>[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 = "<i>[body]</i>"
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 = "<b>[body]</b>"
var/leading = emote_led ? "" : "[msg.verb], "
var/font_open = msg.font_size ? "<font size='[msg.font_size]'>" : ""
var/font_close = msg.font_size ? "</font>" : ""
return "[track][accent_icon ? accent_icon + " " : ""][font_open]<span class='game say'><span class='name'>[speaker_name]</span>[msg.alt_name] [leading]<span class='message'>[body]</span></span>[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 = "<a class='ai_tracking' href='byond://?src=[REF(src)];trackname=[html_encode(speaker_name)];track=[REF(impersonating)]'>[speaker_name] ([jobname])</a>"
else
track = "[speaker_name] ([jobname])"
else
track = "<a class='ai_tracking' href='byond://?src=[REF(src)];trackname=[html_encode(speaker_name)];track=[REF(speaker)]'>[speaker_name] ([jobname])</a>"
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]<span class='message'>[body]</span>[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 "<B>[speaker]</B> [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 "<B>[speaker]</B> [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 "<span class='say_quote'>\[[worldtime2text()]\]</span>"
/// 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]")
+187
View File
@@ -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] <span class='singing'>[body]</span> [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] <span class='singing'>[body]</span> [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 "<span class = 'game_say'>...<i>You almost hear someone talking</i>...</span>"
var/list/messages = text2list(plain_text_for(listener), " ")
if(!length(messages))
return "<span class = 'game_say'>...<i>You almost hear someone talking</i>...</span>"
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 "<span class = 'game_say'>...You hear something about...[heardword]</span>"
/// 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
@@ -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
+3 -3
View File
@@ -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()