Files
MrMelbertandGitHub 18a1d31ca2 Maybe fix sound token hard deletes (#96903)
## About The Pull Request

`remove_listener` would fail on logout so instead I figure we could
track the tokens on the mob, rather than the client

Functionally not much is changed - but if you go from ghost to mob it'll
remove the token from the ghost and adds the token to the new mob, so it
should fix the issue

There also doesn't need to be any code in `/mob/destroy` because we
listen for `listener_deleted` and remove listeners on the token itself
2026-07-10 22:39:25 -06:00

308 lines
11 KiB
Plaintext

// Sound tokens, a datumized handler for spatial sound.
// Uses the spatial grid to track clients in range and add them as listeners
// Updated by the SSsound_tokens subsystem every tick when requested by client so that if the source or listener moves, the sound updates accordingly.
/datum/sound_token
/// The atom playing the sound.
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
var/volume
/// Sound falloff
var/falloff_exponent
/// Sound falloff distance
var/falloff_distance
/// The master copy of the playing sound.
var/sound/sound
/// Null sound for cancelling the sound entirely.
var/sound/null_sound
/// Status of the playing sound
var/sound_status = NONE
/// The channel being used.
var/sound_channel
/// world.time when the sound started (or when the sound file was last changed). Used to calculate playback offset for new listeners.
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, _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))
RegisterSignal(source, COMSIG_ENTER_AREA, PROC_REF(on_enter_area))
range = _range
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)
null_sound = sound(channel = sound_channel)
cell_tracker = new /datum/cell_tracker(range, range)
update_tracked_cells()
RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGIN, PROC_REF(player_login))
RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGOUT, PROC_REF(player_logout))
/datum/sound_token/Destroy(force, ...)
for(var/listener in listeners)
remove_listener(listener)
listeners = null
source = null
return ..()
///Lets us update the sound to a new one.
/datum/sound_token/proc/update_sound(_sound, start_playing = FALSE)
sound = sound(_sound)
if(!sound_channel)
sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
sound.channel = sound_channel
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]))
if(!add_listener(listener_mob))
return FALSE
else
update_listener(listener_mob)
/// 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 TRUE
if(!listener_mob.client || isnewplayer(listener_mob))
return FALSE
if(allowed_listeners && !allowed_listeners[listener_mob])
return FALSE
listeners[listener_mob] = NONE
LAZYOR(listener_mob.sound_tokens, src)
if(source != listener_mob) //this is possible...yea... :/
RegisterSignal(listener_mob, COMSIG_QDELETING, PROC_REF(listener_deleted))
RegisterSignals(listener_mob, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deafness_update))
update_listener(listener_mob, FALSE)
return TRUE
/// Remove a listener from the sound.
/datum/sound_token/proc/remove_listener(mob/listener_mob)
listeners -= listener_mob
LAZYREMOVE(listener_mob.sound_tokens, src)
UnregisterSignal(listener_mob, list(COMSIG_QDELETING, SIGNAL_ADDTRAIT(TRAIT_DEAF),SIGNAL_REMOVETRAIT(TRAIT_DEAF)))
SEND_SOUND(listener_mob, null_sound)
/datum/sound_token/proc/update_listener(mob/listener_mob, update_sound = TRUE)
if(QDELETED(src))
return
if(isnull(listeners[listener_mob]))
return
var/turf/source_turf = get_turf(source)
var/turf/listener_turf = get_turf(listener_mob)
if(!source_turf || !listener_turf)
return
var/is_muted = listeners[listener_mob] & SOUND_MUTE
var/should_be_muted = FALSE
if(source_turf.z != listener_turf.z)
should_be_muted = TRUE
var/distance = get_dist_euclidean(source_turf, listener_turf)
if(distance > range)
should_be_muted = TRUE
if(should_be_muted && is_muted)
return
should_be_muted ||= HAS_TRAIT(listener_mob, TRAIT_DEAF)
if(should_be_muted && is_muted)
return
set_listener_status(listener_mob, should_be_muted ? SOUND_MUTE : NONE)
send_listener_sound(listener_mob, update_sound)
/datum/sound_token/proc/send_listener_sound(mob/listener_mob, update_sound)
PRIVATE_PROC(TRUE)
sound.status = sound_status|listeners[listener_mob]
if(update_sound)
sound.status |= SOUND_UPDATE
else
sound.offset = calculate_offset()
if(sound.status & SOUND_MUTE)
SEND_SOUND(listener_mob, sound)
return
if(!listener_mob.playsound_local(get_turf(source), vol = volume, falloff_exponent = falloff_exponent, channel = sound_channel, sound_to_use = sound, max_distance = range, falloff_distance = falloff_distance, use_reverb = TRUE))
sound.status = SOUND_UPDATE|SOUND_MUTE
SEND_SOUND(listener_mob, sound)
sound.offset = null
/datum/sound_token/proc/update_all_listeners()
for(var/mob/listener_mob in listeners)
if(listener_mob.client)
SSsound_tokens.clients_needing_update[listener_mob.client] = TRUE
/datum/sound_token/proc/force_update_all_listeners(update_sound = TRUE)
for(var/mob/listener_mob in listeners)
if(listener_mob.client)
update_listener(listener_mob, update_sound)
/// Setter for volume
/datum/sound_token/proc/set_volume(new_volume, update_listeners = TRUE)
volume = new_volume
if(update_listeners)
update_all_listeners()
/// Set the status of a listener. Does not update the sound.
/datum/sound_token/proc/set_listener_status(mob/listener_mob, new_status)
if(isnull(listeners[listener_mob]))
return
listeners[listener_mob] = new_status
/// Respond to TRAIT_DEAF addition/removal
/datum/sound_token/proc/listener_deafness_update(atom/movable/source)
SIGNAL_HANDLER
update_listener(source)
/datum/sound_token/proc/listener_deleted(datum/source)
SIGNAL_HANDLER
remove_listener(source)
/// Respond to any mob in the world being logged into. Only adds if the mob is within range.
/datum/sound_token/proc/player_login(datum/source, mob/player)
SIGNAL_HANDLER
var/turf/player_turf = get_turf(player)
var/turf/source_turf = get_turf(src.source)
if(!player_turf || !source_turf)
return
if(player_turf.z != source_turf.z)
return
if(get_dist_euclidean(source_turf, player_turf) > range)
return
add_or_update_listener(player)
/// Respond to any cliented mob becoming uncliented
/datum/sound_token/proc/player_logout(datum/source, mob/player)
SIGNAL_HANDLER
remove_listener(player)
/// If the sound source moves, update tracked cells then refresh all listener positions.
/datum/sound_token/proc/source_moved()
SIGNAL_HANDLER
update_tracked_cells()
update_all_listeners()
/datum/sound_token/proc/source_deleted()
SIGNAL_HANDLER
qdel(src)
///Update env when source is entering new area
/datum/sound_token/proc/on_enter_area(datum/source, area/area_to_register)
SIGNAL_HANDLER
set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE)
/datum/sound_token/proc/set_new_environment(new_env)
if(sound.environment == new_env)
return
sound.environment = new_env
update_all_listeners()
///Calculates the offset to give the sound for people who start hearing it mid-play
/datum/sound_token/proc/calculate_offset()
var/elapsed = REALTIMEOFDAY - start_time
var/freq_factor = (sound.frequency || 100) / 100
var/pitch_factor = (sound.pitch || 100) / 100
var/offset = elapsed * freq_factor * pitch_factor
return offset
///Update tracked cells; happens on movement. We need to check if anyone is now out of cell range and kick them out.
/datum/sound_token/proc/update_tracked_cells()
if(!get_turf(source))
return
var/list/new_and_old = cell_tracker.recalculate_cells(get_turf(source))
var/list/datum/spatial_grid_cell/added_cells = new_and_old[1]
var/list/datum/spatial_grid_cell/removed_cells = new_and_old[2]
for(var/datum/spatial_grid_cell/cell as anything in removed_cells)
UnregisterSignal(cell, list(SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS),))
// Remove listeners whose mob is no longer in any remaining member cell
if(removed_cells.len)
for(var/mob/listener_mob as anything in listeners)
var/still_in_range = FALSE
for(var/datum/spatial_grid_cell/cell as anything in cell_tracker.member_cells)
if(listener_mob in cell.client_contents)
still_in_range = TRUE
break
if(!still_in_range)
remove_listener(listener_mob)
for(var/datum/spatial_grid_cell/cell as anything in added_cells)
RegisterSignal(cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_entered))
RegisterSignal(cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_exited))
for(var/mob/listener_mob as anything in cell.client_contents)
add_or_update_listener(listener_mob)
/// Signal handler for SPATIAL_GRID_CELL_ENTERED on tracked cells. Adds newly arriving mobs as listeners.
/datum/sound_token/proc/on_cell_client_entered(datum/source, list/entering_mobs)
SIGNAL_HANDLER
for(var/mob/listener_mob as anything in entering_mobs)
if(!isnull(listeners[listener_mob])) // already added
continue
add_or_update_listener(listener_mob)
/// Signal handler for SPATIAL_GRID_CELL_EXITED on tracked cells. Removes mobs who have left all member cells.
/datum/sound_token/proc/on_cell_client_exited(datum/source, list/exiting_mobs)
SIGNAL_HANDLER
for(var/mob/listener_mob as anything in exiting_mobs)
var/still_in_range = FALSE
if(SSspatial_grid.get_cell_of(listener_mob) in cell_tracker.member_cells)
still_in_range = TRUE
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)