From c056b2b079fc470204a3bee9c797e961f984b2cf Mon Sep 17 00:00:00 2001 From: Shane Handley Date: Mon, 6 Jul 2020 14:01:46 +1000 Subject: [PATCH] Rework some data structures and persistence. Abandoned the approach of generating row ids for achievements via checksums in favour of static uuids for each item. --- common/src/comp/achievement.rs | 224 +++++++++--------- common/src/comp/mod.rs | 4 +- common/src/msg/server.rs | 2 +- server/data/achievements.ron | 10 + server/src/lib.rs | 17 +- .../2020-06-11-231641_achievements/up.sql | 10 +- .../up.sql | 4 +- server/src/persistence/achievement.rs | 89 ++++--- server/src/persistence/models.rs | 154 ++++-------- server/src/persistence/schema.rs | 14 +- 10 files changed, 235 insertions(+), 293 deletions(-) diff --git a/common/src/comp/achievement.rs b/common/src/comp/achievement.rs index 916ee6b16d..80fb076858 100644 --- a/common/src/comp/achievement.rs +++ b/common/src/comp/achievement.rs @@ -1,4 +1,5 @@ use crate::comp::item::{Consumable, Item, ItemKind}; +use hashbrown::HashMap; use specs::{Component, Entity, FlaggedStorage}; use specs_idvs::IDVStorage; @@ -11,6 +12,12 @@ pub struct AchievementTrigger { pub event: AchievementEvent, } +/// Used to indicate an in-game event that can contribute towards the completion +/// of an achievement. +/// +/// These are paired with `AchievementAction` items - for +/// example an event of type `LevelUp` will trigger a check for `ReachLevel` +/// achievements. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AchievementEvent { None, @@ -30,17 +37,15 @@ pub enum AchievementAction { KillNpcs, } -/// Information about an achievement. This differs from a complete -/// [`Achievement`](struct.Achievement.html) in that it describes the -/// achievement without any information about progress or completion status #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AchievementItem { +pub struct Achievement { + pub uuid: String, pub title: String, pub action: AchievementAction, pub target: usize, } -impl AchievementItem { +impl Achievement { pub fn matches_event(&self, event: &AchievementEvent) -> bool { match event { AchievementEvent::KilledNpc => self.action == AchievementAction::KillNpcs, @@ -63,21 +68,24 @@ impl AchievementItem { /// The complete representation of an achievement that has been #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct Achievement { - pub id: i32, - pub item: AchievementItem, +pub struct CharacterAchievement { + pub achievement: Achievement, pub completed: bool, pub progress: usize, } -impl Achievement { +impl CharacterAchievement { /// Increment the progress of this Achievement based on its type /// /// By default, when an achievement is incremented, its `progress` value is /// incremented by 1. This covers many cases, but using this method allows /// handling of unique types of achievements which are not simple /// counters for events - pub fn increment_progress(&mut self, event: &AchievementEvent) -> bool { + pub fn increment_progress(&mut self, event: &AchievementEvent) -> Option<&mut Self> { + if self.completed { + return None; + } + match event { AchievementEvent::LevelUp(level) => { self.progress = *level as usize; @@ -85,61 +93,67 @@ impl Achievement { _ => self.progress += 1, }; - self.completed = self.progress >= self.item.target; - self.completed + self.completed = self.progress >= self.achievement.target; + + if self.completed == true { + Some(self) + } else { + None + } + } +} + +/// For initialisation of a new CharacterAchievement item when a new achievement +/// is progressed +impl From for CharacterAchievement { + fn from(achievement: Achievement) -> Self { + Self { + achievement, + completed: false, + progress: 0, + } } } /// Each character is assigned an achievement list, which holds information /// about which achievements that the player has made some progress on, or /// completed. -/// -/// This minimises storage of data per-character, and can be merged with a full -/// achievement list -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AchievementList(Vec); +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AchievementList(HashMap); impl Default for AchievementList { - fn default() -> AchievementList { AchievementList(Vec::new()) } + fn default() -> AchievementList { AchievementList(HashMap::new()) } } impl AchievementList { - pub fn from(data: Vec) -> Self { Self(data) } + pub fn from(data: HashMap) -> Self { Self(data) } } impl Component for AchievementList { - type Storage = FlaggedStorage>; + type Storage = FlaggedStorage>; // TODO check } impl AchievementList { - pub fn item_by_id(&mut self, id: i32) -> Option<&mut Achievement> { - self.0.iter_mut().find(|a| a.id == id) - } - - /// Process a single achievement item, inrementing the progress of the - /// achievement. This is called as part of server/sys/Achievements. + /// Process a single CharacterAchievement item based on the occurance of an + /// `AchievementEvent`. + /// + /// When the character has existing progress on the achievement it is + /// updated, otherwise an insert-then-update is performed. + /// + /// Returns the `CharacterAchievement` item that was processed in the event + /// that prcessing resulted in its completion. pub fn process_achievement( &mut self, - achievement: Achievement, + achievement: &Achievement, event: &AchievementEvent, - ) -> bool { - let id = achievement.id; + ) -> Option { + let uuid = achievement.uuid.clone(); - if !self.0.contains(&achievement) { - self.0.push(achievement); - } - - if let Some(char_achievement) = self.item_by_id(id) { - if char_achievement.completed { - return false; - } - - char_achievement.increment_progress(event) - } else { - tracing::warn!("Failed to find achievement after inserting"); - - false - } + self.0 + .entry(uuid) + .or_insert(CharacterAchievement::from(achievement.clone())) + .increment_progress(event) + .cloned() } } @@ -150,8 +164,9 @@ mod tests { #[test] fn inv_collect_event_matches_consumable_achievement_item() { - let item = AchievementItem { - title: String::from("Test"), + let item = Achievement { + uuid: String::new(), + title: String::new(), action: AchievementAction::CollectConsumable(Consumable::Apple), target: 10, }; @@ -164,8 +179,9 @@ mod tests { #[test] fn inv_collect_event_not_matches_consumable_achievement_item() { - let item = AchievementItem { - title: String::from("Test"), + let item = Achievement { + uuid: String::new(), + title: String::new(), action: AchievementAction::CollectConsumable(Consumable::Cheese), target: 10, }; @@ -178,8 +194,9 @@ mod tests { #[test] fn levelup_event_matches_reach_level_achievement_item() { - let item = AchievementItem { - title: String::from("Test"), + let item = Achievement { + uuid: String::new(), + title: String::new(), action: AchievementAction::ReachLevel, target: 100, }; @@ -190,20 +207,16 @@ mod tests { } #[test] - fn process_collect_achievement_increments_progress() { - let item = AchievementItem { + fn process_achievement_increments_progress() { + let uuid = String::from("2ef30659-5884-40aa-ba4d-8f5af32ff9ac"); + + let achievement = Achievement { + uuid: uuid.clone(), title: String::from("Collect 3 Mushrooms"), action: AchievementAction::CollectConsumable(Consumable::Mushroom), target: 3, }; - let achievement = Achievement { - id: 1, - item, - completed: false, - progress: 0, - }; - let event = AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom")); @@ -211,92 +224,75 @@ mod tests { // The first two increments should not indicate that it is complete assert_eq!( - achievement_list.process_achievement(achievement.clone(), &event), - false + achievement_list.process_achievement(&achievement.clone(), &event), + None ); assert_eq!( - achievement_list.process_achievement(achievement.clone(), &event), - false + achievement_list.process_achievement(&achievement.clone(), &event), + None ); - assert_eq!(achievement_list.0.get(0).unwrap().progress, 2); - - // It should return true when completed - assert_eq!( - achievement_list.process_achievement(achievement, &event), - true - ); - - assert_eq!(achievement_list.0.get(0).unwrap().progress, 3); - - // The achievement `completed` field should be true - assert_eq!(achievement_list.0.get(0).unwrap().completed, true); + assert_eq!(achievement_list.0.get(&uuid).unwrap().progress, 2); } #[test] - fn process_levelup_achievement_increments_progress() { - let item = AchievementItem { + fn process_achievement_returns_achievement_when_complete() { + let uuid = String::from("Test"); + + let achievement = Achievement { + uuid: uuid.clone(), title: String::from("Reach Level 10"), action: AchievementAction::ReachLevel, target: 10, }; - let achievement = Achievement { - id: 1, - item, - completed: false, - progress: 1, - }; - let mut achievement_list = AchievementList::default(); - assert_eq!( - achievement_list - .process_achievement(achievement.clone(), &AchievementEvent::LevelUp(6)), - false - ); + achievement_list.process_achievement(&achievement, &AchievementEvent::LevelUp(6)); - // The achievement progress should be the new level value, and be incomplete - assert_eq!(achievement_list.0.get(0).unwrap().progress, 6); - assert_eq!(achievement_list.0.get(0).unwrap().completed, false); + // The achievement progress should be the new level value, but be incomplete + let incomplete_result = achievement_list.0.get(&uuid).unwrap(); - assert_eq!( - achievement_list.process_achievement(achievement, &AchievementEvent::LevelUp(10)), - true - ); + assert_eq!(incomplete_result.progress, 6); + assert_eq!(incomplete_result.completed, false); + + achievement_list.process_achievement(&achievement, &AchievementEvent::LevelUp(10)); // The achievement progress should be the new level value, and be completed - assert_eq!(achievement_list.0.get(0).unwrap().progress, 10); - assert_eq!(achievement_list.0.get(0).unwrap().completed, true); + let complete_result = achievement_list.0.get(&uuid).unwrap(); + + assert_eq!(complete_result.progress, 10); + assert_eq!(complete_result.completed, true); } #[test] fn process_completed_achievement_doesnt_increment_progress() { - let item = AchievementItem { + let uuid = String::from("Test"); + + let achievement = Achievement { + uuid: uuid.clone(), title: String::from("Collect 3 Mushrooms"), - action: AchievementAction::CollectConsumable(Consumable::Mushroom), + action: AchievementAction::KillNpcs, target: 3, }; - let achievement = Achievement { - id: 1, - item, - completed: true, - progress: 3, - }; + // Initialise with the already completed event + let mut achievement_list = AchievementList::default(); - let mut achievement_list = AchievementList(vec![achievement.clone()]); + achievement_list + .0 + .insert(uuid.clone(), CharacterAchievement { + achievement: achievement.clone(), + completed: true, + progress: 3, + }); - let event = - AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom")); + achievement_list.process_achievement(&achievement, &AchievementEvent::KilledNpc); - assert_eq!( - achievement_list.process_achievement(achievement, &event), - false - ); + let result = achievement_list.0.get(&uuid).unwrap(); // The achievement progress should not have incremented - assert_eq!(achievement_list.0.get(0).unwrap().progress, 3); + assert_eq!(result.progress, 3); } } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index d7fbf4de94..1b2ac5d0bb 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -22,8 +22,8 @@ mod visual; // Reexports pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; pub use achievement::{ - Achievement, AchievementAction, AchievementEvent, AchievementItem, AchievementList, - AchievementTrigger, + Achievement, AchievementAction, AchievementEvent, AchievementList, AchievementTrigger, + CharacterAchievement, }; pub use admin::{Admin, AdminList}; pub use agent::{Agent, Alignment}; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index d7922d5bba..d8778145b5 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -65,7 +65,7 @@ pub enum ServerMsg { /// An error occurred while loading character achievements CharacterAchievementDataError(String), /// The client has completed an achievement - AchievementCompletion(comp::AchievementItem), + AchievementCompletion(comp::CharacterAchievement), /// 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 index 3d830dfbd6..034be5d980 100644 --- a/server/data/achievements.ron +++ b/server/data/achievements.ron @@ -1,50 +1,60 @@ [ ( + id: "9d48b606-f70a-4e7f-a3b7-6630e75ba8fe", title: "Collect 10 apples", action: CollectConsumable(Apple), target: 10 ), ( + id: "ffccb88c-2bb0-41bd-942b-3cbdf9882295", title: "Collect 50 apples", action: CollectConsumable(Apple), target: 50 ), ( + id: "290c0bc1-ab0a-450e-82d7-6917d9ea7497", title: "Pick a mushroom", action: CollectConsumable(Mushroom), target: 1 ), ( + id: "598aff28-01f6-46a9-a4c1-d75aead98794", title: "Kill an NPC", action: KillNpcs, target: 1 ), ( + id: "046e08c5-a512-4ee1-b6e4-76e4e11dd502", title: "Kill 10 NPCs", action: KillNpcs, target: 10 ), ( + id: "9c2a326b-c4fa-45bf-9dd6-d4bf9dd85bb5", title: "Kill 100 NPCs", action: KillNpcs, target: 100 ), ( + id: "de782069-4366-41dc-9645-1c02c64b8037", title: "Kill another player", action: KillPlayers, target: 1 ), ( + id: "df6bf984-efdf-40f7-8bac-e09e9927acba", title: "Kill 10 players", action: KillPlayers, target: 10 ), ( + id: "d733c6cc-57c9-4a22-aea6-13f4850d750f", title: "Find a Velorite fragment", action: CollectConsumable(VeloriteFrag), target: 1 ), ( + id: "50140fe9-1811-40c4-bb3b-89503ac60ccf", title: "Find Velorite", action: CollectConsumable(Velorite), target: 1 diff --git a/server/src/lib.rs b/server/src/lib.rs index c080d28330..5bab64fe7c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -43,7 +43,7 @@ use futures_util::{select, FutureExt}; use metrics::{ServerMetrics, TickMetrics}; use network::{Address, Network, Pid}; use persistence::{ - achievement::{Achievement, AchievementLoader, AchievementLoaderResponse}, + achievement::{AchievementLoader, AchievementLoaderResponse}, character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater}, }; use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt}; @@ -90,7 +90,8 @@ pub struct Server { metrics: ServerMetrics, tick_metrics: TickMetrics, - achievement_data: Vec, + /// Holds a list of all available achievements + achievement_data: Vec, } impl Server { @@ -447,9 +448,7 @@ impl Server { 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) { + if achievement.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 @@ -459,14 +458,12 @@ impl Server { .write_storage::() .get_mut(trigger.entity) { - if achievement_list.process_achievement( - comp::Achievement::from(achievement), - &trigger.event, - ) == true + if let Some(character_achievement) = + achievement_list.process_achievement(achievement, &trigger.event) { self.notify_client( trigger.entity, - ServerMsg::AchievementCompletion(achievement_item), + ServerMsg::AchievementCompletion(character_achievement), ) } } diff --git a/server/src/migrations/2020-06-11-231641_achievements/up.sql b/server/src/migrations/2020-06-11-231641_achievements/up.sql index 63ae336bd1..7143067ffb 100644 --- a/server/src/migrations/2020-06-11-231641_achievements/up.sql +++ b/server/src/migrations/2020-06-11-231641_achievements/up.sql @@ -1,5 +1,7 @@ -CREATE TABLE IF NOT EXISTS "achievement" ( - id INTEGER PRIMARY KEY NOT NULL, - checksum VARCHAR(64) NOT NULL UNIQUE, - details TEXT NOT NULL +CREATE TABLE IF NOT EXISTS "achievements" ( + "uuid" TEXT PRIMARY KEY NOT NULL, + "checksum" TEXT NOT NULL UNIQUE, + "title" TEXT NOT NULL, + "action" INT NOT NULL, + "target" INT NOT NULL ); \ 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 index 632f9a8a12..ee1d72a688 100644 --- a/server/src/migrations/2020-06-25-172916_character_achievement/up.sql +++ b/server/src/migrations/2020-06-25-172916_character_achievement/up.sql @@ -1,8 +1,8 @@ CREATE TABLE IF NOT EXISTS "character_achievement" ( character_id INTEGER PRIMARY KEY NOT NULL, - achievement_id INTEGER NOT NULL, + achievement_uuid TEXT 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 + FOREIGN KEY(achievement_uuid) REFERENCES "achievement"(uuid) ON DELETE CASCADE ); \ No newline at end of file diff --git a/server/src/persistence/achievement.rs b/server/src/persistence/achievement.rs index 5bec378c72..e524e63773 100644 --- a/server/src/persistence/achievement.rs +++ b/server/src/persistence/achievement.rs @@ -4,12 +4,12 @@ use super::{ error::Error, establish_connection, models::{ - Achievement as AchievementModel, CharacterAchievement, DataMigration, NewAchievement, - NewDataMigration, + Achievement as AchievementModel, CharacterAchievement, CharacterAchievementJoinData, + DataMigration, NewDataMigration, }, schema, }; -use common::comp::{self, AchievementItem}; +use common::comp; use crossbeam::{channel, channel::TryIter}; use diesel::{ prelude::*, @@ -21,8 +21,6 @@ use std::{ }; use tracing::{error, info, warn}; -pub type Achievement = AchievementModel; - /// Available database operations when modifying a player's characetr list enum AchievementLoaderRequestKind { LoadCharacterAchievementList { @@ -105,25 +103,34 @@ fn load_character_achievement_list( ) -> Result { let character_achievements = schema::character_achievement::dsl::character_achievement .filter(schema::character_achievement::character_id.eq(character_id)) - .load::(&establish_connection(db_dir))?; + .inner_join(schema::achievements::table) + .load::<(CharacterAchievement, AchievementModel)>(&establish_connection(db_dir))?; Ok(comp::AchievementList::from( character_achievements .iter() - .map(comp::Achievement::from) - .collect::>(), + .map(|(character_achievement, achievement)| { + ( + achievement.uuid.clone(), + comp::CharacterAchievement::from(CharacterAchievementJoinData { + character_achievement, + achievement, + }), + ) + }) + .collect(), )) } -pub fn sync(db_dir: &str) -> Result, Error> { +pub fn sync(db_dir: &str) -> Result, Error> { let achievements = load_data(); let connection = establish_connection(db_dir); - // Use the full dataset for checks + // Use the full dataset for checksums let persisted_achievements = - schema::achievement::dsl::achievement.load::(&connection)?; + schema::achievements::dsl::achievements.load::(&connection)?; - // Get a hash of the Vec to compare to the migration table + // Get a hash of the Vec we have in config to compare let result = schema::data_migration::dsl::data_migration .filter(schema::data_migration::title.eq(String::from("achievements"))) .first::(&connection); @@ -132,12 +139,11 @@ pub fn sync(db_dir: &str) -> Result, Error> { let should_run = match result { Ok(migration_entry) => { - let checksum = &migration_entry.checksum; - info!(?checksum, "checksum: "); - + // If these don't match, we need to sync data migration_entry.checksum != hash(&achievements).to_string() }, Err(diesel::result::Error::NotFound) => { + // If there was no migration entry (first run on this server) we need to run let migration = NewDataMigration { title: "achievements", checksum: &hash(&achievements).to_string(), @@ -158,39 +164,29 @@ pub fn sync(db_dir: &str) -> Result, Error> { }; 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 for item in &achievements { - let new_item = NewAchievement::from(item); - - if let Err(error) = diesel::insert_into(schema::achievement::table) - .values(&new_item) + if let Err(error) = diesel::insert_into(schema::achievements::table) + .values(item) .execute(&connection) { match error { DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _) => { - let entry = persisted_achievements + // This uuid already exists, so overwrite the data + if let Some(existing_item) = persisted_achievements .iter() - .find(|&a| &a.checksum == &new_item.checksum); - - if let Some(existing_item) = entry { - if existing_item.details != new_item.details { - match diesel::update( - schema::achievement::dsl::achievement.filter( - schema::achievement::checksum - .eq(String::from(&existing_item.checksum)), - ), - ) - .set(schema::achievement::details.eq(new_item.details)) - .execute(&connection) - { - Ok(_) => warn!(?existing_item.checksum, "Updated achievement"), - Err(err) => return Err(Error::DatabaseError(err)), - } + .find(|&a| &a.uuid == &item.uuid) + { + match diesel::update( + schema::achievements::dsl::achievements + .filter(schema::achievements::uuid.eq(&existing_item.uuid)), + ) + .set(item) + .execute(&connection) + { + Ok(_) => warn!(?existing_item.checksum, "Updated achievement"), + Err(err) => return Err(Error::DatabaseError(err)), } } }, @@ -211,17 +207,16 @@ pub fn sync(db_dir: &str) -> Result, Error> { info!("No achievement updates required"); } - let data = schema::achievement::dsl::achievement.load::(&connection)?; + let data = schema::achievements::dsl::achievements.load::(&connection)?; - Ok(data) - // Ok(data.iter().map(comp::Achievement::from).collect::<_>()) + // Ok(data) + Ok(data.iter().map(comp::Achievement::from).collect::<_>()) } -fn load_data() -> Vec { +fn load_data() -> Vec { + // TODO this better let manifest_dir = format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "data/achievements.ron"); - info!(?manifest_dir, "manifest_dir:"); - 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"), @@ -234,7 +229,7 @@ fn load_data() -> Vec { } } -pub fn hash(t: &T) -> u64 { +fn hash(t: &T) -> u64 { let mut s = DefaultHasher::new(); t.hash(&mut s); s.finish() diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index 2940ac3a64..55c0bf8794 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -1,11 +1,7 @@ extern crate serde_json; -use super::{ - achievement::hash, - schema::{ - achievement, body, character, character_achievement, data_migration, inventory, loadout, - stats, - }, +use super::schema::{ + achievements, body, character, character_achievement, data_migration, inventory, loadout, stats, }; use crate::comp; use chrono::NaiveDateTime; @@ -373,114 +369,58 @@ pub struct NewDataMigration<'a> { pub last_run: NaiveDateTime, } -/// Achievements hold the data related to achievements available in-game. They -/// are the data referenced for characters -/// A wrapper type for Loadout components used to serialise to and from JSON -/// If the column contains malformed JSON, a default loadout is returned, with -/// the starter sword set as the main weapon -#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)] -#[sql_type = "Text"] -pub struct AchievementData(comp::AchievementItem); - -impl diesel::deserialize::FromSql for AchievementData -where - DB: diesel::backend::Backend, - String: diesel::deserialize::FromSql, -{ - fn from_sql( - bytes: Option<&::RawValue>, - ) -> diesel::deserialize::Result { - let t = String::from_sql(bytes)?; - - match serde_json::from_str(&t) { - Ok(data) => Ok(Self(data)), - Err(e) => { - warn!(?e, "Failed to deserialise achevement data"); - - Ok(Self(comp::AchievementItem { - title: String::new(), - action: comp::AchievementAction::None, - target: 0, - })) - }, - } - } -} - -impl diesel::serialize::ToSql for AchievementData -where - DB: diesel::backend::Backend, -{ - fn to_sql( - &self, - out: &mut diesel::serialize::Output, - ) -> diesel::serialize::Result { - let s = serde_json::to_string(&self.0)?; - >::to_sql(&s, out) - } -} - -#[derive(Queryable, Debug, Identifiable)] -#[table_name = "achievement"] +/// The achievements table holds data related to all achievements available to +/// players in-game. They serve as a strong reference for character achievements +/// (see below) and are populated form configuration files holding the data +#[derive(Queryable, Debug, Identifiable, AsChangeset, Hash, Insertable, Deserialize)] +#[primary_key(uuid)] pub struct Achievement { - pub id: i32, + pub uuid: String, pub checksum: String, - 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 { - pub checksum: String, - pub details: AchievementData, -} - -impl From<&comp::AchievementItem> for NewAchievement { - fn from(item: &comp::AchievementItem) -> Self { - Self { - checksum: hash(item).to_string(), - details: AchievementData(item.clone()), - } - } -} - -/// 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 { - id: achievement.achievement_id, - item: comp::AchievementItem { - title: String::from("TODO"), - action: comp::AchievementAction::None, - target: 0, - }, - completed: achievement.completed != 0, - progress: achievement.progress as usize, - } - } + pub title: String, + pub action: i32, + pub target: i32, } 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, + uuid: achievement.uuid.clone(), + title: achievement.title.clone(), + action: comp::AchievementAction::None, // TODO find a way to store this data + target: achievement.target as usize, + } + } +} + +/// Character Achievements belong to characters, and contain information about +/// achievement progress. +/// +/// In the interest of storing as little achievement data per-character as +/// pssible, we only store a 'character_achievement' entry when a character has +/// made some progress towards the completion of an achievement. +#[derive(Queryable, Debug, Identifiable)] +#[primary_key(character_id)] +#[table_name = "character_achievement"] +pub struct CharacterAchievement { + pub character_id: i32, + pub achievement_uuid: String, + pub completed: i32, + pub progress: i32, +} + +/// The required elements to build comp::CharacterAchievement from database data +pub struct CharacterAchievementJoinData<'a> { + pub character_achievement: &'a CharacterAchievement, + pub achievement: &'a Achievement, +} + +impl From> for comp::CharacterAchievement { + fn from(data: CharacterAchievementJoinData) -> comp::CharacterAchievement { + comp::CharacterAchievement { + achievement: comp::Achievement::from(data.achievement), + completed: data.character_achievement.completed == 1, + progress: data.character_achievement.progress as usize, } } } diff --git a/server/src/persistence/schema.rs b/server/src/persistence/schema.rs index 1cf5101551..336277fa5a 100644 --- a/server/src/persistence/schema.rs +++ b/server/src/persistence/schema.rs @@ -1,8 +1,10 @@ table! { - achievement (id) { - id -> Integer, + achievements (uuid) { + uuid -> Text, checksum -> Text, - details -> Text, + title -> Text, + action -> Integer, + target -> Integer, } } @@ -33,7 +35,7 @@ table! { table! { character_achievement (character_id) { character_id -> Integer, - achievement_id -> Integer, + achievement_uuid -> Text, completed -> Integer, progress -> Integer, } @@ -76,14 +78,14 @@ table! { } joinable!(body -> character (character_id)); -joinable!(character_achievement -> achievement (achievement_id)); joinable!(character_achievement -> character (character_id)); +joinable!(character_achievement -> achievements (achievement_uuid)); joinable!(inventory -> character (character_id)); joinable!(loadout -> character (character_id)); joinable!(stats -> character (character_id)); allow_tables_to_appear_in_same_query!( - achievement, + achievements, body, character, character_achievement,