Merge branch 'shandley/sfx-system' into 'master'

SFX events

See merge request veloren/veloren!653
This commit is contained in:
Songtronix 2019-11-23 08:26:39 +00:00
commit 9de10759d1
36 changed files with 693 additions and 139 deletions

View File

@ -0,0 +1,67 @@
(
items: [
(
trigger: Run,
files: [
"voxygen.audio.sfx.footsteps.stepdirt_1",
"voxygen.audio.sfx.footsteps.stepdirt_2",
"voxygen.audio.sfx.footsteps.stepdirt_3",
"voxygen.audio.sfx.footsteps.stepdirt_4",
"voxygen.audio.sfx.footsteps.stepdirt_5",
"voxygen.audio.sfx.footsteps.stepdirt_6",
"voxygen.audio.sfx.footsteps.stepdirt_7",
"voxygen.audio.sfx.footsteps.stepdirt_8",
],
threshold: 0.25,
),
(
trigger: GliderOpen,
files: [
"voxygen.audio.sfx.glider_open",
],
threshold: 0.5,
),
(
trigger: GliderClose,
files: [
"voxygen.audio.sfx.glider_close",
],
threshold: 0.5,
),
(
trigger: Attack(Sword),
files: [
"voxygen.audio.sfx.weapon.sword",
],
threshold: 0.5,
),
(
trigger: Attack(Hammer),
files: [
"voxygen.audio.sfx.weapon.sword",
],
threshold: 0.5,
),
(
trigger: Attack(Bow),
files: [
"voxygen.audio.sfx.weapon.bow",
],
threshold: 0.5,
),
(
trigger: LevelUp,
files: [
"voxygen.audio.sfx.chat_message_received"
],
threshold: 0.2,
),
(
trigger: InventoryAdd,
files: [
"voxygen.audio.sfx.inventory_add"
],
threshold: 0.2,
),
],
)

