Merge branch 'shandley/persistence-stats' into 'master'

Stats Persistence

See merge request veloren/veloren!970
This commit is contained in:
Forest Anderson 2020-05-13 13:43:22 +00:00
commit acab072a63
27 changed files with 474 additions and 94 deletions

View File

@ -136,6 +136,7 @@ https://account.veloren.net."#,
"main.login.already_logged_in": "You are already logged into the server.",
"main.login.network_error": "Network error",
"main.login.failed_sending_request": "Request to Auth server failed",
"main.login.invalid_character": "The selected character is invalid",
"main.login.client_crashed": "Client crashed",
/// End Main screen section

View File

@ -12,6 +12,8 @@ pub enum Error {
AuthErr(String),
AuthClientError(AuthClientError),
AuthServerNotTrusted,
/// Persisted character data is invalid or missing
InvalidCharacter,
//TODO: InvalidAlias,
Other(String),
}

View File

@ -225,6 +225,7 @@ impl Client {
break Err(match err {
RegisterError::AlreadyLoggedIn => Error::AlreadyLoggedIn,
RegisterError::AuthError(err) => Error::AuthErr(err),
RegisterError::InvalidCharacter => Error::InvalidCharacter,
});
},
ServerMsg::StateAnswer(Ok(ClientState::Registered)) => break Ok(()),
@ -234,15 +235,18 @@ 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>) {
self.postbox.send_message(ClientMsg::Character {
character_id,
body,
main,
});
self.client_state = ClientState::Pending;
}
/// Load the current players character list
pub fn load_characters(&mut self) {
pub fn load_character_list(&mut self) {
self.character_list.loading = true;
self.postbox.send_message(ClientMsg::RequestCharacterList);
}

View File

@ -11,10 +11,20 @@ pub struct Character {
pub tool: Option<String>, // TODO: Remove once we start persisting inventories
}
/// Represents the character data sent by the server after loading from the
/// database.
/// Represents a single character item in the character list presented during
/// character selection. This is a subset of the full character data used for
/// presentation purposes.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CharacterItem {
pub character: Character,
pub body: comp::Body,
pub level: usize,
}
/// The full representation of the data we store in the database for each
/// character
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CharacterData {
pub body: comp::Body,
pub stats: comp::Stats,
}

View File

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

View File

@ -15,7 +15,7 @@ pub enum ClientMsg {
},
DeleteCharacter(i32),
Character {
name: String,
character_id: i32,
body: comp::Body,
main: Option<String>, // Specifier for the weapon
},

View File

@ -79,6 +79,7 @@ pub enum RequestStateError {
pub enum RegisterError {
AlreadyLoggedIn,
AuthError(String),
InvalidCharacter,
//TODO: InvalidAlias,
}

View File

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

View File

@ -71,10 +71,10 @@ impl Server {
},
ServerEvent::SelectCharacter {
entity,
name,
character_id,
body,
main,
} => handle_create_character(self, entity, name, body, main),
} => handle_create_character(self, entity, character_id, body, main),
ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity),
ServerEvent::CreateNpc {
pos,

View File

@ -1,5 +1,7 @@
use super::Event;
use crate::{auth_provider::AuthProvider, client::Client, state_ext::StateExt, Server};
use crate::{
auth_provider::AuthProvider, client::Client, persistence, state_ext::StateExt, Server,
};
use common::{
comp,
comp::Player,
@ -68,6 +70,17 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event
}
}
}
// Sync the player's character data to the database
if let (Some(player), Some(stats)) = (
state.read_storage::<Player>().get(entity),
state.read_storage::<comp::Stats>().get(entity),
) {
if let Some(character_id) = player.character_id {
persistence::stats::update(character_id, stats, None);
}
}
// Delete client entity
if let Err(err) = state.delete_entity_recorded(entity) {
error!("Failed to delete disconnected client: {:?}", err);

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,17 @@ impl Server {
state.ecs_mut().insert(sys::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::default());
state
.ecs_mut()
.insert(sys::StatsPersistenceTimer::default());
// System schedulers to control execution of systems
state
.ecs_mut()
.insert(sys::StatsPersistenceScheduler::every(Duration::from_secs(
10,
)));
// Server-only components
state.ecs_mut().register::<RegionSubscription>();
state.ecs_mut().register::<Client>();
@ -374,7 +386,13 @@ impl Server {
.nanos as i64;
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().nanos as i64;
let waypoint_nanos = self.state.ecs().read_resource::<sys::WaypointTimer>().nanos as i64;
let stats_persistence_nanos = self
.state
.ecs()
.read_resource::<sys::StatsPersistenceTimer>()
.nanos as i64;
let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos;
// Report timing info
self.metrics
.tick_time
@ -431,6 +449,11 @@ impl Server {
.tick_time
.with_label_values(&["waypoint"])
.set(waypoint_nanos);
self.metrics
.tick_time
.with_label_values(&["persistence:stats"])
.set(stats_persistence_nanos);
// Report other info
self.metrics
.player_online

View File

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

View File

@ -0,0 +1,9 @@
CREATE TABLE "stats" (
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,
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};
@ -11,53 +12,82 @@ use diesel::prelude::*;
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::*};
/// Load stored data for a character.
///
/// After first logging in, and after a character is selected, we fetch this
/// data for the purpose of inserting their persisted data for the entity.
pub fn load_character_data(character_id: i32) -> Result<comp::Stats, Error> {
let (character_data, body_data, stats_data) = schema::character::dsl::character
.filter(schema::character::id.eq(character_id))
.inner_join(schema::body::table)
.inner_join(schema::stats::table)
.first::<(Character, Body, Stats)>(&establish_connection())?;
let data: Vec<(Character, Body)> = character
.filter(player_uuid.eq(uuid))
.order(id.desc())
.inner_join(body::table)
.load::<(Character, Body)>(&establish_connection())?;
Ok(comp::Stats::from(StatsJoinData {
alias: &character_data.alias,
body: &comp::Body::from(&body_data),
stats: &stats_data,
}))
}
/// Loads a list of characters belonging to the player. This data is a small
/// subset of the character's data, and is used to render the character and
/// their level in the character list.
///
/// In the event that a join fails, for a character (i.e. they lack an entry for
/// stats, body, etc...) the character is skipped, and no entry will be
/// returned.
pub fn load_character_list(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 level = stats_data.level as usize;
CharacterItem {
character,
body,
level,
}
})
.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 +113,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."),
};
@ -90,27 +136,26 @@ pub fn create_character(
Ok(())
})?;
load_characters(uuid)
load_character_list(uuid)
}
/// Delete a character. Returns the updated character list.
pub fn delete_character(uuid: &str, character_id: i32) -> CharacterListResult {
use schema::character::dsl::*;
diesel::delete(character.filter(id.eq(character_id))).execute(&establish_connection())?;
load_characters(uuid)
load_character_list(uuid)
}
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,27 @@
extern crate diesel;
use std::fmt;
#[derive(Debug)]
pub enum Error {
// The player has already reached the max character limit
CharacterLimitReached,
// An error occured when performing a database action
DatabaseError(diesel::result::Error),
// Unable to load body or stats for a character
CharacterDataError,
}
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"),
Self::CharacterDataError => String::from("Error while loading character data"),
})
}
}
impl From<diesel::result::Error> for Error {
fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) }
}

