mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
- Update the stats of characters individually, reverting the change with
big combined updates. - Add a timer to the stats persistence system and change the frequency that it runs to 10s - Seperate the loading of character data for the character list during selection, and the full data we will grab during state creation. Ideally additional persisted bits can get returned at the same point and added to the ecs within the same block.
This commit is contained in:
@ -136,6 +136,7 @@ https://account.veloren.net."#,
|
|||||||
"main.login.already_logged_in": "You are already logged into the server.",
|
"main.login.already_logged_in": "You are already logged into the server.",
|
||||||
"main.login.network_error": "Network error",
|
"main.login.network_error": "Network error",
|
||||||
"main.login.failed_sending_request": "Request to Auth server failed",
|
"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",
|
"main.login.client_crashed": "Client crashed",
|
||||||
|
|
||||||
/// End Main screen section
|
/// End Main screen section
|
||||||
|
@ -12,6 +12,8 @@ pub enum Error {
|
|||||||
AuthErr(String),
|
AuthErr(String),
|
||||||
AuthClientError(AuthClientError),
|
AuthClientError(AuthClientError),
|
||||||
AuthServerNotTrusted,
|
AuthServerNotTrusted,
|
||||||
|
/// Persisted character data is invalid or missing
|
||||||
|
InvalidCharacter,
|
||||||
//TODO: InvalidAlias,
|
//TODO: InvalidAlias,
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
@ -225,6 +225,7 @@ impl Client {
|
|||||||
break Err(match err {
|
break Err(match err {
|
||||||
RegisterError::AlreadyLoggedIn => Error::AlreadyLoggedIn,
|
RegisterError::AlreadyLoggedIn => Error::AlreadyLoggedIn,
|
||||||
RegisterError::AuthError(err) => Error::AuthErr(err),
|
RegisterError::AuthError(err) => Error::AuthErr(err),
|
||||||
|
RegisterError::InvalidCharacter => Error::InvalidCharacter,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ServerMsg::StateAnswer(Ok(ClientState::Registered)) => break Ok(()),
|
ServerMsg::StateAnswer(Ok(ClientState::Registered)) => break Ok(()),
|
||||||
@ -234,25 +235,18 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Request a state transition to `ClientState::Character`.
|
/// Request a state transition to `ClientState::Character`.
|
||||||
pub fn request_character(
|
pub fn request_character(&mut self, character_id: i32, body: comp::Body, main: Option<String>) {
|
||||||
&mut self,
|
|
||||||
character_id: i32,
|
|
||||||
body: comp::Body,
|
|
||||||
main: Option<String>,
|
|
||||||
stats: comp::Stats,
|
|
||||||
) {
|
|
||||||
self.postbox.send_message(ClientMsg::Character {
|
self.postbox.send_message(ClientMsg::Character {
|
||||||
character_id,
|
character_id,
|
||||||
body,
|
body,
|
||||||
main,
|
main,
|
||||||
stats,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client_state = ClientState::Pending;
|
self.client_state = ClientState::Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the current players character list
|
/// 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.character_list.loading = true;
|
||||||
self.postbox.send_message(ClientMsg::RequestCharacterList);
|
self.postbox.send_message(ClientMsg::RequestCharacterList);
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,20 @@ pub struct Character {
|
|||||||
pub tool: Option<String>, // TODO: Remove once we start persisting inventories
|
pub tool: Option<String>, // TODO: Remove once we start persisting inventories
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the character data sent by the server after loading from the
|
/// Represents a single character item in the character list presented during
|
||||||
/// database.
|
/// character selection. This is a subset of the full character data used for
|
||||||
|
/// presentation purposes.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct CharacterItem {
|
pub struct CharacterItem {
|
||||||
pub character: Character,
|
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 body: comp::Body,
|
||||||
pub stats: comp::Stats,
|
pub stats: comp::Stats,
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,6 @@ pub enum ServerEvent {
|
|||||||
character_id: i32,
|
character_id: i32,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
main: Option<String>,
|
main: Option<String>,
|
||||||
stats: comp::Stats,
|
|
||||||
},
|
},
|
||||||
ExitIngame {
|
ExitIngame {
|
||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
|
@ -18,7 +18,6 @@ pub enum ClientMsg {
|
|||||||
character_id: i32,
|
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,
|
||||||
|
@ -79,6 +79,7 @@ pub enum RequestStateError {
|
|||||||
pub enum RegisterError {
|
pub enum RegisterError {
|
||||||
AlreadyLoggedIn,
|
AlreadyLoggedIn,
|
||||||
AuthError(String),
|
AuthError(String),
|
||||||
|
InvalidCharacter,
|
||||||
//TODO: InvalidAlias,
|
//TODO: InvalidAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,12 +15,11 @@ pub fn handle_create_character(
|
|||||||
character_id: i32,
|
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, character_id, body, main, stats, server_settings);
|
state.create_player_character(entity, character_id, body, main, server_settings);
|
||||||
sys::subscription::initialize_region_subscription(state.ecs(), entity);
|
sys::subscription::initialize_region_subscription(state.ecs(), entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,8 +74,7 @@ impl Server {
|
|||||||
character_id,
|
character_id,
|
||||||
body,
|
body,
|
||||||
main,
|
main,
|
||||||
stats,
|
} => handle_create_character(self, entity, character_id, body, main),
|
||||||
} => 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,
|
||||||
|
@ -103,12 +103,15 @@ 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());
|
||||||
|
state
|
||||||
|
.ecs_mut()
|
||||||
|
.insert(sys::StatsPersistenceTimer::default());
|
||||||
|
|
||||||
// System schedulers to control execution of systems
|
// System schedulers to control execution of systems
|
||||||
state
|
state
|
||||||
.ecs_mut()
|
.ecs_mut()
|
||||||
.insert(sys::StatsPersistenceScheduler::every(Duration::from_secs(
|
.insert(sys::StatsPersistenceScheduler::every(Duration::from_secs(
|
||||||
60,
|
10,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Server-only components
|
// Server-only components
|
||||||
@ -383,7 +386,13 @@ impl Server {
|
|||||||
.nanos as i64;
|
.nanos as i64;
|
||||||
let terrain_nanos = self.state.ecs().read_resource::<sys::TerrainTimer>().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 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;
|
let total_sys_ran_in_dispatcher_nanos = terrain_nanos + waypoint_nanos;
|
||||||
|
|
||||||
// Report timing info
|
// Report timing info
|
||||||
self.metrics
|
self.metrics
|
||||||
.tick_time
|
.tick_time
|
||||||
@ -440,6 +449,11 @@ impl Server {
|
|||||||
.tick_time
|
.tick_time
|
||||||
.with_label_values(&["waypoint"])
|
.with_label_values(&["waypoint"])
|
||||||
.set(waypoint_nanos);
|
.set(waypoint_nanos);
|
||||||
|
self.metrics
|
||||||
|
.tick_time
|
||||||
|
.with_label_values(&["persistence:stats"])
|
||||||
|
.set(stats_persistence_nanos);
|
||||||
|
|
||||||
// Report other info
|
// Report other info
|
||||||
self.metrics
|
self.metrics
|
||||||
.player_online
|
.player_online
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE "stats" (
|
CREATE TABLE "stats" (
|
||||||
character_id INT NOT NULL PRIMARY KEY,
|
character_id INT NOT NULL PRIMARY KEY,
|
||||||
"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,
|
||||||
fitness INT NOT NULL DEFAULT 0,
|
fitness INT NOT NULL DEFAULT 0,
|
||||||
|
@ -12,10 +12,32 @@ use diesel::prelude::*;
|
|||||||
|
|
||||||
type CharacterListResult = Result<Vec<CharacterItem>, Error>;
|
type CharacterListResult = Result<Vec<CharacterItem>, Error>;
|
||||||
|
|
||||||
// Loading of characters happens immediately after login, and the data is only
|
/// Load stored data for a character.
|
||||||
// for the purpose of rendering the character and their level in the character
|
///
|
||||||
// list.
|
/// After first logging in, and after a character is selected, we fetch this
|
||||||
pub fn load_characters(player_uuid: &str) -> CharacterListResult {
|
/// 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())?;
|
||||||
|
|
||||||
|
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
|
let data: Vec<(Character, Body, Stats)> = schema::character::dsl::character
|
||||||
.filter(schema::character::player_uuid.eq(player_uuid))
|
.filter(schema::character::player_uuid.eq(player_uuid))
|
||||||
.order(schema::character::id.desc())
|
.order(schema::character::id.desc())
|
||||||
@ -28,22 +50,19 @@ pub fn load_characters(player_uuid: &str) -> CharacterListResult {
|
|||||||
.map(|(character_data, body_data, stats_data)| {
|
.map(|(character_data, body_data, stats_data)| {
|
||||||
let character = CharacterData::from(character_data);
|
let character = CharacterData::from(character_data);
|
||||||
let body = comp::Body::from(body_data);
|
let body = comp::Body::from(body_data);
|
||||||
let stats = comp::Stats::from(StatsJoinData {
|
let level = stats_data.level as usize;
|
||||||
character: &character,
|
|
||||||
body: &body,
|
|
||||||
stats: stats_data,
|
|
||||||
});
|
|
||||||
|
|
||||||
CharacterItem {
|
CharacterItem {
|
||||||
character,
|
character,
|
||||||
body,
|
body,
|
||||||
stats,
|
level,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new character with provided comp::Character and comp::Body data.
|
/// Create a new character with provided comp::Character and comp::Body data.
|
||||||
|
///
|
||||||
/// Note that sqlite does not support 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
|
/// successful insert. To workaround, we wrap this in a transaction which
|
||||||
/// inserts, queries for the newly created chaacter id, then uses the character
|
/// inserts, queries for the newly created chaacter id, then uses the character
|
||||||
@ -117,15 +136,16 @@ pub fn create_character(
|
|||||||
Ok(())
|
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 {
|
pub fn delete_character(uuid: &str, character_id: i32) -> CharacterListResult {
|
||||||
use schema::character::dsl::*;
|
use schema::character::dsl::*;
|
||||||
|
|
||||||
diesel::delete(character.filter(id.eq(character_id))).execute(&establish_connection())?;
|
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> {
|
fn check_character_limit(uuid: &str) -> Result<(), Error> {
|
||||||
|
@ -8,6 +8,8 @@ pub enum Error {
|
|||||||
CharacterLimitReached,
|
CharacterLimitReached,
|
||||||
// An error occured when performing a database action
|
// An error occured when performing a database action
|
||||||
DatabaseError(diesel::result::Error),
|
DatabaseError(diesel::result::Error),
|
||||||
|
// Unable to load body or stats for a character
|
||||||
|
CharacterDataError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@ -15,6 +17,7 @@ impl fmt::Display for Error {
|
|||||||
write!(f, "{}", match self {
|
write!(f, "{}", match self {
|
||||||
Self::DatabaseError(diesel_error) => diesel_error.to_string(),
|
Self::DatabaseError(diesel_error) => diesel_error.to_string(),
|
||||||
Self::CharacterLimitReached => String::from("Character limit exceeded"),
|
Self::CharacterLimitReached => String::from("Character limit exceeded"),
|
||||||
|
Self::CharacterDataError => String::from("Error while loading character data"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,9 @@ use super::schema::{body, character, stats};
|
|||||||
use crate::comp;
|
use crate::comp;
|
||||||
use common::character::Character as CharacterData;
|
use common::character::Character as CharacterData;
|
||||||
|
|
||||||
/// When we want to build player stats from database data, we need data from the
|
/// The required elements to build comp::Stats from database data
|
||||||
/// character, body and stats tables
|
|
||||||
pub struct StatsJoinData<'a> {
|
pub struct StatsJoinData<'a> {
|
||||||
pub character: &'a CharacterData,
|
pub alias: &'a str,
|
||||||
pub body: &'a comp::Body,
|
pub body: &'a comp::Body,
|
||||||
pub stats: &'a Stats,
|
pub stats: &'a Stats,
|
||||||
}
|
}
|
||||||
@ -88,7 +87,7 @@ pub struct Stats {
|
|||||||
|
|
||||||
impl From<StatsJoinData<'_>> for comp::Stats {
|
impl From<StatsJoinData<'_>> for comp::Stats {
|
||||||
fn from(data: StatsJoinData) -> comp::Stats {
|
fn from(data: StatsJoinData) -> 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.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.exp.set_current(data.stats.exp as u32);
|
base_stats.exp.set_current(data.stats.exp as u32);
|
||||||
@ -106,7 +105,7 @@ impl From<StatsJoinData<'_>> for comp::Stats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(AsChangeset)]
|
#[derive(AsChangeset, Debug, PartialEq)]
|
||||||
#[primary_key(character_id)]
|
#[primary_key(character_id)]
|
||||||
#[table_name = "stats"]
|
#[table_name = "stats"]
|
||||||
pub struct StatsUpdate {
|
pub struct StatsUpdate {
|
||||||
@ -116,3 +115,44 @@ pub struct StatsUpdate {
|
|||||||
pub fitness: i32,
|
pub fitness: i32,
|
||||||
pub willpower: 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,83 +1,25 @@
|
|||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
|
|
||||||
use super::establish_connection;
|
use super::{establish_connection, models::StatsUpdate, schema};
|
||||||
use crate::comp;
|
use crate::comp;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
/// Update DB rows for stats given a Vec of (character_id, Stats) tuples
|
pub fn update<'a>(updates: impl Iterator<Item = (i32, &'a comp::Stats)>) {
|
||||||
pub fn update(data: Vec<(i32, &comp::Stats)>) {
|
use schema::stats;
|
||||||
match establish_connection().execute(&build_query(data)) {
|
|
||||||
Err(diesel_error) => log::warn!("Error updating stats: {:?}", diesel_error),
|
let connection = establish_connection();
|
||||||
_ => {},
|
|
||||||
}
|
updates.for_each(|(character_id, stats)| {
|
||||||
}
|
if let Err(error) =
|
||||||
|
diesel::update(stats::table.filter(schema::stats::character_id.eq(character_id)))
|
||||||
/// Takes a Vec of (character_id, Stats) tuples and builds an SQL UPDATE query
|
.set(&StatsUpdate::from(stats))
|
||||||
/// Since there is apprently no sensible way to update > 1 row using diesel, we
|
.execute(&connection)
|
||||||
/// just construct the raw SQL
|
{
|
||||||
fn build_query(data: Vec<(i32, &comp::Stats)>) -> String {
|
log::warn!(
|
||||||
data.iter()
|
"Failed to update stats for character: {:?}: {:?}",
|
||||||
.map(|(character_id, stats)| {
|
character_id,
|
||||||
String::from(format!(
|
error
|
||||||
"UPDATE stats SET level = {}, exp = {}, endurance = {}, fitness = {}, willpower = \
|
)
|
||||||
{} WHERE character_id = {};",
|
}
|
||||||
stats.level.level() as i32,
|
});
|
||||||
stats.exp.current() as i32,
|
|
||||||
stats.endurance as i32,
|
|
||||||
stats.fitness as i32,
|
|
||||||
stats.willpower as i32,
|
|
||||||
*character_id as i32
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn builds_query_for_multiple_characters() {
|
|
||||||
let mut stats_one = comp::Stats::new(
|
|
||||||
String::from("One"),
|
|
||||||
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
|
||||||
);
|
|
||||||
|
|
||||||
stats_one.endurance = 1;
|
|
||||||
stats_one.fitness = 1;
|
|
||||||
stats_one.willpower = 1;
|
|
||||||
|
|
||||||
let mut stats_two = comp::Stats::new(
|
|
||||||
String::from("Two"),
|
|
||||||
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
|
||||||
);
|
|
||||||
|
|
||||||
stats_two.endurance = 2;
|
|
||||||
stats_two.fitness = 2;
|
|
||||||
stats_two.willpower = 2;
|
|
||||||
|
|
||||||
let mut stats_three = comp::Stats::new(
|
|
||||||
String::from("Three"),
|
|
||||||
comp::Body::Humanoid(comp::humanoid::Body::random()),
|
|
||||||
);
|
|
||||||
|
|
||||||
stats_three.endurance = 3;
|
|
||||||
stats_three.fitness = 3;
|
|
||||||
stats_three.willpower = 3;
|
|
||||||
|
|
||||||
let data = vec![
|
|
||||||
(1_i32, &stats_one),
|
|
||||||
(2_i32, &stats_two),
|
|
||||||
(3_i32, &stats_three),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
build_query(data),
|
|
||||||
"UPDATE stats SET level = 1, exp = 0, endurance = 1, fitness = 1, willpower = 1 WHERE \
|
|
||||||
character_id = 1; UPDATE stats SET level = 1, exp = 0, endurance = 2, fitness = 2, \
|
|
||||||
willpower = 2 WHERE character_id = 2; UPDATE stats SET level = 1, exp = 0, endurance \
|
|
||||||
= 3, fitness = 3, willpower = 3 WHERE character_id = 3;"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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::{
|
use common::{
|
||||||
assets,
|
assets,
|
||||||
comp::{self, item},
|
comp::{self, item},
|
||||||
effect::Effect,
|
effect::Effect,
|
||||||
msg::{ClientState, ServerMsg},
|
msg::{ClientState, RegisterError, RequestStateError, ServerMsg},
|
||||||
state::State,
|
state::State,
|
||||||
sync::{Uid, WorldSyncExt},
|
sync::{Uid, WorldSyncExt},
|
||||||
util::Dir,
|
util::Dir,
|
||||||
@ -36,7 +39,6 @@ pub trait StateExt {
|
|||||||
character_id: i32,
|
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);
|
||||||
@ -153,19 +155,39 @@ impl StateExt for State {
|
|||||||
fn create_player_character(
|
fn create_player_character(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
character_id: i32, // TODO
|
character_id: i32,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
main: Option<String>,
|
main: Option<String>,
|
||||||
stats: comp::Stats,
|
|
||||||
server_settings: &ServerSettings,
|
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
|
// Give no item when an invalid specifier is given
|
||||||
let main = main.and_then(|specifier| assets::load_cloned::<comp::Item>(&specifier).ok());
|
let main = main.and_then(|specifier| assets::load_cloned::<comp::Item>(&specifier).ok());
|
||||||
|
|
||||||
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, 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));
|
||||||
@ -251,6 +273,7 @@ impl StateExt for State {
|
|||||||
) {
|
) {
|
||||||
self.write_component(entity, comp::Admin);
|
self.write_component(entity, comp::Admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the client its request was successful.
|
// Tell the client its request was successful.
|
||||||
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
|
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
|
||||||
client.allow_state(ClientState::Character);
|
client.allow_state(ClientState::Character);
|
||||||
|
@ -175,7 +175,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
character_id,
|
character_id,
|
||||||
body,
|
body,
|
||||||
main,
|
main,
|
||||||
stats,
|
|
||||||
} => match client.client_state {
|
} => match client.client_state {
|
||||||
// Become Registered first.
|
// Become Registered first.
|
||||||
ClientState::Connected => client.error_state(RequestStateError::Impossible),
|
ClientState::Connected => client.error_state(RequestStateError::Impossible),
|
||||||
@ -201,7 +200,6 @@ impl<'a> System<'a> for Sys {
|
|||||||
character_id,
|
character_id,
|
||||||
body,
|
body,
|
||||||
main,
|
main,
|
||||||
stats,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ClientState::Character => client.error_state(RequestStateError::Already),
|
ClientState::Character => client.error_state(RequestStateError::Already),
|
||||||
@ -317,7 +315,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
},
|
},
|
||||||
ClientMsg::RequestCharacterList => {
|
ClientMsg::RequestCharacterList => {
|
||||||
if let Some(player) = players.get(entity) {
|
if let Some(player) = players.get(entity) {
|
||||||
match persistence::character::load_characters(
|
match persistence::character::load_character_list(
|
||||||
&player.uuid().to_string(),
|
&player.uuid().to_string(),
|
||||||
) {
|
) {
|
||||||
Ok(character_list) => {
|
Ok(character_list) => {
|
||||||
|
@ -20,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 StatsPersistenceTimer = SysTimer<persistence::stats::Sys>;
|
||||||
pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
|
pub type StatsPersistenceScheduler = SysScheduler<persistence::stats::Sys>;
|
||||||
|
|
||||||
// System names
|
// System names
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use crate::{persistence::stats, sys::SysScheduler};
|
use crate::{
|
||||||
|
persistence::stats,
|
||||||
|
sys::{SysScheduler, SysTimer},
|
||||||
|
};
|
||||||
use common::comp::{Player, Stats};
|
use common::comp::{Player, Stats};
|
||||||
use specs::{Join, ReadStorage, System, Write};
|
use specs::{Join, ReadStorage, System, Write};
|
||||||
|
|
||||||
@ -9,24 +12,20 @@ impl<'a> System<'a> for Sys {
|
|||||||
ReadStorage<'a, Player>,
|
ReadStorage<'a, Player>,
|
||||||
ReadStorage<'a, Stats>,
|
ReadStorage<'a, Stats>,
|
||||||
Write<'a, SysScheduler<Self>>,
|
Write<'a, SysScheduler<Self>>,
|
||||||
|
Write<'a, SysTimer<Self>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fn run(&mut self, (players, player_stats, mut scheduler): Self::SystemData) {
|
fn run(&mut self, (players, player_stats, mut scheduler, mut timer): Self::SystemData) {
|
||||||
if scheduler.should_run() {
|
if scheduler.should_run() {
|
||||||
let updates: Vec<(i32, &Stats)> = (&players, &player_stats)
|
timer.start();
|
||||||
.join()
|
|
||||||
.filter_map(|(player, stats)| {
|
|
||||||
if let Some(character_id) = player.character_id {
|
|
||||||
Some((character_id, stats))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if !updates.is_empty() {
|
stats::update(
|
||||||
stats::update(updates);
|
(&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();
|
let mut clock = Clock::start();
|
||||||
|
|
||||||
// Load the player's character list
|
// 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();
|
let mut current_client_state = self.client.borrow().get_client_state();
|
||||||
while let ClientState::Pending | ClientState::Registered = current_client_state {
|
while let ClientState::Pending | ClientState::Registered = current_client_state {
|
||||||
@ -92,7 +92,6 @@ impl PlayState for CharSelectionState {
|
|||||||
character_id,
|
character_id,
|
||||||
selected_character.body,
|
selected_character.body,
|
||||||
selected_character.character.tool.clone(),
|
selected_character.character.tool.clone(),
|
||||||
selected_character.stats.clone(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -351,7 +351,7 @@ impl CharSelectionUi {
|
|||||||
tool: tool.map(|specifier| specifier.to_string()),
|
tool: tool.map(|specifier| specifier.to_string()),
|
||||||
},
|
},
|
||||||
body,
|
body,
|
||||||
stats: comp::Stats::new(String::from(name), body),
|
level: 1,
|
||||||
}])
|
}])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -803,10 +803,12 @@ 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(&self.voxygen_i18n.get("char_selection.level_fmt").replace(
|
Text::new(
|
||||||
"{level_nb}",
|
&self
|
||||||
&character_item.stats.level.level().to_string(),
|
.voxygen_i18n
|
||||||
))
|
.get("char_selection.level_fmt")
|
||||||
|
.replace("{level_nb}", &character_item.level.to_string()),
|
||||||
|
)
|
||||||
.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)
|
||||||
|
@ -126,6 +126,9 @@ impl PlayState for MainMenuState {
|
|||||||
),
|
),
|
||||||
client::AuthClientError::ServerError(_, e) => format!("{}", e),
|
client::AuthClientError::ServerError(_, e) => format!("{}", e),
|
||||||
},
|
},
|
||||||
|
client::Error::InvalidCharacter => {
|
||||||
|
localized_strings.get("main.login.invalid_character").into()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
InitError::ClientCrashed => {
|
InitError::ClientCrashed => {
|
||||||
localized_strings.get("main.login.client_crashed").into()
|
localized_strings.get("main.login.client_crashed").into()
|
||||||
|
Reference in New Issue
Block a user