diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm index ba5fb447a16..6067760574d 100644 --- a/code/__DEFINES/radio.dm +++ b/code/__DEFINES/radio.dm @@ -169,3 +169,7 @@ #define RADIO_SPECIAL_CENTCOM (1<<1) ///Bitflag for if a headset can use the binary radio channel #define RADIO_SPECIAL_BINARY (1<<2) + +/// Past this amount of compression, the resulting gibberish will actually +/// replace characters, making it even harder to understand. +#define COMPRESSION_REPLACE_CHARACTER_THRESHOLD 30 diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm index fd248f76408..214c1477aea 100644 --- a/code/__HELPERS/spatial_info.dm +++ b/code/__HELPERS/spatial_info.dm @@ -208,24 +208,26 @@ . -= target break +/** + * Returns a list of mobs who are in hearing range of every radio in the list of radios given + */ /proc/get_hearers_in_radio_ranges(list/obj/item/radio/radios) . = list() // Returns a list of mobs who can hear any of the radios given in @radios for(var/obj/item/radio/radio as anything in radios) . |= get_hearers_in_LOS(radio.canhear_range, radio) -/proc/get_hearers_in_radio_ranges_track_radios(list/obj/item/radio/radios, frequency) +/** + * Returns a list of mobs who can hear any of the radios given in the given radio list, indexed by the radio. + * More expensive than get_hearers_in_radio_ranges() + */ +/proc/get_hearers_in_radio_ranges_track_radios(list/obj/item/radio/radios) . = list() - .[TTS_GHOST_RADIO] = list() - // Returns a list of mobs who can hear any of the radios given in @radios, indexed by the radio. More expensive, but needed for radio TTS to sound good. + // Returns a list of mobs who can hear any of the radios given in @radios, indexed by the radio. More expensive than get_hearers_in_radio_ranges() for(var/obj/item/radio/radio as anything in radios) var/list/possible_hearers = get_hearers_in_LOS(radio.canhear_range, radio) - var/list/weakref_hearers = list() - for(var/ref in possible_hearers) - weakref_hearers += WEAKREF(ref) - if(LAZYLEN(possible_hearers)) - .[WEAKREF(radio)] = filter_tts_listeners(weakref_hearers, frequency) - + if(length(possible_hearers)) + .[radio] = possible_hearers /// A filter to be applied to get_hearers_in_x, that removes any non-mob hearers, converting them to their relevant mob if one exists (such as dullahan heads). /// Modifies input list. diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index 8620727d2b4..d3a268f6995 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -1,3 +1,6 @@ +#define TTS_REQUEST_REF "ref" +#define TTS_REQUEST_EXPIRE "expiry_time" + SUBSYSTEM_DEF(tts) name = "Text To Speech" wait = 0.05 SECONDS @@ -276,10 +279,8 @@ SUBSYSTEM_DEF(tts) var/datum/http_response/radio_blips_response = current_request.request_blips_radio.into_response() var/datum/http_response/radio_gibberish_response = current_request.request_radio_gibberish.into_response() if(current_request.requests_errored()) - if(queued_radio_messages[identifier]) - queued_radio_messages.Remove(identifier) - if(queued_radio_messages_compression[identifier]) - queued_radio_messages_compression.Remove(identifier) + queued_radio_messages -= identifier + queued_radio_messages_compression -= identifier current_request.timed_out = TRUE log_tts("TTS HTTP request errored | Normal: [normal_response.error] | Blips: [blips_response.error] | Radio: [radio_response.error] | Radio Blips: [radio_blips_response.error] | Radio Gibberish [radio_gibberish_response.error]", list( "normal" = normal_response, @@ -299,8 +300,6 @@ SUBSYSTEM_DEF(tts) current_request.audio_length_blips_radio = text2num(radio_blips_response.headers["audio-length"]) * 10 || 0 if(length(radio_gibberish_response.headers) && radio_gibberish_response.headers.Find("audio-length")) current_request.audio_length_radio_gibberish = text2num(radio_gibberish_response.headers["audio-length"]) * 10 - if(!current_request.audio_length_radio_gibberish) - current_request.audio_length_radio_gibberish = 0 current_request.audio_file = "tmp/tts/[identifier].ogg" current_request.audio_file_blips = "tmp/tts/[identifier]_blips.ogg" // We aren't as concerned about the audio length for blips as we are with actual speech current_request.audio_file_radio = "tmp/tts/[identifier]_radio.ogg" @@ -334,21 +333,17 @@ SUBSYSTEM_DEF(tts) // For example, if a TTS message plays for more than 7 seconds, which is our current timeout limit, // then the next TTS message would be unable to play. - var/timeout_start = current_target.when_to_play - if(!timeout_start) - // In the normal case, we just set timeout to start_time as it means we aren't waiting on - // a TTS message to finish playing - timeout_start = current_target.start_time + // In the normal case, we just set timeout to start_time as it means we aren't waiting on + // a TTS message to finish playing + var/timeout_start = current_target.when_to_play || current_target.start_time var/timeout = timeout_start + message_timeout // Here, we check if the request has timed out or not. // If current_target.timed_out is set to TRUE, it means the request failed in some way // and there is no TTS audio file to play. if(timeout < world.time || current_target.timed_out) - if(queued_radio_messages[current_target.identifier]) - queued_radio_messages.Remove(current_target.identifier) - if(queued_radio_messages_compression[current_target.identifier]) - queued_radio_messages_compression.Remove(current_target.identifier) + queued_radio_messages -= current_target.identifier + queued_radio_messages_compression -= current_target.identifier SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) continue @@ -367,11 +362,28 @@ SUBSYSTEM_DEF(tts) audio_file = new(current_target.audio_file) SEND_SOUND(current_target.target, audio_file) SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) + else if(current_target.when_to_play < world.time) audio_file = new(current_target.audio_file) audio_file_blips = new(current_target.audio_file_blips) - play_tts(tts_target, current_target.listeners, audio_file, audio_file_blips, current_target.language, current_target.message_range, current_target.volume_offset, FALSE, null, current_target.audio_length, current_target.audio_length_blips) - completed_tts_messages[current_target.identifier] = list("ref" = current_target, "expiry_time" = world.time + 300) + play_tts( + target = tts_target, + listeners = current_target.listeners, + audio = audio_file, + audio_blips = audio_file_blips, + language = current_target.language, + range = current_target.message_range, + volume_offset = current_target.volume_offset, + ignore_observers = FALSE, + source_speaker = null, + audio_length = current_target.audio_length, + audio_length_blips = current_target.audio_length_blips + ) + completed_tts_messages[current_target.identifier] = list( + TTS_REQUEST_REF = current_target, + TTS_REQUEST_EXPIRE = world.time + 30 SECONDS, + ) + if(length(data) != 1) var/datum/tts_request/next_target = data[2] next_target.when_to_play = world.time + current_target.audio_length @@ -384,43 +396,50 @@ SUBSYSTEM_DEF(tts) queued_tts_messages[tts_target] += arbritrary_delay SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data) - for(var/identifier in queued_radio_messages) + for(var/identifier, radio_list in queued_radio_messages) if(MC_TICK_CHECK) return - if(completed_tts_messages[identifier]) - var/list/all_radios = queued_radio_messages[identifier] - for(var/radio in all_radios) - var/list/hearers = all_radios[radio] - if(!istext(radio) && isweakref(radio)) - var/datum/weakref/weakref = radio - var/obj/radio_obj = weakref?.resolve() - if(radio_obj && QDELETED(radio_obj)) - queued_radio_messages[identifier].Remove(radio) - queued_radio_messages_compression[identifier].Remove(radio) - continue + if(!completed_tts_messages[identifier]) + continue - var/datum/tts_request/tts_request = completed_tts_messages[identifier]["ref"] - var/sound/audio_file - var/sound/audio_file_blips - if(queued_radio_messages_compression[identifier] > 30) - audio_file = new(tts_request.audio_file_radio_gibberish) - else - audio_file = new(tts_request.audio_file_radio) - audio_file_blips = new(tts_request.audio_file_blips_radio) - play_tts(radio == TTS_GHOST_RADIO ? null : radio, hearers, audio_file, audio_file_blips, tts_request.language, INFINITY, tts_request.volume_offset, ignore_observers = TRUE, source_speaker = tts_request.target, audio_length = tts_request.audio_length_radio, audio_length_blips = tts_request.audio_length_blips_radio, volume_preference = /datum/preference/numeric/volume/sound_tts_radio_volume, volume_signal = COMSIG_MOB_TTS_RADIO_VOLUME_PREFERENCE_APPLIED) - queued_radio_messages.Remove(identifier) - completed_tts_messages.Remove(identifier) - queued_radio_messages_compression.Remove(identifier) + for(var/radio, hearer_list in radio_list) + if(isweakref(radio)) + var/datum/weakref/weakref = radio + var/obj/radio_obj = weakref.resolve() + if(QDELETED(radio_obj)) + queued_radio_messages[identifier] -= radio + continue + + var/datum/tts_request/tts_request = completed_tts_messages[identifier][TTS_REQUEST_REF] + var/sound/audio_file = new( \ + queued_radio_messages_compression[identifier] > COMPRESSION_REPLACE_CHARACTER_THRESHOLD \ + ? tts_request.audio_file_radio_gibberish \ + : tts_request.audio_file_radio \ + ) + var/sound/audio_file_blips = new(tts_request.audio_file_blips_radio) + play_tts( + target = radio == TTS_GHOST_RADIO ? null : radio, + listeners = hearer_list, + audio = audio_file, + audio_blips = audio_file_blips, + language = tts_request.language, + range = INFINITY, + volume_offset = tts_request.volume_offset, + ignore_observers = TRUE, + source_speaker = tts_request.target, + audio_length = tts_request.audio_length_radio, + audio_length_blips = tts_request.audio_length_blips_radio, + volume_preference = /datum/preference/numeric/volume/sound_tts_radio_volume, + volume_signal = COMSIG_MOB_TTS_RADIO_VOLUME_PREFERENCE_APPLIED, + ) + + clear_radio_message(identifier) for(var/identifier, request in completed_tts_messages) - if(MC_TICK_CHECK) - return - if (completed_tts_messages[identifier]["expiry_time"] >= world.time + 300) - completed_tts_messages[identifier]["ref"] = null - completed_tts_messages[identifier] = null - completed_tts_messages.Remove(identifier) - queued_radio_messages.Remove(identifier) - queued_radio_messages_compression.Remove(identifier) + if (world.time < completed_tts_messages[identifier][TTS_REQUEST_EXPIRE]) + continue + + clear_radio_message(identifier) #undef TTS_ARBRITRARY_DELAY @@ -474,6 +493,12 @@ SUBSYSTEM_DEF(tts) else queued_http_messages.insert(current_request) +/datum/controller/subsystem/tts/proc/clear_radio_message(identifier) + completed_tts_messages[identifier] -= TTS_REQUEST_REF // don't stick around + completed_tts_messages -= identifier + queued_radio_messages -= identifier + queued_radio_messages_compression -= identifier + /// Helper to get a random TTS voice for a certain gender. Passing no gender just results in a random voice. /datum/controller/subsystem/tts/proc/random_tts_voice(gender = NEUTER) if(!tts_enabled) @@ -632,32 +657,33 @@ SUBSYSTEM_DEF(tts) else return request.is_complete() && request_blips.is_complete() && request_blips_radio.is_complete() && request_radio.is_complete() && request_radio_gibberish.is_complete() -/proc/filter_tts_listeners(list/listeners, radio_frequency = null) - if(!SStts.tts_enabled || !listeners) - return +#undef TTS_REQUEST_REF +#undef TTS_REQUEST_EXPIRE - if(isweakref(listeners)) - listeners = list(listeners) - var/list/filtered_listeners = list() +/** + * Checks if the passed mob can hear radio TTS + * + * * hearer - The mob to check if they can hear radio TTS + * * radio_frequency - The frequency of the radio TTS message + */ +/proc/can_hear_radio_tts(mob/hearer, radio_frequency) + if(!SStts.tts_enabled) + return FALSE - for(var/datum/weakref/listener as anything in listeners) - if(!isweakref(listener)) - continue - var/mob/possible_listener = listener?.resolve() - if(!ismob(possible_listener) || !possible_listener.client) - continue - var/tts_pref = possible_listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) - var/radio_tts_pref = possible_listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio) - if(tts_pref == TTS_SOUND_OFF) - continue - if(isliving(possible_listener) && (possible_listener.stat >= UNCONSCIOUS || HAS_TRAIT(possible_listener, TRAIT_DEAF))) - continue - if(radio_tts_pref == TTS_SOUND_NO_RADIO) - continue - if(radio_tts_pref == TTS_SOUND_DEPARTMENTAL_RADIO && radio_frequency == FREQ_COMMON) // don't give them the full common firehose if they turned it off - continue - filtered_listeners += listener + if(HAS_TRAIT(hearer, TRAIT_DEAF)) + return FALSE - return filtered_listeners + var/tts_pref = hearer.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) || TTS_SOUND_OFF + if(tts_pref == TTS_SOUND_OFF) + return FALSE + + var/radio_tts_pref = hearer.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio) || TTS_SOUND_NO_RADIO + switch(radio_tts_pref) + if(TTS_SOUND_NO_RADIO) + return FALSE + if(TTS_SOUND_DEPARTMENTAL_RADIO) // don't give them the full common firehose if they turned it off + return radio_frequency != FREQ_COMMON + + return TRUE #undef SHIFT_DATA_ARRAY diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm index fab4b92f8a9..ee1e3413fc1 100644 --- a/code/game/machinery/telecomms/broadcasting.dm +++ b/code/game/machinery/telecomms/broadcasting.dm @@ -106,10 +106,6 @@ copy.levels = levels return copy -/// Past this amount of compression, the resulting gibberish will actually -/// replace characters, making it even harder to understand. -#define COMPRESSION_REPLACE_CHARACTER_THRESHOLD 30 - /// This is the meat function for making radios hear vocal transmissions. /datum/signal/subspace/vocal/broadcast() set waitfor = FALSE @@ -162,30 +158,43 @@ for(var/obj/item/radio/called_radio as anything in radios) called_radio.on_receive_message(data) var/list/message_mods = data["mods"] - // From the list of radios, find all mobs who can hear those. - var/list/receive = get_hearers_in_radio_ranges(radios) - var/list/receive_radios = null - if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) // only do this if we have a TTS identifier to save on perf - receive_radios = get_hearers_in_radio_ranges_track_radios(radios, frequency) + var/tts_radio_id = LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER) + // Flat list of mobs who can hear the message + var/list/receive + // Assoc list of weakref to a radio to list of weakrefs to mobs who can hear the message + var/list/receive_radios + + if(tts_radio_id) // only do this if we have a TTS identifier to save on perf + receive = list() + receive_radios = list() + for(var/radio, radio_hearers in get_hearers_in_radio_ranges_track_radios(radios)) + receive |= radio_hearers + var/datum/weakref/radio_ref = WEAKREF(radio) + for(var/mob/possible_hearer in radio_hearers) + if(!isnull(possible_hearer.client) && can_hear_radio_tts(possible_hearer, frequency)) + receive_radios[radio_ref] ||= list() + receive_radios[radio_ref] += WEAKREF(possible_hearer) + + else + receive = get_hearers_in_radio_ranges(radios) // Add observers who have ghost radio enabled. for(var/mob/dead/observer/ghost in GLOB.player_list) if(get_chat_toggles(ghost.client) & CHAT_GHOSTRADIO) receive |= ghost - if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) - receive_radios[TTS_GHOST_RADIO] |= ghost + if(tts_radio_id && can_hear_radio_tts(ghost, frequency)) + receive_radios[TTS_GHOST_RADIO] ||= list() + receive_radios[TTS_GHOST_RADIO] += WEAKREF(ghost) + + if(tts_radio_id && length(receive_radios)) + SStts.queued_radio_messages[tts_radio_id] = receive_radios + SStts.queued_radio_messages_compression[tts_radio_id] = compression // Render the message and have everybody hear it. // Always call this on the virtualspeaker to avoid issues. var/spans = data["spans"] - if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) - receive_radios[TTS_GHOST_RADIO] = filter_tts_listeners(receive_radios[TTS_GHOST_RADIO], frequency) - for(var/radio in receive_radios) - LAZYSET(SStts.queued_radio_messages[message_mods[MODE_TTS_IDENTIFIER]], radio, receive_radios[radio]) - LAZYSET(SStts.queued_radio_messages_compression, message_mods[MODE_TTS_IDENTIFIER], compression) - for(var/atom/movable/hearer as anything in receive) if(!hearer) stack_trace("null found in the hearers list returned by the spatial grid. this is bad") @@ -215,5 +224,3 @@ log_telecomms("[virt.source] [log_text] [loc_name(get_turf(virt.source))]") QDEL_IN(virt, 5 SECONDS) // Make extra sure the virtualspeaker gets qdeleted - -#undef COMPRESSION_REPLACE_CHARACTER_THRESHOLD diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index c43cef1d2d8..2df607806be 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -366,13 +366,12 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( message = deaf_message - var/show_message_success = show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight) - if(show_message_success && understood) - return HEAR_HEARD | HEAR_UNDERSTOOD - else if (show_message_success && !understood) - return HEAR_HEARD - else - return FALSE + var/hearflags = NONE + if(show_message(message, MSG_VISUAL, deaf_message, deaf_type, avoid_highlight)) + hearflags |= HEAR_HEARD + if(understood) + hearflags |= HEAR_UNDERSTOOD + return hearflags if(speaker != src) if(!radio_freq) //These checks have to be separate, else people talking on the radio will make "You can't hear yourself!" appear when hearing people over the radio while deaf. @@ -391,13 +390,13 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( // Recompose message for AI hrefs, language incomprehension. message = compose_message(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods) - var/show_message_success = show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight) - if(show_message_success && understood) - return HEAR_HEARD | HEAR_UNDERSTOOD - else if (show_message_success && !understood) - return HEAR_HEARD - else - return FALSE + + var/hearflags = NONE + if(show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight)) + hearflags |= HEAR_HEARD + if(understood) + hearflags |= HEAR_UNDERSTOOD + return hearflags /mob/living/send_speech(message_raw, message_range = 6, obj/source = src, bubble_type = bubble_icon, list/spans, datum/language/message_language = null, list/message_mods = list(), forced = null, tts_message, list/tts_filter) var/whisper_range = 0 diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 4d28aee042a..1a6cb5013a2 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -154,11 +154,7 @@ install_default_programs() register_context() update_appearance() - if(mapload) - return INITIALIZE_HINT_LATELOAD - else - if(SStts.tts_enabled) - voice = SStts.computer_voice + return INITIALIZE_HINT_LATELOAD /obj/item/modular_computer/LateInitialize() if(SStts.tts_enabled)