mirror of
https://github.com/Aurorastation/Aurora.3.git
synced 2026-07-12 00:20:42 +01:00
14b6339d5c
``` - refactor: "Updates the Computer Fabricator to TGUI" - refactor: "Updates the Instruments to TGUI" - refactor: "Updates the Geo Scanner to TGUI" - refactor: "Updates the Stacking Machine to TGUI" - refactor: "Updates the Robotics Console to TGUI" - refactor: "Updates the Requests Console to TGUI" - refactor: "Updates the Synthesizer/Instrument main panel to TGUI" - refactor: "Updates the Nuclear Bomb control panel to TGUI" - rscadd: "Requests Console messages are now typed (Assistance Request, Supply Request, Information, Reply); the type is shown in the recipient's log, audible alert, and PDA notification." - rscadd: "Reconstructs the Synthesizer's Virtual Environment Editor (was broken since added — its NanoUI template never existed). The Custom environment preset that depends on it is gated behind a new musical_config.can_use_custom flag (default off, matching prior behavior)." - bugfix: "Fixes the Autolathe UI re-opening after every print job." - bugfix: "Fixes the Song Editor's currently-playing line indicator not advancing during playback." - bugfix: "Fixes a class of bugs where TGUI windows for the Air Alarm, Portable Turret, Mining Vendor, Mineral Processor, Tank Dispenser, Helm Console, all Chemistry machines, and Suit Cycler would force themselves back open after every action, even when the player had closed them." - bugfix: "Fixes the spatial sound system silently dropping every sound that used the 'None' (-1) environment id or a 23-element custom environment list. The validator was rejecting both as invalid, so synthesizer notes played with Custom or None mode produced no audible output at all." - admin: "Adds two requests consoles and two fax machines to the bridge on the runtime test map." ``` Claude Opus 4.7 has been used during the creation of this PR --------- Signed-off-by: Arrow768 <1331699+Arrow768@users.noreply.github.com> Co-authored-by: Werner <Arrow768@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Batrachophreno <Batrochophreno@gmail.com>
291 lines
11 KiB
Plaintext
291 lines
11 KiB
Plaintext
GLOBAL_DATUM_INIT(sound_player, /singleton/sound_player, new)
|
|
|
|
/**
|
|
* A sound player/manager for looping 3D sound effects.
|
|
* Due to how the BYOND sound engine works a sound datum must be played on a specific channel for updates to work properly.
|
|
* If a channel is not assigned it will just result in a new sound effect playing, even if re-using the same datum instance.
|
|
* We also use the channel to play a null-sound on Stop(), just in case BYOND clients don't like having a large nuber, albeit stopped, looping sounds.
|
|
* As such there is a maximum limit of 1024 sound sources, with further limitations due to some channels already being potentially in use.
|
|
* However, multiple sources may share the same sound_id and there is a best-effort attempt to play the closest source where possible.
|
|
* The line above is currently a lie. Will probably just have to enforce moderately short sound ranges.
|
|
*/
|
|
/singleton/sound_player
|
|
var/list/taken_channels // taken_channels and source_id_uses can be merged into one but would then require a meta-object to store the different values I desire.
|
|
var/list/sound_tokens_by_sound_id
|
|
|
|
/singleton/sound_player/New()
|
|
..()
|
|
taken_channels = list()
|
|
sound_tokens_by_sound_id = list()
|
|
|
|
|
|
/// This can be called if either we're doing whole sound setup ourselves or it will be as part of from-file sound setup
|
|
/singleton/sound_player/proc/PlaySoundDatum(atom/source, sound_id, sound/sound, range, prefer_mute, sound_type = ASFX_AMBIENCE)
|
|
var/token_type = isnum(sound.environment) ? /datum/sound_token : /datum/sound_token/static_environment
|
|
return new token_type(source, sound_id, sound, range, prefer_mute, sound_type)
|
|
|
|
/singleton/sound_player/proc/PlayLoopingSound(atom/source, sound_id, sound, volume, range, falloff = 1, echo, frequency, prefer_mute, sound_type = ASFX_AMBIENCE)
|
|
var/sound/S = istype(sound, /sound) ? sound : new(sound)
|
|
S.environment = 0 // Ensures a 3D effect even if x/y offset happens to be 0 the first time it's played
|
|
S.volume = volume
|
|
S.falloff = falloff
|
|
S.echo = echo
|
|
S.frequency = frequency
|
|
S.repeat = TRUE
|
|
|
|
return PlaySoundDatum(source, sound_id, S, range, prefer_mute, sound_type)
|
|
|
|
/// The 'repeat = FALSE' here seems to cause scattered sound cutoff issues due to any /sounds created reverting to the default channel zero. Refer to https://github.com/Aurorastation/Aurora.3/pull/21845.
|
|
/// This suggests that further work needs to be done with managing channels when a sound gets assigned default channel 0.
|
|
/singleton/sound_player/proc/PlayNonloopingSound(atom/source, sound_id, sound, volume, range, falloff = 1, echo, frequency, prefer_mute, sound_type = ASFX_AMBIENCE)
|
|
var/sound/S = istype(sound, /sound) ? sound : new(sound)
|
|
S.environment = 0 // Ensures a 3D effect even if x/y offset happens to be 0 the first time it's played
|
|
S.volume = volume
|
|
S.falloff = falloff
|
|
S.echo = echo
|
|
S.frequency = frequency
|
|
S.repeat = FALSE
|
|
|
|
return PlaySoundDatum(source, sound_id, S, range, prefer_mute, sound_type)
|
|
|
|
/singleton/sound_player/proc/PrivStopSound(datum/sound_token/sound_token)
|
|
var/channel = sound_token.sound.channel
|
|
var/sound_id = sound_token.sound_id
|
|
|
|
var/sound_tokens = sound_tokens_by_sound_id[sound_id]
|
|
if(!(sound_token in sound_tokens))
|
|
return
|
|
sound_tokens -= sound_token
|
|
if(length(sound_tokens))
|
|
return
|
|
|
|
GLOB.sound_channels.ReleaseChannel(channel)
|
|
taken_channels -= sound_id
|
|
sound_tokens_by_sound_id -= sound_id
|
|
|
|
/singleton/sound_player/proc/PrivGetChannel(datum/sound_token/sound_token)
|
|
var/sound_id = sound_token.sound_id
|
|
|
|
. = taken_channels[sound_id] // Does this sound_id already have an assigned channel?
|
|
if(!.) // If not, request a new one.
|
|
. = GLOB.sound_channels.RequestChannel(sound_id)
|
|
if(!.) // Oh no, still no channel. Abort
|
|
return
|
|
taken_channels[sound_id] = .
|
|
|
|
var/sound_tokens = sound_tokens_by_sound_id[sound_id]
|
|
if(!sound_tokens)
|
|
sound_tokens = list()
|
|
sound_tokens_by_sound_id[sound_id] = sound_tokens
|
|
sound_tokens += sound_token
|
|
|
|
#define SOUND_STOPPED FLAG(15)
|
|
|
|
/**
|
|
* Outwardly this is a merely a toke/little helper that a user utilize to adjust sounds as desired (and possible).
|
|
* In reality this is where the heavy-lifting happens.
|
|
*/
|
|
/datum/sound_token
|
|
/// Where the sound originates from
|
|
var/atom/source
|
|
/// Assoc: Atoms hearing this sound, and their sound datum
|
|
var/list/listeners
|
|
/// How many turfs away the sound will stop playing completely
|
|
var/range
|
|
/// If sound should be muted instead of stopped when mob moves out of range. In the general case this should be avoided because listeners will remain tracked.
|
|
var/prefer_mute
|
|
/// Sound datum, holds most sound relevant data
|
|
var/sound/sound
|
|
/// The associated sound id, used for cleanup
|
|
var/sound_id
|
|
/// Paused, muted, running? Global for all listeners
|
|
var/status = 0
|
|
/// Paused, muted, running? Specific for the given listener.
|
|
var/listener_status
|
|
/// Type of sound this token is handling, it's set by default (arbitrarily) as ambience, gets set on init with the actual one if specified.
|
|
var/sound_type
|
|
|
|
|
|
/datum/sound_token/New(atom/source, sound_id, sound/sound, range = 4, prefer_mute = FALSE, sound_type = ASFX_AMBIENCE)
|
|
..()
|
|
if(!istype(source))
|
|
CRASH("Invalid sound source: [log_info_line(source)]")
|
|
if(!istype(sound))
|
|
CRASH("Invalid sound: [log_info_line(sound)]")
|
|
if(sound.repeat && !sound_id)
|
|
CRASH("No sound id given")
|
|
if(!PrivIsValidEnvironment(sound.environment))
|
|
CRASH("Invalid sound environment: [log_info_line(sound.environment)]")
|
|
|
|
src.prefer_mute = prefer_mute
|
|
src.range = range
|
|
src.source = source
|
|
src.sound = sound
|
|
src.sound_id = sound_id
|
|
src.sound_type = sound_type
|
|
|
|
if(sound.repeat) // Non-looping sounds may not reserve a sound channel due to the risk of not hearing when someone forgets to stop the token
|
|
var/channel = GLOB.sound_player.PrivGetChannel(src) //Attempt to find a channel
|
|
if(!isnum(channel))
|
|
CRASH("All available sound channels are in active use.")
|
|
sound.channel = channel
|
|
else
|
|
sound.channel = 0
|
|
|
|
listeners = list()
|
|
listener_status = list()
|
|
|
|
RegisterSignal(source, COMSIG_QDELETING, TYPE_PROC_REF(/datum, qdel_self))
|
|
|
|
PrivLocateListeners()
|
|
START_PROCESSING(SSprocessing, src)
|
|
|
|
/datum/sound_token/Destroy()
|
|
Stop()
|
|
source = null
|
|
if (listeners)
|
|
listeners.Cut()
|
|
sound = null
|
|
return ..()
|
|
|
|
/datum/sound_token/proc/SetVolume(new_volume)
|
|
new_volume = clamp(new_volume, 0, 100)
|
|
if(sound.volume == new_volume)
|
|
return
|
|
sound.volume = new_volume
|
|
PrivUpdateListeners()
|
|
|
|
/datum/sound_token/proc/Mute()
|
|
PrivUpdateStatus(status|SOUND_MUTE)
|
|
|
|
/datum/sound_token/proc/Unmute()
|
|
PrivUpdateStatus(status & ~SOUND_MUTE)
|
|
|
|
/datum/sound_token/proc/Pause()
|
|
PrivUpdateStatus(status|SOUND_PAUSED)
|
|
|
|
/// Normally called Resume but I don't want to give people false hope about being unable to un-stop a sound
|
|
/datum/sound_token/proc/Unpause()
|
|
PrivUpdateStatus(status & ~SOUND_PAUSED)
|
|
|
|
/datum/sound_token/proc/Stop()
|
|
if(status & SOUND_STOPPED)
|
|
return
|
|
status |= SOUND_STOPPED
|
|
|
|
var/sound/null_sound = new(channel = sound.channel)
|
|
for(var/listener in listeners)
|
|
PrivRemoveListener(listener, null_sound)
|
|
listeners = null
|
|
listener_status = null
|
|
|
|
UnregisterSignal(source, COMSIG_QDELETING)
|
|
source = null
|
|
|
|
GLOB.sound_player.PrivStopSound(src)
|
|
STOP_PROCESSING(SSprocessing, src)
|
|
|
|
/datum/sound_token/process()
|
|
PrivLocateListeners()
|
|
|
|
/datum/sound_token/proc/PrivLocateListeners()
|
|
if(status & SOUND_STOPPED)
|
|
return
|
|
|
|
var/current_listeners = SSspatial_grid.orthogonal_range_search(source, SPATIAL_GRID_CONTENTS_TYPE_HEARING, range)
|
|
var/new_listeners = current_listeners - listeners
|
|
|
|
if(!prefer_mute)
|
|
var/former_listeners = listeners - current_listeners
|
|
for(var/listener in former_listeners)
|
|
PrivRemoveListener(listener)
|
|
|
|
for(var/mob/listener in new_listeners)
|
|
// Only give the sound token if the listener has the preference for the type of token active, and is not deaf
|
|
if((listener.client?.prefs.sfx_toggles & sound_type) && (!isdeaf(listener)))
|
|
PrivAddListener(listener)
|
|
|
|
for(var/mob/listener in current_listeners)
|
|
PrivUpdateListenerLoc(listener)
|
|
|
|
/datum/sound_token/proc/PrivUpdateStatus(new_status)
|
|
// Once stopped, always stopped. Go ask the player to play the sound again.
|
|
if(status & SOUND_STOPPED)
|
|
return
|
|
if(new_status == status)
|
|
return
|
|
status = new_status
|
|
PrivUpdateListeners()
|
|
|
|
/datum/sound_token/proc/PrivAddListener(atom/listener)
|
|
if(listener in listeners)
|
|
return
|
|
|
|
listeners += listener
|
|
|
|
RegisterSignal(listener, COMSIG_MOVABLE_MOVED, PROC_REF(PrivUpdateListenerLoc))
|
|
RegisterSignal(listener, COMSIG_QDELETING, PROC_REF(PrivRemoveListener))
|
|
|
|
PrivUpdateListenerLoc(listener, update_sound = FALSE)
|
|
|
|
/datum/sound_token/proc/PrivRemoveListener(atom/listener, sound/null_sound)
|
|
null_sound = null_sound || new(channel = sound.channel)
|
|
sound_to(listener, null_sound)
|
|
UnregisterSignal(listener, COMSIG_MOVABLE_MOVED)
|
|
UnregisterSignal(listener, COMSIG_QDELETING)
|
|
listeners -= listener
|
|
|
|
/datum/sound_token/proc/PrivUpdateListenerLoc(atom/movable/listener, atom/old_loc, dir, forced, list/old_locs, update_sound = TRUE)
|
|
var/turf/source_turf = get_turf(source)
|
|
var/turf/listener_turf = get_turf(listener)
|
|
|
|
if (!source_turf || !listener_turf)
|
|
return
|
|
|
|
var/distance = get_dist(source_turf, listener_turf)
|
|
if(!listener_turf || (distance > range))
|
|
if(prefer_mute)
|
|
listener_status[listener] |= SOUND_MUTE
|
|
else
|
|
PrivRemoveListener(listener)
|
|
return
|
|
else if(prefer_mute)
|
|
listener_status[listener] &= ~SOUND_MUTE
|
|
|
|
sound.x = source_turf.x - listener_turf.x
|
|
sound.z = source_turf.y - listener_turf.y
|
|
sound.y = 1
|
|
// Far as I can tell from testing, sound priority just doesn't work.
|
|
// Sounds happily steal channels from each other no matter what.
|
|
sound.priority = clamp(255 - distance, 0, 255)
|
|
PrivUpdateListener(listener, update_sound)
|
|
|
|
/datum/sound_token/proc/PrivUpdateListeners()
|
|
for(var/listener in listeners)
|
|
PrivUpdateListener(listener)
|
|
|
|
/datum/sound_token/proc/PrivUpdateListener(mob/listener, update_sound = TRUE)
|
|
sound.environment = PrivGetEnvironment(listener)
|
|
sound.status = status|listener_status[listener]
|
|
if(update_sound)
|
|
sound.status |= SOUND_UPDATE
|
|
// Send the sound only if the preference for the type of sound is set and is not deaf, otherwise remove the listener.
|
|
if((listener.client?.prefs.sfx_toggles & sound_type) && (!isdeaf(listener)))
|
|
sound_to(listener, sound)
|
|
else
|
|
PrivRemoveListener(listener)
|
|
|
|
/datum/sound_token/proc/PrivGetEnvironment(listener)
|
|
var/area/A = get_area(listener)
|
|
return A && PrivIsValidEnvironment(A.sound_environment) ? A.sound_environment : sound.environment
|
|
|
|
/datum/sound_token/proc/PrivIsValidEnvironment(environment)
|
|
if(islist(environment))
|
|
return length(environment) == 23
|
|
if(isnum(environment))
|
|
return environment >= -1 && environment <= 25
|
|
return FALSE
|
|
|
|
/datum/sound_token/static_environment/PrivGetEnvironment()
|
|
return sound.environment
|