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
This commit is contained in:
Shane Handley 2020-04-25 23:41:27 +10:00
parent e5853dbdd4
commit 7c6c9f4302
16 changed files with 156 additions and 30 deletions

View File

@ -234,9 +234,19 @@ impl Client {
} }
/// Request a state transition to `ClientState::Character`. /// Request a state transition to `ClientState::Character`.
pub fn request_character(&mut self, name: String, body: comp::Body, main: Option<String>) { pub fn request_character(
self.postbox &mut self,
.send_message(ClientMsg::Character { name, body, main }); character_id: i32,
body: comp::Body,
main: Option<String>,
stats: comp::Stats,
) {
self.postbox.send_message(ClientMsg::Character {
character_id,
body,
main,
stats,
});
self.client_state = ClientState::Pending; self.client_state = ClientState::Pending;
} }

View File

@ -92,9 +92,10 @@ pub enum ServerEvent {
Possess(Uid, Uid), Possess(Uid, Uid),
SelectCharacter { SelectCharacter {
entity: EcsEntity, entity: EcsEntity,
name: String, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>, main: Option<String>,
stats: comp::Stats,
}, },
ExitIngame { ExitIngame {
entity: EcsEntity, entity: EcsEntity,

View File

@ -15,9 +15,10 @@ pub enum ClientMsg {
}, },
DeleteCharacter(i32), DeleteCharacter(i32),
Character { Character {
name: String, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>, // Specifier for the weapon main: Option<String>, // Specifier for the weapon
stats: comp::Stats,
}, },
/// Request `ClientState::Registered` from an ingame state /// Request `ClientState::Registered` from an ingame state
ExitIngame, ExitIngame,

View File

@ -12,14 +12,15 @@ use vek::{Rgb, Vec3};
pub fn handle_create_character( pub fn handle_create_character(
server: &mut Server, server: &mut Server,
entity: EcsEntity, entity: EcsEntity,
name: String, character_id: i32,
body: Body, body: Body,
main: Option<String>, main: Option<String>,
stats: Stats,
) { ) {
let state = &mut server.state; let state = &mut server.state;
let server_settings = &server.server_settings; 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); sys::subscription::initialize_region_subscription(state.ecs(), entity);
} }

View File

@ -71,10 +71,11 @@ impl Server {
}, },
ServerEvent::SelectCharacter { ServerEvent::SelectCharacter {
entity, entity,
name, character_id,
body, body,
main, 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::ExitIngame { entity } => handle_exit_ingame(self, entity),
ServerEvent::CreateNpc { ServerEvent::CreateNpc {
pos, pos,

View File

@ -94,6 +94,7 @@ impl Server {
.insert(AuthProvider::new(settings.auth_server_address.clone())); .insert(AuthProvider::new(settings.auth_server_address.clone()));
state.ecs_mut().insert(Tick(0)); state.ecs_mut().insert(Tick(0));
state.ecs_mut().insert(ChunkGenerator::new()); state.ecs_mut().insert(ChunkGenerator::new());
// System timers for performance monitoring // System timers for performance monitoring
state.ecs_mut().insert(sys::EntitySyncTimer::default()); state.ecs_mut().insert(sys::EntitySyncTimer::default());
state.ecs_mut().insert(sys::MessageTimer::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::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default()); state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::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 // Server-only components
state.ecs_mut().register::<RegionSubscription>(); state.ecs_mut().register::<RegionSubscription>();
state.ecs_mut().register::<Client>(); state.ecs_mut().register::<Client>();

View File

@ -1,6 +1,5 @@
CREATE TABLE "stats" ( CREATE TABLE "stats" (
id INTEGER NOT NULL PRIMARY KEY, character_id INT NOT NULL PRIMARY KEY,
character_id INT NOT NULL,
"level" INT NOT NULL DEFAULT 1, "level" INT NOT NULL DEFAULT 1,
"exp" INT NOT NULL DEFAULT 0, "exp" INT NOT NULL DEFAULT 0,
endurance INT NOT NULL DEFAULT 0, endurance INT NOT NULL DEFAULT 0,

View File

@ -1,4 +1,6 @@
pub mod character; pub mod character;
pub mod stats;
mod error; mod error;
mod models; mod models;
mod schema; mod schema;

View File

@ -91,10 +91,13 @@ impl From<StatsJoinData<'_>> for comp::Stats {
let mut base_stats = comp::Stats::new(String::from(&data.character.alias), *data.body); 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.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.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.endurance = data.stats.endurance as u32;
base_stats.fitness = data.stats.fitness as u32; base_stats.fitness = data.stats.fitness as u32;
base_stats.willpower = data.stats.willpower as u32; base_stats.willpower = data.stats.willpower as u32;

View File

@ -33,9 +33,10 @@ pub trait StateExt {
fn create_player_character( fn create_player_character(
&mut self, &mut self,
entity: EcsEntity, entity: EcsEntity,
name: String, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>, main: Option<String>,
stats: comp::Stats,
server_settings: &ServerSettings, server_settings: &ServerSettings,
); );
fn notify_registered_clients(&self, msg: ServerMsg); fn notify_registered_clients(&self, msg: ServerMsg);
@ -152,9 +153,10 @@ impl StateExt for State {
fn create_player_character( fn create_player_character(
&mut self, &mut self,
entity: EcsEntity, entity: EcsEntity,
name: String, character_id: i32, // TODO
body: comp::Body, body: comp::Body,
main: Option<String>, main: Option<String>,
stats: comp::Stats,
server_settings: &ServerSettings, server_settings: &ServerSettings,
) { ) {
// Give no item when an invalid specifier is given // Give no item when an invalid specifier is given
@ -163,7 +165,7 @@ impl StateExt for State {
let spawn_point = self.ecs().read_resource::<SpawnPoint>().0; let spawn_point = self.ecs().read_resource::<SpawnPoint>().0;
self.write_component(entity, body); 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::Energy::new(1000));
self.write_component(entity, comp::Controller::default()); self.write_component(entity, comp::Controller::default());
self.write_component(entity, comp::Pos(spawn_point)); 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::<comp::Player>()
.get_mut(entity)
.map(|player| {
player.character_id = Some(character_id);
});
// Make sure physics are accepted. // Make sure physics are accepted.
self.write_component(entity, comp::ForceUpdate); self.write_component(entity, comp::ForceUpdate);

View File

@ -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. // Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator => { ClientState::Registered | ClientState::Spectator => {
@ -193,9 +198,10 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::SelectCharacter { server_emitter.emit(ServerEvent::SelectCharacter {
entity, entity,
name, character_id,
body, body,
main, main,
stats,
}); });
}, },
ClientState::Character => client.error_state(RequestStateError::Already), ClientState::Character => client.error_state(RequestStateError::Already),

View File

@ -1,5 +1,6 @@
pub mod entity_sync; pub mod entity_sync;
pub mod message; pub mod message;
pub mod persistence;
pub mod sentinel; pub mod sentinel;
pub mod subscription; pub mod subscription;
pub mod terrain; pub mod terrain;
@ -7,7 +8,10 @@ pub mod terrain_sync;
pub mod waypoint; pub mod waypoint;
use specs::DispatcherBuilder; use specs::DispatcherBuilder;
use std::{marker::PhantomData, time::Instant}; use std::{
marker::PhantomData,
time::{Duration, Instant},
};
pub type EntitySyncTimer = SysTimer<entity_sync::Sys>; pub type EntitySyncTimer = SysTimer<entity_sync::Sys>;
pub type MessageTimer = SysTimer<message::Sys>; pub type MessageTimer = SysTimer<message::Sys>;
@ -16,6 +20,7 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>; pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>; pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>; pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
// System names // System names
// Note: commented names may be useful in the future // Note: commented names may be useful in the future
@ -25,10 +30,12 @@ pub type WaypointTimer = SysTimer<waypoint::Sys>;
//const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys"; //const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys"; const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "waypoint_sys"; const WAYPOINT_SYS: &str = "waypoint_sys";
const STATS_PERSISTENCE_SYS: &str = "stats_persistence_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]); dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_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) { 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); entity_sync::Sys.run_now(ecs);
} }
/// Used to schedule systems to run at an interval
pub struct SysScheduler<S> {
interval: Duration,
last_run: Instant,
_phantom: PhantomData<S>,
}
impl<S> SysScheduler<S> {
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<S> Default for SysScheduler<S> {
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 /// Used to keep track of how much time each system takes
pub struct SysTimer<S> { pub struct SysTimer<S> {
pub nanos: u64, pub nanos: u64,
start: Option<Instant>, start: Option<Instant>,
_phantom: PhantomData<S>, _phantom: PhantomData<S>,
} }
impl<S> SysTimer<S> { impl<S> SysTimer<S> {
pub fn start(&mut self) { pub fn start(&mut self) {
if self.start.is_some() { if self.start.is_some() {
@ -67,6 +112,7 @@ impl<S> SysTimer<S> {
.as_nanos() as u64; .as_nanos() as u64;
} }
} }
impl<S> Default for SysTimer<S> { impl<S> Default for SysTimer<S> {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -0,0 +1 @@
pub mod stats;

View File

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

View File

@ -87,11 +87,14 @@ impl PlayState for CharSelectionState {
if let Some(selected_character) = if let Some(selected_character) =
char_data.get(self.char_selection_ui.selected_character) char_data.get(self.char_selection_ui.selected_character)
{ {
self.client.borrow_mut().request_character( if let Some(character_id) = selected_character.character.id {
selected_character.character.alias.clone(), self.client.borrow_mut().request_character(
selected_character.body, character_id,
selected_character.character.tool.clone(), selected_character.body,
); selected_character.character.tool.clone(),
selected_character.stats.clone(),
);
}
} }
return PlayStateResult::Switch(Box::new(SessionState::new( return PlayStateResult::Switch(Box::new(SessionState::new(

View File

@ -803,12 +803,10 @@ impl CharSelectionUi {
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(self.ids.character_names[i], ui_widgets); .set(self.ids.character_names[i], ui_widgets);
Text::new( Text::new(&self.voxygen_i18n.get("char_selection.level_fmt").replace(
&self "{level_nb}",
.voxygen_i18n &character_item.stats.level.level().to_string(),
.get("char_selection.level_fmt") ))
.replace("{level_nb}", "1"),
) //TODO Insert real level here as soon as they get saved
.down_from(self.ids.character_names[i], 4.0) .down_from(self.ids.character_names[i], 4.0)
.font_size(self.fonts.cyri.scale(17)) .font_size(self.fonts.cyri.scale(17))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)