Merge branch 'shandley/character-loading' into 'master'

Move character DB ops off the main thread

See merge request veloren/veloren!1075
This commit is contained in:
Imbris 2020-06-25 16:16:15 +00:00
commit 6501611372
16 changed files with 493 additions and 218 deletions

View File

@ -243,9 +243,10 @@ impl Client {
}
/// Request a state transition to `ClientState::Character`.
pub fn request_character(&mut self, character_id: i32, body: comp::Body) {
pub fn request_character(&mut self, character_id: i32) {
self.postbox
.send_message(ClientMsg::Character { character_id, body });
.send_message(ClientMsg::Character(character_id));
self.active_character_id = Some(character_id);
self.client_state = ClientState::Pending;
}
@ -862,23 +863,7 @@ impl Client {
},
// Cleanup for when the client goes back to the `Registered` state
ServerMsg::ExitIngameCleanup => {
// Get client entity Uid
let client_uid = self
.state
.read_component_cloned::<Uid>(self.entity)
.map(|u| u.into())
.expect("Client doesn't have a Uid!!!");
// Clear ecs of all entities
self.state.ecs_mut().delete_all();
self.state.ecs_mut().maintain();
self.state.ecs_mut().insert(UidAllocator::default());
// Recreate client entity with Uid
let entity_builder = self.state.ecs_mut().create_entity();
let uid = entity_builder
.world
.write_resource::<UidAllocator>()
.allocate(entity_builder.entity, Some(client_uid));
self.entity = entity_builder.with(uid).build();
self.clean_state();
},
ServerMsg::InventoryUpdate(inventory, event) => {
match event {
@ -935,6 +920,10 @@ impl Client {
ServerMsg::Notification(n) => {
frontend_events.push(Event::Notification(n));
},
ServerMsg::CharacterDataLoadError(error) => {
self.clean_state();
self.character_list.error = Some(error);
},
}
}
} else if let Some(err) = self.postbox.error() {
@ -979,6 +968,29 @@ impl Client {
.cloned()
.collect()
}
/// Clean client ECS state
fn clean_state(&mut self) {
let client_uid = self
.state
.read_component_cloned::<Uid>(self.entity)
.map(|u| u.into())
.expect("Client doesn't have a Uid!!!");
// Clear ecs of all entities
self.state.ecs_mut().delete_all();
self.state.ecs_mut().maintain();
self.state.ecs_mut().insert(UidAllocator::default());
// Recreate client entity with Uid
let entity_builder = self.state.ecs_mut().create_entity();
let uid = entity_builder
.world
.write_resource::<UidAllocator>()
.allocate(entity_builder.entity, Some(client_uid));
self.entity = entity_builder.with(uid).build();
}
}
impl Drop for Client {

View File

@ -1,3 +1,5 @@
//! Structs representing a playable Character
use crate::comp;
use serde_derive::{Deserialize, Serialize};
@ -13,28 +15,23 @@ pub const MAX_CHARACTERS_PER_PLAYER: usize = 8;
// Once we are happy that all characters have a loadout, or we manually
// update/delete those that don't, it's no longer necessary and we can
// remove this from here, as well as in the DB schema and persistence code.
#[derive(Clone, Debug, Serialize, Deserialize)]
/// The minimum character data we need to create a new character on the server.
/// The `tool` field was historically used to persist the character's weapon
/// before Loadouts were persisted, and will be removed in the future.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Character {
pub id: Option<i32>,
pub alias: String,
pub tool: Option<String>,
}
/// 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)]
/// Data needed to render a single character item in the character list
/// presented during character selection.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CharacterItem {
pub character: Character,
pub body: comp::Body,
pub level: usize,
pub loadout: comp::Loadout,
}
/// 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

