Add mechanisms for loading achievement data, more work on a

migration/sync strategy for dealing with updates to achievements
This commit is contained in:
Shane Handley 2020-06-25 21:15:23 +10:00
parent 707412edc0
commit 3ff5e105dd
21 changed files with 512 additions and 113 deletions

View File

@ -1,10 +0,0 @@
[
(
title: "Collect 10 Apples",
achievement_type: Collect("common.items.apple", 10)
),
(
title: "Collect 50 Apples",
achievement_type: Collect("common.items.apple", 50)
)
]

View File

@ -992,6 +992,14 @@ impl Client {
self.view_distance = Some(vd);
frontend_events.push(Event::SetViewDistance(vd));
},
ServerMsg::AchievementDataUpdate(achievements) => {
// TODO should this happen? Can't you just save it
// against the entity on the server and it will et
// synced?
},
ServerMsg::AchievementDataError(error) => {
// TODO handle somehow
},
}
}
}

View File

@ -1,23 +0,0 @@
use crate::comp::item::{Item, ItemKind};
// TODO: Kill(Race, amount)
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AchievementType {
Collect(String, i32),
ReachLevel(i32),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AchievementItem {
pub title: String,
pub achievement_type: AchievementType,
}
impl Default for AchievementItem {
fn default() -> Self {
Self {
title: String::new(),
achievement_type: AchievementType::ReachLevel(9999),
}
}
}

View File

@ -0,0 +1,99 @@
use crate::{
comp::{self, inventory::item::Consumable, InventoryUpdateEvent},
event::ServerEvent,
};
use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
pub enum AchievementCategory {
CollectConsumable,
ReachLevel,
KillHumanoidSpecies,
KillBodyType,
}
// Potential additions
// - ReachCoordinate
// - CollectCurrency(amount)
// - KillPlayers(amount)
// - OpenChests
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AchievementType {
CollectConsumable(Consumable, i32),
ReachLevel(i32),
KillHumanoidSpecies(comp::body::humanoid::Species, i32),
KillBodyType(comp::Body, i32),
}
impl From<AchievementType> for AchievementCategory {
fn from(achievement_type: AchievementType) -> AchievementCategory {
match achievement_type {
AchievementType::CollectConsumable(_, _) => AchievementCategory::CollectConsumable,
AchievementType::ReachLevel(_) => AchievementCategory::ReachLevel,
AchievementType::KillHumanoidSpecies(_, _) => AchievementCategory::KillHumanoidSpecies,
AchievementType::KillBodyType(_, _) => AchievementCategory::KillBodyType,
}
}
}
/// The representation of an achievement that is declared in .ron config.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AchievementItem {
pub title: String,
pub achievement_type: AchievementType,
}
/// TODO remove this, it's a confusing state
impl Default for AchievementItem {
fn default() -> Self {
Self {
title: String::new(),
achievement_type: AchievementType::ReachLevel(0),
}
}
}
impl AchievementItem {
pub fn matches_event(&self, event: InventoryUpdateEvent) -> bool {
match event {
InventoryUpdateEvent::Collected(_item) => true,
_ => false,
}
}
}
/// The complete representation of an achievement that has been
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Achievement {
pub id: i32,
pub item: AchievementItem,
pub completed: bool,
pub progress: i32,
}
// impl Achievement {
// pub fn incr(&self, event: InventoryUpdateEvent) -> Option<bool> {
//
// }
// }
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AchievementList(Vec<Achievement>);
impl Default for AchievementList {
fn default() -> AchievementList { AchievementList(Vec::new()) }
}
impl AchievementList {
/// Process a single achievement item, inrementing or doing whataver it does
/// to indicate it's one step closer to cmpletion
pub fn process(&self, item: &AchievementItem) -> Option<bool> {
// if self.0.iter().
None
}
}
impl Component for AchievementList {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}

View File

