Replace the Achievement system with just plain events which are processed as part of the server tick. Instead of adding Achievement updates to a component belonging to the entity, use an event queue - this allows an entity to have multiple achievement checks in a single tick.

This commit is contained in:
Shane Handley 2020-07-06 07:43:02 +10:00
parent e607ec498d
commit efe7a62c92
11 changed files with 118 additions and 138 deletions

View File

@ -997,6 +997,7 @@ impl Client {
}, },
ServerMsg::CharacterAchievementDataError(error) => { ServerMsg::CharacterAchievementDataError(error) => {
// TODO handle somehow // TODO handle somehow
tracing::info!(?error, "Failed to load achievements");
}, },
ServerMsg::AchievementCompletion(achievement) => { ServerMsg::AchievementCompletion(achievement) => {
// TODO handle in UI // TODO handle in UI

View File

@ -1,10 +1,16 @@
use crate::comp::{ use crate::comp::item::{Consumable, Item, ItemKind};
self, use specs::{Component, Entity, FlaggedStorage};
item::{Consumable, Item, ItemKind},
};
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
/// Used for in-game events that contribute towards player achievements.
///
/// For example, when an `InventoryManip` is detected, we record that event in
/// order to process achievements which depend on collecting items.
pub struct AchievementTrigger {
pub entity: Entity,
pub event: AchievementEvent,
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AchievementEvent { pub enum AchievementEvent {
None, None,
@ -35,7 +41,7 @@ pub struct AchievementItem {
} }
impl AchievementItem { impl AchievementItem {
pub fn matches_event(&self, event: AchievementEvent) -> bool { pub fn matches_event(&self, event: &AchievementEvent) -> bool {
match event { match event {
AchievementEvent::KilledNpc => self.action == AchievementAction::KillNpcs, AchievementEvent::KilledNpc => self.action == AchievementAction::KillNpcs,
AchievementEvent::KilledPlayer => self.action == AchievementAction::KillPlayers, AchievementEvent::KilledPlayer => self.action == AchievementAction::KillPlayers,
@ -51,14 +57,6 @@ impl AchievementItem {
_ => false, _ => false,
}, },
AchievementEvent::None => false, AchievementEvent::None => false,
_ => {
tracing::warn!(
?event,
"An AchievementEvent was processed but the event was not handled"
);
false
},
} }
} }
} }
@ -79,10 +77,10 @@ impl Achievement {
/// incremented by 1. This covers many cases, but using this method allows /// incremented by 1. This covers many cases, but using this method allows
/// handling of unique types of achievements which are not simple /// handling of unique types of achievements which are not simple
/// counters for events /// counters for events
pub fn increment_progress(&mut self, event: AchievementEvent) -> bool { pub fn increment_progress(&mut self, event: &AchievementEvent) -> bool {
match event { match event {
AchievementEvent::LevelUp(level) => { AchievementEvent::LevelUp(level) => {
self.progress = level as usize; self.progress = *level as usize;
}, },
_ => self.progress += 1, _ => self.progress += 1,
}; };
@ -123,7 +121,7 @@ impl AchievementList {
pub fn process_achievement( pub fn process_achievement(
&mut self, &mut self,
achievement: Achievement, achievement: Achievement,
event: AchievementEvent, event: &AchievementEvent,
) -> bool { ) -> bool {
let id = achievement.id; let id = achievement.id;
@ -131,7 +129,7 @@ impl AchievementList {
self.0.push(achievement); self.0.push(achievement);
} }
return if let Some(char_achievement) = self.item_by_id(id) { if let Some(char_achievement) = self.item_by_id(id) {
if char_achievement.completed { if char_achievement.completed {
return false; return false;
} }
@ -141,30 +139,10 @@ impl AchievementList {
tracing::warn!("Failed to find achievement after inserting"); tracing::warn!("Failed to find achievement after inserting");
false false
}; }
} }
} }
/// Used as a container for in-game events that contribute towards player
/// achievements.
///
/// For example, when an `InventoryManip` is detected, we record that event in
/// order to process achievements which depend on collecting items.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AchievementUpdate {
event: AchievementEvent,
}
impl AchievementUpdate {
pub fn new(event: AchievementEvent) -> Self { Self { event } }
pub fn event(&self) -> AchievementEvent { self.event.clone() }
}
impl Component for AchievementUpdate {
type Storage = IDVStorage<Self>;
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -181,7 +159,7 @@ mod tests {
let event = let event =
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.apple")); AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.apple"));
assert!(item.matches_event(event)); assert!(item.matches_event(&event));
} }
#[test] #[test]
@ -195,7 +173,7 @@ mod tests {
let event = let event =
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.apple")); AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.apple"));
assert_eq!(item.matches_event(event), false); assert_eq!(item.matches_event(&event), false);
} }
#[test] #[test]
@ -208,7 +186,7 @@ mod tests {
let event = AchievementEvent::LevelUp(3); let event = AchievementEvent::LevelUp(3);
assert_eq!(item.matches_event(event), true); assert_eq!(item.matches_event(&event), true);
} }
#[test] #[test]
@ -233,12 +211,12 @@ mod tests {
// The first two increments should not indicate that it is complete // The first two increments should not indicate that it is complete
assert_eq!( assert_eq!(
achievement_list.process_achievement(achievement.clone(), event.clone()), achievement_list.process_achievement(achievement.clone(), &event),
false false
); );
assert_eq!( assert_eq!(
achievement_list.process_achievement(achievement.clone(), event.clone()), achievement_list.process_achievement(achievement.clone(), &event),
false false
); );
@ -246,7 +224,7 @@ mod tests {
// It should return true when completed // It should return true when completed
assert_eq!( assert_eq!(
achievement_list.process_achievement(achievement, event), achievement_list.process_achievement(achievement, &event),
true true
); );
@ -274,7 +252,8 @@ mod tests {
let mut achievement_list = AchievementList::default(); let mut achievement_list = AchievementList::default();
assert_eq!( assert_eq!(
achievement_list.process_achievement(achievement.clone(), AchievementEvent::LevelUp(6)), achievement_list
.process_achievement(achievement.clone(), &AchievementEvent::LevelUp(6)),
false false
); );
@ -283,7 +262,7 @@ mod tests {
assert_eq!(achievement_list.0.get(0).unwrap().completed, false); assert_eq!(achievement_list.0.get(0).unwrap().completed, false);
assert_eq!( assert_eq!(
achievement_list.process_achievement(achievement, AchievementEvent::LevelUp(10)), achievement_list.process_achievement(achievement, &AchievementEvent::LevelUp(10)),
true true
); );
@ -313,7 +292,7 @@ mod tests {
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom")); AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
assert_eq!( assert_eq!(
achievement_list.process_achievement(achievement, event), achievement_list.process_achievement(achievement, &event),
false false
); );

View File

@ -23,7 +23,7 @@ mod visual;
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
pub use achievement::{ pub use achievement::{
Achievement, AchievementAction, AchievementEvent, AchievementItem, AchievementList, Achievement, AchievementAction, AchievementEvent, AchievementItem, AchievementList,
AchievementUpdate, AchievementTrigger,
}; };
pub use admin::{Admin, AdminList}; pub use admin::{Admin, AdminList};
pub use agent::{Agent, Alignment}; pub use agent::{Agent, Alignment};

View File

@ -150,7 +150,6 @@ impl State {
ecs.register::<comp::WaypointArea>(); ecs.register::<comp::WaypointArea>();
ecs.register::<comp::ForceUpdate>(); ecs.register::<comp::ForceUpdate>();
ecs.register::<comp::InventoryUpdate>(); ecs.register::<comp::InventoryUpdate>();
ecs.register::<comp::AchievementUpdate>();
ecs.register::<comp::Admin>(); ecs.register::<comp::Admin>();
ecs.register::<comp::Waypoint>(); ecs.register::<comp::Waypoint>();
ecs.register::<comp::Projectile>(); ecs.register::<comp::Projectile>();

View File

@ -2,9 +2,10 @@ use crate::{client::Client, Server, SpawnPoint, StateExt};
use common::{ use common::{
assets, assets,
comp::{ comp::{
self, item::lottery::Lottery, object, AchievementEvent, Alignment, Body, HealthChange, self, item::lottery::Lottery, object, AchievementEvent, AchievementTrigger, Alignment,
HealthSource, Player, Stats, Body, HealthChange, HealthSource, Player, Stats,
}, },
event::EventBus,
msg::{PlayerListUpdate, ServerMsg}, msg::{PlayerListUpdate, ServerMsg},
state::BlockChange, state::BlockChange,
sync::{Uid, WorldSyncExt}, sync::{Uid, WorldSyncExt},
@ -90,10 +91,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
}, },
_ => None, _ => None,
} { } {
let _ = state state
.ecs() .ecs()
.write_storage() .read_resource::<EventBus<AchievementTrigger>>()
.insert(entity, comp::AchievementUpdate::new(event)); .emit_now(AchievementTrigger { entity, event });
} }
}); });
} }
@ -339,11 +340,15 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
.get(entity) .get(entity)
.expect("Failed to fetch uid component for entity."); .expect("Failed to fetch uid component for entity.");
// Write an achievement update to trigger level achievements // Emit an achievement check for the level up
let _ = server.state.ecs().write_storage().insert( server
entity, .state
comp::AchievementUpdate::new(AchievementEvent::LevelUp(new_level)), .ecs()
); .read_resource::<EventBus<AchievementTrigger>>()
.emit_now(AchievementTrigger {
entity,
event: AchievementEvent::LevelUp(new_level),
});
server server
.state .state