@ -93,10 +93,14 @@ pub enum ServerEvent {
Unmount(EcsEntity),
Possess(Uid, Uid),
LevelUp(EcsEntity, u32),
SelectCharacter {
/// Inserts default components for a character when loading into the game
InitCharacterData {
entity: EcsEntity,
character_id: i32,
body: comp::Body,
},
UpdateCharacterData {
entity: EcsEntity,
components: (comp::Body, comp::Stats, comp::Inventory, comp::Loadout),
},
ExitIngame {
entity: EcsEntity,

View File

@ -14,10 +14,7 @@ pub enum ClientMsg {
body: comp::Body,
},
DeleteCharacter(i32),
Character {
character_id: i32,
body: comp::Body,
},
Character(i32),
/// Request `ClientState::Registered` from an ingame state
ExitIngame,
/// Request `ClientState::Spectator` from a registered or ingame state

View File

@ -53,6 +53,8 @@ pub enum ServerMsg {
time_of_day: state::TimeOfDay,
world_map: (Vec2<u32>, Vec<u32>),
},
/// An error occurred while loading character data
CharacterDataLoadError(String),
/// A list of characters belonging to the a authenticated player was sent
CharacterListUpdate(Vec<CharacterItem>),
/// An error occured while creating or deleting a character

View File

@ -9,16 +9,21 @@ use common::{
use specs::{Builder, Entity as EcsEntity, WorldExt};
use vek::{Rgb, Vec3};
pub fn handle_create_character(
server: &mut Server,
entity: EcsEntity,
character_id: i32,
body: Body,
) {
pub fn handle_initialize_character(server: &mut Server, entity: EcsEntity, character_id: i32) {
let state = &mut server.state;
let server_settings = &server.server_settings;
state.create_player_character(entity, character_id, body, server_settings);
state.initialize_character_data(entity, character_id, server_settings);
}
pub fn handle_loaded_character_data(
server: &mut Server,
entity: EcsEntity,
loaded_components: (comp::Body, comp::Stats, comp::Inventory, comp::Loadout),
) {
let state = &mut server.state;
state.update_character_data(entity, loaded_components);
sys::subscription::initialize_region_subscription(state.ecs(), entity);
}

View File

@ -1,7 +1,8 @@
use crate::Server;
use common::event::{EventBus, ServerEvent};
use entity_creation::{
handle_create_character, handle_create_npc, handle_create_waypoint, handle_shoot,
handle_create_npc, handle_create_waypoint, handle_initialize_character,
handle_loaded_character_data, handle_shoot,
};
use entity_manipulation::{
handle_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_level_up,
@ -70,11 +71,13 @@ impl Server {
ServerEvent::Possess(possessor_uid, possesse_uid) => {
handle_possess(&self, possessor_uid, possesse_uid)
},
ServerEvent::SelectCharacter {
ServerEvent::InitCharacterData {
entity,
character_id,
body,
} => handle_create_character(self, entity, character_id, body),
} => handle_initialize_character(self, entity, character_id),
ServerEvent::UpdateCharacterData { entity, components } => {
handle_loaded_character_data(self, entity, components);
},
ServerEvent::LevelUp(entity, new_level) => handle_level_up(self, entity, new_level),
ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity),
ServerEvent::CreateNpc {

View File

@ -39,6 +39,7 @@ use common::{
vol::{ReadVol, RectVolSize},
};
use metrics::{ServerMetrics, TickMetrics};
use persistence::character::{CharacterLoader, CharacterLoaderResponseType, CharacterUpdater};
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
use std::{
i32,
@ -101,12 +102,10 @@ impl Server {
state.ecs_mut().insert(ChunkGenerator::new());
state
.ecs_mut()
.insert(persistence::character::CharacterUpdater::new(
settings.persistence_db_dir.clone(),
));
state.ecs_mut().insert(crate::settings::PersistenceDBDir(
settings.persistence_db_dir.clone(),
));
.insert(CharacterUpdater::new(settings.persistence_db_dir.clone()));
state
.ecs_mut()
.insert(CharacterLoader::new(settings.persistence_db_dir.clone()));
// System timers for performance monitoring
state.ecs_mut().insert(sys::EntitySyncTimer::default());
@ -303,8 +302,10 @@ impl Server {
// 5) Go through the terrain update queue and apply all changes to
// the terrain
// 6) Send relevant state updates to all clients
// 7) Update Metrics with current data
// 8) Finish the tick, passing control of the main thread back
// 7) Check for persistence updates related to character data, and message the
// relevant entities
// 8) Update Metrics with current data
// 9) Finish the tick, passing control of the main thread back
// to the frontend
// 1) Build up a list of events for this frame, to be passed to the frontend.
@ -375,14 +376,64 @@ impl Server {
.map(|(entity, _, _)| entity)
.collect::<Vec<_>>()
};
for entity in to_delete {
if let Err(e) = self.state.delete_entity_recorded(entity) {
error!(?e, "Failed to delete agent outside the terrain");
}
}
// 7 Persistence updates
let before_persistence_updates = Instant::now();
// Get character-related database responses and notify the requesting client
self.state
.ecs()
.read_resource::<persistence::character::CharacterLoader>()
.messages()
.for_each(|query_result| match query_result.result {
CharacterLoaderResponseType::CharacterList(result) => match result {
Ok(character_list_data) => self.notify_client(
query_result.entity,
ServerMsg::CharacterListUpdate(character_list_data),
),
Err(error) => self.notify_client(
query_result.entity,
ServerMsg::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseType::CharacterData(result) => {
let message = match *result {
Ok(character_data) => ServerEvent::UpdateCharacterData {
entity: query_result.entity,
components: character_data,
},
Err(error) => {
// We failed to load data for the character from the DB. Notify the
// client to push the state back to character selection, with the error
// to display
self.notify_client(
query_result.entity,
ServerMsg::CharacterDataLoadError(error.to_string()),
);
// Clean up the entity data on the server
ServerEvent::ExitIngame {
entity: query_result.entity,
}
},
};
self.state
.ecs()
.read_resource::<EventBus<ServerEvent>>()
.emit_now(message);
},
});
let end_of_server_tick = Instant::now();
// 7) Update Metrics
// 8) Update Metrics
// Get system timing info
let entity_sync_nanos = self
.state
@ -437,7 +488,11 @@ impl Server {
self.tick_metrics
.tick_time
.with_label_values(&["entity cleanup"])
.set((end_of_server_tick - before_entity_cleanup).as_nanos() as i64);
.set((before_persistence_updates - before_entity_cleanup).as_nanos() as i64);
self.tick_metrics
.tick_time
.with_label_values(&["persistence_updates"])
.set((end_of_server_tick - before_persistence_updates).as_nanos() as i64);
self.tick_metrics
.tick_time
.with_label_values(&["entity sync"])
@ -497,7 +552,7 @@ impl Server {
.set(end_of_server_tick.elapsed().as_nanos() as i64);
self.metrics.tick();
// 8) Finish the tick, pass control back to the frontend.
// 9) Finish the tick, pass control back to the frontend.
Ok(frontend_events)
}

View File

@ -1,3 +1,10 @@
//! Database operations related to character data
//!
//! Methods in this module should remain private - database updates and loading
//! are communicated via requests to the [`CharacterLoader`] and
//! [`CharacterUpdater`] while results/responses are polled and handled each
//! server tick.
extern crate diesel;
use super::{
@ -14,26 +21,225 @@ use common::{
character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER},
LoadoutBuilder,
};
use crossbeam::channel;
use crossbeam::{channel, channel::TryIter};
use diesel::prelude::*;
use tracing::{error, warn};
type CharacterLoaderRequest = (specs::Entity, CharacterLoaderRequestKind);
/// Available database operations when modifying a player's characetr list
enum CharacterLoaderRequestKind {
CreateCharacter {
player_uuid: String,
character_alias: String,
character_tool: Option<String>,
body: comp::Body,
},
DeleteCharacter {
player_uuid: String,
character_id: i32,
},
LoadCharacterList {
player_uuid: String,
},
LoadCharacterData {
player_uuid: String,
character_id: i32,
},
}
/// A tuple of the components that are persisted to the DB for each character
pub type PersistedComponents = (comp::Body, comp::Stats, comp::Inventory, comp::Loadout);
type CharacterListResult = Result<Vec<CharacterItem>, Error>;
type CharacterDataResult = Result<PersistedComponents, Error>;
/// Wrapper for results for character actions. Can be a list of
/// characters, or component data belonging to an individual character
#[derive(Debug)]
pub enum CharacterLoaderResponseType {
CharacterList(CharacterListResult),
CharacterData(Box<CharacterDataResult>),
}
/// Common message format dispatched in response to an update request
#[derive(Debug)]
pub struct CharacterLoaderResponse {
pub entity: specs::Entity,
pub result: CharacterLoaderResponseType,
}
/// A bi-directional messaging resource for making requests to modify or load
/// character data in a background thread.
///
/// This is used on the character selection screen, and after character
/// selection when loading the components associated with a character.
///
/// Requests messages are sent in the form of
/// [`CharacterLoaderRequestKind`] and are dispatched at the character select
/// screen.
///
/// Responses are polled on each server tick in the format
/// [`CharacterLoaderResponse`]
pub struct CharacterLoader {
update_rx: Option<channel::Receiver<CharacterLoaderResponse>>,
update_tx: Option<channel::Sender<CharacterLoaderRequest>>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl CharacterLoader {
pub fn new(db_dir: String) -> Self {
let (update_tx, internal_rx) = channel::unbounded::<CharacterLoaderRequest>();
let (internal_tx, update_rx) = channel::unbounded::<CharacterLoaderResponse>();
let handle = std::thread::spawn(move || {
while let Ok(request) = internal_rx.recv() {
let (entity, kind) = request;
if let Err(e) = internal_tx.send(CharacterLoaderResponse {
entity,
result: match kind {
CharacterLoaderRequestKind::CreateCharacter {
player_uuid,
character_alias,
character_tool,
body,
} => CharacterLoaderResponseType::CharacterList(create_character(
&player_uuid,
&character_alias,
character_tool,
&body,
&db_dir,
)),
CharacterLoaderRequestKind::DeleteCharacter {
player_uuid,
character_id,
} => CharacterLoaderResponseType::CharacterList(delete_character(
&player_uuid,
character_id,
&db_dir,
)),
CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => {
CharacterLoaderResponseType::CharacterList(load_character_list(
&player_uuid,
&db_dir,
))
},
CharacterLoaderRequestKind::LoadCharacterData {
player_uuid,
character_id,
} => CharacterLoaderResponseType::CharacterData(Box::new(
load_character_data(&player_uuid, character_id, &db_dir),
)),
},
}) {
error!(?e, "Could not send send persistence request");
}
}
});
Self {
update_tx: Some(update_tx),
update_rx: Some(update_rx),
handle: Some(handle),
}
}
/// Create a new character belonging to the player identified by
/// `player_uuid`
pub fn create_character(
&self,
entity: specs::Entity,
player_uuid: String,
character_alias: String,
character_tool: Option<String>,
body: comp::Body,
) {
if let Err(e) = self.update_tx.as_ref().unwrap().send((
entity,
CharacterLoaderRequestKind::CreateCharacter {
player_uuid,
character_alias,
character_tool,
body,
},
)) {
error!(?e, "Could not send character creation request");
}
}
/// Delete a character by `id` and `player_uuid`
pub fn delete_character(&self, entity: specs::Entity, player_uuid: String, character_id: i32) {
if let Err(e) = self.update_tx.as_ref().unwrap().send((
entity,
CharacterLoaderRequestKind::DeleteCharacter {
player_uuid,
character_id,
},
)) {
error!(?e, "Could not send character deletion request");
}
}
/// Loads a list of characters belonging to the player identified by
/// `player_uuid`
pub fn load_character_list(&self, entity: specs::Entity, player_uuid: String) {
if let Err(e) = self
.update_tx
.as_ref()
.unwrap()
.send((entity, CharacterLoaderRequestKind::LoadCharacterList {
player_uuid,
}))
{
error!(?e, "Could not send character list load request");
}
}
/// Loads components associated with a character
pub fn load_character_data(
&self,
entity: specs::Entity,
player_uuid: String,
character_id: i32,
) {
if let Err(e) = self.update_tx.as_ref().unwrap().send((
entity,
CharacterLoaderRequestKind::LoadCharacterData {
player_uuid,
character_id,
},
)) {
error!(?e, "Could not send character data load request");
}
}
/// Returns a non-blocking iterator over CharacterLoaderResponse messages
pub fn messages(&self) -> TryIter<CharacterLoaderResponse> {
self.update_rx.as_ref().unwrap().try_iter()
}
}
impl Drop for CharacterLoader {
fn drop(&mut self) {
drop(self.update_tx.take());
if let Err(e) = self.handle.take().unwrap().join() {
error!(?e, "Error from joining character loader thread");
}
}
}
/// 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,
db_dir: &str,
) -> Result<(comp::Stats, comp::Inventory, comp::Loadout), Error> {
fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> CharacterDataResult {
let connection = establish_connection(db_dir);
let (character_data, body_data, stats_data, maybe_inventory, maybe_loadout) =
schema::character::dsl::character
.filter(schema::character::id.eq(character_id))
.filter(schema::character::player_uuid.eq(player_uuid))
.inner_join(schema::body::table)
.inner_join(schema::stats::table)
.left_join(schema::inventory::table)
@ -41,6 +247,7 @@ pub fn load_character_data(
.first::<(Character, Body, Stats, Option<Inventory>, Option<Loadout>)>(&connection)?;
Ok((
comp::Body::from(&body_data),
comp::Stats::from(StatsJoinData {
alias: &character_data.alias,
body: &comp::Body::from(&body_data),
@ -103,8 +310,7 @@ pub fn load_character_data(
/// 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, db_dir: &str) -> CharacterListResult {
fn load_character_list(player_uuid: &str, db_dir: &str) -> CharacterListResult {
let data = schema::character::dsl::character
.filter(schema::character::player_uuid.eq(player_uuid))
.order(schema::character::id.desc())
@ -146,10 +352,10 @@ pub fn load_character_list(player_uuid: &str, db_dir: &str) -> CharacterListResu
/// 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(
/// id for subsequent insertions
fn create_character(
uuid: &str,
character_alias: String,
character_alias: &str,
character_tool: Option<String>,
body: &comp::Body,
db_dir: &str,
@ -243,7 +449,7 @@ pub fn create_character(
}
/// Delete a character. Returns the updated character list.
pub fn delete_character(uuid: &str, character_id: i32, db_dir: &str) -> CharacterListResult {
fn delete_character(uuid: &str, character_id: i32, db_dir: &str) -> CharacterListResult {
use schema::character::dsl::*;
diesel::delete(
@ -256,6 +462,8 @@ pub fn delete_character(uuid: &str, character_id: i32, db_dir: &str) -> Characte
load_character_list(uuid, db_dir)
}
/// Before creating a character, we ensure that the limit on the number of
/// characters has not been exceeded
fn check_character_limit(uuid: &str, db_dir: &str) -> Result<(), Error> {
use diesel::dsl::count_star;
use schema::character::dsl::*;
@ -277,8 +485,13 @@ fn check_character_limit(uuid: &str, db_dir: &str) -> Result<(), Error> {
}
}
pub type CharacterUpdateData = (StatsUpdate, InventoryUpdate, LoadoutUpdate);
type CharacterUpdateData = (StatsUpdate, InventoryUpdate, LoadoutUpdate);
/// A unidirectional messaging resource for saving characters in a
/// background thread.
///
/// This is used to make updates to a character and their persisted components,
/// such as inventory, loadout, etc...
pub struct CharacterUpdater {
update_tx: Option<channel::Sender<Vec<(i32, CharacterUpdateData)>>>,
handle: Option<std::thread::JoinHandle<()>>,
@ -299,6 +512,7 @@ impl CharacterUpdater {
}
}
/// Updates a collection of characters based on their id and components
pub fn batch_update<'a>(
&self,
updates: impl Iterator<Item = (i32, &'a comp::Stats, &'a comp::Inventory, &'a comp::Loadout)>,
@ -321,6 +535,7 @@ impl CharacterUpdater {
}
}
/// Updates a single character based on their id and components
pub fn update(
&self,
character_id: i32,

View File

@ -1,11 +1,17 @@
//! Consolidates Diesel and validation errors under a common error type
extern crate diesel;
use std::fmt;
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Error {
// The player has already reached the max character limit
CharacterLimitReached,
// An error occured while establish a db connection
DatabaseConnectionError(diesel::ConnectionError),
// An error occured while running migrations
DatabaseMigrationError(diesel_migrations::RunMigrationsError),
// An error occured when performing a database action
DatabaseError(diesel::result::Error),
// Unable to load body or stats for a character
@ -15,8 +21,10 @@ pub enum 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"),
Self::DatabaseError(error) => error.to_string(),
Self::DatabaseConnectionError(error) => error.to_string(),
Self::DatabaseMigrationError(error) => error.to_string(),
Self::CharacterDataError => String::from("Error while loading character data"),
})
}
@ -25,3 +33,13 @@ impl fmt::Display for Error {
impl From<diesel::result::Error> for Error {
fn from(error: diesel::result::Error) -> Error { Error::DatabaseError(error) }
}
impl From<diesel::ConnectionError> for Error {
fn from(error: diesel::ConnectionError) -> Error { Error::DatabaseConnectionError(error) }
}
impl From<diesel_migrations::RunMigrationsError> for Error {
fn from(error: diesel_migrations::RunMigrationsError) -> Error {
Error::DatabaseMigrationError(error)
}
}

View File

@ -1,3 +1,11 @@
//! DB operations and schema migrations
//!
//! This code uses several [`Diesel ORM`](http://diesel.rs/) tools for DB operations:
//! - [`diesel-migrations`](https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/)
//! for managing table migrations
//! - [`diesel-cli`](https://github.com/diesel-rs/diesel/tree/master/diesel_cli/)
//! for generating and testing migrations
pub mod character;
mod error;
@ -16,6 +24,7 @@ use tracing::warn;
// for the `embedded_migrations` call below.
embed_migrations!();
/// Runs any pending database migrations. This is executed during server startup
pub fn run_migrations(db_dir: &str) -> Result<(), diesel_migrations::RunMigrationsError> {
let db_dir = &apply_saves_dir_override(db_dir);
let _ = fs::create_dir(format!("{}/", db_dir));
@ -47,15 +56,13 @@ fn establish_connection(db_dir: &str) -> SqliteConnection {
connection
}
#[allow(clippy::single_match)] // TODO: Pending review in #587
fn apply_saves_dir_override(db_dir: &str) -> String {
if let Some(saves_dir) = env::var_os("VELOREN_SAVES_DIR") {
let path = PathBuf::from(saves_dir.clone());
if path.exists() || path.parent().map(|x| x.exists()).unwrap_or(false) {
// Only allow paths with valid unicode characters
match path.to_str() {
Some(path) => return path.to_owned(),
None => {},
if let Some(path) = path.to_str() {
return path.to_owned();
}
}
warn!(?saves_dir, "VELOREN_SAVES_DIR points to an invalid path.");

View File

@ -79,8 +79,8 @@ impl From<&Body> for comp::Body {
}
}
/// `Stats` represents the stats for a character, which has a one-to-one
/// relationship with Characters.
/// `Stats` represents the stats for a character, and have a one-to-one
/// relationship with `Character`.
#[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)]
#[belongs_to(Character)]
#[primary_key(character_id)]
@ -144,8 +144,8 @@ impl From<&comp::Stats> for StatsUpdate {
/// Inventory storage and conversion. Inventories have a one-to-one relationship
/// with characters.
///
/// We store the players inventory as a single TEXT column which is serialised
/// JSON representation of the Inventory component.
/// We store inventory rows as a (character_id, json) tuples, where the json is
/// a serialised Inventory component.
#[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)]
#[belongs_to(Character)]
#[primary_key(character_id)]

View File

@ -134,5 +134,3 @@ impl ServerSettings {
fn get_settings_path() -> PathBuf { PathBuf::from(r"server_settings.ron") }
}
pub struct PersistenceDBDir(pub String);

View File

@ -1,13 +1,11 @@
use crate::{
client::Client, persistence, settings::ServerSettings, sys::sentinel::DeletedEntities,
SpawnPoint,
client::Client, persistence::character::PersistedComponents, settings::ServerSettings,
sys::sentinel::DeletedEntities, SpawnPoint,
};
use common::{
comp,
effect::Effect,
msg::{
CharacterInfo, ClientState, PlayerListUpdate, RegisterError, RequestStateError, ServerMsg,
},
msg::{CharacterInfo, ClientState, PlayerListUpdate, ServerMsg},
state::State,
sync::{Uid, WorldSyncExt},
util::Dir,
@ -17,8 +15,11 @@ use tracing::warn;
use vek::*;
pub trait StateExt {
/// Push an item into the provided entity's inventory
fn give_item(&mut self, entity: EcsEntity, item: comp::Item) -> bool;
/// Updates a component associated with the entity based on the `Effect`
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect);
/// Build a non-player character
fn create_npc(
&mut self,
pos: comp::Pos,
@ -26,7 +27,9 @@ pub trait StateExt {
loadout: comp::Loadout,
body: comp::Body,
) -> EcsEntityBuilder;
/// Build a static object entity
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
/// Build a projectile
fn create_projectile(
&mut self,
pos: comp::Pos,
@ -34,14 +37,19 @@ pub trait StateExt {
body: comp::Body,
projectile: comp::Projectile,
) -> EcsEntityBuilder;
fn create_player_character(
/// Insert common/default components for a new character joining the server
fn initialize_character_data(
&mut self,
entity: EcsEntity,
character_id: i32,
body: comp::Body,
server_settings: &ServerSettings,
);
/// Update the components associated with the entity's current character.
/// Performed after loading component data from the database
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents);
/// Iterates over registered clients and send each `ServerMsg`
fn notify_registered_clients(&self, msg: ServerMsg);
/// Delete an entity, recording the deletion in [`DeletedEntities`]
fn delete_entity_recorded(
&mut self,
entity: EcsEntity,
@ -82,7 +90,6 @@ impl StateExt for State {
}
}
/// Build a non-player character.
fn create_npc(
&mut self,
pos: comp::Pos,
@ -115,7 +122,6 @@ impl StateExt for State {
.with(loadout)
}
/// Build a static object entity
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
self.ecs_mut()
.create_entity_synced()
@ -132,7 +138,6 @@ impl StateExt for State {
.with(comp::Gravity(1.0))
}
/// Build a projectile
fn create_projectile(
&mut self,
pos: comp::Pos,
@ -152,44 +157,14 @@ impl StateExt for State {
.with(comp::Sticky)
}
#[allow(clippy::unnecessary_operation)] // TODO: Pending review in #587
fn create_player_character(
fn initialize_character_data(
&mut self,
entity: EcsEntity,
character_id: i32,
body: comp::Body,
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,
&server_settings.persistence_db_dir,
) {
Ok((stats, inventory, loadout)) => {
self.write_component(entity, stats);
self.write_component(entity, inventory);
self.write_component(entity, loadout);
},
Err(e) => {
warn!(
?e,
?character_id,
"Failed to load character data for character_id"
);
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
client.error_state(RequestStateError::RegisterDenied(
RegisterError::InvalidCharacter,
))
}
},
}
let spawn_point = self.ecs().read_resource::<SpawnPoint>().0;
self.write_component(entity, body);
self.write_component(entity, comp::Energy::new(1000));
self.write_component(entity, comp::Controller::default());
self.write_component(entity, comp::Pos(spawn_point));
@ -203,27 +178,19 @@ impl StateExt for State {
self.write_component(entity, comp::Gravity(1.0));
self.write_component(entity, comp::CharacterState::default());
self.write_component(entity, comp::Alignment::Owned(entity));
self.write_component(
entity,
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()),
);
// 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()
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);
// Give the Admin component to the player if their name exists in admin list
if server_settings.admins.contains(
&self
@ -236,30 +203,42 @@ impl StateExt for State {
self.write_component(entity, comp::Admin);
}
let uids = &self.ecs().read_storage::<Uid>();
let uid = uids
.get(entity)
.expect("Failed to fetch uid component for entity.")
.0;
let stats = &self.ecs().read_storage::<comp::Stats>();
let stat = stats
.get(entity)
.expect("Failed to fetch stats component for entity.");
self.notify_registered_clients(ServerMsg::PlayerListUpdate(
PlayerListUpdate::SelectedCharacter(uid, CharacterInfo {
name: stat.name.to_string(),
level: stat.level.level(),
}),
));
// Tell the client its request was successful.
if let Some(client) = self.ecs().write_storage::<Client>().get_mut(entity) {
client.allow_state(ClientState::Character);
}
}
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) {
let (body, stats, inventory, loadout) = components;
// Notify clients of a player list update
let client_uid = self
.read_component_cloned::<Uid>(entity)
.map(|u| u.into())
.expect("Client doesn't have a Uid!!!");
self.notify_registered_clients(ServerMsg::PlayerListUpdate(
PlayerListUpdate::SelectedCharacter(client_uid, CharacterInfo {
name: String::from(&stats.name),
level: stats.level.level(),
}),
));
self.write_component(entity, body);
self.write_component(entity, stats);
self.write_component(entity, inventory);
self.write_component(entity, loadout);
self.write_component(
entity,
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()),
);
// Make sure physics are accepted.
self.write_component(entity, comp::ForceUpdate);
}
fn notify_registered_clients(&self, msg: ServerMsg) {
for client in (&mut self.ecs().write_storage::<Client>())
.join()

View File

@ -1,6 +1,6 @@
use super::SysTimer;
use crate::{
auth_provider::AuthProvider, client::Client, persistence, settings::PersistenceDBDir,
auth_provider::AuthProvider, client::Client, persistence::character::CharacterLoader,
CLIENT_TIMEOUT,
};
use common::{
@ -32,7 +32,7 @@ impl<'a> System<'a> for Sys {
Entities<'a>,
Read<'a, EventBus<ServerEvent>>,
Read<'a, Time>,
ReadExpect<'a, PersistenceDBDir>,
ReadExpect<'a, CharacterLoader>,
ReadExpect<'a, TerrainGrid>,
Write<'a, SysTimer<Self>>,
ReadStorage<'a, Uid>,
@ -60,7 +60,7 @@ impl<'a> System<'a> for Sys {
entities,
server_event_bus,
time,
persistence_db_dir,
character_loader,
terrain,
mut timer,
uids,
@ -81,8 +81,6 @@ impl<'a> System<'a> for Sys {
) {
timer.start();
let persistence_db_dir = &persistence_db_dir.0;
let mut server_emitter = server_event_bus.emitter();
let mut new_chat_msgs = Vec::new();
@ -195,31 +193,45 @@ impl<'a> System<'a> for Sys {
},
_ => {},
},
ClientMsg::Character { character_id, body } => match client.client_state {
ClientMsg::Character(character_id) => match client.client_state {
// Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator => {
// Only send login message if it wasn't already
// sent previously
if let (Some(player), false) =
(players.get(entity), client.login_msg_sent)
{
new_chat_msgs.push((
None,
ServerMsg::broadcast(format!(
"[{}] is now online.",
&player.alias
)),
));
if let Some(player) = players.get(entity) {
// Send a request to load the character's component data from the
// DB. Once loaded, persisted components such as stats and inventory
// will be inserted for the entity
character_loader.load_character_data(
entity,
player.uuid().to_string(),
character_id,
);
client.login_msg_sent = true;
// Start inserting non-persisted/default components for the entity
// while we load the DB data
server_emitter.emit(ServerEvent::InitCharacterData {
entity,
character_id,
});
// Only send login message if it wasn't already
// sent previously
if !client.login_msg_sent {
new_chat_msgs.push((
None,
ServerMsg::broadcast(format!(
"[{}] is now online.",
&player.alias
)),
));
client.login_msg_sent = true;
}
} else {
client.notify(ServerMsg::CharacterDataLoadError(String::from(
"Failed to fetch player entity",
)))
}
server_emitter.emit(ServerEvent::SelectCharacter {
entity,
character_id,
body,
});
},
ClientState::Character => client.error_state(RequestStateError::Already),
ClientState::Pending => {},
@ -334,54 +346,27 @@ impl<'a> System<'a> for Sys {
},
ClientMsg::RequestCharacterList => {
if let Some(player) = players.get(entity) {
match persistence::character::load_character_list(
&player.uuid().to_string(),
persistence_db_dir,
) {
Ok(character_list) => {
client.notify(ServerMsg::CharacterListUpdate(character_list));
},
Err(error) => {
client
.notify(ServerMsg::CharacterActionError(error.to_string()));
},
}
character_loader.load_character_list(entity, player.uuid().to_string())
}
},
ClientMsg::CreateCharacter { alias, tool, body } => {
if let Some(player) = players.get(entity) {
match persistence::character::create_character(
&player.uuid().to_string(),
character_loader.create_character(
entity,
player.uuid().to_string(),
alias,
tool,
&body,
persistence_db_dir,
) {
Ok(character_list) => {
client.notify(ServerMsg::CharacterListUpdate(character_list));
},
Err(error) => {
client
.notify(ServerMsg::CharacterActionError(error.to_string()));
},
}
body,
);
}
},
ClientMsg::DeleteCharacter(character_id) => {
if let Some(player) = players.get(entity) {
match persistence::character::delete_character(
&player.uuid().to_string(),
character_loader.delete_character(
entity,
player.uuid().to_string(),
character_id,
persistence_db_dir,
) {
Ok(character_list) => {
client.notify(ServerMsg::CharacterListUpdate(character_list));
},
Err(error) => {
client
.notify(ServerMsg::CharacterActionError(error.to_string()));
},
}
);
}
},
}

View File

@ -88,9 +88,7 @@ impl PlayState for CharSelectionState {
char_data.get(self.char_selection_ui.selected_character)
{
if let Some(character_id) = selected_character.character.id {
self.client
.borrow_mut()
.request_character(character_id, selected_character.body);
self.client.borrow_mut().request_character(character_id);
}
}