mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
efe7a62c92
commit
c056b2b079
@ -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,
|
||||
// Initialise with the already completed event
|
||||
let mut achievement_list = AchievementList::default();
|
||||
|
||||
achievement_list
|
||||
.0
|
||||
.insert(uuid.clone(), CharacterAchievement {
|
||||
achievement: achievement.clone(),
|
||||
completed: true,
|
||||
progress: 3,
|
||||
};
|
||||
});
|
||||
|
||||
let mut achievement_list = AchievementList(vec![achievement.clone()]);
|
||||
achievement_list.process_achievement(&achievement, &AchievementEvent::KilledNpc);
|
||||
|
||||
let event =
|
||||
AchievementEvent::CollectedItem(assets::load_expect_cloned("common.items.mushroom"));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
@ -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
|
||||
);
|
@ -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,41 +164,31 @@ 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 {
|
||||
.find(|&a| &a.uuid == &item.uuid)
|
||||
{
|
||||
match diesel::update(
|
||||
schema::achievement::dsl::achievement.filter(
|
||||
schema::achievement::checksum
|
||||
.eq(String::from(&existing_item.checksum)),
|
||||
),
|
||||
schema::achievements::dsl::achievements
|
||||
.filter(schema::achievements::uuid.eq(&existing_item.uuid)),
|
||||
)
|
||||
.set(schema::achievement::details.eq(new_item.details))
|
||||
.set(item)
|
||||
.execute(&connection)
|
||||
{
|
||||
Ok(_) => warn!(?existing_item.checksum, "Updated achievement"),
|
||||
Err(err) => return Err(Error::DatabaseError(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => return Err(Error::DatabaseError(error)),
|
||||
}
|
||||
@ -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()
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user