mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
commit
6501611372
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
|
@ -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)]
|
||||
|
@ -134,5 +134,3 @@ impl ServerSettings {
|
||||
|
||||
fn get_settings_path() -> PathBuf { PathBuf::from(r"server_settings.ron") }
|
||||
}
|
||||
|
||||
pub struct PersistenceDBDir(pub String);
|
||||
|
@ -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()
|
||||
|
@ -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()));
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user