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`.
pub fn request_character(&mut self, name: String, body: comp::Body, main: Option<String>) {
self.postbox
.send_message(ClientMsg::Character { name, body, main });
pub fn request_character(
&mut self,
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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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::<RegionSubscription>();
state.ecs_mut().register::<Client>();

View File

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

View File

@ -1,4 +1,6 @@
pub mod character;
pub mod stats;
mod error;
mod models;
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);
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;

View File

@ -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<String>,
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<String>,
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::<SpawnPoint>().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::<comp::Player>()
.get_mut(entity)
.map(|player| {
player.character_id = Some(character_id);
});
// Make sure physics are accepted.
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.
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),

View File

@ -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<entity_sync::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 TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
// System names
// 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_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<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
pub struct SysTimer<S> {
pub nanos: u64,
start: Option<Instant>,
_phantom: PhantomData<S>,
}
impl<S> SysTimer<S> {
pub fn start(&mut self) {
if self.start.is_some() {
@ -67,6 +112,7 @@ impl<S> SysTimer<S> {
.as_nanos() as u64;
}
}
impl<S> Default for SysTimer<S> {
fn default() -> 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) =
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(

View File

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