diff --git a/CHANGELOG.md b/CHANGELOG.md index 3283af2ca5..2f9a333ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - NPCs call for help when attacked - Eyebrows and shapes can now be selected - Character name and level information to chat, social tab and `/players` command. -- Added inventory saving +- Added inventory, armour and weapon saving ### Changed diff --git a/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron b/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron index 6b9c6fa1ea..a5a1bb2358 100644 --- a/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron +++ b/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron @@ -45,11 +45,11 @@ color: None ), Steel0:( - vox_spec: ("armor.foot.steel-0", (-2.5, -3.5, -9.0)), + vox_spec: ("armor.foot.steel-0", (-2.5, -3.5, -2.0)), color: None ), Leather2:( - vox_spec: ("armor.foot.leather-2", (-2.5, -3.5, -9.0)), + vox_spec: ("armor.foot.leather-2", (-2.5, -3.5, -2.0)), color: None ), }, diff --git a/client/src/lib.rs b/client/src/lib.rs index 7d1437cf55..13e540d1c5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -237,12 +237,9 @@ impl Client { } /// Request a state transition to `ClientState::Character`. - pub fn request_character(&mut self, character_id: i32, body: comp::Body, main: Option) { - self.postbox.send_message(ClientMsg::Character { - character_id, - body, - main, - }); + pub fn request_character(&mut self, character_id: i32, body: comp::Body) { + self.postbox + .send_message(ClientMsg::Character { character_id, body }); self.client_state = ClientState::Pending; } diff --git a/common/src/character.rs b/common/src/character.rs index c32ce9fde1..03b0f8193b 100644 --- a/common/src/character.rs +++ b/common/src/character.rs @@ -4,11 +4,20 @@ use serde_derive::{Deserialize, Serialize}; /// The limit on how many characters that a player can have pub const MAX_CHARACTERS_PER_PLAYER: usize = 8; +// TODO: Since loadout persistence came a few weeks after character persistence, +// we stored their main weapon in the `tool` field here. While loadout +// persistence is still new, saved characters may not have an associated loadout +// entry in the DB, so we use this `tool` field to create an entry the first +// time they enter the game. +// +// 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)] pub struct Character { pub id: Option, pub alias: String, - pub tool: Option, // TODO: Remove once we start persisting inventories + pub tool: Option, } /// Represents a single character item in the character list presented during @@ -19,6 +28,7 @@ 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 diff --git a/common/src/event.rs b/common/src/event.rs index b8f5cc6a80..db585e5907 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -95,7 +95,6 @@ pub enum ServerEvent { entity: EcsEntity, character_id: i32, body: comp::Body, - main: Option, }, ExitIngame { entity: EcsEntity, diff --git a/common/src/lib.rs b/common/src/lib.rs index 12600ed3d4..85db925e03 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -22,6 +22,7 @@ pub mod effect; pub mod event; pub mod figure; pub mod generation; +pub mod loadout_builder; pub mod msg; pub mod npc; pub mod path; @@ -38,6 +39,8 @@ pub mod util; pub mod vol; pub mod volumes; +pub use loadout_builder::LoadoutBuilder; + /// The networking module containing high-level wrappers of `TcpListener` and /// `TcpStream` (`PostOffice` and `PostBox` respectively) and data types used by /// both the server and client. # Examples diff --git a/common/src/loadout_builder.rs b/common/src/loadout_builder.rs new file mode 100644 index 0000000000..d2ddc35577 --- /dev/null +++ b/common/src/loadout_builder.rs @@ -0,0 +1,178 @@ +use crate::{ + assets, + comp::{ + item::{Item, ItemKind}, + CharacterAbility, ItemConfig, Loadout, + }, +}; + +/// Builder for character Loadouts, containing weapon and armour items belonging +/// to a character, along with some helper methods for loading Items and +/// ItemConfig +/// +/// ``` +/// use veloren_common::LoadoutBuilder; +/// +/// // Build a loadout with character starter defaults and a specific sword with default sword abilities +/// let loadout = LoadoutBuilder::new() +/// .defaults() +/// .active_item(LoadoutBuilder::default_item_config_from_str( +/// Some("common.items.weapons.sword.zweihander_sword_0"), +/// )) +/// .build(); +/// ``` +pub struct LoadoutBuilder(Loadout); + +impl LoadoutBuilder { + pub fn new() -> Self { + Self(Loadout { + active_item: None, + second_item: None, + shoulder: None, + chest: None, + belt: None, + hand: None, + pants: None, + foot: None, + back: None, + ring: None, + neck: None, + lantern: None, + head: None, + tabard: None, + }) + } + + /// Set default armor items for the loadout. This may vary with game + /// updates, but should be safe defaults for a new character. + pub fn defaults(self) -> Self { + self.chest(Some(assets::load_expect_cloned( + "common.items.armor.starter.rugged_chest", + ))) + .pants(Some(assets::load_expect_cloned( + "common.items.armor.starter.rugged_pants", + ))) + .foot(Some(assets::load_expect_cloned( + "common.items.armor.starter.sandals_0", + ))) + .lantern(Some(assets::load_expect_cloned( + "common.items.armor.starter.lantern", + ))) + } + + /// Get the default [ItemConfig](../comp/struct.ItemConfig.html) for a tool + /// (weapon). This information is required for the `active` and `second` + /// weapon items in a loadout. If some customisation to the item's + /// abilities or their timings is desired, you should create and provide + /// the item config directly to the [active_item](#method.active_item) + /// method + pub fn default_item_config_from_item(maybe_item: Option) -> Option { + if let Some(item) = maybe_item { + if let ItemKind::Tool(tool) = item.kind { + let mut abilities = tool.get_abilities(); + let mut ability_drain = abilities.drain(..); + + return Some(ItemConfig { + item, + ability1: ability_drain.next(), + ability2: ability_drain.next(), + ability3: ability_drain.next(), + block_ability: Some(CharacterAbility::BasicBlock), + dodge_ability: Some(CharacterAbility::Roll), + }); + } + } + + None + } + + /// Get an [Item](../comp/struct.Item.html) by its string + /// reference by loading its asset + pub fn item_from_str(item_ref: Option<&str>) -> Option { + item_ref.and_then(|specifier| assets::load_cloned::(&specifier).ok()) + } + + /// Get an item's (weapon's) default + /// [ItemConfig](../comp/struct.ItemConfig.html) + /// by string reference. This will first attempt to load the Item, then + /// the default abilities for that item via the + /// [default_item_config_from_item](#method.default_item_config_from_item) + /// function + pub fn default_item_config_from_str(item_ref: Option<&str>) -> Option { + Self::default_item_config_from_item(Self::item_from_str(item_ref)) + } + + pub fn active_item(mut self, item: Option) -> Self { + self.0.active_item = item; + + self + } + + pub fn second_item(mut self, item: Option) -> Self { + self.0.active_item = item; + + self + } + + pub fn shoulder(mut self, item: Option) -> Self { + self.0.shoulder = item; + self + } + + pub fn chest(mut self, item: Option) -> Self { + self.0.chest = item; + self + } + + pub fn belt(mut self, item: Option) -> Self { + self.0.belt = item; + self + } + + pub fn hand(mut self, item: Option) -> Self { + self.0.hand = item; + self + } + + pub fn pants(mut self, item: Option) -> Self { + self.0.pants = item; + self + } + + pub fn foot(mut self, item: Option) -> Self { + self.0.foot = item; + self + } + + pub fn back(mut self, item: Option) -> Self { + self.0.back = item; + self + } + + pub fn ring(mut self, item: Option) -> Self { + self.0.ring = item; + self + } + + pub fn neck(mut self, item: Option) -> Self { + self.0.neck = item; + self + } + + pub fn lantern(mut self, item: Option) -> Self { + self.0.lantern = item; + self + } + + pub fn head(mut self, item: Option) -> Self { + self.0.head = item; + self + } + + pub fn tabard(mut self, item: Option) -> Self { + self.0.tabard = item; + self + } + + pub fn build(self) -> Loadout { self.0 } +} diff --git a/common/src/msg/client.rs b/common/src/msg/client.rs index 4b9b4f3c17..550cf5de60 100644 --- a/common/src/msg/client.rs +++ b/common/src/msg/client.rs @@ -17,7 +17,6 @@ pub enum ClientMsg { Character { character_id: i32, body: comp::Body, - main: Option, // Specifier for the weapon }, /// Request `ClientState::Registered` from an ingame state ExitIngame, diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 9cbd194165..93a925ee1d 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -14,12 +14,11 @@ pub fn handle_create_character( entity: EcsEntity, character_id: i32, body: Body, - main: Option, ) { let state = &mut server.state; let server_settings = &server.server_settings; - state.create_player_character(entity, character_id, body, main, server_settings); + state.create_player_character(entity, character_id, body, server_settings); sys::subscription::initialize_region_subscription(state.ecs(), entity); } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 04d540badc..f12329275d 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -74,8 +74,7 @@ impl Server { entity, character_id, body, - main, - } => handle_create_character(self, entity, character_id, body, main), + } => handle_create_character(self, entity, character_id, body), ServerEvent::LevelUp(entity, new_level) => handle_level_up(self, entity, new_level), ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity), ServerEvent::CreateNpc { diff --git a/server/src/events/player.rs b/server/src/events/player.rs index c7649da424..f857e4a1a5 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -72,16 +72,17 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event } // Sync the player's character data to the database - if let (Some(player), Some(stats), Some(inventory), updater) = ( + if let (Some(player), Some(stats), Some(inventory), Some(loadout), updater) = ( state.read_storage::().get(entity), state.read_storage::().get(entity), state.read_storage::().get(entity), + state.read_storage::().get(entity), state .ecs() .read_resource::(), ) { if let Some(character_id) = player.character_id { - updater.update(character_id, stats, inventory); + updater.update(character_id, stats, inventory, loadout); } } diff --git a/server/src/migrations/2020-05-28-210610_loadout/down.sql b/server/src/migrations/2020-05-28-210610_loadout/down.sql new file mode 100644 index 0000000000..2e3f65538b --- /dev/null +++ b/server/src/migrations/2020-05-28-210610_loadout/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "loadout"; \ No newline at end of file diff --git a/server/src/migrations/2020-05-28-210610_loadout/up.sql b/server/src/migrations/2020-05-28-210610_loadout/up.sql new file mode 100644 index 0000000000..0284f5db2b --- /dev/null +++ b/server/src/migrations/2020-05-28-210610_loadout/up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS "loadout" ( + id INTEGER PRIMARY KEY NOT NULL, + character_id INT NOT NULL, + items TEXT NOT NULL, + FOREIGN KEY(character_id) REFERENCES "character"(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index 19d3bad4b4..5d1ba89ae1 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -4,13 +4,16 @@ use super::{ error::Error, establish_connection, models::{ - Body, Character, Inventory, InventoryUpdate, NewCharacter, Stats, StatsJoinData, - StatsUpdate, + Body, Character, Inventory, InventoryUpdate, Loadout, LoadoutUpdate, NewCharacter, + NewLoadout, Stats, StatsJoinData, StatsUpdate, }, schema, }; use crate::comp; -use common::character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER}; +use common::{ + character::{Character as CharacterData, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, + LoadoutBuilder, +}; use crossbeam::channel; use diesel::prelude::*; @@ -23,16 +26,17 @@ type CharacterListResult = Result, Error>; pub fn load_character_data( character_id: i32, db_dir: &str, -) -> Result<(comp::Stats, comp::Inventory), Error> { +) -> Result<(comp::Stats, comp::Inventory, comp::Loadout), Error> { let connection = establish_connection(db_dir); - let (character_data, body_data, stats_data, maybe_inventory) = + let (character_data, body_data, stats_data, maybe_inventory, maybe_loadout) = schema::character::dsl::character .filter(schema::character::id.eq(character_id)) .inner_join(schema::body::table) .inner_join(schema::stats::table) .left_join(schema::inventory::table) - .first::<(Character, Body, Stats, Option)>(&connection)?; + .left_join(schema::loadout::table) + .first::<(Character, Body, Stats, Option, Option)>(&connection)?; Ok(( comp::Stats::from(StatsJoinData { @@ -60,6 +64,33 @@ pub fn load_character_data( }, |inv| comp::Inventory::from(inv), ), + maybe_loadout.map_or_else( + || { + // Create if no record was found + let default_loadout = LoadoutBuilder::new() + .defaults() + .active_item(LoadoutBuilder::default_item_config_from_str( + character_data.tool.as_deref(), + )) + .build(); + + let row = NewLoadout::from((character_data.id, &default_loadout)); + + if let Err(error) = diesel::insert_into(schema::loadout::table) + .values(&row) + .execute(&connection) + { + log::warn!( + "Failed to create an loadout record for character {}: {}", + &character_data.id, + error + ) + } + + default_loadout + }, + |data| comp::Loadout::from(&data), + ), )) } @@ -71,24 +102,37 @@ pub fn load_character_data( /// 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 { - let data: Vec<(Character, Body, Stats)> = schema::character::dsl::character + let data = schema::character::dsl::character .filter(schema::character::player_uuid.eq(player_uuid)) .order(schema::character::id.desc()) .inner_join(schema::body::table) .inner_join(schema::stats::table) - .load::<(Character, Body, Stats)>(&establish_connection(db_dir))?; + .left_join(schema::loadout::table) + .load::<(Character, Body, Stats, Option)>(&establish_connection(db_dir))?; Ok(data .iter() - .map(|(character_data, body_data, stats_data)| { + .map(|(character_data, body_data, stats_data, maybe_loadout)| { let character = CharacterData::from(character_data); let body = comp::Body::from(body_data); let level = stats_data.level as usize; + let loadout = maybe_loadout.as_ref().map_or_else( + || { + LoadoutBuilder::new() + .defaults() + .active_item(LoadoutBuilder::default_item_config_from_str( + character.tool.as_deref(), + )) + .build() + }, + |data| comp::Loadout::from(data), + ); CharacterItem { character, body, level, + loadout, } }) .collect()) @@ -112,7 +156,7 @@ pub fn create_character( let connection = establish_connection(db_dir); connection.transaction::<_, diesel::result::Error, _>(|| { - use schema::{body, character, character::dsl::*, inventory, stats}; + use schema::{body, character, character::dsl::*, inventory, loadout, stats}; match body { comp::Body::Humanoid(body_data) => { @@ -171,6 +215,20 @@ pub fn create_character( diesel::insert_into(inventory::table) .values(&inventory) .execute(&connection)?; + + // Insert a loadout with defaults and the chosen active weapon + let loadout = LoadoutBuilder::new() + .defaults() + .active_item(LoadoutBuilder::default_item_config_from_str( + character_tool.as_deref(), + )) + .build(); + + let new_loadout = NewLoadout::from((inserted_character.id, &loadout)); + + diesel::insert_into(loadout::table) + .values(&new_loadout) + .execute(&connection)?; }, _ => log::warn!("Creating non-humanoid characters is not supported."), }; @@ -216,7 +274,7 @@ fn check_character_limit(uuid: &str, db_dir: &str) -> Result<(), Error> { } } -pub type CharacterUpdateData = (StatsUpdate, InventoryUpdate); +pub type CharacterUpdateData = (StatsUpdate, InventoryUpdate, LoadoutUpdate); pub struct CharacterUpdater { update_tx: Option>>, @@ -240,13 +298,17 @@ impl CharacterUpdater { pub fn batch_update<'a>( &self, - updates: impl Iterator, + updates: impl Iterator, ) { let updates = updates - .map(|(id, stats, inventory)| { + .map(|(id, stats, inventory, loadout)| { ( id, - (StatsUpdate::from(stats), InventoryUpdate::from(inventory)), + ( + StatsUpdate::from(stats), + InventoryUpdate::from(inventory), + LoadoutUpdate::from((id, loadout)), + ), ) }) .collect(); @@ -256,8 +318,14 @@ impl CharacterUpdater { } } - pub fn update(&self, character_id: i32, stats: &comp::Stats, inventory: &comp::Inventory) { - self.batch_update(std::iter::once((character_id, stats, inventory))); + pub fn update( + &self, + character_id: i32, + stats: &comp::Stats, + inventory: &comp::Inventory, + loadout: &comp::Loadout, + ) { + self.batch_update(std::iter::once((character_id, stats, inventory, loadout))); } } @@ -265,9 +333,17 @@ fn batch_update(updates: impl Iterator, db_di let connection = establish_connection(db_dir); if let Err(err) = connection.transaction::<_, diesel::result::Error, _>(|| { - updates.for_each(|(character_id, (stats_update, inventory_update))| { - update(character_id, &stats_update, &inventory_update, &connection) - }); + updates.for_each( + |(character_id, (stats_update, inventory_update, loadout_update))| { + update( + character_id, + &stats_update, + &inventory_update, + &loadout_update, + &connection, + ) + }, + ); Ok(()) }) { @@ -279,6 +355,7 @@ fn update( character_id: i32, stats: &StatsUpdate, inventory: &InventoryUpdate, + loadout: &LoadoutUpdate, connection: &SqliteConnection, ) { if let Err(error) = @@ -305,6 +382,19 @@ fn update( error ) } + + if let Err(error) = diesel::update( + schema::loadout::table.filter(schema::loadout::character_id.eq(character_id)), + ) + .set(loadout) + .execute(connection) + { + log::warn!( + "Failed to update loadout for character: {:?}: {:?}", + character_id, + error + ) + } } impl Drop for CharacterUpdater { diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index 0089d4c879..563d11d12c 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -1,8 +1,8 @@ extern crate serde_json; -use super::schema::{body, character, inventory, stats}; +use super::schema::{body, character, inventory, loadout, stats}; use crate::comp; -use common::character::Character as CharacterData; +use common::{character::Character as CharacterData, LoadoutBuilder}; use diesel::sql_types::Text; use serde::{Deserialize, Serialize}; @@ -41,7 +41,10 @@ impl From<&Character> for CharacterData { } } -/// `Body` represents the body variety for a character +/// `Body` represents the body variety for a character, which has a one-to-one +/// relationship with Characters. This data is set during player creation, and +/// while there is currently no in-game functionality to modify it, it will +/// likely be added in the future. #[derive(Associations, Identifiable, Queryable, Debug, Insertable)] #[belongs_to(Character)] #[primary_key(character_id)] @@ -75,7 +78,8 @@ impl From<&Body> for comp::Body { } } -/// `Stats` represents the stats for a character +/// `Stats` represents the stats for a character, which has a one-to-one +/// relationship with Characters. #[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)] #[belongs_to(Character)] #[primary_key(character_id)] @@ -136,6 +140,11 @@ 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. #[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)] #[belongs_to(Character)] #[primary_key(character_id)] @@ -145,6 +154,45 @@ pub struct Inventory { items: InventoryData, } +/// A wrapper type for Inventory components used to serialise to and from JSON +/// If the column contains malformed JSON, a default inventory is returned +#[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)] +#[sql_type = "Text"] +pub struct InventoryData(comp::Inventory); + +impl diesel::deserialize::FromSql for InventoryData +where + DB: diesel::backend::Backend, + String: diesel::deserialize::FromSql, +{ + fn from_sql( + bytes: Option<&::RawValue>, + ) -> diesel::deserialize::Result { + let t = String::from_sql(bytes)?; + + match serde_json::from_str(&t) { + Ok(data) => Ok(Self(data)), + Err(error) => { + log::warn!("Failed to deserialise inventory data: {}", error); + Ok(Self(comp::Inventory::default())) + }, + } + } +} + +impl diesel::serialize::ToSql for InventoryData +where + DB: diesel::backend::Backend, +{ + fn to_sql( + &self, + out: &mut diesel::serialize::Output, + ) -> diesel::serialize::Result { + let s = serde_json::to_string(&self.0)?; + >::to_sql(&s, out) + } +} + impl From<(i32, comp::Inventory)> for Inventory { fn from(data: (i32, comp::Inventory)) -> Inventory { let (character_id, inventory) = data; @@ -175,12 +223,30 @@ impl From<&comp::Inventory> for InventoryUpdate { } } -/// Type handling for a character's inventory, which is stored as JSON strings +/// Loadout holds the armor and weapons owned by a character. This data is +/// seperate from the inventory. At the moment, characters have a single Loadout +/// which is loaded with their character data, however there are plans for each +/// character to have multiple Loadouts which they can switch between during +/// gameplay. Due to this Loadouts have a many to one relationship with +/// characetrs, and a distinct `id`. +#[derive(Associations, Queryable, Debug, Identifiable)] +#[belongs_to(Character)] +#[primary_key(id)] +#[table_name = "loadout"] +pub struct Loadout { + pub id: i32, + pub character_id: i32, + pub items: LoadoutData, +} + +/// A wrapper type for Loadout components used to serialise to and from JSON +/// If the column contains malformed JSON, a default loadout is returned, with +/// the starter sword set as the main weapon #[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)] #[sql_type = "Text"] -pub struct InventoryData(comp::Inventory); +pub struct LoadoutData(comp::Loadout); -impl diesel::deserialize::FromSql for InventoryData +impl diesel::deserialize::FromSql for LoadoutData where DB: diesel::backend::Backend, String: diesel::deserialize::FromSql, @@ -193,15 +259,23 @@ where match serde_json::from_str(&t) { Ok(data) => Ok(Self(data)), Err(error) => { - log::warn!("Failed to deserialise inventory data: {}", error); + log::warn!("Failed to deserialise loadout data: {}", error); - Ok(Self(comp::Inventory::default())) + // We don't have a weapon reference here, so we default to sword + let loadout = LoadoutBuilder::new() + .defaults() + .active_item(LoadoutBuilder::default_item_config_from_str(Some( + "common.items.weapons.sword.starter_sword", + ))) + .build(); + + Ok(Self(loadout)) }, } } } -impl diesel::serialize::ToSql for InventoryData +impl diesel::serialize::ToSql for LoadoutData where DB: diesel::backend::Backend, { @@ -214,6 +288,46 @@ where } } +impl From<&Loadout> for comp::Loadout { + fn from(loadout: &Loadout) -> comp::Loadout { loadout.items.0.clone() } +} + +#[derive(Insertable, PartialEq, Debug)] +#[table_name = "loadout"] +pub struct NewLoadout { + pub character_id: i32, + pub items: LoadoutData, +} + +impl From<(i32, &comp::Loadout)> for NewLoadout { + fn from(data: (i32, &comp::Loadout)) -> NewLoadout { + let (character_id, loadout) = data; + + NewLoadout { + character_id, + items: LoadoutData(loadout.clone()), + } + } +} + +#[derive(Insertable, PartialEq, Debug, AsChangeset)] +#[table_name = "loadout"] +pub struct LoadoutUpdate { + pub character_id: i32, + pub items: LoadoutData, +} + +impl From<(i32, &comp::Loadout)> for LoadoutUpdate { + fn from(data: (i32, &comp::Loadout)) -> LoadoutUpdate { + let (character_id, loadout) = data; + + LoadoutUpdate { + character_id, + items: LoadoutData(loadout.clone()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/persistence/schema.rs b/server/src/persistence/schema.rs index 45f4003443..e3e3e9cbc4 100644 --- a/server/src/persistence/schema.rs +++ b/server/src/persistence/schema.rs @@ -29,6 +29,14 @@ table! { } } +table! { + loadout (id) { + id -> Integer, + character_id -> Integer, + items -> Text, + } +} + table! { stats (character_id) { character_id -> Integer, @@ -42,6 +50,7 @@ table! { joinable!(body -> character (character_id)); joinable!(inventory -> character (character_id)); +joinable!(loadout -> character (character_id)); joinable!(stats -> character (character_id)); -allow_tables_to_appear_in_same_query!(body, character, inventory, stats); +allow_tables_to_appear_in_same_query!(body, character, inventory, loadout, stats,); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 847685a8c6..c231aae6d3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -3,8 +3,7 @@ use crate::{ SpawnPoint, }; use common::{ - assets, - comp::{self, item}, + comp, effect::Effect, msg::{ CharacterInfo, ClientState, PlayerListUpdate, RegisterError, RequestStateError, ServerMsg, @@ -40,7 +39,6 @@ pub trait StateExt { entity: EcsEntity, character_id: i32, body: comp::Body, - main: Option, server_settings: &ServerSettings, ); fn notify_registered_clients(&self, msg: ServerMsg); @@ -159,7 +157,6 @@ impl StateExt for State { entity: EcsEntity, character_id: i32, body: comp::Body, - main: Option, server_settings: &ServerSettings, ) { // Grab persisted character data from the db and insert their associated @@ -169,9 +166,10 @@ impl StateExt for State { character_id, &server_settings.persistence_db_dir, ) { - Ok((stats, inventory)) => { + Ok((stats, inventory, loadout)) => { self.write_component(entity, stats); self.write_component(entity, inventory); + self.write_component(entity, loadout); }, Err(error) => { log::warn!( @@ -190,9 +188,6 @@ impl StateExt for State { }, } - // Give no item when an invalid specifier is given - let main = main.and_then(|specifier| assets::load_cloned::(&specifier).ok()); - let spawn_point = self.ecs().read_resource::().0; self.write_component(entity, body); @@ -214,47 +209,6 @@ impl StateExt for State { comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()), ); - self.write_component( - entity, - if let Some(item::ItemKind::Tool(tool)) = main.as_ref().map(|i| &i.kind) { - let mut abilities = tool.get_abilities(); - let mut ability_drain = abilities.drain(..); - comp::Loadout { - active_item: main.map(|item| comp::ItemConfig { - item, - ability1: ability_drain.next(), - ability2: ability_drain.next(), - ability3: ability_drain.next(), - block_ability: Some(comp::CharacterAbility::BasicBlock), - dodge_ability: Some(comp::CharacterAbility::Roll), - }), - second_item: None, - shoulder: None, - chest: Some(assets::load_expect_cloned( - "common.items.armor.starter.rugged_chest", - )), - belt: None, - hand: None, - pants: Some(assets::load_expect_cloned( - "common.items.armor.starter.rugged_pants", - )), - foot: Some(assets::load_expect_cloned( - "common.items.armor.starter.sandals_0", - )), - back: None, - ring: None, - neck: None, - lantern: Some(assets::load_expect_cloned( - "common.items.armor.starter.lantern", - )), - head: None, - tabard: None, - } - } else { - comp::Loadout::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" diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 1c639f61c1..f990979bf2 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -190,11 +190,7 @@ impl<'a> System<'a> for Sys { }, _ => {}, }, - ClientMsg::Character { - character_id, - body, - main, - } => match client.client_state { + ClientMsg::Character { character_id, body } => match client.client_state { // Become Registered first. ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Registered | ClientState::Spectator => { @@ -218,7 +214,6 @@ impl<'a> System<'a> for Sys { entity, character_id, body, - main, }); }, ClientState::Character => client.error_state(RequestStateError::Already), diff --git a/server/src/sys/persistence.rs b/server/src/sys/persistence.rs index b458d149be..a5d8137d66 100644 --- a/server/src/sys/persistence.rs +++ b/server/src/sys/persistence.rs @@ -2,7 +2,7 @@ use crate::{ persistence::character, sys::{SysScheduler, SysTimer}, }; -use common::comp::{Inventory, Player, Stats}; +use common::comp::{Inventory, Loadout, Player, Stats}; use specs::{Join, ReadExpect, ReadStorage, System, Write}; pub struct Sys; @@ -12,6 +12,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Player>, ReadStorage<'a, Stats>, ReadStorage<'a, Inventory>, + ReadStorage<'a, Loadout>, ReadExpect<'a, character::CharacterUpdater>, Write<'a, SysScheduler>, Write<'a, SysTimer>, @@ -19,15 +20,30 @@ impl<'a> System<'a> for Sys { fn run( &mut self, - (players, player_stats, player_inventories, updater, mut scheduler, mut timer): Self::SystemData, + ( + players, + player_stats, + player_inventories, + player_loadouts, + updater, + mut scheduler, + mut timer, + ): Self::SystemData, ) { if scheduler.should_run() { timer.start(); updater.batch_update( - (&players, &player_stats, &player_inventories) + ( + &players, + &player_stats, + &player_inventories, + &player_loadouts, + ) .join() - .filter_map(|(player, stats, inventory)| { - player.character_id.map(|id| (id, stats, inventory)) + .filter_map(|(player, stats, inventory, loadout)| { + player + .character_id + .map(|id| (id, stats, inventory, loadout)) }), ); timer.end(); diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index ef3cb924e6..983e244d86 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -88,11 +88,9 @@ 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, - selected_character.character.tool.clone(), - ); + self.client + .borrow_mut() + .request_character(character_id, selected_character.body); } } diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index fa02e93cd3..3e705bb163 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -12,9 +12,10 @@ use crate::{ use client::Client; use common::{ assets, - assets::{load, load_expect}, + assets::load_expect, character::{Character, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, comp::{self, humanoid}, + LoadoutBuilder, }; use conrod_core::{ color, @@ -25,7 +26,6 @@ use conrod_core::{ widget::{text_box::Event as TextBoxEvent, Button, Image, Rectangle, Scrollbar, Text, TextBox}, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, }; -use log::error; use std::sync::Arc; const STARTER_HAMMER: &str = "common.items.weapons.hammer.starter_hammer"; @@ -354,6 +354,10 @@ impl CharSelectionUi { }, body, level: 1, + loadout: LoadoutBuilder::new() + .defaults() + .active_item(LoadoutBuilder::default_item_config_from_str(*tool)) + .build(), }]) }, } @@ -363,50 +367,7 @@ impl CharSelectionUi { match &mut self.mode { Mode::Select(character_list) => { if let Some(data) = character_list { - if let Some(character_item) = data.get(self.selected_character) { - let loadout = comp::Loadout { - active_item: character_item.character.tool.as_ref().map(|tool| { - comp::ItemConfig { - item: (*load::(&tool).unwrap_or_else(|err| { - error!( - "Could not load item {} maybe it no longer exists: \ - {:?}", - &tool, err - ); - load_expect("common.items.weapons.sword.starter_sword") - })) - .clone(), - ability1: None, - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - } - }), - second_item: None, - shoulder: None, - chest: Some(assets::load_expect_cloned( - "common.items.armor.starter.rugged_chest", - )), - belt: None, - hand: None, - pants: Some(assets::load_expect_cloned( - "common.items.armor.starter.rugged_pants", - )), - foot: Some(assets::load_expect_cloned( - "common.items.armor.starter.sandals_0", - )), - back: None, - ring: None, - neck: None, - lantern: None, - head: None, - tabard: None, - }; - Some(loadout) - } else { - None - } + data.get(self.selected_character).map(|c| c.loadout.clone()) } else { None }