View File

@ -3,9 +3,9 @@ use common::{
comp::{ comp::{
self, item, self, item,
slot::{self, Slot}, slot::{self, Slot},
AchievementEvent, Pos, MAX_PICKUP_RANGE_SQR, AchievementEvent, AchievementTrigger, Pos, MAX_PICKUP_RANGE_SQR,
}, },
event::AchievementEvent, event::{AchievementEvent, EventBus},
sync::{Uid, WorldSyncExt}, sync::{Uid, WorldSyncExt},
terrain::block::Block, terrain::block::Block,
vol::{ReadVol, Vox}, vol::{ReadVol, Vox},
@ -86,10 +86,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let item = picked_up_item.unwrap(); let item = picked_up_item.unwrap();
state.write_component( state
entity, .ecs()
comp::AchievementUpdate::new(AchievementEvent::CollectedItem(item.clone())), .read_resource::<EventBus<AchievementTrigger>>()
); .emit_now(AchievementTrigger {
entity,
event: AchievementEvent::CollectedItem(item.clone()),
});
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(item)) comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(item))
} else { } else {
@ -119,12 +122,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
&& state.try_set_block(pos, Block::empty()).is_some() && state.try_set_block(pos, Block::empty()).is_some()
{ {
comp::Item::try_reclaim_from_block(block).map(|item| { comp::Item::try_reclaim_from_block(block).map(|item| {
state.write_component( state
entity, .ecs()
comp::AchievementUpdate::new(AchievementEvent::CollectedItem( .read_resource::<EventBus<AchievementTrigger>>()
item.clone(), .emit_now(AchievementTrigger {
)), entity,
); event: AchievementEvent::CollectedItem(item.clone()),
});
state.give_item(entity, item); state.give_item(entity, item);
}); });

View File

@ -125,4 +125,8 @@ impl Server {
frontend_events frontend_events
} }
// pub fn handle_achievement_events(&mut self) -> {
// }
} }

