Initial models, migration and client code for stats persistence.

This commit is contained in:
Shane Handley 2020-04-20 18:44:29 +10:00
parent 19a0ffb673
commit e5853dbdd4
10 changed files with 200 additions and 60 deletions

View File

@ -17,4 +17,5 @@ pub struct Character {
pub struct CharacterItem {
pub character: Character,
pub body: comp::Body,
pub stats: comp::Stats,
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS "stats";

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

View File

@ -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<Vec<CharacterItem>, 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<String>,
character_alias: String,
character_tool: Option<String>,
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::<i64>(&connection)?;
.load::<i64>(&establish_connection())?;
match character_count.first() {
Some(count) => {

View 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) }
}

View File

@ -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<diesel::result::Error> 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

View File

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

View File

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

View 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
),
_ => {},
};
}

View File

@ -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),
}])
},
}
}