BIN
assets/voxygen/audio/sfx/blank.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/chat_message_received.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/audio/sfx/glider_close.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/glider_open.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/inventory_add.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/steps/step_1.wav (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/audio/sfx/steps/step_2.wav (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/audio/sfx/steps/step_3.wav (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/audio/sfx/steps/step_4.wav (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/audio/sfx/steps/step_5.wav (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/audio/sfx/weapon/bow.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/weapon/sword.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,10 +1,53 @@
use crate::comp;
use comp::item::Tool;
use parking_lot::Mutex;
use serde::Deserialize;
use specs::Entity as EcsEntity;
use sphynx::Uid;
use std::{collections::VecDeque, ops::DerefMut};
use vek::*;
pub struct SfxEventItem {
pub sfx: SfxEvent,
pub pos: Option<Vec3<f32>>,
}
impl SfxEventItem {
pub fn new(sfx: SfxEvent, pos: Option<Vec3<f32>>) -> Self {
Self { sfx, pos }
}
pub fn at_player_position(sfx: SfxEvent) -> Self {
Self { sfx, pos: None }
}
}
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
pub enum SfxEvent {
Idle,
PlaceBlock,
RemoveBlock,
OpenChest,
ChatMessageReceived,
OpenBag,
LevelUp,
Roll,
Climb,
Swim,
Run,
GliderOpen,
Glide,
GliderClose,
Jump,
Fall,
InventoryAdd,
InventoryDrop,
LightLantern,
ExtinguishLantern,
Attack(Tool),
AttackWolf,
}
pub enum LocalEvent {
Jump(EcsEntity),
WallLeap {

View File

@ -5,7 +5,7 @@ use serde_json;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq)]
pub enum NpcKind {
Humanoid,
Wolf,

View File

@ -3,7 +3,7 @@ pub use sphynx::Uid;
use crate::{
comp,
event::{EventBus, LocalEvent, ServerEvent},
event::{EventBus, LocalEvent, ServerEvent, SfxEventItem},
msg::{EcsCompPacket, EcsResPacket},
region::RegionMap,
sys,
@ -173,6 +173,7 @@ impl State {
ecs.add_resource(TerrainChanges::default());
ecs.add_resource(EventBus::<ServerEvent>::default());
ecs.add_resource(EventBus::<LocalEvent>::default());
ecs.add_resource(EventBus::<SfxEventItem>::default());
ecs.add_resource(RegionMap::new());
}

View File

@ -1,26 +1,31 @@
use crate::{
comp::{HealthSource, Stats},
event::{EventBus, ServerEvent},
event::{EventBus, ServerEvent, SfxEvent, SfxEventItem},
state::DeltaTime,
};
use specs::{Entities, Join, Read, System, WriteStorage};
/// This system kills players
/// and handles players levelling up
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, DeltaTime>,
Read<'a, EventBus<ServerEvent>>,
Read<'a, EventBus<SfxEventItem>>,
WriteStorage<'a, Stats>,
);
fn run(&mut self, (entities, dt, event_bus, mut stats): Self::SystemData) {
let mut event_emitter = event_bus.emitter();
fn run(
&mut self,
(entities, dt, server_event_bus, audio_event_bus, mut stats): Self::SystemData,
) {
let mut server_event_emitter = server_event_bus.emitter();
for (entity, mut stat) in (&entities, &mut stats).join() {
if stat.should_die() && !stat.is_dead {
event_emitter.emit(ServerEvent::Destroy {
server_event_emitter.emit(ServerEvent::Destroy {
entity,
cause: stat.health.last_change.1.cause,
});
@ -36,6 +41,11 @@ impl<'a> System<'a> for Sys {
stat.exp.change_maximum_by(25);
stat.level.change_by(1);
}
audio_event_bus
.emitter()
.emit(SfxEventItem::at_player_position(SfxEvent::LevelUp));
stat.update_max_hp();
stat.health
.set_to(stat.health.maximum(), HealthSource::LevelUp)

View File

@ -1,6 +1,8 @@
pub mod channel;
pub mod fader;
pub mod sfx;
pub mod soundcache;
use channel::{AudioType, Channel};
use fader::Fader;
use soundcache::SoundCache;

View File

@ -0,0 +1,443 @@
/// sfx::event_mapper watches the local entities and determines which sfx to emit,
/// and the position at which the sound should be emitted from
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers};
use client::Client;
use common::{
comp::{ActionState, Body, CharacterState, ItemKind, MovementState, Pos, Stats, Vel},
event::{EventBus, SfxEvent, SfxEventItem},
};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join};
use std::time::{Duration, Instant};
use vek::*;
#[derive(Clone)]
struct LastSfxEvent {
event: SfxEvent,
time: Instant,
}
pub struct SfxEventMapper {
event_history: HashMap<EcsEntity, LastSfxEvent>,
}
impl SfxEventMapper {
pub fn new() -> Self {
Self {
event_history: HashMap::new(),
}
}
pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) {
const SFX_DIST_LIMIT_SQR: f32 = 22500.0;
let ecs = client.state().ecs();
let player_position = ecs
.read_storage::<Pos>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
for (entity, pos, body, vel, stats, character) in (
&ecs.entities(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Vel>(),
&ecs.read_storage::<Stats>(),
ecs.read_storage::<CharacterState>().maybe(),
)
.join()
.filter(|(_, e_pos, ..)| {
(e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR
})
{
if let (pos, body, Some(character), stats, vel) = (pos, body, character, stats, vel) {
let state = self
.event_history
.entry(entity)
.or_insert_with(|| LastSfxEvent {
event: SfxEvent::Idle,
time: Instant::now(),
});
let mapped_event = match body {
Body::Humanoid(_) => {
Self::map_character_event(character, state.event.clone(), vel.0, stats)
}
Body::QuadrupedMedium(_) => {
Self::map_quadriped_event(character, state.event.clone(), vel.0, stats)
}
_ => SfxEvent::Idle,
};
// Check for SFX config entry for this movement
let sfx_trigger_item: Option<&SfxTriggerItem> = triggers
.items
.iter()
.find(|item| item.trigger == mapped_event);
if Self::should_emit(state, sfx_trigger_item) {
ecs.read_resource::<EventBus<SfxEventItem>>()
.emitter()
.emit(SfxEventItem::new(mapped_event, Some(pos.0)));
// Update the last play time
state.event = mapped_event;
state.time = Instant::now();
} else {
// Keep the last event, it may not have an SFX trigger but it helps us determine the next one
state.event = mapped_event;
}
}
}
self.cleanup(client.entity());
}
/// As the player explores the world, we track the last event of the nearby entities to determine the correct
/// SFX item to play next based on their activity. `cleanup` will remove entities from event tracking if they
/// have not triggered an event for > n seconds. This prevents stale records from bloating the Map size.
fn cleanup(&mut self, player: EcsEntity) {
const TRACKING_TIMEOUT: u64 = 15;
let now = Instant::now();
self.event_history.retain(|entity, event| {
now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
|| entity.id() == player.id()
});
}
/// When specific entity movements are detected, the associated sound (if any) needs to satisfy two conditions to
/// be allowed to play:
/// 1. An sfx.ron entry exists for the movement (we need to know which sound file(s) to play)
/// 2. The sfx has not been played since it's timeout threshold has elapsed, which prevents firing every tick
fn should_emit(
last_play_entry: &LastSfxEvent,
sfx_trigger_item: Option<&SfxTriggerItem>,
) -> bool {
if let Some(item) = sfx_trigger_item {
if last_play_entry.event == item.trigger {
last_play_entry.time.elapsed().as_secs_f64() >= item.threshold
} else {
true
}
} else {
false
}
}
/// Voxygen has an existing list of character states via `MovementState::*` and `ActivityState::*`
/// however that list does not provide enough resolution to target specific entity events, such
/// as opening or closing the glider. These methods translate those entity states with some additional
/// data into more specific `SfxEvent`'s which we attach sounds to
fn map_quadriped_event(
current_event: &CharacterState,
previous_event: SfxEvent,
vel: Vec3<f32>,
stats: &Stats,
) -> SfxEvent {
match (
current_event.movement,
current_event.action,
previous_event,
vel,
stats,
) {
(_, ActionState::Attack { .. }, _, _, stats) => match stats.name.as_ref() {
"Wolf" => SfxEvent::AttackWolf,
_ => SfxEvent::Idle,
},
_ => SfxEvent::Idle,
}
}
fn map_character_event(
current_event: &CharacterState,
previous_event: SfxEvent,
vel: Vec3<f32>,
stats: &Stats,
) -> SfxEvent {
match (
current_event.movement,
current_event.action,
previous_event,
vel,
stats,
) {
(MovementState::Roll { .. }, ..) => SfxEvent::Roll,
(MovementState::Climb, ..) => SfxEvent::Climb,
(MovementState::Swim, ..) => SfxEvent::Swim,
(MovementState::Run, ..) => SfxEvent::Run,
(MovementState::Jump, _, previous_event, vel, _) => {
// MovementState::Jump only indicates !on_ground
if previous_event != SfxEvent::Glide {
if vel.z > 0.0 {
SfxEvent::Jump
} else {
SfxEvent::Fall
}
} else {
SfxEvent::GliderClose
}
}
(MovementState::Glide, _, previous_event, ..) => {
if previous_event != SfxEvent::GliderOpen && previous_event != SfxEvent::Glide {
SfxEvent::GliderOpen
} else {
SfxEvent::Glide
}
}
(_, ActionState::Attack { .. }, _, _, stats) => {
match &stats.equipment.main.as_ref().map(|i| &i.kind) {
Some(ItemKind::Tool { kind, .. }) => SfxEvent::Attack(*kind),
_ => SfxEvent::Idle,
}
}
_ => SfxEvent::Idle,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use common::{
assets,
comp::{item::Tool, ActionState, MovementState, Stats},
event::SfxEvent,
};
use std::time::{Duration, Instant};
#[test]
fn no_item_config_no_emit() {
let last_sfx_event = LastSfxEvent {
event: SfxEvent::Idle,
time: Instant::now(),
};
let result = SfxEventMapper::should_emit(&last_sfx_event, None);
assert_eq!(result, false);
}
#[test]
fn config_but_played_since_threshold_no_emit() {
let trigger_item = SfxTriggerItem {
trigger: SfxEvent::Run,
files: vec![String::from("some.path.to.sfx.file")],
threshold: 1.0,
};
// Triggered a 'Run' 0 seconds ago
let last_sfx_event = LastSfxEvent {
event: SfxEvent::Run,
time: Instant::now(),
};
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
assert_eq!(result, false);
}
#[test]
fn config_and_not_played_since_threshold_emits() {
let trigger_item = SfxTriggerItem {
trigger: SfxEvent::Run,
files: vec![String::from("some.path.to.sfx.file")],
threshold: 0.5,
};
let last_sfx_event = LastSfxEvent {
event: SfxEvent::Idle,
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
};
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
assert_eq!(result, true);
}
#[test]
fn same_previous_event_elapsed_emits() {
let trigger_item = SfxTriggerItem {
trigger: SfxEvent::Run,
files: vec![String::from("some.path.to.sfx.file")],
threshold: 0.5,
};
let last_sfx_event = LastSfxEvent {
event: SfxEvent::Run,
time: Instant::now()
.checked_sub(Duration::from_millis(500))
.unwrap(),
};
let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item));
assert_eq!(result, true);
}
#[test]
fn maps_idle() {
let stats = Stats::new(String::from("Test"), None);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Stand,
action: ActionState::Idle,
},
SfxEvent::Idle,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::Idle);
}
#[test]
fn maps_run() {
let stats = Stats::new(String::from("Test"), None);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Run,
action: ActionState::Idle,
},
SfxEvent::Idle,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::Run);
}
#[test]
fn maps_roll() {
let stats = Stats::new(String::from("Test"), None);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Roll {
time_left: Duration::new(1, 0),
},
action: ActionState::Idle,
},
SfxEvent::Run,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::Roll);
}
#[test]
fn maps_jump_or_fall() {
let stats = Stats::new(String::from("Test"), None);
// positive z velocity, the character is on the rise (jumping)
let vel_jumping = Vec3::new(0.0, 0.0, 1.0);
let positive_result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Jump,
action: ActionState::Idle,
},
SfxEvent::Idle,
vel_jumping,
&stats,
);
assert_eq!(positive_result, SfxEvent::Jump);
// negative z velocity, the character is on the way down (!jumping)
let vel_falling = Vec3::new(0.0, 0.0, -1.0);
let negative_result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Jump,
action: ActionState::Idle,
},
SfxEvent::Idle,
vel_falling,
&stats,
);
assert_eq!(negative_result, SfxEvent::Fall);
}
#[test]
fn maps_glider_open() {
let stats = Stats::new(String::from("Test"), None);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Glide,
action: ActionState::Idle,
},
SfxEvent::Jump,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::GliderOpen);
}
#[test]
fn maps_glide() {
let stats = Stats::new(String::from("Test"), None);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Glide,
action: ActionState::Idle,
},
SfxEvent::Glide,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::Glide);
}
#[test]
fn maps_glider_close() {
let stats = Stats::new(String::from("Test"), None);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Jump,
action: ActionState::Idle,
},
SfxEvent::Glide,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::GliderClose);
}
#[test]
fn maps_attack() {
let stats = Stats::new(
String::from("Test"),
Some(assets::load_expect_cloned(
"common.items.weapons.starter_sword",
)),
);
let result = SfxEventMapper::map_character_event(
&CharacterState {
movement: MovementState::Stand,
action: ActionState::Attack {
time_left: Duration::new(1, 0),
applied: true,
},
},
SfxEvent::Idle,
Vec3::zero(),
&stats,
);
assert_eq!(result, SfxEvent::Attack(Tool::Sword));
}
}

