Update CHANGELOG and a TODO, fix safer deserialisation for inventory

data.
This commit is contained in:
S Handley 2020-06-04 11:44:33 +00:00 committed by Monty Marz
parent ceee05f757
commit e0633a238e
21 changed files with 488 additions and 159 deletions

View File

@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- NPCs call for help when attacked - NPCs call for help when attacked
- Eyebrows and shapes can now be selected - Eyebrows and shapes can now be selected
- Character name and level information to chat, social tab and `/players` command. - Character name and level information to chat, social tab and `/players` command.
- Added inventory saving - Added inventory, armour and weapon saving
### Changed ### Changed

View File

@ -45,11 +45,11 @@
color: None color: None
), ),
Steel0:( 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 color: None
), ),
Leather2:( 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 color: None
), ),
}, },

View File

@ -237,12 +237,9 @@ impl Client {
} }
/// Request a state transition to `ClientState::Character`. /// Request a state transition to `ClientState::Character`.
pub fn request_character(&mut self, character_id: i32, body: comp::Body, main: Option<String>) { pub fn request_character(&mut self, character_id: i32, body: comp::Body) {
self.postbox.send_message(ClientMsg::Character { self.postbox
character_id, .send_message(ClientMsg::Character { character_id, body });
body,
main,
});
self.client_state = ClientState::Pending; self.client_state = ClientState::Pending;
} }

View File

@ -4,11 +4,20 @@ use serde_derive::{Deserialize, Serialize};
/// The limit on how many characters that a player can have /// The limit on how many characters that a player can have
pub const MAX_CHARACTERS_PER_PLAYER: usize = 8; 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Character { pub struct Character {
pub id: Option<i32>, pub id: Option<i32>,
pub alias: String, pub alias: String,
pub tool: Option<String>, // TODO: Remove once we start persisting inventories pub tool: Option<String>,
} }
/// Represents a single character item in the character list presented during /// Represents a single character item in the character list presented during
@ -19,6 +28,7 @@ pub struct CharacterItem {
pub character: Character, pub character: Character,
pub body: comp::Body, pub body: comp::Body,
pub level: usize, pub level: usize,
pub loadout: comp::Loadout,
} }
/// The full representation of the data we store in the database for each /// The full representation of the data we store in the database for each

View File

