mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Update CHANGELOG and a TODO, fix safer deserialisation for inventory
data.
This commit is contained in:
parent
ceee05f757
commit
e0633a238e
@ -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
|
||||
|
||||
|
@ -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
|
||||
),
|
||||
},
|
||||
|
@ -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<String>) {
|
||||
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;
|
||||
}
|
||||
|
@ -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<i32>,
|
||||
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
|
||||
@ -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
|
||||
|
@ -95,7 +95,6 @@ pub enum ServerEvent {
|
||||
entity: EcsEntity,
|
||||
character_id: i32,
|
||||
body: comp::Body,
|
||||
main: Option<String>,
|
||||
},
|
||||
ExitIngame {
|
||||
entity: EcsEntity,
|
||||
|
@ -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
|
||||
|
178
common/src/loadout_builder.rs
Normal file
178
common/src/loadout_builder.rs
Normal 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 }
|
||||
}
|
@ -17,7 +17,6 @@ pub enum ClientMsg {
|
||||
Character {
|
||||
character_id: i32,
|
||||
body: comp::Body,
|
||||
main: Option<String>, // Specifier for the weapon
|
||||
},
|
||||
/// Request `ClientState::Registered` from an ingame state
|
||||
ExitIngame,
|
||||
|
@ -14,12 +14,11 @@ pub fn handle_create_character(
|
||||
entity: EcsEntity,
|
||||
character_id: i32,
|
||||
body: Body,
|
||||
main: Option<String>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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::<Player>().get(entity),
|
||||
state.read_storage::<comp::Stats>().get(entity),
|
||||
state.read_storage::<comp::Inventory>().get(entity),
|
||||
state.read_storage::<comp::Loadout>().get(entity),
|
||||
state
|
||||
.ecs()
|
||||
.read_resource::<persistence::character::CharacterUpdater>(),
|
||||
) {
|
||||
if let Some(character_id) = player.character_id {
|
||||
updater.update(character_id, stats, inventory);
|
||||
updater.update(character_id, stats, inventory, loadout);
|
||||
}
|
||||
}
|
||||
|
||||
|
1
server/src/migrations/2020-05-28-210610_loadout/down.sql
Normal file
1
server/src/migrations/2020-05-28-210610_loadout/down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "loadout";
|
6
server/src/migrations/2020-05-28-210610_loadout/up.sql
Normal file
6
server/src/migrations/2020-05-28-210610_loadout/up.sql
Normal 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
|
||||
);
|
@ -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<Vec<CharacterItem>, 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<Inventory>)>(&connection)?;
|
||||
.left_join(schema::loadout::table)
|
||||
.first::<(Character, Body, Stats, Option<Inventory>, Option<Loadout>)>(&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<Loadout>)>(&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<channel::Sender<Vec<(i32, CharacterUpdateData)>>>,
|
||||
@ -240,13 +298,17 @@ impl CharacterUpdater {
|
||||
|
||||
pub fn batch_update<'a>(
|
||||
&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
|
||||
.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<Item = (i32, CharacterUpdateData)>, 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 {
|
||||
|
@ -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<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 {
|
||||
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<DB> diesel::deserialize::FromSql<Text, DB> for InventoryData
|
||||
impl<DB> diesel::deserialize::FromSql<Text, DB> for LoadoutData
|
||||
where
|
||||
DB: diesel::backend::Backend,
|
||||
String: diesel::deserialize::FromSql<Text, DB>,
|
||||
@ -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<DB> diesel::serialize::ToSql<Text, DB> for InventoryData
|
||||
impl<DB> diesel::serialize::ToSql<Text, DB> 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::*;
|
||||
|
@ -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,);
|
||||
|
@ -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<String>,
|
||||
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<String>,
|
||||
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::<comp::Item>(&specifier).ok());
|
||||
|
||||
let spawn_point = self.ecs().read_resource::<SpawnPoint>().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"
|
||||
|
@ -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),
|
||||
|
@ -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<Self>>,
|
||||
Write<'a, SysTimer<Self>>,
|
||||
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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::<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
|
||||
}
|
||||
data.get(self.selected_character).map(|c| c.loadout.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user