use crate::persistence::{ character::{create_character, delete_character, load_character_data, load_character_list}, error::Error, establish_connection, PersistedComponents, }; use common::{ character::{CharacterId, CharacterItem}, comp::inventory::item::MaterialStatManifest, }; use crossbeam_channel::{self, TryIter}; use lazy_static::lazy_static; use std::path::Path; use tracing::error; pub(crate) type CharacterListResult = Result, Error>; pub(crate) type CharacterCreationResult = Result<(CharacterId, Vec), Error>; pub(crate) type CharacterDataResult = Result; type CharacterLoaderRequest = (specs::Entity, CharacterLoaderRequestKind); /// Available database operations when modifying a player's character list enum CharacterLoaderRequestKind { CreateCharacter { player_uuid: String, character_alias: String, persisted_components: PersistedComponents, }, DeleteCharacter { player_uuid: String, character_id: CharacterId, }, LoadCharacterList { player_uuid: String, }, LoadCharacterData { player_uuid: String, character_id: CharacterId, }, } /// Wrapper for results for character actions. Can be a list of /// characters, or component data belonging to an individual character #[derive(Debug)] pub enum CharacterLoaderResponseKind { CharacterList(CharacterListResult), CharacterData(Box), CharacterCreation(CharacterCreationResult), } /// Common message format dispatched in response to an update request #[derive(Debug)] pub struct CharacterLoaderResponse { pub entity: specs::Entity, pub result: CharacterLoaderResponseKind, } /// 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: crossbeam_channel::Receiver, update_tx: crossbeam_channel::Sender, } // Decoupled from the ECS resource because the plumbing is getting complicated; // shouldn't matter unless someone's hot-reloading material stats on the live // server lazy_static! { pub static ref MATERIAL_STATS_MANIFEST: MaterialStatManifest = MaterialStatManifest::default(); } impl CharacterLoader { pub fn new(db_dir: &Path) -> diesel::QueryResult { let (update_tx, internal_rx) = crossbeam_channel::unbounded::(); let (internal_tx, update_rx) = crossbeam_channel::unbounded::(); let mut conn = establish_connection(db_dir)?; let builder = std::thread::Builder::new().name("persistence_loader".into()); builder .spawn(move || { for request in internal_rx { let (entity, kind) = request; if let Err(e) = internal_tx.send(CharacterLoaderResponse { entity, result: match kind { CharacterLoaderRequestKind::CreateCharacter { player_uuid, character_alias, persisted_components, } => CharacterLoaderResponseKind::CharacterCreation(conn.transaction( |txn| { create_character( &player_uuid, &character_alias, persisted_components, txn, &MATERIAL_STATS_MANIFEST, ) }, )), CharacterLoaderRequestKind::DeleteCharacter { player_uuid, character_id, } => CharacterLoaderResponseKind::CharacterList(conn.transaction( |txn| { delete_character( &player_uuid, character_id, txn, &MATERIAL_STATS_MANIFEST, ) }, )), CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => { CharacterLoaderResponseKind::CharacterList(conn.transaction( |txn| { load_character_list( &player_uuid, txn, &MATERIAL_STATS_MANIFEST, ) }, )) }, CharacterLoaderRequestKind::LoadCharacterData { player_uuid, character_id, } => { let result = conn.transaction(|txn| { load_character_data( player_uuid, character_id, txn, &MATERIAL_STATS_MANIFEST, ) }); if result.is_err() { error!( ?result, "Error loading character data for character_id: {}", character_id ); } CharacterLoaderResponseKind::CharacterData(Box::new(result)) }, }, }) { error!(?e, "Could not send send persistence request"); } } }) .unwrap(); Ok(Self { update_rx, update_tx, }) } /// 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, persisted_components: PersistedComponents, ) { if let Err(e) = self .update_tx .send((entity, CharacterLoaderRequestKind::CreateCharacter { player_uuid, character_alias, persisted_components, })) { 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: CharacterId, ) { if let Err(e) = self .update_tx .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 .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: CharacterId, ) { if let Err(e) = self.update_tx .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 { self.update_rx.try_iter() } }