diff --git a/assets/voxygen/data/achievements.ron b/assets/voxygen/data/achievements.ron deleted file mode 100644 index d09dbf65b4..0000000000 --- a/assets/voxygen/data/achievements.ron +++ /dev/null @@ -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) - ) -] \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index 91db147335..3213bd4e9f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -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 + }, } } } diff --git a/common/src/achievement.rs b/common/src/achievement.rs deleted file mode 100644 index 556298f75a..0000000000 --- a/common/src/achievement.rs +++ /dev/null @@ -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), - } - } -} diff --git a/common/src/comp/achievement.rs b/common/src/comp/achievement.rs new file mode 100644 index 0000000000..4d1ebfb0c8 --- /dev/null +++ b/common/src/comp/achievement.rs @@ -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 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 { +// +// } +// } + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AchievementList(Vec); + +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 { + // if self.0.iter(). + + None + } +} + +impl Component for AchievementList { + type Storage = FlaggedStorage>; +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 3efd7e9381..15c6b16358 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -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::{ diff --git a/common/src/event.rs b/common/src/event.rs index 7bcb6683db..19f5806584 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -83,6 +83,10 @@ pub enum ServerEvent { Chat(comp::ChatMsg), } +pub enum AchievementEvent { + CollectedItem { entity: EcsEntity, item: comp::Item }, +} + pub struct EventBus { queue: Mutex>, } diff --git a/common/src/lib.rs b/common/src/lib.rs index 908f3d87af..62c4ccf073 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -11,7 +11,7 @@ option_zip )] -pub mod achievement; +#[macro_use] extern crate serde_derive; pub mod assets; pub mod astar; pub mod character; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index a766a7ef37..10fcfa1d3d 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -59,6 +59,11 @@ pub enum ServerMsg { time_of_day: state::TimeOfDay, world_map: (Vec2, Vec), }, + /// A list of achievements which the character has fully or partially + /// completed + AchievementDataUpdate(Vec), + /// 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 diff --git a/server/data/achievements.ron b/server/data/achievements.ron new file mode 100644 index 0000000000..d45aa782ad --- /dev/null +++ b/server/data/achievements.ron @@ -0,0 +1,10 @@ +[ + ( + title: "Collect 10 Apples", + achievement_type: CollectConsumable(Apple, 10) + ), + ( + title: "Collect 50 Apples", + achievement_type: CollectConsumable(Apple, 50) + ), +] \ No newline at end of file diff --git a/server/src/data/achievements.ron b/server/src/data/achievements.ron deleted file mode 100644 index d09dbf65b4..0000000000 --- a/server/src/data/achievements.ron +++ /dev/null @@ -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) - ) -] \ No newline at end of file diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index b33a170e0b..8d25e7f1c0 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -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::>() + .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::>() + .emit_now(AchievementEvent::CollectedItem { + entity, + item: item.clone(), + }); + + state.give_item(entity, item); + }); } } }, diff --git a/server/src/lib.rs b/server/src/lib.rs index 68cf20d5f1..2b0c21144e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -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 { let mut state = State::default(); state.ecs_mut().insert(settings.clone()); + + // Event Emitters state.ecs_mut().insert(EventBus::::default()); + state + .ecs_mut() + .insert(EventBus::::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::>() + .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::() + .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 diff --git a/server/src/migrations/2020-06-25-172916_character_achievement/down.sql b/server/src/migrations/2020-06-25-172916_character_achievement/down.sql new file mode 100644 index 0000000000..4497af0cb7 --- /dev/null +++ b/server/src/migrations/2020-06-25-172916_character_achievement/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "character_achievement"; \ No newline at end of file diff --git a/server/src/migrations/2020-06-25-172916_character_achievement/up.sql b/server/src/migrations/2020-06-25-172916_character_achievement/up.sql new file mode 100644 index 0000000000..632f9a8a12 --- /dev/null +++ b/server/src/migrations/2020-06-25-172916_character_achievement/up.sql @@ -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 +); \ No newline at end of file diff --git a/server/src/persistence/achievement.rs b/server/src/persistence/achievement.rs index 93fc306424..d62b8da742 100644 --- a/server/src/persistence/achievement.rs +++ b/server/src/persistence/achievement.rs @@ -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, Error>); + +/// Wrapper for results +#[derive(Debug)] +pub enum AchievementLoaderResponse { + LoadCharacterAchievementListResponse(LoadCharacterAchievementsResult), +} + +pub struct AchievementLoader { + update_rx: Option>, + update_tx: Option>, + handle: Option>, +} + +impl AchievementLoader { + pub fn new(db_dir: String) -> Self { + let (update_tx, internal_rx) = channel::unbounded::(); + let (internal_tx, update_rx) = channel::unbounded::(); + + 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 { + 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, Error> { + let character_achievements = schema::character_achievement::dsl::character_achievement + .filter(schema::character_achievement::character_id.eq(character_id)) + .load::(&establish_connection(db_dir))?; + + Ok(character_achievements + .iter() + .map(comp::Achievement::from) + .collect::>()) +} + +pub fn sync(db_dir: &str) -> Result, Error> { let achievements = load_data(); let connection = establish_connection(db_dir); - // Get a hash of the Vec and compare to the migration table + // Use the full dataset for checks + let persisted_achievements = + schema::achievement::dsl::achievement.load::(&connection)?; + + // Get a hash of the Vec to compare to the migration table let result = schema::data_migration::dsl::data_migration .filter(schema::data_migration::title.eq(String::from("achievements"))) - .load::(&connection)?; + .first::(&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::(&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::(&connection)?) } fn load_data() -> Vec { - 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: &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); diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index 8c84f96ce4..033ca90b99 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -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 diesel::deserialize::FromSql 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 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::*; diff --git a/server/src/persistence/schema.rs b/server/src/persistence/schema.rs index 4aa04f420b..1cf5101551 100644 --- a/server/src/persistence/schema.rs +++ b/server/src/persistence/schema.rs @@ -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, +); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 645e7843d4..164634e60e 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -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 diff --git a/server/src/sys/achievement.rs b/server/src/sys/achievement.rs new file mode 100644 index 0000000000..c8c8c5c722 --- /dev/null +++ b/server/src/sys/achievement.rs @@ -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)) + // } + } + }); + } + } +} diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 3ea09a0003..badde20e91 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -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>, Read<'a, Time>, ReadExpect<'a, CharacterLoader>, + ReadExpect<'a, AchievementLoader>, ReadExpect<'a, TerrainGrid>, Write<'a, SysTimer>, 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()) } diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index 5afef7795c..3d47ea7b6d 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -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