diff --git a/common/src/character.rs b/common/src/character.rs index ed20c15092..73f824613d 100644 --- a/common/src/character.rs +++ b/common/src/character.rs @@ -17,4 +17,5 @@ pub struct Character { pub struct CharacterItem { pub character: Character, pub body: comp::Body, + pub stats: comp::Stats, } diff --git a/server/src/migrations/2020-04-20-072214_stats/down.sql b/server/src/migrations/2020-04-20-072214_stats/down.sql new file mode 100644 index 0000000000..88d1797186 --- /dev/null +++ b/server/src/migrations/2020-04-20-072214_stats/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "stats"; \ No newline at end of file diff --git a/server/src/migrations/2020-04-20-072214_stats/up.sql b/server/src/migrations/2020-04-20-072214_stats/up.sql new file mode 100644 index 0000000000..6ef9f3fc95 --- /dev/null +++ b/server/src/migrations/2020-04-20-072214_stats/up.sql @@ -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 +); \ No newline at end of file diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index 2f45619534..9c58814105 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -1,9 +1,10 @@ extern crate diesel; use super::{ + error::Error, establish_connection, - models::{Body, Character, NewCharacter}, - schema, Error, + models::{Body, Character, NewCharacter, Stats, StatsJoinData}, + schema, }; use crate::comp; use common::character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER}; @@ -14,50 +15,60 @@ type CharacterListResult = Result, Error>; // 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 // list. -pub fn load_characters(uuid: &str) -> CharacterListResult { - use schema::{body, character::dsl::*}; - - let data: Vec<(Character, Body)> = character - .filter(player_uuid.eq(uuid)) - .order(id.desc()) - .inner_join(body::table) - .load::<(Character, Body)>(&establish_connection())?; +pub fn load_characters(player_uuid: &str) -> CharacterListResult { + let data: Vec<(Character, Body, Stats)> = schema::character::dsl::character + .filter(schema::character::player_uuid.eq(player_uuid)) + .order(schema::character::id.desc()) + .inner_join(schema::body::table) + .inner_join(schema::stats::table) + .load::<(Character, Body, Stats)>(&establish_connection())?; Ok(data .iter() - .map(|(character_data, body_data)| CharacterItem { - character: CharacterData::from(character_data), - body: comp::Body::from(body_data), + .map(|(character_data, body_data, stats_data)| { + let character = CharacterData::from(character_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()) } /// 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 /// inserts, queries for the newly created chaacter id, then uses the character /// id for insertion of the `body` table entry pub fn create_character( uuid: &str, - alias: String, - tool: Option, + character_alias: String, + character_tool: Option, body: &comp::Body, ) -> CharacterListResult { check_character_limit(uuid)?; - let new_character = NewCharacter { - player_uuid: uuid, - alias: &alias, - tool: tool.as_deref(), - }; - let connection = establish_connection(); connection.transaction::<_, diesel::result::Error, _>(|| { - use schema::{body, character, character::dsl::*}; + use schema::{body, character, character::dsl::*, stats}; match body { 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) .values(&new_character) .execute(&connection)?; @@ -83,6 +94,22 @@ pub fn create_character( diesel::insert_into(body::table) .values(&new_body) .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."), }; @@ -105,12 +132,10 @@ fn check_character_limit(uuid: &str) -> Result<(), Error> { use diesel::dsl::count_star; use schema::character::dsl::*; - let connection = establish_connection(); - let character_count = character .select(count_star()) .filter(player_uuid.eq(uuid)) - .load::(&connection)?; + .load::(&establish_connection())?; match character_count.first() { Some(count) => { diff --git a/server/src/persistence/error.rs b/server/src/persistence/error.rs new file mode 100644 index 0000000000..11ae1e1790 --- /dev/null +++ b/server/src/persistence/error.rs @@ -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 for Error { + fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) } +} diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index 6aa08213f0..690dc2e7a4 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -1,34 +1,13 @@ pub mod character; +mod error; mod models; - mod schema; extern crate diesel; use diesel::prelude::*; use diesel_migrations::embed_migrations; -use std::{env, fmt, 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 for Error { - fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) } -} +use std::{env, fs, path::Path}; // 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 diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index 0ddc108b45..20b67befbd 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -1,7 +1,15 @@ -use super::schema::{body, character}; +use super::schema::{body, character, stats}; use crate::comp; 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 #[derive(Identifiable, Queryable, Debug)] #[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> 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, + pub exp: Option, + pub endurance: Option, + pub fitness: Option, + pub willpower: Option, +} diff --git a/server/src/persistence/schema.rs b/server/src/persistence/schema.rs index 36a3f975d4..6b3528d970 100644 --- a/server/src/persistence/schema.rs +++ b/server/src/persistence/schema.rs @@ -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,); diff --git a/server/src/persistence/stats.rs b/server/src/persistence/stats.rs new file mode 100644 index 0000000000..75a0c9b371 --- /dev/null +++ b/server/src/persistence/stats.rs @@ -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, + exp: Option, + endurance: Option, + fitness: Option, + willpower: Option, +) { + 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 + ), + _ => {}, + }; +} diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index 8587b59679..8d61fffbfb 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -341,14 +341,19 @@ impl CharSelectionUi { Mode::Select(data) => data.clone(), Mode::Create { name, body, tool, .. - } => Some(vec![CharacterItem { - character: Character { - id: None, - alias: name.clone(), - tool: tool.map(|specifier| specifier.to_string()), - }, - body: comp::Body::Humanoid(body.clone()), - }]), + } => { + let body = comp::Body::Humanoid(body.clone()); + + Some(vec![CharacterItem { + character: Character { + id: None, + alias: name.clone(), + tool: tool.map(|specifier| specifier.to_string()), + }, + body, + stats: comp::Stats::new(String::from(name), body), + }]) + }, } }