From ceb3e26341e217f9f8bc880b5a8fe9d78215e2b2 Mon Sep 17 00:00:00 2001 From: Shane Handley Date: Sat, 25 Apr 2020 23:41:27 +1000 Subject: [PATCH] Stats persistence - Update client code to use persisted stats - Add a system for stats persistence - Add a basic scheduler to control duration between execution of persistence systems --- client/src/lib.rs | 16 +++++-- common/src/event.rs | 3 +- common/src/msg/client.rs | 3 +- server/src/events/entity_creation.rs | 5 +- server/src/events/mod.rs | 5 +- server/src/lib.rs | 9 ++++ .../migrations/2020-04-20-072214_stats/up.sql | 3 +- server/src/persistence/mod.rs | 2 + server/src/persistence/models.rs | 7 ++- server/src/state_ext.rs | 21 ++++++-- server/src/sys/message.rs | 10 +++- server/src/sys/mod.rs | 48 ++++++++++++++++++- server/src/sys/persistence/mod.rs | 1 + server/src/sys/persistence/stats.rs | 30 ++++++++++++ voxygen/src/menu/char_selection/mod.rs | 13 +++-- voxygen/src/menu/char_selection/ui.rs | 10 ++-- 16 files changed, 156 insertions(+), 30 deletions(-) create mode 100644 server/src/sys/persistence/mod.rs create mode 100644 server/src/sys/persistence/stats.rs diff --git a/client/src/lib.rs b/client/src/lib.rs index 3c57766420..47b61d96e1 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -234,9 +234,19 @@ impl Client { } /// Request a state transition to `ClientState::Character`. - pub fn request_character(&mut self, name: String, body: comp::Body, main: Option) { - self.postbox - .send_message(ClientMsg::Character { name, body, main }); + pub fn request_character( + &mut self, + character_id: i32, + body: comp::Body, + main: Option, + stats: comp::Stats, + ) { + self.postbox.send_message(ClientMsg::Character { + character_id, + body, + main, + stats, + }); self.client_state = ClientState::Pending; } diff --git a/common/src/event.rs b/common/src/event.rs index 8c9949105e..4ae936c39a 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -92,9 +92,10 @@ pub enum ServerEvent { Possess(Uid, Uid), SelectCharacter { entity: EcsEntity, - name: String, + character_id: i32, body: comp::Body, main: Option, + stats: comp::Stats, }, ExitIngame { entity: EcsEntity, diff --git a/common/src/msg/client.rs b/common/src/msg/client.rs index 225d324188..989b17d64c 100644 --- a/common/src/msg/client.rs +++ b/common/src/msg/client.rs @@ -15,9 +15,10 @@ pub enum ClientMsg { }, DeleteCharacter(i32), Character { - name: String, + character_id: i32, body: comp::Body, main: Option, // Specifier for the weapon + stats: comp::Stats, }, /// Request `ClientState::Registered` from an ingame state ExitIngame, diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 1c9bf9f821..be46f2545a 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -12,14 +12,15 @@ use vek::{Rgb, Vec3}; pub fn handle_create_character( server: &mut Server, entity: EcsEntity, - name: String, + character_id: i32, body: Body, main: Option, + stats: Stats, ) { let state = &mut server.state; let server_settings = &server.server_settings; - state.create_player_character(entity, name, body, main, server_settings); + state.create_player_character(entity, character_id, body, main, stats, server_settings); sys::subscription::initialize_region_subscription(state.ecs(), entity); } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index c56b695682..9dd9c7bdae 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -71,10 +71,11 @@ impl Server { }, ServerEvent::SelectCharacter { entity, - name, + character_id, body, main, - } => handle_create_character(self, entity, name, body, main), + stats, + } => handle_create_character(self, entity, character_id, body, main, stats), ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity), ServerEvent::CreateNpc { pos, diff --git a/server/src/lib.rs b/server/src/lib.rs index eb29844b80..f59dd838fe 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -94,6 +94,7 @@ impl Server { .insert(AuthProvider::new(settings.auth_server_address.clone())); state.ecs_mut().insert(Tick(0)); state.ecs_mut().insert(ChunkGenerator::new()); + // System timers for performance monitoring state.ecs_mut().insert(sys::EntitySyncTimer::default()); state.ecs_mut().insert(sys::MessageTimer::default()); @@ -102,6 +103,14 @@ impl Server { state.ecs_mut().insert(sys::TerrainSyncTimer::default()); state.ecs_mut().insert(sys::TerrainTimer::default()); state.ecs_mut().insert(sys::WaypointTimer::default()); + + // System schedulers to control execution of systems + state + .ecs_mut() + .insert(sys::StatsPersistenceScheduler::every(Duration::from_secs( + 5, + ))); + // Server-only components state.ecs_mut().register::(); state.ecs_mut().register::(); 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 6ef9f3fc95..8f18ca9709 100644 --- a/server/src/migrations/2020-04-20-072214_stats/up.sql +++ b/server/src/migrations/2020-04-20-072214_stats/up.sql @@ -1,6 +1,5 @@ CREATE TABLE "stats" ( - id INTEGER NOT NULL PRIMARY KEY, - character_id INT NOT NULL, + character_id INT NOT NULL PRIMARY KEY, "level" INT NOT NULL DEFAULT 1, "exp" INT NOT NULL DEFAULT 0, endurance INT NOT NULL DEFAULT 0, diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index 690dc2e7a4..e7d20d4415 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -1,4 +1,6 @@ pub mod character; +pub mod stats; + mod error; mod models; mod schema; diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index 20b67befbd..a081199d29 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -91,10 +91,13 @@ impl From> for 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.update_max_hp(); + base_stats + .health + .set_to(base_stats.health.maximum(), comp::HealthSource::Revive); + base_stats.endurance = data.stats.endurance as u32; base_stats.fitness = data.stats.fitness as u32; base_stats.willpower = data.stats.willpower as u32; diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 744248ce9a..aa58db16cc 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -33,9 +33,10 @@ pub trait StateExt { fn create_player_character( &mut self, entity: EcsEntity, - name: String, + character_id: i32, body: comp::Body, main: Option, + stats: comp::Stats, server_settings: &ServerSettings, ); fn notify_registered_clients(&self, msg: ServerMsg); @@ -152,9 +153,10 @@ impl StateExt for State { fn create_player_character( &mut self, entity: EcsEntity, - name: String, + character_id: i32, // TODO body: comp::Body, main: Option, + stats: comp::Stats, server_settings: &ServerSettings, ) { // Give no item when an invalid specifier is given @@ -163,7 +165,7 @@ impl StateExt for State { let spawn_point = self.ecs().read_resource::().0; self.write_component(entity, body); - self.write_component(entity, comp::Stats::new(name, body)); + self.write_component(entity, stats); self.write_component(entity, comp::Energy::new(1000)); self.write_component(entity, comp::Controller::default()); self.write_component(entity, comp::Pos(spawn_point)); @@ -222,6 +224,19 @@ impl StateExt for State { }, ); + // Set the character id for the player + // TODO this results in a warning in the console: "Error modifying synced + // component, it doesn't seem to exist" + // It appears to be caused by the player not yet existing on the client at this + // point, despite being able to write the data on the server + &self + .ecs() + .write_storage::() + .get_mut(entity) + .map(|player| { + player.character_id = Some(character_id); + }); + // Make sure physics are accepted. self.write_component(entity, comp::ForceUpdate); diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 1f8531f9c4..eff3730ba4 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -171,7 +171,12 @@ impl<'a> System<'a> for Sys { }, _ => {}, }, - ClientMsg::Character { name, body, main } => match client.client_state { + ClientMsg::Character { + character_id, + body, + main, + stats, + } => match client.client_state { // Become Registered first. ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Registered | ClientState::Spectator => { @@ -193,9 +198,10 @@ impl<'a> System<'a> for Sys { server_emitter.emit(ServerEvent::SelectCharacter { entity, - name, + character_id, body, main, + stats, }); }, ClientState::Character => client.error_state(RequestStateError::Already), diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index d622582e9b..b37ffd4a68 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -1,5 +1,6 @@ pub mod entity_sync; pub mod message; +pub mod persistence; pub mod sentinel; pub mod subscription; pub mod terrain; @@ -7,7 +8,10 @@ pub mod terrain_sync; pub mod waypoint; use specs::DispatcherBuilder; -use std::{marker::PhantomData, time::Instant}; +use std::{ + marker::PhantomData, + time::{Duration, Instant}, +}; pub type EntitySyncTimer = SysTimer; pub type MessageTimer = SysTimer; @@ -16,6 +20,7 @@ pub type SubscriptionTimer = SysTimer; pub type TerrainTimer = SysTimer; pub type TerrainSyncTimer = SysTimer; pub type WaypointTimer = SysTimer; +pub type StatsPersistenceScheduler = SysScheduler; // System names // Note: commented names may be useful in the future @@ -25,10 +30,12 @@ pub type WaypointTimer = SysTimer; //const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys"; const TERRAIN_SYS: &str = "server_terrain_sys"; const WAYPOINT_SYS: &str = "waypoint_sys"; +const STATS_PERSISTENCE_SYS: &str = "stats_persistence_sys"; pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]); dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]); + dispatch_builder.add(persistence::stats::Sys, STATS_PERSISTENCE_SYS, &[]); } pub fn run_sync_systems(ecs: &mut specs::World) { @@ -44,12 +51,50 @@ pub fn run_sync_systems(ecs: &mut specs::World) { entity_sync::Sys.run_now(ecs); } +/// Used to schedule systems to run at an interval +pub struct SysScheduler { + interval: Duration, + last_run: Instant, + _phantom: PhantomData, +} + +impl SysScheduler { + pub fn every(interval: Duration) -> Self { + Self { + interval, + last_run: Instant::now(), + _phantom: PhantomData, + } + } + + pub fn should_run(&mut self) -> bool { + if self.last_run.elapsed() > self.interval { + self.last_run = Instant::now(); + + true + } else { + false + } + } +} + +impl Default for SysScheduler { + fn default() -> Self { + Self { + interval: Duration::from_secs(30), + last_run: Instant::now(), + _phantom: PhantomData, + } + } +} + /// Used to keep track of how much time each system takes pub struct SysTimer { pub nanos: u64, start: Option, _phantom: PhantomData, } + impl SysTimer { pub fn start(&mut self) { if self.start.is_some() { @@ -67,6 +112,7 @@ impl SysTimer { .as_nanos() as u64; } } + impl Default for SysTimer { fn default() -> Self { Self { diff --git a/server/src/sys/persistence/mod.rs b/server/src/sys/persistence/mod.rs new file mode 100644 index 0000000000..9d34677fce --- /dev/null +++ b/server/src/sys/persistence/mod.rs @@ -0,0 +1 @@ +pub mod stats; diff --git a/server/src/sys/persistence/stats.rs b/server/src/sys/persistence/stats.rs new file mode 100644 index 0000000000..71e04f1669 --- /dev/null +++ b/server/src/sys/persistence/stats.rs @@ -0,0 +1,30 @@ +use crate::{persistence::stats, sys::SysScheduler}; +use common::comp::{Player, Stats}; +use specs::{Join, ReadStorage, System, Write}; + +pub struct Sys; + +impl<'a> System<'a> for Sys { + type SystemData = ( + ReadStorage<'a, Player>, + ReadStorage<'a, Stats>, + Write<'a, SysScheduler>, + ); + + 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, + ); + } + } + } + } +} diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index e33d905191..ec4189af8c 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -87,11 +87,14 @@ impl PlayState for CharSelectionState { if let Some(selected_character) = char_data.get(self.char_selection_ui.selected_character) { - self.client.borrow_mut().request_character( - selected_character.character.alias.clone(), - selected_character.body, - selected_character.character.tool.clone(), - ); + if let Some(character_id) = selected_character.character.id { + self.client.borrow_mut().request_character( + character_id, + selected_character.body, + selected_character.character.tool.clone(), + selected_character.stats.clone(), + ); + } } return PlayStateResult::Switch(Box::new(SessionState::new( diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index 8d61fffbfb..f3b638dddf 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -803,12 +803,10 @@ impl CharSelectionUi { .color(TEXT_COLOR) .set(self.ids.character_names[i], ui_widgets); - Text::new( - &self - .voxygen_i18n - .get("char_selection.level_fmt") - .replace("{level_nb}", "1"), - ) //TODO Insert real level here as soon as they get saved + Text::new(&self.voxygen_i18n.get("char_selection.level_fmt").replace( + "{level_nb}", + &character_item.stats.level.level().to_string(), + )) .down_from(self.ids.character_names[i], 4.0) .font_size(self.fonts.cyri.scale(17)) .font_id(self.fonts.cyri.conrod_id)