mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'shandley/persistence-stats' into 'master'
Stats Persistence See merge request veloren/veloren!970
This commit is contained in:
commit
acab072a63
@ -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
|
||||
|
@ -12,6 +12,8 @@ pub enum Error {
|
||||
AuthErr(String),
|
||||
AuthClientError(AuthClientError),
|
||||
AuthServerNotTrusted,
|
||||
/// Persisted character data is invalid or missing
|
||||
InvalidCharacter,
|
||||
//TODO: InvalidAlias,
|
||||
Other(String),
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ pub enum ServerEvent {
|
||||
Possess(Uid, Uid),
|
||||
SelectCharacter {
|
||||
entity: EcsEntity,
|
||||
name: String,
|
||||
character_id: i32,
|
||||
body: comp::Body,
|
||||
main: Option<String>,
|
||||
},
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -79,6 +79,7 @@ pub enum RequestStateError {
|
||||
pub enum RegisterError {
|
||||
AlreadyLoggedIn,
|
||||
AuthError(String),
|
||||
InvalidCharacter,
|
||||
//TODO: InvalidAlias,
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
1
server/src/migrations/2020-04-20-072214_stats/down.sql
Normal file
1
server/src/migrations/2020-04-20-072214_stats/down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "stats";
|
9
server/src/migrations/2020-04-20-072214_stats/up.sql
Normal file
9
server/src/migrations/2020-04-20-072214_stats/up.sql
Normal 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
|
||||
);
|
@ -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) => {
|
||||
|
27
server/src/persistence/error.rs
Normal file
27
server/src/persistence/error.rs
Normal 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) }
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,);
|
||||
|
27
server/src/persistence/stats.rs
Normal file
27
server/src/persistence/stats.rs
Normal 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)));
|
||||
}
|
@ -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);
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
1
server/src/sys/persistence/mod.rs
Normal file
1
server/src/sys/persistence/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod stats;
|
31
server/src/sys/persistence/stats.rs
Normal file
31
server/src/sys/persistence/stats.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user