View File

@ -0,0 +1,95 @@
/// The SfxManager listens for SFX events and plays the sound at the provided position
mod event_mapper;
use crate::audio::AudioFrontend;
use client::Client;
use common::{
assets,
comp::{Ori, Pos},
event::{EventBus, SfxEvent, SfxEventItem},
};
use serde::Deserialize;
use vek::*;
#[derive(Deserialize)]
pub struct SfxTriggerItem {
pub trigger: SfxEvent,
pub files: Vec<String>,
pub threshold: f64,
}
#[derive(Deserialize)]
pub struct SfxTriggers {
pub items: Vec<SfxTriggerItem>,
}
pub struct SfxMgr {
triggers: SfxTriggers,
event_mapper: event_mapper::SfxEventMapper,
}
impl SfxMgr {
pub fn new() -> Self {
Self {
triggers: Self::load_sfx_items(),
event_mapper: event_mapper::SfxEventMapper::new(),
}
}
pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) {
self.event_mapper.maintain(client, &self.triggers);
let ecs = client.state().ecs();
let player_position = ecs
.read_storage::<Pos>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
let player_ori = ecs
.read_storage::<Ori>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
audio.set_listener_pos(&player_position, &player_ori);
let events = ecs.read_resource::<EventBus<SfxEventItem>>().recv_all();
for event in events {
let position = match event.pos {
Some(pos) => pos,
_ => player_position,
};
// Get the SFX config entry for this movement
let sfx_trigger_item: Option<&SfxTriggerItem> = self
.triggers
.items
.iter()
.find(|item| item.trigger == event.sfx);
if sfx_trigger_item.is_some() {
let item = sfx_trigger_item.expect("Invalid sfx item");
let sfx_file = match item.files.len() {
1 => item
.files
.last()
.expect("Failed to determine sound file for this trigger item."),
_ => {
let rand_step = rand::random::<usize>() % item.files.len();
&item.files[rand_step]
}
};
audio.play_sound(sfx_file, position);
}
}
}
fn load_sfx_items() -> SfxTriggers {
let file = assets::load_file("voxygen.audio.sfx", &["ron"])
.expect("Failed to load the sfx config file");
ron::de::from_reader(file).expect("Error parsing sfx manifest")
}
}

