From 3ac18f73eab75442c4ddd5d892c18ffcc8cd24ec Mon Sep 17 00:00:00 2001 From: Ben Wallis <atomyc@gmail.com> Date: Mon, 6 Jul 2020 22:06:03 +0100 Subject: [PATCH 1/3] Removed de-serialization failure tolerance to prevent player inventory/loadout wipes --- server/src/persistence/character.rs | 90 +++++++++-------------------- server/src/persistence/models.rs | 29 +--------- 2 files changed, 29 insertions(+), 90 deletions(-) diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index 92d506b219..13b9f30d95 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -236,71 +236,33 @@ impl Drop for CharacterLoader { fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> CharacterDataResult { let connection = establish_connection(db_dir); - let (character_data, body_data, stats_data, maybe_inventory, maybe_loadout) = - schema::character::dsl::character - .filter(schema::character::id.eq(character_id)) - .filter(schema::character::player_uuid.eq(player_uuid)) - .inner_join(schema::body::table) - .inner_join(schema::stats::table) - .left_join(schema::inventory::table) - .left_join(schema::loadout::table) - .first::<(Character, Body, Stats, Option<Inventory>, Option<Loadout>)>(&connection)?; + let result = schema::character::dsl::character + .filter(schema::character::id.eq(character_id)) + .filter(schema::character::player_uuid.eq(player_uuid)) + .inner_join(schema::body::table) + .inner_join(schema::stats::table) + .inner_join(schema::inventory::table) + .inner_join(schema::loadout::table) + .first::<(Character, Body, Stats, Inventory, Loadout)>(&connection); - Ok(( - comp::Body::from(&body_data), - comp::Stats::from(StatsJoinData { - alias: &character_data.alias, - body: &comp::Body::from(&body_data), - stats: &stats_data, - }), - maybe_inventory.map_or_else( - || { - // If no inventory record was found for the character, create it now - let row = Inventory::from((character_data.id, comp::Inventory::default())); - - if let Err(error) = diesel::insert_into(schema::inventory::table) - .values(&row) - .execute(&connection) - { - warn!( - "Failed to create an inventory record for character {}: {}", - &character_data.id, error - ) - } - - comp::Inventory::default() - }, - comp::Inventory::from, - ), - 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(e) = diesel::insert_into(schema::loadout::table) - .values(&row) - .execute(&connection) - { - let char_id = character_data.id; - warn!( - ?e, - ?char_id, - "Failed to create an loadout record for character", - ) - } - - default_loadout - }, - |data| comp::Loadout::from(&data), - ), - )) + if let Ok((character_data, body_data, stats_data, inventory, loadout)) = result { + Ok(( + comp::Body::from(&body_data), + comp::Stats::from(StatsJoinData { + alias: &character_data.alias, + body: &comp::Body::from(&body_data), + stats: &stats_data, + }), + comp::Inventory::from(inventory), + comp::Loadout::from(&loadout), + )) + } else { + error!( + ?result, + "Failed to load character data for character id {}", character_id + ); + Err(Error::CharacterDataError) + } } /// Loads a list of characters belonging to the player. This data is a small diff --git a/server/src/persistence/models.rs b/server/src/persistence/models.rs index d5267c24a2..1d7e3e7043 100644 --- a/server/src/persistence/models.rs +++ b/server/src/persistence/models.rs @@ -2,7 +2,7 @@ extern crate serde_json; use super::schema::{body, character, inventory, loadout, stats}; use crate::comp; -use common::{character::Character as CharacterData, LoadoutBuilder}; +use common::character::Character as CharacterData; use diesel::sql_types::Text; use serde::{Deserialize, Serialize}; use tracing::warn; @@ -212,14 +212,7 @@ where 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(e) => { - warn!(?e, "Failed to deserialise inventory data"); - Ok(Self(comp::Inventory::default())) - }, - } + serde_json::from_str(&t).map_err(Box::from) } } @@ -298,23 +291,7 @@ where 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(e) => { - warn!(?e, "Failed to deserialise loadout data"); - - // 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)) - }, - } + serde_json::from_str(&t).map_err(Box::from) } } From e83d0abd336d4f7b32ef429f06f41977f719cfdf Mon Sep 17 00:00:00 2001 From: Ben Wallis <atomyc@gmail.com> Date: Tue, 7 Jul 2020 21:29:16 +0100 Subject: [PATCH 2/3] Added migration to create a default inventory and loadout for characters without one --- .../down.sql | 1 + .../up.sql | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/down.sql create mode 100644 server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/up.sql diff --git a/server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/down.sql b/server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/down.sql new file mode 100644 index 0000000000..7a6b1eb34b --- /dev/null +++ b/server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/down.sql @@ -0,0 +1 @@ +-- Nothing to undo since up.sql only creates missing inventory/loadout records \ No newline at end of file diff --git a/server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/up.sql b/server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/up.sql new file mode 100644 index 0000000000..ade82ad00e --- /dev/null +++ b/server/src/migrations/2020-07-07-201052_add_missing_inv_loadout/up.sql @@ -0,0 +1,15 @@ +-- This migration was added in conjunction with a change that no longer creates missing inventory/loadout records +-- for characters that don't have one, to prevent such characters from being unable to log in. + +-- Create a default loadout for any characters that don't have one +INSERT INTO loadout (character_id, items) +SELECT c."id", "{""active_item"":{""item"":{""name"":""Battered Sword"",""description"":""Two-Hand Sword\n\nPower: 2-10\n\nHeld together by Rust and hope.\n\n<Right-Click to use>"",""kind"":{""Tool"":{""kind"":{""Sword"":""BasicSword""},""equip_time_millis"":300}}},""ability1"":{""TripleStrike"":{""base_damage"":5,""needs_timing"":false}},""ability2"":{""DashMelee"":{""energy_cost"":700,""buildup_duration"":{""secs"":0,""nanos"":500000000},""recover_duration"":{""secs"":0,""nanos"":500000000},""base_damage"":10}},""ability3"":null,""block_ability"":""BasicBlock"",""dodge_ability"":""Roll""},""second_item"":null,""shoulder"":null,""chest"":{""name"":""Rugged Shirt"",""description"":""Chest\n\nArmor: 0\n\nSmells like Adventure.\n\n<Right-Click to use>"",""kind"":{""Armor"":{""kind"":{""Chest"":""Rugged0""},""stats"":20}}},""belt"":null,""hand"":null,""pants"":{""name"":""Rugged Commoner's Pants"",""description"":""Legs\n\nArmor: 0\n\nThey remind you of the old days.\n\n<Right-Click to use>"",""kind"":{""Armor"":{""kind"":{""Pants"":""Rugged0""},""stats"":20}}},""foot"":{""name"":""Worn out Sandals"",""description"":""Feet\n\nArmor: 0\n\nLoyal companions.\n\n<Right-Click to use>"",""kind"":{""Armor"":{""kind"":{""Foot"":""Sandal0""},""stats"":20}}},""back"":null,""ring"":null,""neck"":null,""lantern"":{""name"":""Black Lantern"",""description"":""Used by city guards."",""kind"":{""Lantern"":{""kind"":""Black0"",""color"":{""r"":255,""g"":190,""b"":75},""strength_thousandths"":3000,""flicker_thousandths"":300}}},""head"":null,""tabard"":null}" +FROM character c +WHERE (SELECT COUNT(1) FROM loadout WHERE character_id = c.id) = 0; + + +-- Create a default inventory for any characters that don't have one +INSERT INTO inventory (character_id, items) +SELECT c."id", "{""slots"":[{""name"":""Dwarven Cheese"",""description"":""Restores 15 Health\n\nAromatic and nutritious\n\n<Right-Click to use>"",""kind"":{""Consumable"":{""kind"":""Cheese"",""effect"":{""Health"":{""amount"":15,""cause"":""Item""}},""amount"":1}}},{""name"":""Apple"",""description"":""Restores 20 Health\n\nRed and juicy\n\n<Right-Click to use>"",""kind"":{""Consumable"":{""kind"":""Apple"",""effect"":{""Health"":{""amount"":20,""cause"":""Item""}},""amount"":1}}},null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],""amount"":2}" +FROM character c +WHERE (SELECT COUNT(1) FROM inventory WHERE character_id = c.id) = 0; \ No newline at end of file From ddb66ec73b9215247c26a88cfba60271a6c2cd66 Mon Sep 17 00:00:00 2001 From: Ben Wallis <atomyc@gmail.com> Date: Wed, 8 Jul 2020 08:04:28 +0100 Subject: [PATCH 3/3] Added CharacterDataError when character list fails to load due to Loadout de-serialization error --- server/src/persistence/character.rs | 72 ++++++++++++++--------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/server/src/persistence/character.rs b/server/src/persistence/character.rs index 13b9f30d95..1fe6f8f657 100644 --- a/server/src/persistence/character.rs +++ b/server/src/persistence/character.rs @@ -245,8 +245,8 @@ fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> Ch .inner_join(schema::loadout::table) .first::<(Character, Body, Stats, Inventory, Loadout)>(&connection); - if let Ok((character_data, body_data, stats_data, inventory, loadout)) = result { - Ok(( + match result { + Ok((character_data, body_data, stats_data, inventory, loadout)) => Ok(( comp::Body::from(&body_data), comp::Stats::from(StatsJoinData { alias: &character_data.alias, @@ -255,13 +255,15 @@ fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> Ch }), comp::Inventory::from(inventory), comp::Loadout::from(&loadout), - )) - } else { - error!( - ?result, - "Failed to load character data for character id {}", character_id - ); - Err(Error::CharacterDataError) + )), + Err(e) => { + error!( + ?e, + ?character_id, + "Failed to load character data for character" + ); + Err(Error::CharacterDataError) + }, } } @@ -273,40 +275,36 @@ fn load_character_data(player_uuid: &str, character_id: i32, db_dir: &str) -> Ch /// stats, body, etc...) the character is skipped, and no entry will be /// returned. fn load_character_list(player_uuid: &str, db_dir: &str) -> CharacterListResult { - let data = schema::character::dsl::character + let result = 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) - .left_join(schema::loadout::table) - .load::<(Character, Body, Stats, Option<Loadout>)>(&establish_connection(db_dir))?; + .inner_join(schema::loadout::table) + .load::<(Character, Body, Stats, Loadout)>(&establish_connection(db_dir)); - Ok(data - .iter() - .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() - }, - comp::Loadout::from, - ); + match result { + Ok(data) => Ok(data + .iter() + .map(|(character_data, body_data, stats_data, loadout)| { + let character = CharacterData::from(character_data); + let body = comp::Body::from(body_data); + let level = stats_data.level as usize; + let loadout = comp::Loadout::from(loadout); - CharacterItem { - character, - body, - level, - loadout, - } - }) - .collect()) + CharacterItem { + character, + body, + level, + loadout, + } + }) + .collect()), + Err(e) => { + error!(?e, ?player_uuid, "Failed to load character list for player"); + Err(Error::CharacterDataError) + }, + } } /// Create a new character with provided comp::Character and comp::Body data.