mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Initial models, migration and client code for stats persistence.
This commit is contained in:
parent
19a0ffb673
commit
e5853dbdd4
@ -17,4 +17,5 @@ pub struct Character {
|
|||||||
pub struct CharacterItem {
|
pub struct CharacterItem {
|
||||||
pub character: Character,
|
pub character: Character,
|
||||||
pub body: comp::Body,
|
pub body: comp::Body,
|
||||||
|
pub stats: comp::Stats,
|
||||||
}
|
}
|
||||||
|
1
server/src/migrations/2020-04-20-072214_stats/down.sql
Normal file
1
server/src/migrations/2020-04-20-072214_stats/down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS "stats";
|
10
server/src/migrations/2020-04-20-072214_stats/up.sql
Normal file
10
server/src/migrations/2020-04-20-072214_stats/up.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE "stats" (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
character_id INT NOT NULL,
|
||||||
|
"level" INT NOT NULL DEFAULT 1,
|
||||||
|
"exp" INT NOT NULL DEFAULT 0,
|
||||||
|
endurance INT NOT NULL DEFAULT 0,
|
||||||
|
fitness INT NOT NULL DEFAULT 0,
|
||||||
|
willpower INT NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE
|
||||||
|
);
|
@ -1,9 +1,10 @@
|
|||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
error::Error,
|
||||||
establish_connection,
|
establish_connection,
|
||||||
models::{Body, Character, NewCharacter},
|
models::{Body, Character, NewCharacter, Stats, StatsJoinData},
|
||||||
schema, Error,
|
schema,
|
||||||
};
|
};
|
||||||
use crate::comp;
|
use crate::comp;
|
||||||
use common::character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
|
use common::character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
|
||||||
@ -14,50 +15,60 @@ type CharacterListResult = Result<Vec<CharacterItem>, Error>;
|
|||||||
// Loading of characters happens immediately after login, and the data is only
|
// Loading of characters happens immediately after login, and the data is only
|
||||||
// for the purpose of rendering the character and their level in the character
|
// for the purpose of rendering the character and their level in the character
|
||||||
// list.
|
// list.
|
||||||
pub fn load_characters(uuid: &str) -> CharacterListResult {
|
pub fn load_characters(player_uuid: &str) -> CharacterListResult {
|
||||||
use schema::{body, character::dsl::*};
|
let data: Vec<(Character, Body, Stats)> = schema::character::dsl::character
|
||||||
|
.filter(schema::character::player_uuid.eq(player_uuid))
|
||||||
let data: Vec<(Character, Body)> = character
|
.order(schema::character::id.desc())
|
||||||
.filter(player_uuid.eq(uuid))
|
.inner_join(schema::body::table)
|
||||||
.order(id.desc())
|
.inner_join(schema::stats::table)
|
||||||
.inner_join(body::table)
|
.load::<(Character, Body, Stats)>(&establish_connection())?;
|
||||||
.load::<(Character, Body)>(&establish_connection())?;
|
|
||||||
|
|
||||||
Ok(data
|
Ok(data
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(character_data, body_data)| CharacterItem {
|
.map(|(character_data, body_data, stats_data)| {
|
||||||
character: CharacterData::from(character_data),
|
let character = CharacterData::from(character_data);
|
||||||
body: comp::Body::from(body_data),
|
let body = comp::Body::from(body_data);
|
||||||
|
let stats = comp::Stats::from(StatsJoinData {
|
||||||
|
character: &character,
|
||||||
|
body: &body,
|
||||||
|
stats: stats_data,
|
||||||
|
});
|
||||||
|
|
||||||
|
CharacterItem {
|
||||||
|
character,
|
||||||
|
body,
|
||||||
|
stats,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new character with provided comp::Character and comp::Body data.
|
/// Create a new character with provided comp::Character and comp::Body data.
|
||||||
/// Note that sqlite does not suppport returning the inserted data after a
|
/// Note that sqlite does not support returning the inserted data after a
|
||||||
/// successful insert. To workaround, we wrap this in a transaction which
|
/// successful insert. To workaround, we wrap this in a transaction which
|
||||||
/// inserts, queries for the newly created chaacter id, then uses the character
|
/// inserts, queries for the newly created chaacter id, then uses the character
|
||||||
/// id for insertion of the `body` table entry
|
/// id for insertion of the `body` table entry
|
||||||
pub fn create_character(
|
pub fn create_character(
|
||||||
uuid: &str,
|
uuid: &str,
|
||||||
alias: String,
|
character_alias: String,
|
||||||
tool: Option<String>,
|
character_tool: Option<String>,
|
||||||
body: &comp::Body,
|
body: &comp::Body,
|
||||||
) -> CharacterListResult {
|
) -> CharacterListResult {
|
||||||
check_character_limit(uuid)?;
|
check_character_limit(uuid)?;
|
||||||
|
|
||||||
let new_character = NewCharacter {
|
|
||||||
player_uuid: uuid,
|
|
||||||
alias: &alias,
|
|
||||||
tool: tool.as_deref(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let connection = establish_connection();
|
let connection = establish_connection();
|
||||||
|
|
||||||
connection.transaction::<_, diesel::result::Error, _>(|| {
|
connection.transaction::<_, diesel::result::Error, _>(|| {
|
||||||
use schema::{body, character, character::dsl::*};
|
use schema::{body, character, character::dsl::*, stats};
|
||||||
|
|
||||||
match body {
|
match body {
|
||||||
comp::Body::Humanoid(body_data) => {
|
comp::Body::Humanoid(body_data) => {
|
||||||
|
let new_character = NewCharacter {
|
||||||
|
player_uuid: uuid,
|
||||||
|
alias: &character_alias,
|
||||||
|
tool: character_tool.as_deref(),
|
||||||
|
};
|
||||||
|
|
||||||
diesel::insert_into(character::table)
|
diesel::insert_into(character::table)
|
||||||
.values(&new_character)
|
.values(&new_character)
|
||||||
.execute(&connection)?;
|
.execute(&connection)?;
|
||||||
@ -83,6 +94,22 @@ pub fn create_character(
|
|||||||
diesel::insert_into(body::table)
|
diesel::insert_into(body::table)
|
||||||
.values(&new_body)
|
.values(&new_body)
|
||||||
.execute(&connection)?;
|
.execute(&connection)?;
|
||||||
|
|
||||||
|
let default_stats = comp::Stats::new(String::from(new_character.alias), *body);
|
||||||
|
|
||||||
|
// Insert some default stats
|
||||||
|
let new_stats = Stats {
|
||||||
|
character_id: inserted_character.id as i32,
|
||||||
|
level: default_stats.level.level() as i32,
|
||||||
|
exp: default_stats.exp.current() as i32,
|
||||||
|
endurance: default_stats.endurance as i32,
|
||||||
|
fitness: default_stats.fitness as i32,
|
||||||
|
willpower: default_stats.willpower as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(stats::table)
|
||||||
|
.values(&new_stats)
|
||||||
|
.execute(&connection)?;
|
||||||
},
|
},
|
||||||
_ => log::warn!("Creating non-humanoid characters is not supported."),
|
_ => log::warn!("Creating non-humanoid characters is not supported."),
|
||||||
};
|
};
|
||||||
@ -105,12 +132,10 @@ fn check_character_limit(uuid: &str) -> Result<(), Error> {
|
|||||||
use diesel::dsl::count_star;
|
use diesel::dsl::count_star;
|
||||||
use schema::character::dsl::*;
|
use schema::character::dsl::*;
|
||||||
|
|
||||||
let connection = establish_connection();
|
|
||||||
|
|
||||||
let character_count = character
|
let character_count = character
|
||||||
.select(count_star())
|
.select(count_star())
|
||||||
.filter(player_uuid.eq(uuid))
|
.filter(player_uuid.eq(uuid))
|
||||||
.load::<i64>(&connection)?;
|
.load::<i64>(&establish_connection())?;
|
||||||
|
|
||||||
match character_count.first() {
|
match character_count.first() {
|
||||||
Some(count) => {
|
Some(count) => {
|
||||||
|
24
server/src/persistence/error.rs
Normal file
24
server/src/persistence/error.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
// The player has alredy reached the max character limit
|
||||||
|
CharacterLimitReached,
|
||||||
|
// An error occured when performing a database action
|
||||||
|
DatabaseError(diesel::result::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", match self {
|
||||||
|
Self::DatabaseError(diesel_error) => diesel_error.to_string(),
|
||||||
|
Self::CharacterLimitReached => String::from("Character limit exceeded"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for Error {
|
||||||
|
fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) }
|
||||||
|
}
|
@ -1,34 +1,13 @@
|
|||||||
pub mod character;
|
pub mod character;
|
||||||
|
mod error;
|
||||||
mod models;
|
mod models;
|
||||||
|
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel_migrations::embed_migrations;
|
use diesel_migrations::embed_migrations;
|
||||||
use std::{env, fmt, fs, path::Path};
|
use std::{env, fs, path::Path};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
// The player has alredy reached the max character limit
|
|
||||||
CharacterLimitReached,
|
|
||||||
// An error occured when performing a database action
|
|
||||||
DatabaseError(diesel::result::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", match self {
|
|
||||||
Self::DatabaseError(diesel_error) => diesel_error.to_string(),
|
|
||||||
Self::CharacterLimitReached => String::from("Character limit exceeded"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<diesel::result::Error> for Error {
|
|
||||||
fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html
|
// See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html
|
||||||
// This macro is called at build-time, and produces the necessary migration info
|
// This macro is called at build-time, and produces the necessary migration info
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
use super::schema::{body, character};
|
use super::schema::{body, character, stats};
|
||||||
use crate::comp;
|
use crate::comp;
|
||||||
use common::character::Character as CharacterData;
|
use common::character::Character as CharacterData;
|
||||||
|
|
||||||
|
/// When we want to build player stats from database data, we need data from the
|
||||||
|
/// character, body and stats tables
|
||||||
|
pub struct StatsJoinData<'a> {
|
||||||
|
pub character: &'a CharacterData,
|
||||||
|
pub body: &'a comp::Body,
|
||||||
|
pub stats: &'a Stats,
|
||||||
|
}
|
||||||
|
|
||||||
/// `Character` represents a playable character belonging to a player
|
/// `Character` represents a playable character belonging to a player
|
||||||
#[derive(Identifiable, Queryable, Debug)]
|
#[derive(Identifiable, Queryable, Debug)]
|
||||||
#[table_name = "character"]
|
#[table_name = "character"]
|
||||||
@ -63,3 +71,45 @@ impl From<&Body> for comp::Body {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Stats` represents the stats for a character
|
||||||
|
#[derive(Associations, Identifiable, Queryable, Debug, Insertable)]
|
||||||
|
#[belongs_to(Character)]
|
||||||
|
#[primary_key(character_id)]
|
||||||
|
#[table_name = "stats"]
|
||||||
|
pub struct Stats {
|
||||||
|
pub character_id: i32,
|
||||||
|
pub level: i32,
|
||||||
|
pub exp: i32,
|
||||||
|
pub endurance: i32,
|
||||||
|
pub fitness: i32,
|
||||||
|
pub willpower: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StatsJoinData<'_>> for comp::Stats {
|
||||||
|
fn from(data: StatsJoinData) -> comp::Stats {
|
||||||
|
let mut base_stats = comp::Stats::new(String::from(&data.character.alias), *data.body);
|
||||||
|
|
||||||
|
base_stats.level.set_level(data.stats.level as u32);
|
||||||
|
base_stats.update_max_hp();
|
||||||
|
|
||||||
|
base_stats.exp.set_current(data.stats.exp as u32);
|
||||||
|
|
||||||
|
base_stats.endurance = data.stats.endurance as u32;
|
||||||
|
base_stats.fitness = data.stats.fitness as u32;
|
||||||
|
base_stats.willpower = data.stats.willpower as u32;
|
||||||
|
|
||||||
|
base_stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AsChangeset)]
|
||||||
|
#[primary_key(character_id)]
|
||||||
|
#[table_name = "stats"]
|
||||||
|
pub struct StatsUpdate {
|
||||||
|
pub level: Option<i32>,
|
||||||
|
pub exp: Option<i32>,
|
||||||
|
pub endurance: Option<i32>,
|
||||||
|
pub fitness: Option<i32>,
|
||||||
|
pub willpower: Option<i32>,
|
||||||
|
}
|
||||||
|
@ -22,6 +22,18 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinable!(body -> character (character_id));
|
table! {
|
||||||
|
stats (character_id) {
|
||||||
|
character_id -> Integer,
|
||||||
|
level -> Integer,
|
||||||
|
exp -> Integer,
|
||||||
|
endurance -> Integer,
|
||||||
|
fitness -> Integer,
|
||||||
|
willpower -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(body, character,);
|
joinable!(body -> character (character_id));
|
||||||
|
joinable!(stats -> character (character_id));
|
||||||
|
|
||||||
|
allow_tables_to_appear_in_same_query!(body, character, stats,);
|
||||||
|
33
server/src/persistence/stats.rs
Normal file
33
server/src/persistence/stats.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use super::{establish_connection, models::StatsUpdate, schema};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
character_id: i32,
|
||||||
|
level: Option<i32>,
|
||||||
|
exp: Option<i32>,
|
||||||
|
endurance: Option<i32>,
|
||||||
|
fitness: Option<i32>,
|
||||||
|
willpower: Option<i32>,
|
||||||
|
) {
|
||||||
|
use schema::stats;
|
||||||
|
|
||||||
|
match diesel::update(stats::table)
|
||||||
|
.set(&StatsUpdate {
|
||||||
|
level,
|
||||||
|
exp,
|
||||||
|
endurance,
|
||||||
|
fitness,
|
||||||
|
willpower,
|
||||||
|
})
|
||||||
|
.execute(&establish_connection())
|
||||||
|
{
|
||||||
|
Err(error) => log::warn!(
|
||||||
|
"Failed to update stats for player with character_id: {:?}: {:?}",
|
||||||
|
character_id,
|
||||||
|
error
|
||||||
|
),
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
}
|
@ -341,14 +341,19 @@ impl CharSelectionUi {
|
|||||||
Mode::Select(data) => data.clone(),
|
Mode::Select(data) => data.clone(),
|
||||||
Mode::Create {
|
Mode::Create {
|
||||||
name, body, tool, ..
|
name, body, tool, ..
|
||||||
} => Some(vec![CharacterItem {
|
} => {
|
||||||
character: Character {
|
let body = comp::Body::Humanoid(body.clone());
|
||||||
id: None,
|
|
||||||
alias: name.clone(),
|
Some(vec![CharacterItem {
|
||||||
tool: tool.map(|specifier| specifier.to_string()),
|
character: Character {
|
||||||
},
|
id: None,
|
||||||
body: comp::Body::Humanoid(body.clone()),
|
alias: name.clone(),
|
||||||
}]),
|
tool: tool.map(|specifier| specifier.to_string()),
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
stats: comp::Stats::new(String::from(name), body),
|
||||||
|
}])
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user