use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use bevy::asset::{AssetLoadFailedEvent, AssetPath};
use bevy::prelude::*;
use bevy_cobweb::prelude::*;
use fluent_langneg::{negotiate_languages, LanguageIdentifier, NegotiationStrategy};
use serde::{Deserialize, Serialize};
use crate::prelude::*;
fn load_localized_audio_sources(
In(loaded): In<Vec<LocalizedAudio>>,
mut c: Commands,
asset_server: Res<AssetServer>,
mut audios: ResMut<AudioMap>,
manifest: Res<LocalizationManifest>,
)
{
audios.insert_localized(loaded, &asset_server, &manifest, &mut c);
}
fn load_audio_sources(In(loaded): In<Vec<String>>, asset_server: Res<AssetServer>, mut audios: ResMut<AudioMap>)
{
for path in loaded {
audios.insert(&path, &asset_server);
}
}
fn handle_new_lang_list(
asset_server: Res<AssetServer>,
manifest: Res<LocalizationManifest>,
mut audios: ResMut<AudioMap>,
)
{
audios.negotiate_languages(&manifest, &asset_server);
}
fn check_loaded_audios(
mut c: Commands,
mut errors: MessageReader<AssetLoadFailedEvent<AudioSource>>,
mut events: MessageReader<AssetEvent<AudioSource>>,
mut audios: ResMut<AudioMap>,
)
{
for error in errors.read() {
let AssetLoadFailedEvent { id, .. } = error;
audios.remove_pending(id);
}
for event in events.read() {
let AssetEvent::Added { id } = event else { continue };
audios.remove_pending(id);
}
audios.try_emit_load_event(&mut c);
}
fn relocalize_audios(audios: Res<AudioMap>, mut query: Query<&mut AudioPlayer>)
{
for mut player in query.iter_mut() {
audios.localize_audio(&mut player.0);
}
}
pub struct AudioMapLoaded;
#[derive(Resource, Default)]
pub struct AudioMap
{
waiting_for_load: bool,
pending: HashSet<AssetId<AudioSource>>,
localization_map: HashMap<Arc<str>, (AssetPath<'static>, HashMap<LanguageIdentifier, AssetPath<'static>>)>,
localized_audios_id_helper: HashMap<AssetPath<'static>, Arc<str>>,
localized_audios: HashMap<Arc<str>, Handle<AudioSource>>,
cached_audios: HashMap<Arc<str>, Handle<AudioSource>>,
}
impl AudioMap
{
pub fn is_loading(&self) -> bool
{
!self.pending.is_empty()
}
fn try_add_pending(
handle: &Handle<AudioSource>,
asset_server: &AssetServer,
pending: &mut HashSet<AssetId<AudioSource>>,
)
{
match asset_server.load_state(handle) {
bevy::asset::LoadState::Loaded => (),
_ => {
pending.insert(handle.id());
}
}
}
fn try_emit_load_event(&mut self, c: &mut Commands)
{
if self.is_loading() {
return;
}
if !self.waiting_for_load {
return;
}
self.waiting_for_load = false;
c.react().broadcast(AudioMapLoaded);
}
fn negotiate_languages(&mut self, manifest: &LocalizationManifest, asset_server: &AssetServer) -> bool
{
let app_negotiated = manifest.negotiated();
if app_negotiated.len() == 0 {
return false;
}
let prev_localized_audios = std::mem::take(&mut self.localized_audios);
self.localized_audios.reserve(self.localization_map.len());
let mut langs_buffer = Vec::default();
self.localization_map
.iter()
.for_each(|(main_path, (main_asset_path, fallbacks))| {
langs_buffer.clear();
langs_buffer.extend(fallbacks.keys());
let asset_path =
negotiate_languages(&langs_buffer, app_negotiated, None, NegotiationStrategy::Lookup)
.get(0)
.map(|lang| {
fallbacks
.get(lang)
.expect("negotiation should only return fallback langs")
})
.unwrap_or(main_asset_path);
let handle = prev_localized_audios
.get(main_path)
.or_else(|| self.cached_audios.get(main_path))
.filter(|handle| {
handle.path().filter(|path| *path == asset_path).is_some()
})
.cloned()
.unwrap_or_else(|| {
let handle = asset_server.load(asset_path.clone());
Self::try_add_pending(&handle, asset_server, &mut self.pending);
handle
});
self.localized_audios.insert(main_path.clone(), handle);
});
true
}
fn remove_pending(&mut self, id: &AssetId<AudioSource>)
{
let _ = self.pending.remove(id);
}
pub fn insert(&mut self, path: impl AsRef<str>, asset_server: &AssetServer)
{
let path = path.as_ref();
if self.cached_audios.contains_key(path) {
tracing::warn!("ignoring duplicate insert for audio {}", path);
return;
}
let asset_path = match AssetPath::try_parse(path) {
Ok(asset_path) => asset_path,
Err(err) => {
tracing::error!("failed parsing audio path {:?} on insert to AudioMap: {:?}", path, err);
return;
}
};
if let Some((key, handle)) = self
.localized_audios
.get_key_value(path)
.filter(|(_, handle)| {
*handle
.path()
.expect("handles in localized_audios should have paths")
== asset_path
})
{
self.cached_audios.insert(key.clone(), handle.clone());
return;
}
let handle = asset_server.load(asset_path);
Self::try_add_pending(&handle, asset_server, &mut self.pending);
self.cached_audios.insert(Arc::from(path), handle);
}
pub fn insert_localized(
&mut self,
mut loaded: Vec<LocalizedAudio>,
asset_server: &AssetServer,
manifest: &LocalizationManifest,
c: &mut Commands,
)
{
for mut loaded in loaded.drain(..) {
let main_path = Arc::<str>::from(loaded.audio.as_str());
let (main_asset_path, fallbacks) = self
.localization_map
.entry(main_path.clone())
.or_insert_with(|| {
let main_asset_path = match AssetPath::try_parse(&main_path) {
Ok(asset_path) => asset_path.clone_owned(),
Err(err) => {
tracing::error!("failed parsing audio path {:?} on insert loaded to AudioMap: {:?}",
main_path, err);
AssetPath::<'static>::default()
}
};
(main_asset_path, HashMap::default())
});
self.localized_audios_id_helper
.insert(main_asset_path.clone(), main_path.clone());
#[cfg(not(feature = "hot_reload"))]
if fallbacks.len() > 0 {
tracing::warn!("overwritting audio fallbacks for main audio {:?}; main audio sources should only appear \
in one LoadAudio command per app", main_path);
}
fallbacks.clear();
fallbacks.reserve(loaded.fallbacks.len());
for LocalizedAudioFallback { lang, audio } in loaded.fallbacks.drain(..) {
let lang_id = match LanguageIdentifier::from_str(lang.as_str()) {
Ok(lang_id) => lang_id,
Err(err) => {
tracing::error!("failed parsing target language id {:?} for audio fallback {:?} for audio {:?}: \
{:?}", lang, audio, main_path, err);
continue;
}
};
let fallback_asset_path = match AssetPath::try_parse(audio.as_str()) {
Ok(asset_path) => asset_path.clone_owned(),
Err(err) => {
tracing::error!("failed parsing fallback audio path {:?} for {:?} on insert loaded to \
AudioMap: {:?}", audio, main_path, err);
continue;
}
};
if let Some(prev) = fallbacks.insert(lang_id, fallback_asset_path.clone()) {
tracing::warn!("overwriting audio fallback {:?} for audio {:?} for lang {:?}",
prev, main_path, lang);
}
self.localized_audios_id_helper
.insert(fallback_asset_path, main_path.clone());
}
}
if self.negotiate_languages(manifest, asset_server) {
self.waiting_for_load = true;
self.try_emit_load_event(c);
}
}
pub fn localize_audio(&self, handle: &mut Handle<AudioSource>)
{
let Some(path) = handle.path().cloned() else {
tracing::debug!("failed localizing audio handle that doesn't have a path");
return;
};
if let Some(localized_handle) = self
.localized_audios_id_helper
.get(&path)
.and_then(|main_path| self.localized_audios.get(main_path))
{
*handle = localized_handle.clone();
} else {
tracing::debug!("failed localizing audio handle with {:?} that doesn't have a localization entry", path);
}
}
pub fn get(&self, path: impl AsRef<str>) -> Handle<AudioSource>
{
let path = path.as_ref();
self.localized_audios
.get(path)
.or_else(|| self.cached_audios.get(path))
.cloned()
.unwrap_or_else(|| {
tracing::error!("failed getting audio {} that was not loaded to AudioMap", path);
Default::default()
})
}
pub fn get_or_load(&mut self, path: impl AsRef<str>, asset_server: &AssetServer) -> Handle<AudioSource>
{
let path = path.as_ref();
self.localized_audios
.get(path)
.or_else(|| self.cached_audios.get(path))
.cloned()
.unwrap_or_else(|| {
let handle = asset_server.load(String::from(path));
Self::try_add_pending(&handle, asset_server, &mut self.pending);
self.cached_audios.insert(Arc::from(path), handle.clone());
handle
})
}
}
impl AssetLoadProgress for AudioMap
{
fn pending_assets(&self) -> usize
{
self.pending.len()
}
fn total_assets(&self) -> usize
{
self.localized_audios.len() + self.cached_audios.len()
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocalizedAudioFallback
{
pub lang: String,
pub audio: String,
}
#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocalizedAudio
{
pub audio: String,
#[reflect(default)]
pub fallbacks: Vec<LocalizedAudioFallback>,
}
#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LoadLocalizedAudio(pub Vec<LocalizedAudio>);
impl Command for LoadLocalizedAudio
{
fn apply(self, world: &mut World)
{
world.syscall(self.0, load_localized_audio_sources);
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LoadAudio(pub Vec<String>);
impl Command for LoadAudio
{
fn apply(self, world: &mut World)
{
world.syscall(self.0, load_audio_sources);
}
}
pub(crate) struct AudioLoadPlugin;
impl Plugin for AudioLoadPlugin
{
fn build(&self, app: &mut App)
{
app.init_resource::<AudioMap>()
.register_asset_tracker::<AudioMap>()
.register_command_type::<LoadAudio>()
.register_command_type::<LoadLocalizedAudio>()
.react(|rc| rc.on_persistent(broadcast::<LanguagesNegotiated>(), handle_new_lang_list))
.react(|rc| {
rc.on_persistent(
(broadcast::<AudioMapLoaded>(), broadcast::<RelocalizeApp>()),
relocalize_audios,
)
})
.add_systems(PreUpdate, check_loaded_audios.in_set(LoadProgressSet::Prepare));
}
}