@ -1,4 +1,5 @@
mod ability;
mod achievement;
mod admin;
pub mod agent;
mod body;
@ -20,6 +21,7 @@ mod visual;
// Reexports
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
pub use achievement::{Achievement, AchievementItem, AchievementList, AchievementType};
pub use admin::{Admin, AdminList};
pub use agent::{Agent, Alignment};
pub use body::{

View File

@ -83,6 +83,10 @@ pub enum ServerEvent {
Chat(comp::ChatMsg),
}
pub enum AchievementEvent {
CollectedItem { entity: EcsEntity, item: comp::Item },
}
pub struct EventBus<E> {
queue: Mutex<VecDeque<E>>,
}

View File

@ -11,7 +11,7 @@
option_zip
)]
pub mod achievement;
#[macro_use] extern crate serde_derive;
pub mod assets;
pub mod astar;
pub mod character;

View File

@ -59,6 +59,11 @@ pub enum ServerMsg {
time_of_day: state::TimeOfDay,
world_map: (Vec2<u32>, Vec<u32>),
},
/// A list of achievements which the character has fully or partially
/// completed
AchievementDataUpdate(Vec<comp::Achievement>),
/// An error occurred while loading character achievements
AchievementDataError(String),
/// An error occurred while loading character data
CharacterDataLoadError(String),
/// A list of characters belonging to the a authenticated player was sent

View File

@ -0,0 +1,10 @@
[
(
title: "Collect 10 Apples",
achievement_type: CollectConsumable(Apple, 10)
),
(
title: "Collect 50 Apples",
achievement_type: CollectConsumable(Apple, 50)
),
]

View File

@ -1,10 +0,0 @@
[
(
title: "Collect 10 Apples",
achievement_type: Collect("common.items.apple", 10)
),
(
title: "Collect 50 Apples",
achievement_type: Collect("common.items.apple", 50)
)
]

View File

@ -5,6 +5,7 @@ use common::{
slot::{self, Slot},
Pos, MAX_PICKUP_RANGE_SQR,
},
event::{AchievementEvent, EventBus},
sync::{Uid, WorldSyncExt},
terrain::block::Block,
vol::{ReadVol, Vox},
@ -82,9 +83,18 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
// player's inventory but also left on the ground
panic!("Failed to delete picked up item entity: {:?}", err);
}
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(
picked_up_item.unwrap(),
))
let item = picked_up_item.unwrap();
state
.ecs()
.read_resource::<EventBus<AchievementEvent>>()
.emit_now(AchievementEvent::CollectedItem {
entity,
item: item.clone(),
});
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Collected(item))
} else {
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::CollectFailed)
};
@ -111,8 +121,17 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
} else if block.is_collectible()
&& state.try_set_block(pos, Block::empty()).is_some()
{
comp::Item::try_reclaim_from_block(block)
.map(|item| state.give_item(entity, item));
comp::Item::try_reclaim_from_block(block).map(|item| {
state
.ecs()
.read_resource::<EventBus<AchievementEvent>>()
.emit_now(AchievementEvent::CollectedItem {
entity,
item: item.clone(),
});
state.give_item(entity, item);
});
}
}
},

View File

