use bevy::{
audio::PlaybackMode,
platform::collections::{HashMap, HashSet},
prelude::*,
};
#[cfg(feature = "bevy_ggrs")]
use bevy_ggrs::RollbackApp;
use std::time::Duration;
use crate::{RollbackPostUpdate, RollbackPreUpdate};
pub struct RollbackAudioPlugin;
impl Plugin for RollbackAudioPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, sync_rollback_sounds);
app.add_systems(RollbackPreUpdate, remove_finished_sounds);
app.add_systems(RollbackPostUpdate, start_rollback_sounds);
#[cfg(feature = "bevy_ggrs")]
{
app.rollback_component_with_clone::<RollbackAudioPlayer>();
app.rollback_component_with_clone::<RollbackAudioPlayerStartTime>();
app.rollback_component_with_clone::<PlaybackSettings>();
app.add_systems(RollbackPostUpdate, add_rollback_to_rollback_sounds);
}
}
}
#[derive(Component, Clone)]
pub struct RollbackAudioPlayer(pub AudioPlayer);
impl From<AudioPlayer> for RollbackAudioPlayer {
fn from(audio_player: AudioPlayer) -> Self {
Self(audio_player)
}
}
#[derive(Component, Clone, Debug)]
pub struct RollbackAudioPlayerStartTime(pub Duration);
#[derive(Component)]
pub struct RollbackAudioPlayerInstance {
desired_start_time: Duration,
}
#[derive(PartialEq, Eq, Hash)]
struct PlayingRollbackAudioKey {
audio_source: Handle<AudioSource>,
start_time: Duration,
}
pub fn sync_rollback_sounds(
mut commands: Commands,
rollback_audio_players: Query<(
&RollbackAudioPlayer,
&RollbackAudioPlayerStartTime,
Option<&PlaybackSettings>,
)>,
instances: Query<(Entity, &RollbackAudioPlayerInstance, &AudioPlayer)>,
) {
let desired_state: HashMap<PlayingRollbackAudioKey, Option<&PlaybackSettings>> =
rollback_audio_players
.iter()
.map(|(player, start_time, playback_settings)| {
(
PlayingRollbackAudioKey {
audio_source: player.0 .0.clone(),
start_time: start_time.0,
},
playback_settings,
)
})
.collect();
let mut playing_sounds = HashSet::new();
for (instance_entity, instance, audio_player) in &instances {
let rollback_sound_key = PlayingRollbackAudioKey {
audio_source: audio_player.0.clone(),
start_time: instance.desired_start_time,
};
if !desired_state.contains_key(&rollback_sound_key) {
commands.entity(instance_entity).despawn();
} else {
playing_sounds.insert(rollback_sound_key);
}
}
for (sound, settings) in desired_state {
if playing_sounds.contains(&sound) {
continue;
}
debug!("Spawning sound: {:?}", sound.audio_source);
let settings = settings.cloned().unwrap_or(PlaybackSettings::ONCE);
commands.spawn((
AudioPlayer::new(sound.audio_source.clone()),
settings,
RollbackAudioPlayerInstance {
desired_start_time: sound.start_time,
},
));
}
}
pub fn start_rollback_sounds(
mut commands: Commands,
mut rollback_audio_players: Query<
Entity,
(
With<RollbackAudioPlayer>,
Without<RollbackAudioPlayerStartTime>,
),
>,
time: Res<Time>,
) {
let start_time = time.elapsed();
for entity in rollback_audio_players.iter_mut() {
trace!("adding RollbackAudioPlayerStartTime: {entity:?} {start_time:?}");
commands
.entity(entity)
.insert(RollbackAudioPlayerStartTime(start_time));
}
}
#[cfg(feature = "bevy_ggrs")]
fn add_rollback_to_rollback_sounds(
mut commands: Commands,
mut rollback_audio_players: Query<
Entity,
(With<RollbackAudioPlayer>, Without<bevy_ggrs::Rollback>),
>,
) {
for entity in rollback_audio_players.iter_mut() {
use bevy_ggrs::AddRollbackCommandExtension;
debug!("adding ggrs rollback to audio player: {entity:?}");
commands.entity(entity).add_rollback();
}
}
pub fn remove_finished_sounds(
rollback_audio_players: Query<(
Entity,
&RollbackAudioPlayer,
&RollbackAudioPlayerStartTime,
Option<&PlaybackSettings>,
)>,
mut commands: Commands,
audio_sources: Res<Assets<AudioSource>>,
time: Res<Time>,
mut durations: Local<HashMap<Handle<AudioSource>, Duration>>,
) {
for (entity, player, start_time, settings) in rollback_audio_players.iter() {
if let Some(audio_source) = audio_sources.get(&player.0 .0) {
use bevy::audio::Source;
let duration = durations
.entry(player.0.0.clone())
.or_insert_with(|| {
audio_source
.decoder()
.total_duration()
.unwrap_or_else(|| {
const FALLBACK_DURATION_SECS: u64 = 10;
warn!(
"Audio source {:?} has no total duration, defaulting to {} seconds. Make sure you use a format that supports querying duration.",
player.0.0,
FALLBACK_DURATION_SECS
);
Duration::from_secs(FALLBACK_DURATION_SECS)
})
});
let time_played = time.elapsed() - start_time.0;
let speed = settings.map_or(1.0, |s| s.speed);
let scaled_duration = duration.div_f32(speed);
if time_played >= scaled_duration {
trace!("handling finished sound: {:?} {:?}", entity, player.0 .0);
let mode = settings.map_or(PlaybackMode::Once, |s| s.mode);
match mode {
PlaybackMode::Despawn => commands.entity(entity).despawn(),
PlaybackMode::Remove => {
commands.entity(entity).remove::<(
RollbackAudioPlayer,
RollbackAudioPlayerStartTime,
PlaybackSettings,
)>();
}
PlaybackMode::Once => {}
PlaybackMode::Loop => {
commands
.entity(entity)
.insert(RollbackAudioPlayerStartTime(time.elapsed()));
}
}
}
}
}
}