View File

@ -1,34 +1,15 @@
pub mod character;
mod models;
pub mod stats;
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,14 @@
use super::schema::{body, character};
use super::schema::{body, character, stats};
use crate::comp;
use common::character::Character as CharacterData;
/// The required elements to build comp::Stats from database data
pub struct StatsJoinData<'a> {
pub alias: &'a str,
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 +70,89 @@ impl From<&Body> for comp::Body {
})
}
}
/// `Stats` represents the stats for a character
#[derive(Associations, AsChangeset, 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.alias), *data.body);
base_stats.level.set_level(data.stats.level 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.fitness = data.stats.fitness as u32;
base_stats.willpower = data.stats.willpower as u32;
base_stats
}
}
#[derive(AsChangeset, Debug, PartialEq)]
#[primary_key(character_id)]
#[table_name = "stats"]
pub struct StatsUpdate {
pub level: i32,
pub exp: i32,
pub endurance: i32,
pub fitness: i32,
pub willpower: i32,
}
impl From<&comp::Stats> for StatsUpdate {
fn from(stats: &comp::Stats) -> StatsUpdate {
StatsUpdate {
level: stats.level.level() as i32,
exp: stats.exp.current() as i32,
endurance: stats.endurance as i32,
fitness: stats.fitness as i32,
willpower: stats.willpower as i32,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::comp;
#[test]
fn stats_update_from_stats() {
let mut stats = comp::Stats::new(
String::from("Test"),
comp::Body::Humanoid(comp::humanoid::Body::random()),
);
stats.level.set_level(2);
stats.exp.set_current(20);
stats.endurance = 2;
stats.fitness = 3;
stats.willpower = 4;
assert_eq!(StatsUpdate::from(&stats), StatsUpdate {
level: 2,
exp: 20,
endurance: 2,
fitness: 3,
willpower: 4,
})
}
}

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,27 @@
extern crate diesel;
use super::{establish_connection, models::StatsUpdate, schema};
use crate::comp;
use diesel::prelude::*;
pub fn update(character_id: i32, stats: &comp::Stats, conn: Option<&SqliteConnection>) {
log::warn!("stats persisting...");
if let Err(error) =
diesel::update(schema::stats::table.filter(schema::stats::character_id.eq(character_id)))
.set(&StatsUpdate::from(stats))
.execute(conn.unwrap_or(&establish_connection()))
{
log::warn!(
"Failed to update stats for character: {:?}: {:?}",
character_id,
error
)
}
}
pub fn batch_update<'a>(updates: impl Iterator<Item = (i32, &'a comp::Stats)>) {
let connection = &establish_connection();
updates.for_each(|(character_id, stats)| update(character_id, stats, Some(connection)));
}

View File