@ -30,7 +30,7 @@ use crate::{
use common::{
cmd::ChatCommand,
comp::{self, ChatType},
event::{EventBus, ServerEvent},
event::{AchievementEvent, EventBus, ServerEvent},
msg::{ClientState, ServerInfo, ServerMsg},
state::{State, TimeOfDay},
sync::WorldSyncExt,
@ -42,7 +42,10 @@ use futures_timer::Delay;
use futures_util::{select, FutureExt};
use metrics::{ServerMetrics, TickMetrics};
use network::{Address, Network, Pid};
use persistence::character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater};
use persistence::{
achievement::{AchievementLoader, AchievementLoaderResponse, AvailableAchievements},
character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater},
};
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
use std::{
i32,
@ -95,13 +98,24 @@ impl Server {
pub fn new(settings: ServerSettings) -> Result<Self, Error> {
let mut state = State::default();
state.ecs_mut().insert(settings.clone());
// Event Emitters
state.ecs_mut().insert(EventBus::<ServerEvent>::default());
state
.ecs_mut()
.insert(EventBus::<AchievementEvent>::default());
state.ecs_mut().insert(AuthProvider::new(
settings.auth_server_address.clone(),
settings.whitelist.clone(),
));
state.ecs_mut().insert(Tick(0));
state.ecs_mut().insert(ChunkGenerator::new());
state
.ecs_mut()
.insert(comp::AdminList(settings.admins.clone()));
// Persistence interaction
state
.ecs_mut()
.insert(CharacterUpdater::new(settings.persistence_db_dir.clone()));
@ -110,12 +124,7 @@ impl Server {
.insert(CharacterLoader::new(settings.persistence_db_dir.clone()));
state
.ecs_mut()
.insert(persistence::character::CharacterUpdater::new(
settings.persistence_db_dir.clone(),
));
state
.ecs_mut()
.insert(comp::AdminList(settings.admins.clone()));
.insert(AchievementLoader::new(settings.persistence_db_dir.clone()));
// System timers for performance monitoring
state.ecs_mut().insert(sys::EntitySyncTimer::default());
@ -243,6 +252,25 @@ impl Server {
thread_pool.execute(f);
block_on(network.listen(Address::Tcp(settings.gameserver_address)))?;
// Run pending DB migrations (if any)
debug!("Running DB migrations...");
if let Some(e) = persistence::run_migrations(&settings.persistence_db_dir).err() {
info!(?e, "Migration error");
}
// Sync and Load Achievement Data
debug!("Syncing Achievement data...");
match persistence::achievement::sync(&settings.persistence_db_dir) {
Ok(achievements) => {
info!("Achievement data loaded...");
info!(?achievements, "data:");
state.ecs_mut().insert(AvailableAchievements(achievements));
},
Err(e) => error!(?e, "Achievement data migration error"),
}
let this = Self {
state,
world: Arc::new(world),
@ -256,22 +284,6 @@ impl Server {
tick_metrics,
};
// Run pending DB migrations (if any)
debug!("Running DB migrations...");
if let Some(e) = persistence::run_migrations(&settings.persistence_db_dir).err() {
info!(?e, "Migration error");
}
// Sync Achievement Data
debug!("Syncing Achievement data...");
if let Some(e) =
persistence::achievement::sync(&this.server_settings.persistence_db_dir).err()
{
info!(?e, "Achievement data migration error");
}
debug!(?settings, "created veloren server with");
let git_hash = *common::util::GIT_HASH;
@ -419,6 +431,20 @@ impl Server {
}
}
let achievement_events = self
.state
.ecs()
.read_resource::<EventBus<AchievementEvent>>()
.recv_all();
for event in achievement_events {
match event {
AchievementEvent::CollectedItem { entity, item } => {
info!(?item, "Achievement event: item");
},
}
}
// 7 Persistence updates
let before_persistence_updates = Instant::now();
@ -467,6 +493,22 @@ impl Server {
},
});
self.state
.ecs()
.read_resource::<persistence::achievement::AchievementLoader>()
.messages()
.for_each(|query_result| match query_result {
AchievementLoaderResponse::LoadCharacterAchievementListResponse((
entity,
result,
)) => match result {
Ok(achievement_data) => self
.notify_client(entity, ServerMsg::AchievementDataUpdate(achievement_data)),
Err(error) => self
.notify_client(entity, ServerMsg::AchievementDataError(error.to_string())),
},
});
let end_of_server_tick = Instant::now();
// 8) Update Metrics

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS "character_achievement";

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS "character_achievement" (
character_id INTEGER PRIMARY KEY NOT NULL,
achievement_id INTEGER NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
progress INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE,
FOREIGN KEY(achievement_id) REFERENCES "achievement"(id) ON DELETE CASCADE
);

View File

@ -3,10 +3,14 @@ extern crate diesel;
use super::{
error::Error,
establish_connection,
models::{Achievement as AchievementModel, DataMigration, NewAchievement, NewDataMigration},
models::{
Achievement as AchievementModel, CharacterAchievement, DataMigration, NewAchievement,
NewDataMigration,
},
schema,
};
use common::achievement::*;
use common::comp::{self, AchievementItem};
use crossbeam::{channel, channel::TryIter};
use diesel::{
prelude::*,
result::{DatabaseErrorKind, Error as DieselError},
@ -15,37 +19,144 @@ use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use tracing::{info, warn};
use tracing::{error, info, warn};
pub fn sync(db_dir: &str) -> Result<(), Error> {
/// Available database operations when modifying a player's characetr list
enum AchievementLoaderRequestKind {
LoadCharacterAchievementList {
entity: specs::Entity,
character_id: i32,
},
}
type LoadCharacterAchievementsResult = (specs::Entity, Result<Vec<comp::Achievement>, Error>);
/// Wrapper for results
#[derive(Debug)]
pub enum AchievementLoaderResponse {
LoadCharacterAchievementListResponse(LoadCharacterAchievementsResult),
}
pub struct AchievementLoader {
update_rx: Option<channel::Receiver<AchievementLoaderResponse>>,
update_tx: Option<channel::Sender<AchievementLoaderRequestKind>>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl AchievementLoader {
pub fn new(db_dir: String) -> Self {
let (update_tx, internal_rx) = channel::unbounded::<AchievementLoaderRequestKind>();
let (internal_tx, update_rx) = channel::unbounded::<AchievementLoaderResponse>();
let handle = std::thread::spawn(move || {
while let Ok(request) = internal_rx.recv() {
if let Err(e) = internal_tx.send(match request {
AchievementLoaderRequestKind::LoadCharacterAchievementList {
entity,
character_id,
} => AchievementLoaderResponse::LoadCharacterAchievementListResponse((
entity,
load_character_achievement_list(character_id, &db_dir),
)),
}) {
error!(?e, "Could not send send persistence request");
}
}
});
Self {
update_tx: Some(update_tx),
update_rx: Some(update_rx),
handle: Some(handle),
}
}
pub fn load_character_achievement_list(&self, entity: specs::Entity, character_id: i32) {
if let Err(e) = self.update_tx.as_ref().unwrap().send(
AchievementLoaderRequestKind::LoadCharacterAchievementList {
entity,
character_id,
},
) {
error!(?e, "Could not send character achievement load request");
}
}
/// Returns a non-blocking iterator over AchievementLoaderResponse messages
pub fn messages(&self) -> TryIter<AchievementLoaderResponse> {
self.update_rx.as_ref().unwrap().try_iter()
}
}
impl Drop for AchievementLoader {
fn drop(&mut self) {
drop(self.update_tx.take());
if let Err(e) = self.handle.take().unwrap().join() {
error!(?e, "Error from joining character loader thread");
}
}
}
fn load_character_achievement_list(
character_id: i32,
db_dir: &str,
) -> Result<Vec<comp::Achievement>, Error> {
let character_achievements = schema::character_achievement::dsl::character_achievement
.filter(schema::character_achievement::character_id.eq(character_id))
.load::<CharacterAchievement>(&establish_connection(db_dir))?;
Ok(character_achievements
.iter()
.map(comp::Achievement::from)
.collect::<Vec<comp::Achievement>>())
}
pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
let achievements = load_data();
let connection = establish_connection(db_dir);
// Get a hash of the Vec<Achievement> and compare to the migration table
// Use the full dataset for checks
let persisted_achievements =
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
// Get a hash of the Vec<Achievement> to compare to the migration table
let result = schema::data_migration::dsl::data_migration
.filter(schema::data_migration::title.eq(String::from("achievements")))
.load::<DataMigration>(&connection)?;
.first::<DataMigration>(&connection);
// First check whether the table has an entry for this data type
if result.is_empty() {
let migration = NewDataMigration {
title: "achievements",
checksum: &hash(&achievements).to_string(),
last_run: chrono::Utc::now().naive_utc(),
};
info!(?result, "result: ");
diesel::insert_into(schema::data_migration::table)
.values(&migration)
.execute(&connection)?;
}
let should_run = match result {
Ok(migration_entry) => {
let checksum = &migration_entry.checksum;
info!(?checksum, "checksum: ");
// Also check checksum. Bail if same, continue if changed
if result.is_empty() {
info!("Achievements need updating...");
migration_entry.checksum != hash(&achievements).to_string()
}
Err(diesel::result::Error::NotFound) => {
let migration = NewDataMigration {
title: "achievements",
checksum: &hash(&achievements).to_string(),
last_run: chrono::Utc::now().naive_utc(),
};
// Use the full dataset for checks
let persisted_achievements =
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
diesel::insert_into(schema::data_migration::table)
.values(&migration)
.execute(&connection)?;
true
}
Err(_) => {
error!("Failed to run migrations"); // TODO better error messaging
false
}
};
if (should_run || persisted_achievements.is_empty()) && !achievements.is_empty() {
let items = &achievements;
info!(?items, "Achievements need updating...");
// Make use of the unique constraint in the DB, attempt to insert, on unique
// failure check if it needs updating and do so if necessary
@ -78,7 +189,7 @@ pub fn sync(db_dir: &str) -> Result<(), Error> {
}
}
}
},
}
_ => return Err(Error::DatabaseError(error)),
}
}
@ -96,21 +207,23 @@ pub fn sync(db_dir: &str) -> Result<(), Error> {
info!("No achievement updates required");
}
Ok(())
Ok(schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?)
}
fn load_data() -> Vec<AchievementItem> {
if let Ok(path) = std::fs::canonicalize("../data/achievements.ron") {
let path = std::path::PathBuf::from(path);
let manifest_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "data/achievements.ron");
info!(?path, "Path: ");
info!(?manifest_dir, "manifest_dir:");
match std::fs::File::open(path) {
match std::fs::canonicalize(manifest_dir) {
Ok(path) => match std::fs::File::open(path) {
Ok(file) => ron::de::from_reader(file).expect("Error parsing achievement data"),
Err(error) => panic!(error.to_string()),
},
Err(error) => {
warn!(?error, "Unable to find achievement data file");
Vec::new()
}
} else {
Vec::new()
}
}
@ -119,3 +232,10 @@ pub fn hash<T: Hash>(t: &T) -> u64 {
t.hash(&mut s);
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

@ -2,7 +2,10 @@ extern crate serde_json;
use super::{
achievement::hash,
schema::{achievement, body, character, data_migration, inventory, loadout, stats},
schema::{
achievement, body, character, character_achievement, data_migration, inventory, loadout,
stats,
},
};
use crate::comp;
use chrono::NaiveDateTime;
@ -377,7 +380,7 @@ pub struct NewDataMigration<'a> {
/// the starter sword set as the main weapon
#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
#[sql_type = "Text"]
pub struct AchievementData(AchievementItem);
pub struct AchievementData(comp::AchievementItem);
impl<DB> diesel::deserialize::FromSql<Text, DB> for AchievementData
where
@ -394,7 +397,7 @@ where
Err(e) => {
warn!(?e, "Failed to deserialise achevement data");
Ok(Self(AchievementItem::default()))
Ok(Self(comp::AchievementItem::default()))
},
}
}
@ -421,6 +424,10 @@ pub struct Achievement {
pub details: AchievementData,
}
impl From<&AchievementData> for comp::AchievementItem {
fn from(data: &AchievementData) -> comp::AchievementItem { data.0.clone() }
}
#[derive(Insertable, PartialEq, Debug)]
#[table_name = "achievement"]
pub struct NewAchievement {
@ -428,8 +435,8 @@ pub struct NewAchievement {
pub details: AchievementData,
}
impl From<&AchievementItem> for NewAchievement {
fn from(item: &AchievementItem) -> Self {
impl From<&comp::AchievementItem> for NewAchievement {
fn from(item: &comp::AchievementItem) -> Self {
Self {
checksum: hash(item).to_string(),
details: AchievementData(item.clone()),
@ -437,6 +444,34 @@ impl From<&AchievementItem> for NewAchievement {
}
}
/// Character Achievements belong to characters
#[derive(Queryable, Debug, Identifiable)]
#[primary_key(character_id)]
#[table_name = "character_achievement"]
pub struct CharacterAchievement {
pub character_id: i32,
pub achievement_id: i32,
pub completed: i32,
pub progress: i32,
}
impl From<&CharacterAchievement> for comp::Achievement {
fn from(_achievement: &CharacterAchievement) -> comp::Achievement {
comp::Achievement::default()
}
}
impl From<Achievement> for comp::Achievement {
fn from(achievement: Achievement) -> comp::Achievement {
comp::Achievement {
id: achievement.id,
item: comp::AchievementItem::from(&achievement.details),
completed: false,
progress: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -30,6 +30,15 @@ table! {
}
}
table! {
character_achievement (character_id) {
character_id -> Integer,
achievement_id -> Integer,
completed -> Integer,
progress -> Integer,
}
}
table! {
data_migration (id) {
id -> Integer,
@ -67,8 +76,18 @@ table! {
}
joinable!(body -> character (character_id));
joinable!(character_achievement -> achievement (achievement_id));
joinable!(character_achievement -> character (character_id));
joinable!(inventory -> character (character_id));
joinable!(loadout -> character (character_id));
joinable!(stats -> character (character_id));
allow_tables_to_appear_in_same_query!(achievement, body, character, inventory, loadout, stats);
allow_tables_to_appear_in_same_query!(
achievement,
body,
character,
character_achievement,
inventory,
loadout,
stats,
);

View File

@ -175,6 +175,7 @@ impl StateExt for State {
entity,
comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()),
);
self.write_component(entity, comp::AchievementList::default());
// Set the character id for the player
// TODO this results in a warning in the console: "Error modifying synced

View File

@ -0,0 +1,56 @@
use crate::persistence::achievement::AvailableAchievements;
use common::comp::{AchievementItem, AchievementList, InventoryUpdate, Player};
use specs::{Entities, Join, ReadExpect, ReadStorage, System};
use tracing::info;
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)] // TODO: Pending review in #587
type SystemData = (
Entities<'a>,
ReadStorage<'a, Player>,
ReadStorage<'a, AchievementList>,
ReadStorage<'a, InventoryUpdate>,
ReadExpect<'a, AvailableAchievements>,
);
fn run(
&mut self,
(entities, players, achievement_lists, inventory_updates, available_achievements): Self::SystemData,
) {
for (_entity, _player, ach_list, inv_event) in
(&entities, &players, &achievement_lists, &inventory_updates).join()
{
(available_achievements.0)
.iter()
.for_each(|achievement_item| {
let ach_item = AchievementItem::from(&achievement_item.details);
// pass the event to each achievement
// achievement checks if the event matches what it is looking for
if ach_item.matches_event(inv_event.event()) {
if let Some(event) = ach_list.process(&ach_item) {
info!(?event, "Achievement event");
}
// if it's a match, pass it to the characters
// achievement list
// get a result from the call to the players achievement
// list
// - It checks for an entry No entry = append and
// increment Entry = Increment
// inrement(achievement_id, ?amount)
// if let Some(results) =
// _player_character_list.process(achievement_item) {
//
// - if its a completion result, dispatch an event which
// notifies the client
// server_events.dispatch(ServerEvent::
// AchievementUpdate(TheAchievementInfo))
// }
}
});
}
}
}

View File

@ -1,6 +1,8 @@
use super::SysTimer;
use crate::{
auth_provider::AuthProvider, client::Client, persistence::character::CharacterLoader,
auth_provider::AuthProvider,
client::Client,
persistence::{achievement::AchievementLoader, character::CharacterLoader},
ServerSettings, CLIENT_TIMEOUT,
};
use common::{
@ -395,6 +397,7 @@ impl<'a> System<'a> for Sys {
Read<'a, EventBus<ServerEvent>>,
Read<'a, Time>,
ReadExpect<'a, CharacterLoader>,
ReadExpect<'a, AchievementLoader>,
ReadExpect<'a, TerrainGrid>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Uid>,
@ -425,6 +428,7 @@ impl<'a> System<'a> for Sys {
server_event_bus,
time,
character_loader,
achievement_loader,
terrain,
mut timer,
uids,
@ -520,16 +524,21 @@ impl<'a> System<'a> for Sys {
}
}
// Handle new players.
// Tell all clients to add them to the player list.
// Handle new players: Tell all clients to add them to the player list, and load
// non-critical data for their character
for entity in new_players {
if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) {
if let Some(character_id) = player.character_id {
achievement_loader.load_character_achievement_list(entity, character_id);
}
let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo {
player_alias: player.alias.clone(),
is_online: true,
is_admin: admins.get(entity).is_some(),
character: None, // new players will be on character select.
}));
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone())
}

View File

@ -1,3 +1,4 @@
pub mod achievement;
pub mod entity_sync;
pub mod message;
pub mod object;
@ -53,6 +54,9 @@ pub fn run_sync_systems(ecs: &mut specs::World) {
// Sync
terrain_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