mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Initial DB schama, migrations and persistence models for achievements.
This commit is contained in:
parent
0b66a88ae5
commit
707412edc0
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -993,6 +993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
|
||||
dependencies = [
|
||||
"byteorder 1.3.4",
|
||||
"chrono",
|
||||
"diesel_derives",
|
||||
"libsqlite3-sys",
|
||||
]
|
||||
|
10
assets/voxygen/data/achievements.ron
Normal file
10
assets/voxygen/data/achievements.ron
Normal file
@ -0,0 +1,10 @@
|
||||
[
|
||||
(
|
||||
title: "Collect 10 Apples",
|
||||
achievement_type: Collect("common.items.apple", 10)
|
||||
),
|
||||
(
|
||||
title: "Collect 50 Apples",
|
||||
achievement_type: Collect("common.items.apple", 50)
|
||||
)
|
||||
]
|
23
common/src/achievement.rs
Normal file
23
common/src/achievement.rs
Normal file
@ -0,0 +1,23 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
option_zip
|
||||
)]
|
||||
|
||||
pub mod achievement;
|
||||
pub mod assets;
|
||||
pub mod astar;
|
||||
pub mod character;
|
||||
|
@ -36,6 +36,6 @@ tiny_http = "0.7.0"
|
||||
portpicker = { git = "https://github.com/xMAC94x/portpicker-rs" }
|
||||
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "223a4097f7ebc8d451936dccb5e6517194bbf086" }
|
||||
libsqlite3-sys = { version = "0.18", features = ["bundled"] }
|
||||
diesel = { version = "1.4.3", features = ["sqlite"] }
|
||||
diesel = { version = "1.4.3", features = ["sqlite", "chrono"] }
|
||||
diesel_migrations = "1.4.0"
|
||||
dotenv = "0.15.0"
|
||||
|
10
server/src/data/achievements.ron
Normal file
10
server/src/data/achievements.ron
Normal file
@ -0,0 +1,10 @@
|
||||
[
|
||||
(
|
||||
title: "Collect 10 Apples",
|
||||
achievement_type: Collect("common.items.apple", 10)
|
||||
),
|
||||
(
|
||||
title: "Collect 50 Apples",
|
||||
achievement_type: Collect("common.items.apple", 50)
|
||||
)
|
||||
]
|
@ -263,6 +263,15 @@ impl Server {
|
||||
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;
|
||||
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "achievement";
|
@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS "achievement" (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
checksum VARCHAR(64) NOT NULL UNIQUE,
|
||||
details TEXT NOT NULL
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "data_migration";
|
@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS "data_migration" (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
title VARCHAR(64) NOT NULL,
|
||||
checksum VARCHAR(64) NOT NULL,
|
||||
last_run TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
121
server/src/persistence/achievement.rs
Normal file
121
server/src/persistence/achievement.rs
Normal file
@ -0,0 +1,121 @@
|
||||
extern crate diesel;
|
||||
|
||||
use super::{
|
||||
error::Error,
|
||||
establish_connection,
|
||||
models::{Achievement as AchievementModel, DataMigration, NewAchievement, NewDataMigration},
|
||||
schema,
|
||||
};
|
||||
use common::achievement::*;
|
||||
use diesel::{
|
||||
prelude::*,
|
||||
result::{DatabaseErrorKind, Error as DieselError},
|
||||
};
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub fn sync(db_dir: &str) -> Result<(), Error> {
|
||||
let achievements = load_data();
|
||||
let connection = establish_connection(db_dir);
|
||||
|
||||
// Get a hash of the Vec<Achievement> and compare to the migration table
|
||||
let result = schema::data_migration::dsl::data_migration
|
||||
.filter(schema::data_migration::title.eq(String::from("achievements")))
|
||||
.load::<DataMigration>(&connection)?;
|
||||
|
||||
// First 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(),
|
||||
};
|
||||
|
||||
diesel::insert_into(schema::data_migration::table)
|
||||
.values(&migration)
|
||||
.execute(&connection)?;
|
||||
}
|
||||
|
||||
// Also check checksum. Bail if same, continue if changed
|
||||
if result.is_empty() {
|
||||
info!("Achievements need updating...");
|
||||
|
||||
// Use the full dataset for checks
|
||||
let persisted_achievements =
|
||||
schema::achievement::dsl::achievement.load::<AchievementModel>(&connection)?;
|
||||
|
||||
// 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)
|
||||
.execute(&connection)
|
||||
{
|
||||
match error {
|
||||
DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _) => {
|
||||
let entry = 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => return Err(Error::DatabaseError(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the checksum for the migration
|
||||
diesel::update(schema::data_migration::dsl::data_migration)
|
||||
.filter(schema::data_migration::title.eq(String::from("achievements")))
|
||||
.set((
|
||||
schema::data_migration::checksum.eq(hash(&achievements).to_string()),
|
||||
schema::data_migration::last_run.eq(chrono::Utc::now().naive_utc()),
|
||||
))
|
||||
.execute(&connection)?;
|
||||
} else {
|
||||
info!("No achievement updates required");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_data() -> Vec<AchievementItem> {
|
||||
if let Ok(path) = std::fs::canonicalize("../data/achievements.ron") {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
|
||||
info!(?path, "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()),
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash<T: Hash>(t: &T) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
t.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
//! for managing table migrations
|
||||
//! - [`diesel-cli`](https://github.com/diesel-rs/diesel/tree/master/diesel_cli/)
|
||||
//! for generating and testing migrations
|
||||
|
||||
pub mod achievement;
|
||||
pub mod character;
|
||||
|
||||
mod error;
|
||||
|
@ -1,8 +1,12 @@
|
||||
extern crate serde_json;
|
||||
|
||||
use super::schema::{body, character, inventory, loadout, stats};
|
||||
use super::{
|
||||
achievement::hash,
|
||||
schema::{achievement, body, character, data_migration, inventory, loadout, stats},
|
||||
};
|
||||
use crate::comp;
|
||||
use common::character::Character as CharacterData;
|
||||
use chrono::NaiveDateTime;
|
||||
use common::{achievement::AchievementItem, character::Character as CharacterData};
|
||||
use diesel::sql_types::Text;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
@ -348,6 +352,91 @@ impl From<(i32, &comp::Loadout)> for LoadoutUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic datamigration entry
|
||||
#[derive(Queryable, Debug, Identifiable)]
|
||||
#[table_name = "data_migration"]
|
||||
pub struct DataMigration {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub checksum: String,
|
||||
pub last_run: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable, PartialEq, Debug)]
|
||||
#[table_name = "data_migration"]
|
||||
pub struct NewDataMigration<'a> {
|
||||
pub title: &'a str,
|
||||
pub checksum: &'a str,
|
||||
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(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(AchievementItem::default()))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"]
|
||||
pub struct Achievement {
|
||||
pub id: i32,
|
||||
pub checksum: String,
|
||||
pub details: AchievementData,
|
||||
}
|
||||
|
||||
#[derive(Insertable, PartialEq, Debug)]
|
||||
#[table_name = "achievement"]
|
||||
pub struct NewAchievement {
|
||||
pub checksum: String,
|
||||
pub details: AchievementData,
|
||||
}
|
||||
|
||||
impl From<&AchievementItem> for NewAchievement {
|
||||
fn from(item: &AchievementItem) -> Self {
|
||||
Self {
|
||||
checksum: hash(item).to_string(),
|
||||
details: AchievementData(item.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -1,3 +1,11 @@
|
||||
table! {
|
||||
achievement (id) {
|
||||
id -> Integer,
|
||||
checksum -> Text,
|
||||
details -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
body (character_id) {
|
||||
character_id -> Integer,
|
||||
@ -22,6 +30,15 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
data_migration (id) {
|
||||
id -> Integer,
|
||||
title -> Text,
|
||||
checksum -> Text,
|
||||
last_run -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
inventory (character_id) {
|
||||
character_id -> Integer,
|
||||
@ -54,4 +71,4 @@ joinable!(inventory -> character (character_id));
|
||||
joinable!(loadout -> character (character_id));
|
||||
joinable!(stats -> character (character_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(body, character, inventory, loadout, stats);
|
||||
allow_tables_to_appear_in_same_query!(achievement, body, character, inventory, loadout, stats);
|
||||
|
Loading…
Reference in New Issue
Block a user