View File

@ -43,7 +43,7 @@ use futures_util::{select, FutureExt};
use metrics::{ServerMetrics, TickMetrics}; use metrics::{ServerMetrics, TickMetrics};
use network::{Address, Network, Pid}; use network::{Address, Network, Pid};
use persistence::{ use persistence::{
achievement::{AchievementLoader, AchievementLoaderResponse, AvailableAchievements}, achievement::{Achievement, AchievementLoader, AchievementLoaderResponse},
character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater}, character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater},
}; };
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt}; use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
@ -89,6 +89,8 @@ pub struct Server {
metrics: ServerMetrics, metrics: ServerMetrics,
tick_metrics: TickMetrics, tick_metrics: TickMetrics,
achievement_data: Vec<Achievement>,
} }
impl Server { impl Server {
@ -101,6 +103,9 @@ impl Server {
// Event Emitters // Event Emitters
state.ecs_mut().insert(EventBus::<ServerEvent>::default()); state.ecs_mut().insert(EventBus::<ServerEvent>::default());
state
.ecs_mut()
.insert(EventBus::<comp::AchievementTrigger>::default());
state.ecs_mut().insert(AuthProvider::new( state.ecs_mut().insert(AuthProvider::new(
settings.auth_server_address.clone(), settings.auth_server_address.clone(),
@ -261,13 +266,14 @@ impl Server {
// TODO I switched this to return comp::Achievement but that's not right...we // TODO I switched this to return comp::Achievement but that's not right...we
// want the id really, // want the id really,
match persistence::achievement::sync(&settings.persistence_db_dir) { let achievement_data = match persistence::achievement::sync(&settings.persistence_db_dir) {
Ok(achievements) => { Ok(achievements) => achievements,
info!("Achievement data loaded..."); Err(e) => {
state.ecs_mut().insert(AvailableAchievements(achievements)); error!(?e, "Achievement data migration error");
Vec::new()
}, },
Err(e) => error!(?e, "Achievement data migration error"), };
}
let this = Self { let this = Self {
state, state,
@ -280,6 +286,8 @@ impl Server {
metrics, metrics,
tick_metrics, tick_metrics,
achievement_data,
}; };
debug!(?settings, "created veloren server with"); debug!(?settings, "created veloren server with");
@ -429,6 +437,43 @@ impl Server {
} }
} }
// Achievement processing
let achievement_events = self
.state
.ecs()
.read_resource::<EventBus<comp::AchievementTrigger>>()
.recv_all();
for trigger in achievement_events {
// Get the achievement that matches this event
for achievement in &self.achievement_data {
let achievement_item = comp::AchievementItem::from(&achievement.details);
if achievement_item.matches_event(&trigger.event) {
// Calls to `process_achievement` return true to indicate that the
// achievement is complete. In this case, we notify the client to notify them of
// completing the achievement
if let Some(achievement_list) = self
.state
.ecs()
.write_storage::<comp::AchievementList>()
.get_mut(trigger.entity)
{
if achievement_list.process_achievement(
comp::Achievement::from(achievement),
&trigger.event,
) == true
{
self.notify_client(
trigger.entity,
ServerMsg::AchievementCompletion(achievement_item),
)
}
}
}
}
}
// 7 Persistence updates // 7 Persistence updates
let before_persistence_updates = Instant::now(); let before_persistence_updates = Instant::now();

View File

@ -21,6 +21,8 @@ use std::{
}; };
use tracing::{error, info, warn}; use tracing::{error, info, warn};
pub type Achievement = AchievementModel;
/// Available database operations when modifying a player's characetr list /// Available database operations when modifying a player's characetr list
enum AchievementLoaderRequestKind { enum AchievementLoaderRequestKind {
LoadCharacterAchievementList { LoadCharacterAchievementList {
@ -237,10 +239,3 @@ pub fn hash<T: Hash>(t: &T) -> u64 {
t.hash(&mut s); t.hash(&mut s);
s.finish() s.finish()
} }
/// Holds a list of achievements available to players.
///
/// This acts as the reference for checks on achievements, and holds id's as
/// well as details of achievement
#[derive(Debug)]
pub struct AvailableAchievements(pub Vec<AchievementModel>);

View File

@ -1,48 +0,0 @@
use crate::persistence::achievement::AvailableAchievements;
use crate::client::Client;
use common::{
comp::{Achievement, AchievementItem, AchievementList, AchievementUpdate},
msg::ServerMsg,
};
use specs::{Join, ReadExpect, System, WriteStorage};
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
type SystemData = (
WriteStorage<'a, Client>,
WriteStorage<'a, AchievementList>,
WriteStorage<'a, AchievementUpdate>,
ReadExpect<'a, AvailableAchievements>,
);
fn run(
&mut self,
(mut clients, mut achievement_lists, mut achievement_updates, available_achievements): Self::SystemData,
) {
// TODO filter out achievements which do not care about this event here, then
// iterate over them
for (client, achievement_list, ach_update) in
(&mut clients, &mut achievement_lists, &achievement_updates).join()
{
(available_achievements.0).iter().for_each(|achievement| {
let achievement_item = AchievementItem::from(&achievement.details);
if achievement_item.matches_event(ach_update.event()) {
// Calls to `process_achievement` return true to indicate that the
// achievement is complete. In this case, we notify the client to notify them of
// completing the achievement
if achievement_list
.process_achievement(Achievement::from(achievement), ach_update.event())
== true
{
client.notify(ServerMsg::AchievementCompletion(achievement_item));
}
}
});
}
achievement_updates.clear();
}
}

View File

@ -1,4 +1,3 @@
pub mod achievement;
pub mod entity_sync; pub mod entity_sync;
pub mod message; pub mod message;
pub mod object; pub mod object;
@ -54,9 +53,6 @@ pub fn run_sync_systems(ecs: &mut specs::World) {
// Sync // Sync
terrain_sync::Sys.run_now(ecs); terrain_sync::Sys.run_now(ecs);
entity_sync::Sys.run_now(ecs); entity_sync::Sys.run_now(ecs);
// Test
achievement::Sys.run_now(ecs);
} }
/// Used to schedule systems to run at an interval /// Used to schedule systems to run at an interval