@ -95,7 +95,6 @@ pub enum ServerEvent {
entity: EcsEntity, entity: EcsEntity,
character_id: i32, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>,
}, },
ExitIngame { ExitIngame {
entity: EcsEntity, entity: EcsEntity,

View File

@ -22,6 +22,7 @@ pub mod effect;
pub mod event; pub mod event;
pub mod figure; pub mod figure;
pub mod generation; pub mod generation;
pub mod loadout_builder;
pub mod msg; pub mod msg;
pub mod npc; pub mod npc;
pub mod path; pub mod path;
@ -38,6 +39,8 @@ pub mod util;
pub mod vol; pub mod vol;
pub mod volumes; pub mod volumes;
pub use loadout_builder::LoadoutBuilder;
/// The networking module containing high-level wrappers of `TcpListener` and /// The networking module containing high-level wrappers of `TcpListener` and
/// `TcpStream` (`PostOffice` and `PostBox` respectively) and data types used by /// `TcpStream` (`PostOffice` and `PostBox` respectively) and data types used by
/// both the server and client. # Examples /// both the server and client. # Examples

View File

@ -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<Item>) -> Option<ItemConfig> {
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> {
item_ref.and_then(|specifier| assets::load_cloned::<Item>(&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<ItemConfig> {
Self::default_item_config_from_item(Self::item_from_str(item_ref))
}
pub fn active_item(mut self, item: Option<ItemConfig>) -> Self {
self.0.active_item = item;
self
}
pub fn second_item(mut self, item: Option<ItemConfig>) -> Self {
self.0.active_item = item;
self
}
pub fn shoulder(mut self, item: Option<Item>) -> Self {
self.0.shoulder = item;
self
}
pub fn chest(mut self, item: Option<Item>) -> Self {
self.0.chest = item;
self
}
pub fn belt(mut self, item: Option<Item>) -> Self {
self.0.belt = item;
self
}
pub fn hand(mut self, item: Option<Item>) -> Self {
self.0.hand = item;
self
}
pub fn pants(mut self, item: Option<Item>) -> Self {
self.0.pants = item;
self
}
pub fn foot(mut self, item: Option<Item>) -> Self {
self.0.foot = item;
self
}
pub fn back(mut self, item: Option<Item>) -> Self {
self.0.back = item;
self
}
pub fn ring(mut self, item: Option<Item>) -> Self {
self.0.ring = item;
self
}
pub fn neck(mut self, item: Option<Item>) -> Self {
self.0.neck = item;
self
}
pub fn lantern(mut self, item: Option<Item>) -> Self {
self.0.lantern = item;
self
}
pub fn head(mut self, item: Option<Item>) -> Self {
self.0.head = item;
self
}
pub fn tabard(mut self, item: Option<Item>) -> Self {
self.0.tabard = item;
self
}
pub fn build(self) -> Loadout { self.0 }
}

View File

@ -17,7 +17,6 @@ pub enum ClientMsg {
Character { Character {
character_id: i32, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>, // Specifier for the weapon
}, },
/// Request `ClientState::Registered` from an ingame state /// Request `ClientState::Registered` from an ingame state
ExitIngame, ExitIngame,

View File

@ -14,12 +14,11 @@ pub fn handle_create_character(
entity: EcsEntity, entity: EcsEntity,
character_id: i32, character_id: i32,
body: Body, body: Body,
main: Option<String>,
) { ) {
let state = &mut server.state; let state = &mut server.state;
let server_settings = &server.server_settings; let server_settings = &server.server_settings;
state.create_player_character(entity, character_id, body, main, server_settings); state.create_player_character(entity, character_id, body, server_settings);
sys::subscription::initialize_region_subscription(state.ecs(), entity); sys::subscription::initialize_region_subscription(state.ecs(), entity);
} }

View File

@ -74,8 +74,7 @@ impl Server {
entity, entity,
character_id, character_id,
body, body,
main, } => handle_create_character(self, entity, character_id, body),
} => handle_create_character(self, entity, character_id, body, main),
ServerEvent::LevelUp(entity, new_level) => handle_level_up(self, entity, new_level), ServerEvent::LevelUp(entity, new_level) => handle_level_up(self, entity, new_level),
ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity), ServerEvent::ExitIngame { entity } => handle_exit_ingame(self, entity),
ServerEvent::CreateNpc { ServerEvent::CreateNpc {

View File

@ -72,16 +72,17 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event
} }
// Sync the player's character data to the database // 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::<Player>().get(entity), state.read_storage::<Player>().get(entity),
state.read_storage::<comp::Stats>().get(entity), state.read_storage::<comp::Stats>().get(entity),
state.read_storage::<comp::Inventory>().get(entity), state.read_storage::<comp::Inventory>().get(entity),
state.read_storage::<comp::Loadout>().get(entity),
state state
.ecs() .ecs()
.read_resource::<persistence::character::CharacterUpdater>(), .read_resource::<persistence::character::CharacterUpdater>(),
) { ) {
if let Some(character_id) = player.character_id { if let Some(character_id) = player.character_id {
updater.update(character_id, stats, inventory); updater.update(character_id, stats, inventory, loadout);
} }
} }

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS "loadout";

View File

@ -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
);

View File

