mirror of
https://github.com/Aurorastation/Aurora.3.git
synced 2026-08-27 15:01:04 +01:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = ..()
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]")
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user