2020-06-14 16:56:32 +00:00
|
|
|
//! Handles music playback and transitions
|
|
|
|
//!
|
|
|
|
//! Game music is controlled though a configuration file found in the source at
|
|
|
|
//! `/assets/voxygen/audio/soundtrack.ron`. Each track enabled in game has a
|
|
|
|
//! configuration corresponding to the
|
|
|
|
//! [`SoundtrackItem`](struct.SoundtrackItem.html) format, as well as the
|
|
|
|
//! corresponding `.ogg` file in the `/assets/voxygen/audio/soundtrack/`
|
|
|
|
//! directory.
|
|
|
|
//!
|
|
|
|
//! If there are errors while reading or deserialising the configuration file, a
|
|
|
|
//! warning is logged and music will be disabled.
|
|
|
|
//!
|
|
|
|
//! ## Adding new music
|
|
|
|
//!
|
|
|
|
//! To add a new item, append the details to the audio configuration file, and
|
|
|
|
//! add the audio file (in `.ogg` format) to the assets directory.
|
|
|
|
//!
|
|
|
|
//! The `length` should be provided in seconds. This allows us to know when to
|
|
|
|
//! transition to another track, without having to spend time determining track
|
|
|
|
//! length programmatically.
|
|
|
|
//!
|
|
|
|
//! An example of a new night time track:
|
|
|
|
//! ```text
|
|
|
|
//! (
|
|
|
|
//! title: "Sleepy Song",
|
|
|
|
//! path: "voxygen.audio.soundtrack.sleepy",
|
|
|
|
//! length: 400.0,
|
|
|
|
//! timing: Some(Night),
|
2020-11-12 03:55:40 +00:00
|
|
|
//! biomes: [
|
2020-11-11 08:51:14 +00:00
|
|
|
//! (Forest, 1),
|
|
|
|
//! (Grassland, 2),
|
|
|
|
//! ],
|
|
|
|
//! site: None,
|
2021-04-12 00:43:08 +00:00
|
|
|
//! activity: Explore,
|
2020-06-14 16:56:32 +00:00
|
|
|
//! artist: "Elvis",
|
|
|
|
//! ),
|
|
|
|
//! ```
|
|
|
|
//!
|
|
|
|
//! Before sending an MR for your new track item:
|
|
|
|
//! - Be conscious of the file size for your new track. Assets contribute to
|
|
|
|
//! download sizes
|
|
|
|
//! - Ensure that the track is mastered to a volume proportionate to other music
|
|
|
|
//! tracks
|
|
|
|
//! - If you are not the author of the track, ensure that the song's licensing
|
|
|
|
//! permits usage of the track for non-commercial use
|
2020-11-14 00:27:09 +00:00
|
|
|
use crate::audio::{AudioFrontend, MusicChannelTag};
|
2020-10-16 23:52:09 +00:00
|
|
|
use client::Client;
|
2020-11-02 07:19:28 +00:00
|
|
|
use common::{
|
2020-12-12 22:14:24 +00:00
|
|
|
assets::{self, AssetExt, AssetHandle},
|
2020-11-02 07:19:28 +00:00
|
|
|
terrain::{BiomeKind, SitesKind},
|
|
|
|
};
|
2021-04-06 15:47:03 +00:00
|
|
|
use common_state::State;
|
2021-04-08 21:42:39 +00:00
|
|
|
use hashbrown::HashMap;
|
2020-11-06 08:05:29 +00:00
|
|
|
use rand::{prelude::SliceRandom, thread_rng, Rng};
|
2020-02-03 11:55:32 +00:00
|
|
|
use serde::Deserialize;
|
|
|
|
use std::time::Instant;
|
2021-04-09 01:26:03 +00:00
|
|
|
use tracing::{debug, warn};
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-11-09 07:05:07 +00:00
|
|
|
/// Collection of all the tracks
|
2021-04-07 01:54:01 +00:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
struct SoundtrackCollection<T> {
|
2020-11-09 07:05:07 +00:00
|
|
|
/// List of tracks
|
2021-04-07 01:54:01 +00:00
|
|
|
tracks: Vec<T>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T> Default for SoundtrackCollection<T> {
|
|
|
|
fn default() -> Self { Self { tracks: Vec::new() } }
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Configuration for a single music track in the soundtrack
|
2021-04-07 01:54:01 +00:00
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
2020-02-03 11:55:32 +00:00
|
|
|
pub struct SoundtrackItem {
|
2020-11-09 07:05:07 +00:00
|
|
|
/// Song title
|
2020-02-03 11:55:32 +00:00
|
|
|
title: String,
|
2020-11-09 07:05:07 +00:00
|
|
|
/// File path to asset
|
2020-02-03 11:55:32 +00:00
|
|
|
path: String,
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Length of the track in seconds
|
2020-11-06 08:05:29 +00:00
|
|
|
length: f32,
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Whether this track should play during day or night
|
2020-02-03 11:55:32 +00:00
|
|
|
timing: Option<DayPeriod>,
|
2020-11-09 07:05:07 +00:00
|
|
|
/// What biomes this track should play in with chance of play
|
2020-11-04 23:22:56 +00:00
|
|
|
biomes: Vec<(BiomeKind, u8)>,
|
2020-11-09 07:05:07 +00:00
|
|
|
/// Whether this track should play in a specific site
|
2020-11-02 07:19:28 +00:00
|
|
|
site: Option<SitesKind>,
|
2021-03-12 23:00:09 +00:00
|
|
|
/// What the player is doing when the track is played (i.e. exploring,
|
|
|
|
/// combat)
|
2021-04-11 21:47:45 +00:00
|
|
|
music_state: MusicState,
|
2021-04-07 19:53:41 +00:00
|
|
|
/// What activity to override the activity state with, if any (e.g. to make
|
|
|
|
/// a long combat intro also act like the loop for the purposes of outro
|
|
|
|
/// transitions)
|
|
|
|
#[serde(default)]
|
2021-04-11 21:47:45 +00:00
|
|
|
activity_override: Option<MusicActivity>,
|
2021-03-12 23:00:09 +00:00
|
|
|
}
|
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
enum RawSoundtrackItem {
|
|
|
|
Individual(SoundtrackItem),
|
|
|
|
Segmented {
|
|
|
|
title: String,
|
|
|
|
timing: Option<DayPeriod>,
|
|
|
|
biomes: Vec<(BiomeKind, u8)>,
|
|
|
|
site: Option<SitesKind>,
|
2021-04-11 21:47:45 +00:00
|
|
|
segments: Vec<(String, f32, MusicState, Option<MusicActivity>)>,
|
2021-04-07 01:54:01 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
|
|
|
enum CombatIntensity {
|
|
|
|
Low,
|
|
|
|
High,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
2021-04-11 21:47:45 +00:00
|
|
|
enum MusicActivity {
|
2021-03-12 23:00:09 +00:00
|
|
|
Explore,
|
2021-04-07 01:54:01 +00:00
|
|
|
Combat(CombatIntensity),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
2021-04-11 21:47:45 +00:00
|
|
|
enum MusicState {
|
|
|
|
Activity(MusicActivity),
|
|
|
|
Transition(MusicActivity, MusicActivity),
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Allows control over when a track should play based on in-game time of day
|
2021-04-07 01:54:01 +00:00
|
|
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
2020-02-03 11:55:32 +00:00
|
|
|
enum DayPeriod {
|
2020-06-14 16:56:32 +00:00
|
|
|
/// 8:00 AM to 7:30 PM
|
|
|
|
Day,
|
|
|
|
/// 7:31 PM to 6:59 AM
|
|
|
|
Night,
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 22:10:32 +00:00
|
|
|
/// Determines whether the sound is stopped, playing, or fading
|
|
|
|
#[derive(Debug, Deserialize, PartialEq)]
|
|
|
|
enum PlayState {
|
|
|
|
Playing,
|
|
|
|
Stopped,
|
|
|
|
FadingOut,
|
|
|
|
FadingIn,
|
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Provides methods to control music playback
|
2020-02-03 11:55:32 +00:00
|
|
|
pub struct MusicMgr {
|
2020-11-09 07:05:07 +00:00
|
|
|
/// Collection of all the tracks
|
2021-04-07 01:54:01 +00:00
|
|
|
soundtrack: AssetHandle<SoundtrackCollection<SoundtrackItem>>,
|
2020-11-09 07:05:07 +00:00
|
|
|
/// Instant at which the current track began playing
|
2020-02-03 11:55:32 +00:00
|
|
|
began_playing: Instant,
|
2020-11-09 07:05:07 +00:00
|
|
|
/// Time until the next track should be played
|
2020-11-06 08:05:29 +00:00
|
|
|
next_track_change: f32,
|
2020-06-14 16:56:32 +00:00
|
|
|
/// The title of the last track played. Used to prevent a track
|
|
|
|
/// being played twice in a row
|
2020-02-03 11:55:32 +00:00
|
|
|
last_track: String,
|
2021-04-11 21:47:45 +00:00
|
|
|
/// Time of the last interrupt (to avoid rapid switching)
|
|
|
|
last_interrupt: Instant,
|
2021-04-07 01:54:01 +00:00
|
|
|
/// The previous track's activity kind, for transitions
|
2021-04-11 21:47:45 +00:00
|
|
|
last_activity: MusicState,
|
2021-04-07 19:53:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
2021-04-08 21:42:39 +00:00
|
|
|
pub struct MusicTransitionManifest {
|
2021-04-07 19:53:41 +00:00
|
|
|
/// Within what radius do enemies count towards combat music?
|
|
|
|
combat_nearby_radius: f32,
|
|
|
|
/// Each multiple of this factor that an enemy has health counts as an extra
|
|
|
|
/// enemy
|
|
|
|
combat_health_factor: u32,
|
|
|
|
/// How many nearby enemies trigger High combat music
|
|
|
|
combat_nearby_high_thresh: u32,
|
|
|
|
/// How many nearby enemies trigger Low combat music
|
|
|
|
combat_nearby_low_thresh: u32,
|
2021-04-08 21:42:39 +00:00
|
|
|
/// Fade in and fade out timings for transitions between channels
|
|
|
|
pub fade_timings: HashMap<(MusicChannelTag, MusicChannelTag), (f32, f32)>,
|
2021-04-11 21:47:45 +00:00
|
|
|
/// How many seconds between interrupt checks
|
|
|
|
pub interrupt_delay: f32,
|
2021-04-07 19:53:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for MusicTransitionManifest {
|
|
|
|
fn default() -> MusicTransitionManifest {
|
|
|
|
MusicTransitionManifest {
|
|
|
|
combat_nearby_radius: 40.0,
|
|
|
|
combat_health_factor: 1000,
|
|
|
|
combat_nearby_high_thresh: 3,
|
|
|
|
combat_nearby_low_thresh: 1,
|
2021-04-08 21:42:39 +00:00
|
|
|
fade_timings: HashMap::new(),
|
2021-04-11 21:47:45 +00:00
|
|
|
interrupt_delay: 5.0,
|
2021-04-07 19:53:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl assets::Asset for MusicTransitionManifest {
|
|
|
|
type Loader = assets::RonLoader;
|
|
|
|
|
|
|
|
const EXTENSION: &'static str = "ron";
|
|
|
|
}
|
|
|
|
|
2020-11-09 07:05:07 +00:00
|
|
|
impl Default for MusicMgr {
|
|
|
|
fn default() -> Self {
|
2020-02-03 11:55:32 +00:00
|
|
|
Self {
|
|
|
|
soundtrack: Self::load_soundtrack_items(),
|
|
|
|
began_playing: Instant::now(),
|
|
|
|
next_track_change: 0.0,
|
|
|
|
last_track: String::from("None"),
|
2021-04-11 21:47:45 +00:00
|
|
|
last_interrupt: Instant::now(),
|
|
|
|
last_activity: MusicState::Activity(MusicActivity::Explore),
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-09 07:05:07 +00:00
|
|
|
}
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-11-09 07:05:07 +00:00
|
|
|
impl MusicMgr {
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Checks whether the previous track has completed. If so, sends a
|
|
|
|
/// request to play the next (random) track
|
2020-10-16 23:52:09 +00:00
|
|
|
pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
|
2020-11-07 01:55:59 +00:00
|
|
|
//if let Some(current_chunk) = client.current_chunk() {
|
|
|
|
//println!("biome: {:?}", current_chunk.meta().biome());
|
|
|
|
//println!("chaos: {}", current_chunk.meta().chaos());
|
|
|
|
//println!("alt: {}", current_chunk.meta().alt());
|
2020-11-11 08:51:14 +00:00
|
|
|
//println!("tree_density: {}",
|
|
|
|
// current_chunk.meta().tree_density());
|
2021-04-28 05:07:59 +00:00
|
|
|
// let current_site = client.current_site();
|
|
|
|
// println!("{:?}", current_site);
|
2020-11-14 00:27:09 +00:00
|
|
|
//if let Some(position) = client.current::<comp::Pos>() {
|
|
|
|
// player_alt = position.0.z;
|
2020-11-07 01:55:59 +00:00
|
|
|
//}
|
2020-11-02 07:19:28 +00:00
|
|
|
|
2021-04-07 02:32:16 +00:00
|
|
|
use common::comp::{group::ENEMY, Group, Health, Pos};
|
2021-04-07 01:54:01 +00:00
|
|
|
use specs::{Join, WorldExt};
|
2021-04-11 21:47:45 +00:00
|
|
|
|
|
|
|
let mut activity_state = MusicActivity::Explore;
|
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
let player = client.entity();
|
|
|
|
let ecs = state.ecs();
|
|
|
|
let entities = ecs.entities();
|
|
|
|
let positions = ecs.read_component::<Pos>();
|
|
|
|
let healths = ecs.read_component::<Health>();
|
2021-04-07 02:32:16 +00:00
|
|
|
let groups = ecs.read_component::<Group>();
|
2021-04-11 21:47:45 +00:00
|
|
|
let mtm = audio.mtm.read();
|
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
if let Some(player_pos) = positions.get(player) {
|
2021-04-09 01:26:03 +00:00
|
|
|
// TODO: `group::ENEMY` will eventually be moved server-side with an
|
|
|
|
// alignment/faction rework, so this will need an alternative way to measure
|
|
|
|
// "in-combat-ness"
|
2021-04-07 02:32:16 +00:00
|
|
|
let num_nearby_entities: u32 = (&entities, &positions, &healths, &groups)
|
2021-04-07 01:54:01 +00:00
|
|
|
.join()
|
2021-04-07 02:32:16 +00:00
|
|
|
.map(|(entity, pos, health, group)| {
|
2021-04-07 01:54:01 +00:00
|
|
|
if entity != player
|
2021-04-07 02:32:16 +00:00
|
|
|
&& group == &ENEMY
|
2021-04-07 19:53:41 +00:00
|
|
|
&& (player_pos.0 - pos.0).magnitude_squared()
|
|
|
|
< mtm.combat_nearby_radius.powf(2.0)
|
2021-04-07 01:54:01 +00:00
|
|
|
{
|
2021-04-07 19:53:41 +00:00
|
|
|
(health.maximum() / mtm.combat_health_factor).max(1)
|
2021-04-07 01:54:01 +00:00
|
|
|
} else {
|
|
|
|
0
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.sum();
|
|
|
|
|
2021-04-07 19:53:41 +00:00
|
|
|
if num_nearby_entities >= mtm.combat_nearby_high_thresh {
|
2021-04-11 21:47:45 +00:00
|
|
|
activity_state = MusicActivity::Combat(CombatIntensity::High);
|
2021-04-07 19:53:41 +00:00
|
|
|
} else if num_nearby_entities >= mtm.combat_nearby_low_thresh {
|
2021-04-11 21:47:45 +00:00
|
|
|
activity_state = MusicActivity::Combat(CombatIntensity::Low);
|
2021-04-07 01:54:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-08 21:42:39 +00:00
|
|
|
// Override combat music with explore music if the player is dead
|
|
|
|
if let Some(health) = healths.get(player) {
|
|
|
|
if health.is_dead {
|
2021-04-11 21:47:45 +00:00
|
|
|
activity_state = MusicActivity::Explore;
|
2021-04-08 21:42:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-11 21:47:45 +00:00
|
|
|
let music_state = match self.last_activity {
|
|
|
|
MusicState::Activity(prev) => {
|
|
|
|
if prev != activity_state {
|
|
|
|
MusicState::Transition(prev, activity_state)
|
|
|
|
} else {
|
|
|
|
MusicState::Activity(activity_state)
|
|
|
|
}
|
2021-04-07 19:53:41 +00:00
|
|
|
},
|
2021-04-11 21:47:45 +00:00
|
|
|
MusicState::Transition(_, next) => MusicState::Activity(next),
|
2021-04-07 01:54:01 +00:00
|
|
|
};
|
|
|
|
|
2021-04-11 21:47:45 +00:00
|
|
|
let interrupt = matches!(music_state, MusicState::Transition(_, _))
|
|
|
|
&& self.last_interrupt.elapsed().as_secs_f32() > mtm.interrupt_delay;
|
2021-04-07 01:54:01 +00:00
|
|
|
|
2020-02-03 11:55:32 +00:00
|
|
|
if audio.music_enabled()
|
2020-12-12 22:14:24 +00:00
|
|
|
&& !self.soundtrack.read().tracks.is_empty()
|
2021-04-07 01:54:01 +00:00
|
|
|
&& (self.began_playing.elapsed().as_secs_f32() > self.next_track_change || interrupt)
|
2020-02-03 11:55:32 +00:00
|
|
|
{
|
2021-04-11 21:47:45 +00:00
|
|
|
if interrupt {
|
|
|
|
self.last_interrupt = Instant::now();
|
|
|
|
}
|
2021-04-09 01:26:03 +00:00
|
|
|
debug!(
|
2021-04-07 19:53:41 +00:00
|
|
|
"pre-play_random_track: {:?} {:?}",
|
2021-04-11 21:47:45 +00:00
|
|
|
self.last_activity, music_state
|
2021-04-07 19:53:41 +00:00
|
|
|
);
|
2021-04-11 21:47:45 +00:00
|
|
|
if let Ok(next_activity) = self.play_random_track(audio, state, client, &music_state) {
|
2021-04-07 19:53:41 +00:00
|
|
|
self.last_activity = next_activity;
|
2021-04-07 01:54:01 +00:00
|
|
|
}
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
fn play_random_track(
|
|
|
|
&mut self,
|
|
|
|
audio: &mut AudioFrontend,
|
|
|
|
state: &State,
|
|
|
|
client: &Client,
|
2021-04-11 21:47:45 +00:00
|
|
|
music_state: &MusicState,
|
|
|
|
) -> Result<MusicState, ()> {
|
2020-11-09 07:05:07 +00:00
|
|
|
let mut rng = thread_rng();
|
|
|
|
|
|
|
|
// Adds a bit of randomness between plays
|
2021-04-07 01:54:01 +00:00
|
|
|
let silence_between_tracks_seconds: f32 =
|
2021-04-11 21:47:45 +00:00
|
|
|
if matches!(music_state, MusicState::Activity(MusicActivity::Explore)) {
|
2021-06-17 05:49:09 +00:00
|
|
|
rng.gen_range(90.0..180.0)
|
2021-04-07 01:54:01 +00:00
|
|
|
} else {
|
|
|
|
0.0
|
|
|
|
};
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2021-04-12 00:43:08 +00:00
|
|
|
let is_dark = (state.get_day_period().is_dark()) as bool;
|
|
|
|
let current_period_of_day = Self::get_current_day_period(is_dark);
|
2020-11-14 06:52:20 +00:00
|
|
|
let current_biome = client.current_biome();
|
|
|
|
let current_site = client.current_site();
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
// Filter the soundtrack in stages, so that we don't overprune it if there are
|
|
|
|
// too many constraints. Returning Err(()) signals that we couldn't find
|
|
|
|
// an appropriate track for the current state, and hence the state
|
|
|
|
// machine for the activity shouldn't be updated.
|
2020-12-12 22:14:24 +00:00
|
|
|
let soundtrack = self.soundtrack.read();
|
2021-04-09 01:26:03 +00:00
|
|
|
// First, filter out tracks not matching the timing, site, biome, and current
|
|
|
|
// activity
|
2021-04-07 01:54:01 +00:00
|
|
|
let mut maybe_tracks = soundtrack
|
2020-02-03 11:55:32 +00:00
|
|
|
.tracks
|
|
|
|
.iter()
|
|
|
|
.filter(|track| {
|
2021-04-07 01:54:01 +00:00
|
|
|
(match &track.timing {
|
|
|
|
Some(period_of_day) => period_of_day == ¤t_period_of_day,
|
|
|
|
None => true,
|
|
|
|
}) && match &track.site {
|
|
|
|
Some(site) => site == ¤t_site,
|
|
|
|
None => true,
|
|
|
|
}
|
2020-11-02 07:19:28 +00:00
|
|
|
})
|
2020-11-04 23:22:56 +00:00
|
|
|
.filter(|track| {
|
2021-04-11 21:47:45 +00:00
|
|
|
track.biomes.is_empty() || track.biomes.iter().any(|b| b.0 == current_biome)
|
2020-11-04 06:41:54 +00:00
|
|
|
})
|
2021-04-11 21:47:45 +00:00
|
|
|
.filter(|track| &track.music_state == music_state)
|
2021-04-09 01:26:03 +00:00
|
|
|
.collect::<Vec<&SoundtrackItem>>();
|
|
|
|
if maybe_tracks.is_empty() {
|
2021-04-08 21:42:39 +00:00
|
|
|
return Err(());
|
2021-04-07 01:54:01 +00:00
|
|
|
}
|
2021-04-09 01:26:03 +00:00
|
|
|
// Second, prevent playing the last track if possible (though don't return Err
|
2021-04-07 01:54:01 +00:00
|
|
|
// here, since the combat music is intended to loop)
|
|
|
|
let filtered_tracks: Vec<_> = maybe_tracks
|
|
|
|
.iter()
|
|
|
|
.filter(|track| !track.title.eq(&self.last_track))
|
2021-04-11 21:47:45 +00:00
|
|
|
.copied()
|
2021-04-07 01:54:01 +00:00
|
|
|
.collect();
|
|
|
|
if !filtered_tracks.is_empty() {
|
|
|
|
maybe_tracks = filtered_tracks;
|
|
|
|
}
|
2020-11-04 06:41:54 +00:00
|
|
|
|
2020-11-09 07:05:07 +00:00
|
|
|
// Randomly selects a track from the remaining tracks weighted based
|
|
|
|
// on the biome
|
|
|
|
let new_maybe_track = maybe_tracks.choose_weighted(&mut rng, |track| {
|
2021-04-11 21:47:45 +00:00
|
|
|
// If no biome is listed, the song is still added to the
|
|
|
|
// rotation to allow for site specific songs to play
|
|
|
|
// in any biome
|
|
|
|
track
|
|
|
|
.biomes
|
|
|
|
.iter()
|
|
|
|
.find(|b| b.0 == current_biome)
|
|
|
|
.map_or(1, |b| b.1)
|
2020-11-04 06:41:54 +00:00
|
|
|
});
|
2021-04-07 01:54:01 +00:00
|
|
|
debug!(
|
|
|
|
"selecting new track for {:?}: {:?}",
|
2021-04-11 21:47:45 +00:00
|
|
|
music_state, new_maybe_track
|
2021-04-07 01:54:01 +00:00
|
|
|
);
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-11-04 06:41:54 +00:00
|
|
|
if let Ok(track) = new_maybe_track {
|
2020-11-12 03:55:40 +00:00
|
|
|
//println!("Now playing {:?}", track.title);
|
2020-06-14 16:56:32 +00:00
|
|
|
self.last_track = String::from(&track.title);
|
|
|
|
self.began_playing = Instant::now();
|
2020-11-06 08:05:29 +00:00
|
|
|
self.next_track_change = track.length + silence_between_tracks_seconds;
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2021-04-11 21:47:45 +00:00
|
|
|
let tag = if matches!(music_state, MusicState::Activity(MusicActivity::Explore)) {
|
2021-04-07 01:54:01 +00:00
|
|
|
MusicChannelTag::Exploration
|
|
|
|
} else {
|
|
|
|
MusicChannelTag::Combat
|
|
|
|
};
|
|
|
|
audio.play_music(&track.path, tag);
|
2021-04-08 21:42:39 +00:00
|
|
|
|
|
|
|
if let Some(state) = track.activity_override {
|
2021-04-11 21:47:45 +00:00
|
|
|
Ok(MusicState::Activity(state))
|
2021-04-08 21:42:39 +00:00
|
|
|
} else {
|
2021-04-11 21:47:45 +00:00
|
|
|
Ok(*music_state)
|
2021-04-08 21:42:39 +00:00
|
|
|
}
|
2021-04-07 01:54:01 +00:00
|
|
|
} else {
|
|
|
|
Err(())
|
2020-06-14 16:56:32 +00:00
|
|
|
}
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2021-04-12 00:43:08 +00:00
|
|
|
fn get_current_day_period(is_dark: bool) -> DayPeriod {
|
|
|
|
if is_dark {
|
2020-02-03 11:55:32 +00:00
|
|
|
DayPeriod::Night
|
2021-04-12 00:43:08 +00:00
|
|
|
} else {
|
|
|
|
DayPeriod::Day
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
fn load_soundtrack_items() -> AssetHandle<SoundtrackCollection<SoundtrackItem>> {
|
2020-12-12 22:14:24 +00:00
|
|
|
// Cannot fail: A default value is always provided
|
|
|
|
SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
2021-04-07 01:54:01 +00:00
|
|
|
impl assets::Asset for SoundtrackCollection<RawSoundtrackItem> {
|
2020-12-12 22:14:24 +00:00
|
|
|
type Loader = assets::RonLoader;
|
|
|
|
|
2020-12-13 01:09:57 +00:00
|
|
|
const EXTENSION: &'static str = "ron";
|
2021-04-07 01:54:01 +00:00
|
|
|
}
|
2020-12-13 01:09:57 +00:00
|
|
|
|
2021-04-07 01:54:01 +00:00
|
|
|
impl assets::Compound for SoundtrackCollection<SoundtrackItem> {
|
|
|
|
fn load<S: assets::source::Source>(
|
|
|
|
_: &assets::AssetCache<S>,
|
|
|
|
id: &str,
|
|
|
|
) -> Result<Self, assets::Error> {
|
|
|
|
let inner = || -> Result<_, assets::Error> {
|
2021-06-25 16:47:03 +00:00
|
|
|
let manifest: AssetHandle<SoundtrackCollection<RawSoundtrackItem>> =
|
2021-04-07 01:54:01 +00:00
|
|
|
AssetExt::load(id)?;
|
|
|
|
let mut soundtracks = SoundtrackCollection::default();
|
2021-06-25 16:47:03 +00:00
|
|
|
for item in manifest.read().tracks.iter().cloned() {
|
2021-04-07 01:54:01 +00:00
|
|
|
match item {
|
|
|
|
RawSoundtrackItem::Individual(track) => soundtracks.tracks.push(track),
|
|
|
|
RawSoundtrackItem::Segmented {
|
|
|
|
title,
|
|
|
|
timing,
|
|
|
|
biomes,
|
|
|
|
site,
|
|
|
|
segments,
|
|
|
|
} => {
|
2021-04-11 21:47:45 +00:00
|
|
|
for (path, length, music_state, activity_override) in segments.into_iter() {
|
2021-04-07 01:54:01 +00:00
|
|
|
soundtracks.tracks.push(SoundtrackItem {
|
|
|
|
title: title.clone(),
|
|
|
|
path,
|
|
|
|
length,
|
|
|
|
timing: timing.clone(),
|
|
|
|
biomes: biomes.clone(),
|
|
|
|
site,
|
2021-04-11 21:47:45 +00:00
|
|
|
music_state,
|
2021-04-07 19:53:41 +00:00
|
|
|
activity_override,
|
2021-04-07 01:54:01 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(soundtracks)
|
|
|
|
};
|
|
|
|
match inner() {
|
|
|
|
Ok(soundtracks) => Ok(soundtracks),
|
|
|
|
Err(e) => {
|
|
|
|
warn!("Error loading soundtracks: {:?}", e);
|
|
|
|
Ok(SoundtrackCollection::default())
|
|
|
|
},
|
|
|
|
}
|
2020-12-12 22:14:24 +00:00
|
|
|
}
|
2020-12-13 01:09:57 +00:00
|
|
|
}
|