/// Resolves how clearly this listener perceives the message. /mob/proc/get_clarity(datum/say_message/msg) var/mob/speaker = msg.speaker if(msg.say_mode == SAYMODE_SIGN) if(isghost(src)) return msg.base_clarity if((src.sdisabilities & BLIND) || src.blinded || !speaker || !(speaker in view(src))) return CLARITY_NONE return msg.base_clarity if(msg.say_mode != SAYMODE_RADIO && !(msg.single_language?.flags & PRESSUREPROOF) && !isghost(src)) var/turf/T = get_turf(src) if(T) var/datum/gas_mixture/air = T.return_air() var/pressure = SAFE_XGM_PRESSURE(air) if(pressure < SOUND_MINIMUM_PRESSURE && get_dist(speaker, src) > 1) if(get_dist(speaker, src) <= 4 && !msg.italics) to_chat(src, SPAN_NOTICE("[speaker?.name] talks, but you're in a vacuum. Maybe if you were close enough for the sound to transmit through touch...")) return CLARITY_NONE // vr_mob is a Psionic mob in the dream. They hear clearly even when not conscious. if(!vr_mob && (sleeping || stat == UNCONSCIOUS)) return CLARITY_DROWSY if(isdeaf(src)) // INNATE is an audible emote. Gives no 'cannot hear' message. if(!(msg.single_language?.flags & INNATE)) if(msg.say_mode == SAYMODE_RADIO) if(prob(20)) to_chat(src, SPAN_WARNING("You feel your headset vibrate but can hear nothing from it!")) else if(speaker == src) to_chat(src, SPAN_WARNING("You cannot hear yourself speak!")) else to_chat(src, "[speaker?.name][msg.alt_name] talks but you cannot hear them.") return CLARITY_NONE return msg.base_clarity return msg.base_clarity /// Whether a message can be shown to this mob at all. /mob/proc/has_chat_sink() return client || vr_mob /mob/living/carbon/has_chat_sink() . = ..() if(.) return TRUE var/datum/dionastats/DS = get_dionastats() return DS?.nym ? TRUE : FALSE /mob/living/carbon/alien/diona/has_chat_sink() . = ..() if(.) return TRUE return (detached && gestalt) ? TRUE : FALSE /// Builds the correct envelope around the body depending on SAYMODE. /mob/proc/format_envelope(datum/say_message/msg, muffled = FALSE, clarity = CLARITY_CLEAR) switch(msg.say_mode) if(SAYMODE_SIGN) return format_envelope_sign(msg, msg.text_for(src, clarity)) if(SAYMODE_RADIO) return format_envelope_radio(msg, clarity) else return format_envelope_spoken(msg, clarity, muffled) /// Wraps a say envelope around the message body. /mob/proc/format_envelope_spoken(datum/say_message/msg, clarity = CLARITY_CLEAR, muffled = FALSE) var/list/rendered = msg.render_body(src, clarity) var/body = rendered[1] if(!length(body)) return "" var/emote_led = rendered[2] var/mob/speaker = msg.speaker var/speaker_name = speaker?.name if(ishuman(speaker)) var/mob/living/carbon/human/H = speaker speaker_name = H.GetVoice() var/datum/language/accent_language for(var/datum/say_segment/segment as anything in msg.segments) if(segment.language?.allow_accents) accent_language = segment.language break var/accent_icon = accent_language ? speaker?.get_accent_icon(accent_language, src) : null if(msg.italics || muffled) body = "[body]" var/track = null if(isghost(src)) if(speaker_name != speaker.real_name && speaker.real_name) speaker_name = "[speaker.real_name] ([speaker_name])" track = "[ghost_follow_link(speaker, src)] " if((client.prefs.toggles & CHAT_GHOSTEARS) && (get_turf(speaker) in view(src))) body = "[body]" var/leading = emote_led ? "" : "[msg.verb], " var/font_open = msg.font_size ? "" : "" var/font_close = msg.font_size ? "" : "" return "[track][accent_icon ? accent_icon + " " : ""][font_open][speaker_name][msg.alt_name] [leading][body][font_close]" /// Wraps a radio envelope around the message body. /mob/proc/format_envelope_radio(datum/say_message/msg, clarity = CLARITY_CLEAR) var/mob/speaker = msg.speaker var/hard_to_hear = (msg.base_clarity == CLARITY_FAINT) var/list/radio_parts = msg.radio_parts var/part_a = LAZYACCESS(radio_parts, 1) var/part_b = LAZYACCESS(radio_parts, 2) var/part_c = LAZYACCESS(radio_parts, 3) var/speaker_name = speaker ? speaker.name : "Unknown" if(ishuman(speaker)) speaker_name = speaker.GetVoice() if(hard_to_hear) speaker_name = "Unknown" var/datum/language/language = msg.single_language var/accent_icon = speaker?.get_accent_icon(language, src) accent_icon = accent_icon ? accent_icon + " " : "" part_a = replacetext(part_a, "%ACCENT%", accent_icon) var/track = null if(istype(src, /mob/living/silicon/ai) && !hard_to_hear) var/changed_voice var/jobname var/mob/living/carbon/human/impersonating if(ishuman(speaker)) var/mob/living/carbon/human/H = speaker if(H.wear_mask && istype(H.wear_mask, /obj/item/clothing/mask/gas/voice)) changed_voice = 1 var/mob/living/carbon/human/I for(var/mob/living/carbon/human/M in GLOB.mob_list) if(M.real_name == speaker_name) I = M break if(I && !(I.name != speaker_name && I.wear_id && istype(I.wear_id, /obj/item/card/id/syndicate))) impersonating = I jobname = impersonating.get_assignment() else jobname = "Unknown" else jobname = H.get_assignment() else if(iscarbon(speaker)) jobname = "No id" else if(isAI(speaker)) jobname = "AI" else if(isrobot(speaker)) jobname = "Cyborg" else if(istype(speaker, /mob/living/silicon/pai)) jobname = "Personal AI" else jobname = "Unknown" if(changed_voice) if(impersonating) track = "[speaker_name] ([jobname])" else track = "[speaker_name] ([jobname])" else track = "[speaker_name] ([jobname])" if(isghost(src)) if(speaker != null) if(speaker_name != speaker.real_name && !isAI(speaker)) speaker_name = "[speaker.real_name] ([speaker_name])" track = "[ghost_follow_link(speaker, src)] " var/list/rendered = msg.render_body(src, clarity) var/body = rendered[1] if(!length(body)) return "" var/leading = rendered[2] ? "" : "[msg.verb], " var/formatted = "[leading][body][part_c]" if(istype(src, /mob/living/silicon/ai) && track && !hard_to_hear) return "[part_a][track][part_b][formatted]" if(isghost(src)) return "[track][part_a][speaker_name][part_b][formatted]" return "[part_a][speaker_name][part_b][formatted]" /mob/proc/format_envelope_sign(datum/say_message/msg, body) var/mob/speaker = msg.speaker if(say_understands(speaker, msg.single_language)) return "[speaker] [msg.verb], \"[body]\"" var/list/sign_adv_length = msg.single_language?.sign_adv_length if(length(sign_adv_length) <= 4) sign_adv_length = list(" briefly", " a short message", " a message", " a lengthy message", " a very lengthy message") var/adverb var/length = length(body) * pick(0.8, 0.9, 1.0, 1.1, 1.2) // a little fuzziness switch(length) if(0 to 12) adverb = sign_adv_length[1] if(12 to 30) adverb = sign_adv_length[2] if(30 to 48) adverb = sign_adv_length[3] if(48 to 90) adverb = sign_adv_length[4] else adverb = sign_adv_length[5] return "[speaker] [msg.verb][adverb]." /// Handles a mob receiving a say. /mob/proc/hear_message(datum/say_message/msg) var/clarity = get_clarity(msg) if(clarity == CLARITY_NONE) return FALSE . = FALSE if(has_chat_sink()) display_message(msg, clarity) . = TRUE react_to_message(msg) /// Noop by default. Overriden by clientless mobs that react to speech. /mob/proc/react_to_message(datum/say_message/msg) return /// Renders the body for this listener, then decorates and outputs it. /mob/proc/display_message(datum/say_message/msg, clarity) var/mob/speaker = msg.speaker // Ghosts skip clientless out-of-view chatter. if(msg.say_mode != SAYMODE_RADIO && isghost(src) && speaker && !speaker.client && (client?.prefs.toggles & CHAT_GHOSTEARS) && !(speaker in view(src))) return if(clarity == CLARITY_DROWSY) var/drowsy = msg.text_for(src, clarity) if(length(drowsy)) on_hear_message(drowsy) return // In thin air, non-pressureproof languages are muffled. var/muffled = FALSE var/sound_vol = msg.sound_vol if(msg.say_mode != SAYMODE_RADIO && !isghost(src) && !(msg.single_language?.flags & PRESSUREPROOF)) var/turf/T = get_turf(src) if(T) var/datum/gas_mixture/air = T.return_air() if(SAFE_XGM_PRESSURE(air) < ONE_ATMOSPHERE * 0.4) muffled = TRUE sound_vol *= 0.5 var/envelope = format_envelope(msg, muffled, clarity) if(!length(envelope)) return on_hear_message(envelope) // This is a special case for internal mobs like cortical borers. // They see through the eyes of their host so we must relay the message. if(msg.say_mode == SAYMODE_SIGN && (status_flags & PASSEMOTES)) for(var/obj/item/holder/H in contents) H.show_message(envelope) for(var/mob/living/M in contents) M.show_message(envelope) if(msg.say_mode != SAYMODE_RADIO) play_speech_sound(msg, sound_vol) /// Plays the speech sound for a given message. /mob/proc/play_speech_sound(datum/say_message/msg, sound_vol) var/mob/speaker = msg.speaker if(msg.speech_sound && (get_dist(speaker, src) <= world.view && src.z == speaker?.z)) var/turf/source = speaker ? get_turf(speaker) : get_turf(src) playsound(source, msg.speech_sound, sound_vol, vary = TRUE) /proc/say_timestamp() return "\[[worldtime2text()]\]" /// The output sink for a given message. /mob/proc/on_hear_message(message) to_chat(src, message) if(vr_mob) to_chat(vr_mob, message) /// Dionae require special treatment. Messages should be forwarded to the nymph. /mob/living/carbon/on_hear_message(message) ..() var/datum/dionastats/DS = get_dionastats() if(DS?.nym) var/mob/living/carbon/alien/diona/D = DS.nym.resolve() if(D) to_chat(D, message) /// A detached gestalt should be forwarded heard messages. /mob/living/carbon/alien/diona/on_hear_message(message) to_chat(src, message) if(detached && gestalt) to_chat(gestalt, message) /// Silicons receive a timestamp with heard messages. /mob/living/silicon/on_hear_message(message) var/time = say_timestamp() to_chat(src, "[time] [message]") if(vr_mob) to_chat(vr_mob, "[time] [message]")