View File

@ -1,16 +1,14 @@
pub mod camera;
pub mod figure;
pub mod sound;
pub mod terrain;
use self::{
camera::{Camera, CameraMode},
figure::FigureMgr,
sound::SoundMgr,
terrain::Terrain,
};
use crate::{
audio::AudioFrontend,
audio::{sfx::SfxMgr, AudioFrontend},
render::{
create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals,
PostProcessPipeline, Renderer, Shadow, SkyboxLocals, SkyboxPipeline,
@ -58,7 +56,7 @@ pub struct Scene {
select_pos: Option<Vec3<i32>>,
figure_mgr: FigureMgr,
sound_mgr: SoundMgr,
sfx_mgr: SfxMgr,
}
impl Scene {
@ -91,7 +89,7 @@ impl Scene {
select_pos: None,
figure_mgr: FigureMgr::new(),
sound_mgr: SoundMgr::new(),
sfx_mgr: SfxMgr::new(),
}
}
@ -287,8 +285,8 @@ impl Scene {
// Remove unused figures.
self.figure_mgr.clean(client.get_tick());
// Maintain audio
self.sound_mgr.maintain(audio, client);
// Maintain sfx
self.sfx_mgr.maintain(audio, client);
}
/// Render the scene using the provided `Renderer`.

View File

@ -1,69 +0,0 @@
use crate::audio::AudioFrontend;
use client::Client;
use common::comp::{Body, CharacterState, MovementState::*, Ori, Pos};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join};
use std::time::Instant;
use vek::*;
pub struct AnimState {
last_step_sound: Instant,
}
pub struct SoundMgr {
character_states: HashMap<EcsEntity, AnimState>,
}
impl SoundMgr {
pub fn new() -> Self {
Self {
character_states: HashMap::new(),
}
}
pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) {
const SFX_DIST_LIMIT_SQR: f32 = 22500.0;
let ecs = client.state().ecs();
// Get player position.
let player_pos = ecs
.read_storage::<Pos>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
let player_ori = ecs
.read_storage::<Ori>()
.get(client.entity())
.map_or(Vec3::zero(), |pos| pos.0);
audio.set_listener_pos(&player_pos, &player_ori);
for (entity, pos, body, character) in (
&ecs.entities(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<Body>(),
ecs.read_storage::<CharacterState>().maybe(),
)
.join()
.filter(|(_, e_pos, _, _)| (e_pos.0.distance_squared(player_pos)) < SFX_DIST_LIMIT_SQR)
{
if let (Body::Humanoid(_), Some(character)) = (body, character) {
let state = self
.character_states
.entry(entity)
.or_insert_with(|| AnimState {
last_step_sound: Instant::now(),
});
if character.movement == Run && state.last_step_sound.elapsed().as_secs_f64() > 0.25
{
let rand_step = (rand::random::<usize>() % 5) + 1;
audio.play_sound(
&format!("voxygen.audio.sfx.steps.step_{}", rand_step),
pos.0,
);
state.last_step_sound = Instant::now();
}
}
}
}
}