@ -4,13 +4,16 @@ use super::{
error::Error, error::Error,
establish_connection, establish_connection,
models::{ models::{
Body, Character, Inventory, InventoryUpdate, NewCharacter, Stats, StatsJoinData, Body, Character, Inventory, InventoryUpdate, Loadout, LoadoutUpdate, NewCharacter,
StatsUpdate, NewLoadout, Stats, StatsJoinData, StatsUpdate,
}, },
schema, schema,
}; };
use crate::comp; 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 crossbeam::channel;
use diesel::prelude::*; use diesel::prelude::*;
@ -23,16 +26,17 @@ type CharacterListResult = Result<Vec<CharacterItem>, Error>;
pub fn load_character_data( pub fn load_character_data(
character_id: i32, character_id: i32,
db_dir: &str, db_dir: &str,
) -> Result<(comp::Stats, comp::Inventory), Error> { ) -> Result<(comp::Stats, comp::Inventory, comp::Loadout), Error> {
let connection = establish_connection(db_dir); 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 schema::character::dsl::character
.filter(schema::character::id.eq(character_id)) .filter(schema::character::id.eq(character_id))
.inner_join(schema::body::table) .inner_join(schema::body::table)
.inner_join(schema::stats::table) .inner_join(schema::stats::table)
.left_join(schema::inventory::table) .left_join(schema::inventory::table)
.first::<(Character, Body, Stats, Option<Inventory>)>(&connection)?; .left_join(schema::loadout::table)
.first::<(Character, Body, Stats, Option<Inventory>, Option<Loadout>)>(&connection)?;
Ok(( Ok((
comp::Stats::from(StatsJoinData { comp::Stats::from(StatsJoinData {
@ -60,6 +64,33 @@ pub fn load_character_data(
}, },
|inv| comp::Inventory::from(inv), |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 /// stats, body, etc...) the character is skipped, and no entry will be
/// returned. /// returned.
pub fn load_character_list(player_uuid: &str, db_dir: &str) -> CharacterListResult { 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)) .filter(schema::character::player_uuid.eq(player_uuid))
.order(schema::character::id.desc()) .order(schema::character::id.desc())
.inner_join(schema::body::table) .inner_join(schema::body::table)
.inner_join(schema::stats::table) .inner_join(schema::stats::table)
.load::<(Character, Body, Stats)>(&establish_connection(db_dir))?; .left_join(schema::loadout::table)
.load::<(Character, Body, Stats, Option<Loadout>)>(&establish_connection(db_dir))?;
Ok(data Ok(data
.iter() .iter()
.map(|(character_data, body_data, stats_data)| { .map(|(character_data, body_data, stats_data, maybe_loadout)| {
let character = CharacterData::from(character_data); let character = CharacterData::from(character_data);
let body = comp::Body::from(body_data); let body = comp::Body::from(body_data);
let level = stats_data.level as usize; 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 { CharacterItem {
character, character,
body, body,
level, level,
loadout,
} }
}) })
.collect()) .collect())
@ -112,7 +156,7 @@ pub fn create_character(
let connection = establish_connection(db_dir); let connection = establish_connection(db_dir);
connection.transaction::<_, diesel::result::Error, _>(|| { connection.transaction::<_, diesel::result::Error, _>(|| {
use schema::{body, character, character::dsl::*, inventory, stats}; use schema::{body, character, character::dsl::*, inventory, loadout, stats};
match body { match body {
comp::Body::Humanoid(body_data) => { comp::Body::Humanoid(body_data) => {
@ -171,6 +215,20 @@ pub fn create_character(
diesel::insert_into(inventory::table) diesel::insert_into(inventory::table)
.values(&inventory) .values(&inventory)
.execute(&connection)?; .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."), _ => 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 { pub struct CharacterUpdater {
update_tx: Option<channel::Sender<Vec<(i32, CharacterUpdateData)>>>, update_tx: Option<channel::Sender<Vec<(i32, CharacterUpdateData)>>>,
@ -240,13 +298,17 @@ impl CharacterUpdater {
pub fn batch_update<'a>( pub fn batch_update<'a>(
&self, &self,
updates: impl Iterator<Item = (i32, &'a comp::Stats, &'a comp::Inventory)>, updates: impl Iterator<Item = (i32, &'a comp::Stats, &'a comp::Inventory, &'a comp::Loadout)>,
) { ) {
let updates = updates let updates = updates
.map(|(id, stats, inventory)| { .map(|(id, stats, inventory, loadout)| {
( (
id, id,
(StatsUpdate::from(stats), InventoryUpdate::from(inventory)), (
StatsUpdate::from(stats),
InventoryUpdate::from(inventory),
LoadoutUpdate::from((id, loadout)),
),
) )
}) })
.collect(); .collect();
@ -256,8 +318,14 @@ impl CharacterUpdater {
} }
} }
pub fn update(&self, character_id: i32, stats: &comp::Stats, inventory: &comp::Inventory) { pub fn update(
self.batch_update(std::iter::once((character_id, stats, inventory))); &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<Item = (i32, CharacterUpdateData)>, db_di
let connection = establish_connection(db_dir); let connection = establish_connection(db_dir);
if let Err(err) = connection.transaction::<_, diesel::result::Error, _>(|| { if let Err(err) = connection.transaction::<_, diesel::result::Error, _>(|| {
updates.for_each(|(character_id, (stats_update, inventory_update))| { updates.for_each(
update(character_id, &stats_update, &inventory_update, &connection) |(character_id, (stats_update, inventory_update, loadout_update))| {
}); update(
character_id,
&stats_update,
&inventory_update,
&loadout_update,
&connection,
)
},
);
Ok(()) Ok(())
}) { }) {
@ -279,6 +355,7 @@ fn update(
character_id: i32, character_id: i32,
stats: &StatsUpdate, stats: &StatsUpdate,
inventory: &InventoryUpdate, inventory: &InventoryUpdate,
loadout: &LoadoutUpdate,
connection: &SqliteConnection, connection: &SqliteConnection,
) { ) {
if let Err(error) = if let Err(error) =
@ -305,6 +382,19 @@ fn update(
error 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 { impl Drop for CharacterUpdater {

View File

@ -1,8 +1,8 @@
extern crate serde_json; extern crate serde_json;
use super::schema::{body, character, inventory, stats}; use super::schema::{body, character, inventory, loadout, stats};
use crate::comp; use crate::comp;
use common::character::Character as CharacterData; use common::{character::Character as CharacterData, LoadoutBuilder};
use diesel::sql_types::Text; use diesel::sql_types::Text;
use serde::{Deserialize, Serialize}; 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)] #[derive(Associations, Identifiable, Queryable, Debug, Insertable)]
#[belongs_to(Character)] #[belongs_to(Character)]
#[primary_key(character_id)] #[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)] #[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)]
#[belongs_to(Character)] #[belongs_to(Character)]
#[primary_key(character_id)] #[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)] #[derive(Associations, AsChangeset, Identifiable, Queryable, Debug, Insertable)]
#[belongs_to(Character)] #[belongs_to(Character)]
#[primary_key(character_id)] #[primary_key(character_id)]
@ -145,6 +154,45 @@ pub struct Inventory {
items: InventoryData, 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<DB> diesel::deserialize::FromSql<Text, DB> for InventoryData
where
DB: diesel::backend::Backend,
String: diesel::deserialize::FromSql<Text, DB>,
{
fn from_sql(
bytes: Option<&<DB as diesel::backend::Backend>::RawValue>,
) -> diesel::deserialize::Result<Self> {
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<DB> diesel::serialize::ToSql<Text, DB> for InventoryData
where
DB: diesel::backend::Backend,
{
fn to_sql<W: std::io::Write>(
&self,
out: &mut diesel::serialize::Output<W, DB>,
) -> diesel::serialize::Result {
let s = serde_json::to_string(&self.0)?;
<String as diesel::serialize::ToSql<Text, DB>>::to_sql(&s, out)
}
}
impl From<(i32, comp::Inventory)> for Inventory { impl From<(i32, comp::Inventory)> for Inventory {
fn from(data: (i32, comp::Inventory)) -> Inventory { fn from(data: (i32, comp::Inventory)) -> Inventory {
let (character_id, inventory) = data; 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)] #[derive(SqlType, AsExpression, Debug, Deserialize, Serialize, FromSqlRow, PartialEq)]
#[sql_type = "Text"] #[sql_type = "Text"]
pub struct InventoryData(comp::Inventory); pub struct LoadoutData(comp::Loadout);
impl<DB> diesel::deserialize::FromSql<Text, DB> for InventoryData impl<DB> diesel::deserialize::FromSql<Text, DB> for LoadoutData
where where
DB: diesel::backend::Backend, DB: diesel::backend::Backend,
String: diesel::deserialize::FromSql<Text, DB>, String: diesel::deserialize::FromSql<Text, DB>,
@ -193,15 +259,23 @@ where
match serde_json::from_str(&t) { match serde_json::from_str(&t) {
Ok(data) => Ok(Self(data)), Ok(data) => Ok(Self(data)),
Err(error) => { 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<DB> diesel::serialize::ToSql<Text, DB> for InventoryData impl<DB> diesel::serialize::ToSql<Text, DB> for LoadoutData
where where
DB: diesel::backend::Backend, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -29,6 +29,14 @@ table! {
} }
} }
table! {
loadout (id) {
id -> Integer,
character_id -> Integer,
items -> Text,
}
}
table! { table! {
stats (character_id) { stats (character_id) {
character_id -> Integer, character_id -> Integer,
@ -42,6 +50,7 @@ table! {
joinable!(body -> character (character_id)); joinable!(body -> character (character_id));
joinable!(inventory -> character (character_id)); joinable!(inventory -> character (character_id));
joinable!(loadout -> character (character_id));
joinable!(stats -> 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,);

View File

@ -3,8 +3,7 @@ use crate::{
SpawnPoint, SpawnPoint,
}; };
use common::{ use common::{
assets, comp,
comp::{self, item},
effect::Effect, effect::Effect,
msg::{ msg::{
CharacterInfo, ClientState, PlayerListUpdate, RegisterError, RequestStateError, ServerMsg, CharacterInfo, ClientState, PlayerListUpdate, RegisterError, RequestStateError, ServerMsg,
@ -40,7 +39,6 @@ pub trait StateExt {
entity: EcsEntity, entity: EcsEntity,
character_id: i32, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>,
server_settings: &ServerSettings, server_settings: &ServerSettings,
); );
fn notify_registered_clients(&self, msg: ServerMsg); fn notify_registered_clients(&self, msg: ServerMsg);
@ -159,7 +157,6 @@ impl StateExt for State {
entity: EcsEntity, entity: EcsEntity,
character_id: i32, character_id: i32,
body: comp::Body, body: comp::Body,
main: Option<String>,
server_settings: &ServerSettings, server_settings: &ServerSettings,
) { ) {
// Grab persisted character data from the db and insert their associated // Grab persisted character data from the db and insert their associated
@ -169,9 +166,10 @@ impl StateExt for State {
character_id, character_id,
&server_settings.persistence_db_dir, &server_settings.persistence_db_dir,
) { ) {
Ok((stats, inventory)) => { Ok((stats, inventory, loadout)) => {
self.write_component(entity, stats); self.write_component(entity, stats);
self.write_component(entity, inventory); self.write_component(entity, inventory);
self.write_component(entity, loadout);
}, },
Err(error) => { Err(error) => {
log::warn!( 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::<comp::Item>(&specifier).ok());
let spawn_point = self.ecs().read_resource::<SpawnPoint>().0; let spawn_point = self.ecs().read_resource::<SpawnPoint>().0;
self.write_component(entity, body); self.write_component(entity, body);
@ -214,47 +209,6 @@ impl StateExt for State {
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::default()), 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 // Set the character id for the player
// TODO this results in a warning in the console: "Error modifying synced // TODO this results in a warning in the console: "Error modifying synced
// component, it doesn't seem to exist" // component, it doesn't seem to exist"

View File

@ -190,11 +190,7 @@ impl<'a> System<'a> for Sys {
}, },
_ => {}, _ => {},
}, },
ClientMsg::Character { ClientMsg::Character { character_id, body } => match client.client_state {
character_id,
body,
main,
} => match client.client_state {
// Become Registered first. // Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator => { ClientState::Registered | ClientState::Spectator => {
@ -218,7 +214,6 @@ impl<'a> System<'a> for Sys {
entity, entity,
character_id, character_id,
body, body,
main,
}); });
}, },
ClientState::Character => client.error_state(RequestStateError::Already), ClientState::Character => client.error_state(RequestStateError::Already),

View File

@ -2,7 +2,7 @@ use crate::{
persistence::character, persistence::character,
sys::{SysScheduler, SysTimer}, sys::{SysScheduler, SysTimer},
}; };
use common::comp::{Inventory, Player, Stats}; use common::comp::{Inventory, Loadout, Player, Stats};
use specs::{Join, ReadExpect, ReadStorage, System, Write}; use specs::{Join, ReadExpect, ReadStorage, System, Write};
pub struct Sys; pub struct Sys;
@ -12,6 +12,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Player>, ReadStorage<'a, Player>,
ReadStorage<'a, Stats>, ReadStorage<'a, Stats>,
ReadStorage<'a, Inventory>, ReadStorage<'a, Inventory>,
ReadStorage<'a, Loadout>,
ReadExpect<'a, character::CharacterUpdater>, ReadExpect<'a, character::CharacterUpdater>,
Write<'a, SysScheduler<Self>>, Write<'a, SysScheduler<Self>>,
Write<'a, SysTimer<Self>>, Write<'a, SysTimer<Self>>,
@ -19,15 +20,30 @@ impl<'a> System<'a> for Sys {
fn run( fn run(
&mut self, &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() { if scheduler.should_run() {
timer.start(); timer.start();
updater.batch_update( updater.batch_update(
(&players, &player_stats, &player_inventories) (
&players,
&player_stats,
&player_inventories,
&player_loadouts,
)
.join() .join()
.filter_map(|(player, stats, inventory)| { .filter_map(|(player, stats, inventory, loadout)| {
player.character_id.map(|id| (id, stats, inventory)) player
.character_id
.map(|id| (id, stats, inventory, loadout))
}), }),
); );
timer.end(); timer.end();

View File

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

View File

@ -12,9 +12,10 @@ use crate::{
use client::Client; use client::Client;
use common::{ use common::{
assets, assets,
assets::{load, load_expect}, assets::load_expect,
character::{Character, CharacterItem, MAX_CHARACTERS_PER_PLAYER}, character::{Character, CharacterItem, MAX_CHARACTERS_PER_PLAYER},
comp::{self, humanoid}, comp::{self, humanoid},
LoadoutBuilder,
}; };
use conrod_core::{ use conrod_core::{
color, color,
@ -25,7 +26,6 @@ use conrod_core::{
widget::{text_box::Event as TextBoxEvent, Button, Image, Rectangle, Scrollbar, Text, TextBox}, widget::{text_box::Event as TextBoxEvent, Button, Image, Rectangle, Scrollbar, Text, TextBox},
widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget,
}; };
use log::error;
use std::sync::Arc; use std::sync::Arc;
const STARTER_HAMMER: &str = "common.items.weapons.hammer.starter_hammer"; const STARTER_HAMMER: &str = "common.items.weapons.hammer.starter_hammer";
@ -354,6 +354,10 @@ impl CharSelectionUi {
}, },
body, body,
level: 1, 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 { match &mut self.mode {
Mode::Select(character_list) => { Mode::Select(character_list) => {
if let Some(data) = character_list { if let Some(data) = character_list {
if let Some(character_item) = data.get(self.selected_character) { data.get(self.selected_character).map(|c| c.loadout.clone())
let loadout = comp::Loadout {
active_item: character_item.character.tool.as_ref().map(|tool| {
comp::ItemConfig {
item: (*load::<comp::Item>(&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
}
} else { } else {
None None
} }