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.
This commit is contained in:
Shane Handley 2020-07-06 14:01:46 +10:00
parent efe7a62c92
commit c056b2b079
10 changed files with 235 additions and 293 deletions

View File

@ -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<Achievement> 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<Achievement>);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AchievementList(HashMap<String, CharacterAchievement>);
impl Default for AchievementList {
fn default() -> AchievementList { AchievementList(Vec::new()) }
fn default() -> AchievementList { AchievementList(HashMap::new()) }
}
impl AchievementList {
pub fn from(data: Vec<Achievement>) -> Self { Self(data) }
pub fn from(data: HashMap<String, CharacterAchievement>) -> Self { Self(data) }
}
impl Component for AchievementList {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
type Storage = FlaggedStorage<Self, IDVStorage<Self>>; // 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<CharacterAchievement> {
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);
}
}

View File

@ -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};

View File

@ -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

View File

@ -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

View File

@ -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<Achievement>,
/// Holds a list of all available achievements
achievement_data: Vec<comp::Achievement>,
}
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::<comp::AchievementList>()
.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),
)
}
}

View File

@ -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
);

View File

@ -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
);

View File

@ -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<comp::AchievementList, 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))?;
.inner_join(schema::achievements::table)
.load::<(CharacterAchievement, AchievementModel)>(&establish_connection(db_dir))?;
Ok(comp::AchievementList::from(
character_achievements
.iter()
.map(comp::Achievement::from)
.collect::<Vec<comp::Achievement>>(),
.map(|(character_achievement, achievement)| {
(
achievement.uuid.clone(),
comp::CharacterAchievement::from(CharacterAchievementJoinData {
character_achievement,
achievement,
}),
)
})
.collect(),
))
}
pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, Error> {
pub fn sync(db_dir: &str) -> Result<Vec<comp::Achievement>, 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::<AchievementModel>(&connection)?;
schema::achievements::dsl::achievements.load::<AchievementModel>(&connection)?;
// Get a hash of the Vec<Achievement> to compare to the migration table
// Get a hash of the Vec<Achievement> we have in config to compare
let result = schema::data_migration::dsl::data_migration
.filter(schema::data_migration::title.eq(String::from("achievements")))
.first::<DataMigration>(&connection);
@ -132,12 +139,11 @@ pub fn sync(db_dir: &str) -> Result<Vec<AchievementModel>, 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<Vec<AchievementModel>, 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<Vec<AchievementModel>, Error> {
info!("No achievement updates required");
}
let data = schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
let data = schema::achievements::dsl::achievements.load::<AchievementModel>(&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<AchievementItem> {
fn load_data() -> Vec<AchievementModel> {
// 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<AchievementItem> {
}
}
pub fn hash<T: Hash>(t: &T) -> u64 {
fn hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()

View File

@ -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<DB> diesel::deserialize::FromSql<Text, DB> for AchievementData
where
DB: diesel::backend::Backend,
String: diesel::deserialize::FromSql<Text, DB>,
{
fn from_sql(
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
) -> diesel::deserialize::Result<Self> {
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<DB> diesel::serialize::ToSql<Text, DB> for AchievementData
where
DB: diesel::backend::Backend,
{
fn to_sql<W: std::io::Write>(
&self,
out: &mut diesel::serialize::Output<W, DB>,
) -> diesel::serialize::Result {
let s = serde_json::to_string(&self.0)?;
<String as diesel::serialize::ToSql<Text, DB>>::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<CharacterAchievementJoinData<'_>> 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,
}
}
}

View File

@ -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,