@ -1,9 +1,12 @@
use crate::{client::Client, settings::ServerSettings, sys::sentinel::DeletedEntities, SpawnPoint};
use crate::{
client::Client, persistence, settings::ServerSettings, sys::sentinel::DeletedEntities,
SpawnPoint,
};
use common::{
assets,
comp::{self, item},
effect::Effect,
msg::{ClientState, ServerMsg},
msg::{ClientState, RegisterError, RequestStateError, ServerMsg},
state::State,
sync::{Uid, WorldSyncExt},
util::Dir,
@ -33,7 +36,7 @@ pub trait StateExt {
fn create_player_character(
&mut self,
entity: EcsEntity,
name: String,
character_id: i32,
body: comp::Body,
main: Option<String>,
server_settings: &ServerSettings,
@ -152,18 +155,39 @@ impl StateExt for State {
fn create_player_character(
&mut self,
entity: EcsEntity,
name: String,
character_id: i32,
body: comp::Body,
main: Option<String>,
server_settings: &ServerSettings,
) {
// Grab persisted character data from the db and insert their associated
// components. If for some reason the data can't be returned (missing
// data, DB error), kick the client back to the character select screen.
match persistence::character::load_character_data(character_id) {
Ok(stats) => self.write_component(entity, stats),
Err(error) => {
log::warn!(
"{}",
format!(
"Failed to load character data for character_id {}: {}",
character_id, error
)
);
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
client.error_state(RequestStateError::RegisterDenied(
RegisterError::InvalidCharacter,
))
}
},
}
// Give no item when an invalid specifier is given
let main = main.and_then(|specifier| assets::load_cloned::<comp::Item>(&specifier).ok());
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, comp::Energy::new(1000));
self.write_component(entity, comp::Controller::default());
self.write_component(entity, comp::Pos(spawn_point));
@ -222,6 +246,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);
@ -236,6 +273,7 @@ impl StateExt for State {
) {
self.write_component(entity, comp::Admin);
}
// Tell the client its request was successful.
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
client.allow_state(ClientState::Character);

View File

@ -171,7 +171,11 @@ impl<'a> System<'a> for Sys {
},
_ => {},
},
ClientMsg::Character { name, body, main } => match client.client_state {
ClientMsg::Character {
character_id,
body,
main,
} => match client.client_state {
// Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator => {
@ -193,7 +197,7 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::SelectCharacter {
entity,
name,
character_id,
body,
main,
});
@ -311,7 +315,7 @@ impl<'a> System<'a> for Sys {
},
ClientMsg::RequestCharacterList => {
if let Some(player) = players.get(entity) {
match persistence::character::load_characters(
match persistence::character::load_character_list(
&player.uuid().to_string(),
) {
Ok(character_list) => {

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,8 @@ 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 StatsPersistenceTimer = SysTimer<persistence::stats::Sys>;
pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
// System names
// Note: commented names may be useful in the future
@ -25,10 +31,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 +52,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 +113,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,31 @@
use crate::{
persistence::stats,
sys::{SysScheduler, SysTimer},
};
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>>,
Write<'a, SysTimer<Self>>,
);
fn run(&mut self, (players, player_stats, mut scheduler, mut timer): Self::SystemData) {
if scheduler.should_run() {
timer.start();
stats::batch_update(
(&players, &player_stats)
.join()
.filter_map(|(player, stats)| player.character_id.map(|id| (id, stats))),
);
timer.end();
}
}
}

View File

@ -40,7 +40,7 @@ impl PlayState for CharSelectionState {
let mut clock = Clock::start();
// Load the player's character list
self.client.borrow_mut().load_characters();
self.client.borrow_mut().load_character_list();
let mut current_client_state = self.client.borrow().get_client_state();
while let ClientState::Pending | ClientState::Registered = current_client_state {
@ -87,12 +87,14 @@ impl PlayState for CharSelectionState {
if let Some(selected_character) =
char_data.get(self.char_selection_ui.selected_character)
{
if let Some(character_id) = selected_character.character.id {
self.client.borrow_mut().request_character(
selected_character.character.alias.clone(),
character_id,
selected_character.body,
selected_character.character.tool.clone(),
);
}
}
return PlayStateResult::Switch(Box::new(SessionState::new(
global_state,

View File

@ -341,14 +341,19 @@ impl CharSelectionUi {
Mode::Select(data) => data.clone(),
Mode::Create {
name, body, tool, ..
} => Some(vec![CharacterItem {
} => {
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: comp::Body::Humanoid(body.clone()),
}]),
body,
level: 1,
}])
},
}
}
@ -802,8 +807,8 @@ impl CharSelectionUi {
&self
.voxygen_i18n
.get("char_selection.level_fmt")
.replace("{level_nb}", "1"),
) //TODO Insert real level here as soon as they get saved
.replace("{level_nb}", &character_item.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)

View File

@ -126,6 +126,9 @@ impl PlayState for MainMenuState {
),
client::AuthClientError::ServerError(_, e) => format!("{}", e),
},
client::Error::InvalidCharacter => {
localized_strings.get("main.login.invalid_character").into()
},
},
InitError::ClientCrashed => {
localized_strings.get("main.login.client_crashed").into()