From d5b366d47af6deac2bfa30c61603be6b3a2cec53 Mon Sep 17 00:00:00 2001 From: CabinetOnFire Date: Tue, 16 Jun 2026 12:06:13 +0200 Subject: [PATCH] adds a playsoundtoken proc that allows one-shot sound tokens playing, and adds support for it to emotes. (#96519) --- code/__DEFINES/mobs.dm | 9 +++++ code/datums/emotes.dm | 34 +++++++++++++---- code/datums/sound_token.dm | 37 +++++++++++++++---- code/game/sound/sound.dm | 9 +++++ code/modules/mob/living/carbon/human/emote.dm | 2 +- code/modules/mob/living/emote.dm | 6 ++- 6 files changed, 79 insertions(+), 18 deletions(-) diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index 0f4a28a1f94..82809492ec0 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -1096,6 +1096,15 @@ GLOBAL_LIST_INIT(layers_to_offset, list( /// Helper macro that determines if the mob is at the threshold to start vomitting due to high toxin levels #define AT_TOXIN_VOMIT_THRESHOLD(mob) (mob.get_tox_loss() > 45 && mob.nutrition > 20) +/// Shared cooldown for manually triggered emote audio +#define MANUAL_GENERAL_EMOTE_AUDIO_COOLDOWN "manual_general_emote_audio_cooldown" +/// Per emote cooldown for manually triggered emote audio +#define MANUAL_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type) "manual_specific_emote_audio_cooldown_[type]" +/// Shared cooldown for forced emote audio +#define FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN "forced_general_emote_audio_cooldown" +/// Per emote cooldown for forced emote audio +#define FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type) "forced_specific_emote_audio_cooldown_[type]" + /// The duration of the flip emote animation #define FLIP_EMOTE_DURATION 0.7 SECONDS ///The duration of a taunt emote, so how long they can deflect projectiles diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm index 2f0b0c030a0..9bdb31c3561 100644 --- a/code/datums/emotes.dm +++ b/code/datums/emotes.dm @@ -61,12 +61,18 @@ var/cooldown = 0.8 SECONDS /// Does this message have a message that can be modified by the user? var/can_message_change = FALSE - /// How long is the shared emote cooldown triggered by this emote? - var/general_emote_audio_cooldown = 2 SECONDS - /// How long is the specific emote cooldown triggered by this emote? - var/specific_emote_audio_cooldown = 5 SECONDS + /// How long is the shared emote cooldown triggered by this emote when used intentionally? + var/manual_general_emote_audio_cooldown = 2 SECONDS + /// How long is the specific emote cooldown triggered by this emote when used intentionally? + var/manual_specific_emote_audio_cooldown = 5 SECONDS + /// How long is the shared emote cooldown triggered by this emote when forced? + var/forced_general_emote_audio_cooldown = 2 SECONDS + /// How long is the specific emote cooldown triggered by this emote when forced? + var/forced_specific_emote_audio_cooldown = 2 SECONDS /// Does this emote's sound ignore walls? var/sound_wall_ignore = FALSE + ///Does this emote use sound tokens? this means it also ignores walls. + var/use_sound_tokens = FALSE /datum/emote/New() switch(mob_type_allowed_typecache) @@ -112,15 +118,27 @@ user.log_message(msg, LOG_EMOTE) var/tmp_sound = get_sound(user) - if(tmp_sound && should_play_sound(user, intentional) && TIMER_COOLDOWN_FINISHED(user, "general_emote_audio_cooldown") && TIMER_COOLDOWN_FINISHED(user, type)) - TIMER_COOLDOWN_START(user, type, specific_emote_audio_cooldown) - TIMER_COOLDOWN_START(user, "general_emote_audio_cooldown", general_emote_audio_cooldown) + if(tmp_sound && should_play_sound(user, intentional)) + if(intentional) + if(!TIMER_COOLDOWN_FINISHED(user, MANUAL_GENERAL_EMOTE_AUDIO_COOLDOWN) || !TIMER_COOLDOWN_FINISHED(user, MANUAL_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type)) || !TIMER_COOLDOWN_FINISHED(user, FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN) || !TIMER_COOLDOWN_FINISHED(user, FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type))) + return FALSE + else + if(!TIMER_COOLDOWN_FINISHED(user, FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN) || !TIMER_COOLDOWN_FINISHED(user, FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type))) + return FALSE + TIMER_COOLDOWN_START(user, MANUAL_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type), manual_specific_emote_audio_cooldown) + TIMER_COOLDOWN_START(user, MANUAL_GENERAL_EMOTE_AUDIO_COOLDOWN, manual_general_emote_audio_cooldown) + TIMER_COOLDOWN_START(user, FORCED_SPECIFIC_EMOTE_AUDIO_COOLDOWN(type), forced_specific_emote_audio_cooldown) + TIMER_COOLDOWN_START(user, FORCED_GENERAL_EMOTE_AUDIO_COOLDOWN, forced_general_emote_audio_cooldown) + var/frequency = null if (affected_by_pitch && SStts.tts_enabled && SStts.pitch_enabled) frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) * (1 + sqrt(abs(user.pitch)) * sign(user.pitch) * EMOTE_TTS_PITCH_MULTIPLIER) else if(vary) frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) - playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency) + if(use_sound_tokens && sound_wall_ignore) + playsoundtoken(source = user, soundin = tmp_sound, range = SOUND_RANGE, volume = 50) + else + playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency) var/is_important = running_emote_type & EMOTE_IMPORTANT diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm index 516d1c272aa..1eedde7c85e 100644 --- a/code/datums/sound_token.dm +++ b/code/datums/sound_token.dm @@ -6,7 +6,8 @@ var/atom/source /// k:v list of mob : sound status var/list/listeners = list() - + ///k:v list of mobs : bool. Used to quickly check whether a mob is allowed to hear this noise. This is null by default which means ANY MOB can hear this. + var/list/allowed_listeners /// Sound maximum range var/range /// Sound volume @@ -29,10 +30,14 @@ var/start_time /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds. var/sound_duration + /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds. + var/sound_duration_override /// Cell tracker managing spatial grid cells within range of the source. The wizards say this is the fastest. var/datum/cell_tracker/cell_tracker + ///Should we destroy the datum when the sound is done? + var/delete_on_end = FALSE -/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE) +/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, _allowed_listeners, _sound_duration_override, _delete_on_end) source = _source RegisterSignal(source, COMSIG_QDELETING, PROC_REF(source_deleted)) RegisterSignal(source, COMSIG_MOVABLE_MOVED, PROC_REF(source_moved)) @@ -42,6 +47,13 @@ volume = _volume falloff_exponent = _falloff_exponent falloff_distance = _falloff_distance + sound_duration_override = _sound_duration_override + if(_delete_on_end) + delete_on_end = _delete_on_end + + if(_allowed_listeners) + for(var/allowed_mob in _allowed_listeners) + allowed_listeners[allowed_mob] = TRUE update_sound(_sound) @@ -68,25 +80,32 @@ if(!sound_channel) sound_channel = SSsounds.reserve_sound_channel_for_datum(src) sound.channel = sound_channel - sound_duration = SSsounds.get_sound_length(_sound) + sound_duration = sound_duration_override || SSsounds.get_sound_length(_sound) start_time = REALTIMEOFDAY if(start_playing) force_update_all_listeners(FALSE) + if(delete_on_end) + addtimer(CALLBACK(src, PROC_REF(on_sound_ended)), sound_duration, TIMER_UNIQUE | TIMER_OVERRIDE) + /// Updates the data of a listener, or adds them if they are not present. /datum/sound_token/proc/add_or_update_listener(mob/listener_mob) if(isnull(listeners[listener_mob])) - add_listener(listener_mob) + if(!add_listener(listener_mob)) + return FALSE else update_listener(listener_mob) -/// Adds a listener to the sound. +/// Adds a listener to the sound. returns TRUE if we already were added, or for some reason couldnt be added. /datum/sound_token/proc/add_listener(mob/listener_mob) if(!isnull(listeners[listener_mob])) - return FALSE + return TRUE if(!listener_mob.client || isnewplayer(listener_mob)) - return + return FALSE + + if(allowed_listeners && !allowed_listeners[listener_mob]) + return FALSE listeners[listener_mob] = NONE listener_mob.client.sound_tokens += src @@ -284,3 +303,7 @@ if(!still_in_range) remove_listener(listener_mob) + +///The sound should have ended on all clients. Time to destroy the sound token. +/datum/sound_token/proc/on_sound_ended() + qdel(src) diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm index 35d560510ad..a406cd14a68 100644 --- a/code/game/sound/sound.dm +++ b/code/game/sound/sound.dm @@ -219,3 +219,12 @@ return soundin var/datum/sound_effect/sfx = GLOB.sfx_datum_by_key[soundin] return sfx?.return_sfx() || soundin + + +/** + * Creates a soundtoken datum (a sound that updates for movement). + * allowed_listeners is an optional list of mobs that are the only ones that can hear this sound ever. + * sound_length is an optional length of the sound. Things like TTS need to pass this since we can't dynamically grab the length in that case. + */ +/proc/playsoundtoken(atom/source, soundin, volume, range, falloff_exponent = SOUND_FALLOFF_EXPONENT, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, allowed_listeners, sound_length) + return new /datum/sound_token(source, soundin, range, volume, falloff_exponent, falloff_distance, allowed_listeners, sound_length, _delete_on_end = TRUE) diff --git a/code/modules/mob/living/carbon/human/emote.dm b/code/modules/mob/living/carbon/human/emote.dm index 4a9f62ef24a..dfa413a45ec 100644 --- a/code/modules/mob/living/carbon/human/emote.dm +++ b/code/modules/mob/living/carbon/human/emote.dm @@ -63,7 +63,7 @@ message = "screeches!" message_mime = "screeches silently." emote_type = EMOTE_AUDIBLE | EMOTE_VISIBLE - specific_emote_audio_cooldown = 10 SECONDS + manual_specific_emote_audio_cooldown = 10 SECONDS vary = FALSE /datum/emote/living/carbon/human/screech/get_sound(mob/living/carbon/human/user) diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm index d33b6fd9878..3db4a61a06d 100644 --- a/code/modules/mob/living/emote.dm +++ b/code/modules/mob/living/emote.dm @@ -303,7 +303,7 @@ message = "laughs." message_mime = "laughs silently!" emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE - specific_emote_audio_cooldown = 8 SECONDS + manual_specific_emote_audio_cooldown = 8 SECONDS vary = TRUE /datum/emote/living/laugh/can_run_emote(mob/living/user, status_check = TRUE , intentional, params) @@ -419,7 +419,9 @@ emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE mob_type_blacklist_typecache = list(/mob/living/brain) sound_wall_ignore = TRUE - specific_emote_audio_cooldown = 10 SECONDS + use_sound_tokens = TRUE + manual_specific_emote_audio_cooldown = 10 SECONDS + forced_specific_emote_audio_cooldown = 4 SECONDS vary = TRUE /datum/emote/living/scream/run_emote(mob/user, params, type_override, intentional = FALSE)