diff --git a/server/src/lib.rs b/server/src/lib.rs index f59dd838fe..bb2999167f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -108,7 +108,7 @@ impl Server { state .ecs_mut() .insert(sys::StatsPersistenceScheduler::every(Duration::from_secs( - 5, + 60, ))); // Server-only components diff --git a/server/src/migrations/2020-04-20-072214_stats/up.sql b/server/src/migrations/2020-04-20-072214_stats/up.sql index 8f18ca9709..b9eb11121c 100644 --- a/server/src/migrations/2020-04-20-072214_stats/up.sql +++ b/server/src/migrations/2020-04-20-072214_stats/up.sql @@ -1,7 +1,7 @@ CREATE TABLE "stats" ( character_id INT NOT NULL PRIMARY KEY, "level" INT NOT NULL DEFAULT 1, - "exp" INT NOT NULL DEFAULT 0, + exp INT NOT NULL DEFAULT 0, endurance INT NOT NULL DEFAULT 0, fitness INT NOT NULL DEFAULT 0, willpower INT NOT NULL DEFAULT 0, diff --git a/server/src/persistence/error.rs b/server/src/persistence/error.rs index 11ae1e1790..90b4fd15da 100644 --- a/server/src/persistence/error.rs +++ b/server/src/persistence/error.rs @@ -4,7 +4,7 @@ use std::fmt; #[derive(Debug)] pub enum Error { - // The player has alredy reached the max character limit + // The player has already reached the max character limit CharacterLimitReached, // An error occured when performing a database action DatabaseError(diesel::result::Error), diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index a081199d29..542d0b57d2 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -73,7 +73,7 @@ impl From<&Body> for comp::Body { } /// `Stats` represents the stats for a character -#[derive(Associations, Identifiable, Queryable, Debug, Insertable)] +#[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)] #[belongs_to(Character)] #[primary_key(character_id)] #[table_name = "stats"] @@ -110,9 +110,9 @@ impl From> for comp::Stats { #[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, + pub level: i32, + pub exp: i32, + pub endurance: i32, + pub fitness: i32, + pub willpower: i32, } diff --git a/server/src/persistence/stats.rs b/server/src/persistence/stats.rs index 75a0c9b371..9bf8ee41a7 100644 --- a/server/src/persistence/stats.rs +++ b/server/src/persistence/stats.rs @@ -1,33 +1,83 @@ extern crate diesel; -use super::{establish_connection, models::StatsUpdate, schema}; +use super::establish_connection; +use crate::comp; 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 - ), +/// Update DB rows for stats given a Vec of (character_id, Stats) tuples +pub fn update(data: Vec<(i32, &comp::Stats)>) { + match establish_connection().execute(&build_query(data)) { + Err(diesel_error) => log::warn!("Error updating stats: {:?}", diesel_error), _ => {}, - }; + } +} + +/// Takes a Vec of (character_id, Stats) tuples and builds an SQL UPDATE query +/// Since there is apprently no sensible way to update > 1 row using diesel, we +/// just construct the raw SQL +fn build_query(data: Vec<(i32, &comp::Stats)>) -> String { + data.iter() + .map(|(character_id, stats)| { + String::from(format!( + "UPDATE stats SET level = {}, exp = {}, endurance = {}, fitness = {}, willpower = \ + {} WHERE character_id = {};", + stats.level.level() as i32, + stats.exp.current() as i32, + stats.endurance as i32, + stats.fitness as i32, + stats.willpower as i32, + *character_id as i32 + )) + }) + .collect::>() + .join(" ") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_query_for_multiple_characters() { + let mut stats_one = comp::Stats::new( + String::from("One"), + comp::Body::Humanoid(comp::humanoid::Body::random()), + ); + + stats_one.endurance = 1; + stats_one.fitness = 1; + stats_one.willpower = 1; + + let mut stats_two = comp::Stats::new( + String::from("Two"), + comp::Body::Humanoid(comp::humanoid::Body::random()), + ); + + stats_two.endurance = 2; + stats_two.fitness = 2; + stats_two.willpower = 2; + + let mut stats_three = comp::Stats::new( + String::from("Three"), + comp::Body::Humanoid(comp::humanoid::Body::random()), + ); + + stats_three.endurance = 3; + stats_three.fitness = 3; + stats_three.willpower = 3; + + let data = vec![ + (1_i32, &stats_one), + (2_i32, &stats_two), + (3_i32, &stats_three), + ]; + + assert_eq!( + build_query(data), + "UPDATE stats SET level = 1, exp = 0, endurance = 1, fitness = 1, willpower = 1 WHERE \ + character_id = 1; UPDATE stats SET level = 1, exp = 0, endurance = 2, fitness = 2, \ + willpower = 2 WHERE character_id = 2; UPDATE stats SET level = 1, exp = 0, endurance \ + = 3, fitness = 3, willpower = 3 WHERE character_id = 3;" + ); + } } diff --git a/server/src/sys/persistence/stats.rs b/server/src/sys/persistence/stats.rs index 71e04f1669..d193f7e7de 100644 --- a/server/src/sys/persistence/stats.rs +++ b/server/src/sys/persistence/stats.rs @@ -13,17 +13,19 @@ impl<'a> System<'a> for Sys { fn run(&mut self, (players, player_stats, mut scheduler): Self::SystemData) { if scheduler.should_run() { - for (player, stats) in (&players, &player_stats).join() { - if let Some(character_id) = player.character_id { - stats::update( - character_id, - Some(stats.level.level() as i32), - Some(stats.exp.current() as i32), - None, - None, - None, - ); - } + let updates: Vec<(i32, &Stats)> = (&players, &player_stats) + .join() + .filter_map(|(player, stats)| { + if let Some(character_id) = player.character_id { + Some((character_id, stats)) + } else { + None + } + }) + .collect::>(); + + if !updates.is_empty() { + stats::update(updates); } } }