1use crate::audio::{AudioFrontend, MusicChannelTag};
47use client::Client;
48use common::{
49 assets::{Asset, AssetCache, AssetExt, AssetHandle, BoxedError, Ron, SharedString},
50 calendar::{Calendar, CalendarEvent},
51 terrain::{
52 BiomeKind, SiteKindMeta,
53 site::{DungeonKindMeta, SettlementKindMeta},
54 },
55 weather::WeatherKind,
56};
57use common_state::State;
58use hashbrown::HashMap;
59use kira::clock::ClockTime;
60use rand::{RngExt, prelude::IndexedRandom, rng, rngs::ThreadRng};
61use serde::Deserialize;
62use tracing::{debug, trace, warn};
63
64#[derive(Debug, Deserialize)]
66struct SoundtrackCollection<T> {
67 tracks: Vec<T>,
69}
70
71impl<T> Default for SoundtrackCollection<T> {
72 fn default() -> Self { Self { tracks: Vec::new() } }
73}
74
75#[derive(Clone, Debug, Deserialize)]
77pub struct SoundtrackItem {
78 title: String,
80 path: String,
82 length: f32,
84 loop_points: Option<(f32, f32)>,
85 timing: Option<DayPeriod>,
87 weather: Option<WeatherKind>,
89 biomes: Vec<(BiomeKind, u8)>,
91 sites: Vec<SiteKindMeta>,
93 music_state: MusicState,
96 #[serde(default)]
100 activity_override: Option<MusicActivity>,
101 artist: (String, Option<String>),
103}
104
105#[derive(Clone, Debug, Deserialize)]
106enum RawSoundtrackItem {
107 Individual(SoundtrackItem),
108 Segmented {
109 title: String,
110 timing: Option<DayPeriod>,
111 weather: Option<WeatherKind>,
112 biomes: Vec<(BiomeKind, u8)>,
113 sites: Vec<SiteKindMeta>,
114 segments: Vec<(String, f32, MusicState, Option<MusicActivity>)>,
115 loop_points: (f32, f32),
116 artist: (String, Option<String>),
117 },
118}
119
120#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
121enum CombatIntensity {
122 Low,
123 High,
124}
125
126#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
127enum MusicActivity {
128 Explore,
129 Combat(CombatIntensity),
130}
131
132#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
133enum MusicState {
134 Activity(MusicActivity),
135 Transition(MusicActivity, MusicActivity),
136}
137
138#[derive(Clone, Debug, Deserialize, PartialEq)]
140enum DayPeriod {
141 Day,
143 Night,
145}
146
147pub struct MusicMgr {
149 soundtrack: SoundtrackCollection<SoundtrackItem>,
151 began_playing: Option<ClockTime>,
153 song_end: Option<ClockTime>,
155 is_gap: bool,
157 gap_length: f32,
159 gap_time: f64,
161 last_track: String,
164 last_combat_track: String,
165 last_interrupt_attempt: Option<ClockTime>,
167 last_activity: MusicState,
169 current_track: String,
171 current_artist: String,
172 track_length: f32,
173 loop_points: Option<(f32, f32)>,
174 last_site: SiteKindMeta,
176}
177
178#[derive(Deserialize)]
179pub struct MusicTransitionManifest {
180 combat_nearby_radius: f32,
182 combat_health_factor: f32,
185 combat_nearby_high_thresh: u32,
187 combat_nearby_low_thresh: u32,
189 pub fade_timings: HashMap<(MusicChannelTag, MusicChannelTag), (f32, f32)>,
191 pub interrupt_delay: f32,
193}
194
195impl Default for MusicTransitionManifest {
196 fn default() -> MusicTransitionManifest {
197 MusicTransitionManifest {
198 combat_nearby_radius: 40.0,
199 combat_health_factor: 100.0,
200 combat_nearby_high_thresh: 3,
201 combat_nearby_low_thresh: 1,
202 fade_timings: HashMap::new(),
203 interrupt_delay: 5.0,
204 }
205 }
206}
207
208fn time_f64(clock_time: ClockTime) -> f64 { clock_time.ticks as f64 + clock_time.fraction }
209
210impl MusicMgr {
211 pub fn new(calendar: &Calendar) -> Self {
212 Self {
213 soundtrack: Self::load_soundtrack_items(calendar),
214 began_playing: None,
215 song_end: None,
216 is_gap: true,
217 gap_length: 0.0,
218 gap_time: -1.0,
219 last_track: String::from("None"),
220 last_combat_track: String::from("None"),
221 last_interrupt_attempt: None,
222 last_activity: MusicState::Activity(MusicActivity::Explore),
223 current_track: String::from("None"),
224 current_artist: String::from("None"),
225 track_length: 0.0,
226 loop_points: None,
227 last_site: SiteKindMeta::Void,
228 }
229 }
230
231 pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
234 use common::comp::{Group, Health, Pos, group::ENEMY};
235 use specs::{Join, WorldExt};
236
237 if !audio.music_enabled() || audio.get_clock().is_none() || audio.get_clock_time().is_none()
238 {
239 return;
240 }
241
242 let mut activity_state = MusicActivity::Explore;
243
244 let player = client.entity();
245 let ecs = state.ecs();
246 let entities = ecs.entities();
247 let positions = ecs.read_component::<Pos>();
248 let healths = ecs.read_component::<Health>();
249 let groups = ecs.read_component::<Group>();
250 let mtm = audio.mtm.read();
251 let mut rng = rng();
252
253 if audio.combat_music_enabled
254 && let Some(player_pos) = positions.get(player)
255 {
256 let num_nearby_entities: u32 = (&entities, &positions, &healths, &groups)
260 .join()
261 .map(|(entity, pos, health, group)| {
262 if entity != player
263 && group == &ENEMY
264 && (player_pos.0 - pos.0).magnitude_squared()
265 < mtm.0.combat_nearby_radius.powf(2.0)
266 {
267 (health.maximum() / mtm.0.combat_health_factor).ceil() as u32
268 } else {
269 0
270 }
271 })
272 .sum();
273
274 if num_nearby_entities >= mtm.0.combat_nearby_high_thresh {
275 activity_state = MusicActivity::Combat(CombatIntensity::High);
276 } else if num_nearby_entities >= mtm.0.combat_nearby_low_thresh {
277 activity_state = MusicActivity::Combat(CombatIntensity::Low);
278 }
279 }
280
281 if let Some(health) = healths.get(player)
283 && health.is_dead
284 {
285 activity_state = MusicActivity::Explore;
286 }
287
288 let mut music_state = match self.last_activity {
289 MusicState::Activity(prev) => {
290 if prev != activity_state {
291 MusicState::Transition(prev, activity_state)
292 } else {
293 MusicState::Activity(activity_state)
294 }
295 },
296 MusicState::Transition(_, next) => {
297 warn!("Transitioning: {:?}", self.last_activity);
298 MusicState::Activity(next)
299 },
300 };
301
302 let now = audio.get_clock_time().unwrap();
303
304 let began_playing = *self.began_playing.get_or_insert(now);
305 let last_interrupt_attempt = *self.last_interrupt_attempt.get_or_insert(now);
306 let song_end = *self.song_end.get_or_insert(now);
307 let time_since_began_playing = time_f64(now) - time_f64(began_playing);
308
309 let current_site = client.current_site();
311 let site_changed = current_site != self.last_site;
312 self.last_site = current_site;
313
314 if (site_changed)
318 && !matches!(
319 music_state,
320 MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
321 )
322 {
323 music_state = MusicState::Transition(MusicActivity::Explore, MusicActivity::Explore);
324 }
325
326 let interrupt = matches!(music_state, MusicState::Transition(_, _))
333 && (matches!(
334 music_state,
335 MusicState::Transition(MusicActivity::Explore, MusicActivity::Explore)
336 ) || time_f64(now) - time_f64(last_interrupt_attempt)
337 > mtm.0.interrupt_delay as f64);
338
339 if matches!(
342 music_state,
343 MusicState::Transition(
344 MusicActivity::Combat(CombatIntensity::High),
345 MusicActivity::Explore
346 )
347 ) {
348 music_state = MusicState::Activity(MusicActivity::Explore)
349 }
350
351 if audio.music_enabled()
352 && !self.soundtrack.tracks.is_empty()
353 && (time_since_began_playing
354 > time_f64(song_end) - time_f64(began_playing) || interrupt)
356 {
357 if time_since_began_playing > self.track_length as f64
358 && self.last_activity
359 != MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
360 {
361 self.current_track = String::from("None");
362 self.current_artist = String::from("None");
363 }
364
365 if interrupt {
366 self.last_interrupt_attempt = Some(now);
367 self.is_gap = false;
368 self.gap_time = 0.0;
369 self.gap_length = 0.0;
370 let track_state = if music_state
374 == MusicState::Transition(MusicActivity::Explore, MusicActivity::Explore)
375 {
376 MusicState::Activity(MusicActivity::Explore)
377 } else {
378 music_state
379 };
380 if let Ok(next_activity) =
381 self.play_random_track(audio, state, client, &track_state, &mut rng)
382 {
383 trace!(
384 "pre-play_random_track: {:?} {:?}",
385 self.last_activity, music_state
386 );
387 self.last_activity = next_activity;
388 }
389 } else if music_state == MusicState::Activity(MusicActivity::Explore)
390 || music_state
391 == MusicState::Transition(
392 MusicActivity::Explore,
393 MusicActivity::Combat(CombatIntensity::High),
394 )
395 {
396 if !self.is_gap {
398 self.gap_length = self.generate_silence_between_tracks(
399 audio.music_spacing,
400 client,
401 &music_state,
402 &mut rng,
403 );
404 self.gap_time = self.gap_length as f64;
405 self.song_end = audio.get_clock_time();
406 self.is_gap = true
407 } else if self.gap_time < 0.0 {
408 if music_state
411 == MusicState::Transition(
412 MusicActivity::Explore,
413 MusicActivity::Combat(CombatIntensity::High),
414 )
415 {
416 music_state = MusicState::Activity(MusicActivity::Explore)
417 }
418 if let Ok(next_activity) =
419 self.play_random_track(audio, state, client, &music_state, &mut rng)
420 {
421 self.last_activity = next_activity;
422 self.gap_time = 0.0;
423 self.gap_length = 0.0;
424 self.is_gap = false;
425 }
426 }
427 } else if music_state
428 == MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
429 {
430 self.began_playing = Some(now);
432 self.song_end = Some(ClockTime::from_ticks_f64(
433 audio.get_clock().unwrap().id(),
434 time_f64(now) + self.loop_points.unwrap_or((0.0, 0.0)).1 as f64
435 - self.loop_points.unwrap_or((0.0, 0.0)).0 as f64,
436 ));
437 } else {
438 trace!(
439 "pre-play_random_track: {:?} {:?}",
440 self.last_activity, music_state
441 );
442 }
443 } else {
444 if self.began_playing.is_none() {
445 self.began_playing = Some(now)
446 }
447 if self.soundtrack.tracks.is_empty() {
448 warn!("No tracks available to play")
449 }
450 }
451
452 if time_since_began_playing > self.track_length as f64 {
453 if self.is_gap {
455 self.gap_time = (self.gap_length as f64) - (time_f64(now) - time_f64(song_end));
456 }
457 }
458 }
459
460 fn play_random_track(
461 &mut self,
462 audio: &mut AudioFrontend,
463 state: &State,
464 client: &Client,
465 music_state: &MusicState,
466 rng: &mut ThreadRng,
467 ) -> Result<MusicState, String> {
468 let is_dark = state.get_day_period().is_dark();
469 let current_period_of_day = Self::get_current_day_period(is_dark);
470 let current_weather = client.weather_at_player();
471 let current_biome = client.current_biome();
472 let current_site = client.current_site();
473
474 let mut maybe_tracks = self
481 .soundtrack
482 .tracks
483 .iter()
484 .filter(|track| {
485 (match &track.timing {
486 Some(period_of_day) => period_of_day == ¤t_period_of_day,
487 None => true,
488 }) && match &track.weather {
489 Some(weather) => weather == ¤t_weather.get_kind(),
490 None => true,
491 }
492 })
493 .filter(|track| track.sites.iter().any(|s| s == ¤t_site))
494 .filter(|track| {
495 track.biomes.is_empty() || track.biomes.iter().any(|b| b.0 == current_biome)
496 })
497 .filter(|track| &track.music_state == music_state)
498 .collect::<Vec<&SoundtrackItem>>();
499 if maybe_tracks.is_empty() {
500 maybe_tracks =
503 self.soundtrack
504 .tracks
505 .iter()
506 .filter(|track| match ¤t_site {
507 SiteKindMeta::Settlement(_) => track.sites.iter().any(|site| {
508 site == &SiteKindMeta::Settlement(SettlementKindMeta::Default)
509 }),
510 SiteKindMeta::Dungeon(_) => track
511 .sites
512 .iter()
513 .any(|site| site == &SiteKindMeta::Dungeon(DungeonKindMeta::Cultist)),
514 _ => false,
515 })
516 .collect::<Vec<&SoundtrackItem>>();
517 let mut error_string = format!(
518 "No tracks for {:?}, {:?}, {:?}, {:?}, {:?}",
519 ¤t_period_of_day,
520 ¤t_weather,
521 ¤t_site,
522 ¤t_biome,
523 &music_state
524 );
525 if maybe_tracks.is_empty() {
526 return Err(error_string);
527 } else {
528 error_string.push_str(", using default music for current site.");
529 warn!(error_string);
530 }
531 }
532 if matches!(
535 music_state,
536 &MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
537 | &MusicState::Transition(
538 MusicActivity::Combat(CombatIntensity::High),
539 MusicActivity::Explore
540 )
541 ) {
542 let filtered_tracks: Vec<_> = maybe_tracks
543 .iter()
544 .filter(|track| track.title.eq(&self.last_track))
545 .copied()
546 .collect();
547 if !filtered_tracks.is_empty() {
548 maybe_tracks = filtered_tracks;
549 }
550 } else {
551 let filtered_tracks: Vec<_> = maybe_tracks
552 .iter()
553 .filter(|track| !track.title.eq(&self.last_track))
554 .filter(|track| !track.title.eq(&self.last_combat_track))
555 .copied()
556 .collect();
557 if !filtered_tracks.is_empty() {
558 maybe_tracks = filtered_tracks;
559 }
560 }
561
562 let new_maybe_track = maybe_tracks.choose_weighted(rng, |track| {
565 track
569 .biomes
570 .iter()
571 .find(|b| b.0 == current_biome)
572 .map_or(1.0, |b| 1.0_f32 / (b.1 as f32))
573 });
574 debug!(
575 "selecting new track for {:?}: {:?}",
576 music_state, new_maybe_track
577 );
578
579 if let Ok(track) = new_maybe_track {
580 let now = audio.get_clock_time().unwrap();
581 self.last_track = String::from(&track.title);
582 self.began_playing = Some(now);
583 self.song_end = Some(ClockTime::from_ticks_f64(
584 audio.get_clock().unwrap().id(),
585 time_f64(now) + track.length as f64,
586 ));
587 self.track_length = track.length;
588 self.gap_length = 0.0;
589 if audio.music_enabled() {
590 self.current_track = String::from(&track.title);
591 self.current_artist = String::from(&track.artist.0);
592 } else {
593 self.current_track = String::from("None");
594 self.current_artist = String::from("None");
595 }
596
597 let tag = if matches!(
598 music_state,
599 MusicState::Activity(MusicActivity::Explore)
600 | MusicState::Transition(MusicActivity::Explore, MusicActivity::Explore)
601 ) {
602 MusicChannelTag::Exploration
603 } else {
604 self.last_combat_track = String::from(&track.title);
605 MusicChannelTag::Combat
606 };
607 audio.play_music(&track.path, tag, track.length);
608 if tag == MusicChannelTag::Combat {
609 audio.set_loop_points(
610 tag,
611 track.loop_points.unwrap_or((0.0, 0.0)).0,
612 track.loop_points.unwrap_or((0.0, 0.0)).1,
613 );
614 self.loop_points = track.loop_points
615 } else {
616 self.loop_points = None
617 };
618
619 if let Some(state) = track.activity_override {
620 Ok(MusicState::Activity(state))
621 } else {
622 Ok(*music_state)
623 }
624 } else {
625 Err(format!("{:?}", new_maybe_track))
626 }
627 }
628
629 fn generate_silence_between_tracks(
630 &self,
631 spacing_multiplier: f32,
632 client: &Client,
633 music_state: &MusicState,
634 rng: &mut ThreadRng,
635 ) -> f32 {
636 let mut silence_between_tracks_seconds: f32 = 0.0;
637 if spacing_multiplier > f32::EPSILON {
638 silence_between_tracks_seconds =
639 if matches!(
640 music_state,
641 MusicState::Activity(MusicActivity::Explore)
642 | MusicState::Transition(
643 MusicActivity::Explore,
644 MusicActivity::Combat(CombatIntensity::High)
645 )
646 ) && matches!(client.current_site(), SiteKindMeta::Settlement(_))
647 {
648 rng.random_range(120.0 * spacing_multiplier..180.0 * spacing_multiplier)
649 } else if matches!(
650 music_state,
651 MusicState::Activity(MusicActivity::Explore)
652 | MusicState::Transition(
653 MusicActivity::Explore,
654 MusicActivity::Combat(CombatIntensity::High)
655 )
656 ) && matches!(client.current_site(), SiteKindMeta::Dungeon(_))
657 {
658 rng.random_range(10.0 * spacing_multiplier..20.0 * spacing_multiplier)
659 } else if matches!(
660 music_state,
661 MusicState::Activity(MusicActivity::Explore)
662 | MusicState::Transition(
663 MusicActivity::Explore,
664 MusicActivity::Combat(CombatIntensity::High)
665 )
666 ) && matches!(client.current_site(), SiteKindMeta::Cave)
667 {
668 rng.random_range(20.0 * spacing_multiplier..40.0 * spacing_multiplier)
669 } else if matches!(
670 music_state,
671 MusicState::Activity(MusicActivity::Explore)
672 | MusicState::Transition(
673 MusicActivity::Explore,
674 MusicActivity::Combat(CombatIntensity::High)
675 )
676 ) {
677 rng.random_range(120.0 * spacing_multiplier..240.0 * spacing_multiplier)
678 } else if matches!(
679 music_state,
680 MusicState::Activity(MusicActivity::Combat(_)) | MusicState::Transition(_, _)
681 ) {
682 0.0
683 } else {
684 rng.random_range(30.0 * spacing_multiplier..60.0 * spacing_multiplier)
685 };
686 }
687 silence_between_tracks_seconds
688 }
689
690 fn get_current_day_period(is_dark: bool) -> DayPeriod {
691 if is_dark {
692 DayPeriod::Night
693 } else {
694 DayPeriod::Day
695 }
696 }
697
698 pub fn current_track(&self) -> String { self.current_track.clone() }
699
700 pub fn current_artist(&self) -> String { self.current_artist.clone() }
701
702 pub fn reset_track(&mut self, audio: &mut AudioFrontend) {
703 self.current_artist = String::from("None");
704 self.current_track = String::from("None");
705 self.gap_length = 1.0;
706 self.gap_time = 1.0;
707 self.is_gap = true;
708 self.track_length = 0.0;
709 self.began_playing = audio.get_clock_time();
710 self.song_end = audio.get_clock_time();
711 }
712
713 fn load_soundtrack_items(calendar: &Calendar) -> SoundtrackCollection<SoundtrackItem> {
717 let mut soundtrack = SoundtrackCollection::default();
718 if calendar.events().len() == 0 {
720 for track in SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
721 .read()
722 .tracks
723 .clone()
724 {
725 soundtrack.tracks.push(track)
726 }
727 } else {
728 for event in calendar.events() {
730 match event {
731 CalendarEvent::Halloween => {
732 for track in SoundtrackCollection::load_expect(
733 "voxygen.audio.calendar.halloween.soundtrack",
734 )
735 .read()
736 .tracks
737 .clone()
738 {
739 soundtrack.tracks.push(track)
740 }
741 },
742 CalendarEvent::Christmas => {
743 for track in SoundtrackCollection::load_expect(
744 "voxygen.audio.calendar.christmas.soundtrack",
745 )
746 .read()
747 .tracks
748 .clone()
749 {
750 soundtrack.tracks.push(track)
751 }
752 },
753 _ => {
754 for track in SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
755 .read()
756 .tracks
757 .clone()
758 {
759 soundtrack.tracks.push(track)
760 }
761 },
762 }
763 }
764 }
765 if soundtrack.tracks.is_empty() {
767 for track in SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
768 .read()
769 .tracks
770 .clone()
771 {
772 soundtrack.tracks.push(track)
773 }
774 soundtrack
775 } else {
776 soundtrack
777 }
778 }
779}
780impl Asset for SoundtrackCollection<SoundtrackItem> {
781 fn load(_: &AssetCache, id: &SharedString) -> Result<Self, BoxedError> {
782 let manifest: AssetHandle<Ron<SoundtrackCollection<RawSoundtrackItem>>> =
783 AssetExt::load(id)?;
784 let mut soundtrack = SoundtrackCollection::default();
785 for item in manifest.read().0.tracks.iter().cloned() {
786 match item {
787 RawSoundtrackItem::Individual(track) => soundtrack.tracks.push(track),
788 RawSoundtrackItem::Segmented {
789 title,
790 timing,
791 weather,
792 biomes,
793 sites,
794 segments,
795 loop_points,
796 artist,
797 } => {
798 for (path, length, music_state, activity_override) in segments.into_iter() {
799 soundtrack.tracks.push(SoundtrackItem {
800 title: title.clone(),
801 path,
802 length,
803 loop_points: Some(loop_points),
804 timing: timing.clone(),
805 weather,
806 biomes: biomes.clone(),
807 sites: sites.clone(),
808 music_state,
809 activity_override,
810 artist: artist.clone(),
811 });
812 }
813 },
814 }
815 }
816 Ok(soundtrack)
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823 use strum::IntoEnumIterator;
824
825 #[test]
826 fn test_load_soundtracks() {
827 let _: AssetHandle<SoundtrackCollection<SoundtrackItem>> =
828 AssetExt::load_expect("voxygen.audio.soundtrack");
829 for event in CalendarEvent::iter() {
830 match event {
831 CalendarEvent::Halloween => {
832 let _: AssetHandle<SoundtrackCollection<SoundtrackItem>> =
833 SoundtrackCollection::load_expect(
834 "voxygen.audio.calendar.halloween.soundtrack",
835 );
836 },
837 CalendarEvent::Christmas => {
838 let _: AssetHandle<SoundtrackCollection<SoundtrackItem>> =
839 SoundtrackCollection::load_expect(
840 "voxygen.audio.calendar.christmas.soundtrack",
841 );
842 },
843 _ => {},
844 }
845 }
846 }
847}