diff --git a/client/src/lib.rs b/client/src/lib.rs index b095de3155..41f364b938 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -756,7 +756,8 @@ impl Client { | ClientGeneral::RequestSiteInfo(_) | ClientGeneral::UnlockSkillGroup(_) | ClientGeneral::RequestPlayerPhysics { .. } - | ClientGeneral::RequestLossyTerrainCompression { .. } => { + | ClientGeneral::RequestLossyTerrainCompression { .. } + | ClientGeneral::AcknowledgePersistenceLoadError => { #[cfg(feature = "tracy")] { ingame = 1.0; @@ -1409,6 +1410,10 @@ impl Client { })) } + pub fn acknolwedge_persistence_load_error(&mut self) { + self.send_msg(ClientGeneral::AcknowledgePersistenceLoadError) + } + /// Execute a single client tick, handle input and update the game state by /// the given duration. pub fn tick( diff --git a/common/net/src/msg/client.rs b/common/net/src/msg/client.rs index 0d24d77659..a4030044d3 100644 --- a/common/net/src/msg/client.rs +++ b/common/net/src/msg/client.rs @@ -92,6 +92,7 @@ pub enum ClientGeneral { RequestLossyTerrainCompression { lossy_terrain_compression: bool, }, + AcknowledgePersistenceLoadError, } impl ClientMsg { @@ -130,7 +131,8 @@ impl ClientMsg { | ClientGeneral::RequestSiteInfo(_) | ClientGeneral::UnlockSkillGroup(_) | ClientGeneral::RequestPlayerPhysics { .. } - | ClientGeneral::RequestLossyTerrainCompression { .. } => { + | ClientGeneral::RequestLossyTerrainCompression { .. } + | ClientGeneral::AcknowledgePersistenceLoadError => { c_type == ClientType::Game && presence.is_some() }, //Always possible diff --git a/common/src/comp/skillset/mod.rs b/common/src/comp/skillset/mod.rs index 2fa66b5f7c..42c51d66b9 100644 --- a/common/src/comp/skillset/mod.rs +++ b/common/src/comp/skillset/mod.rs @@ -237,6 +237,9 @@ pub struct SkillSet { skills: HashMap, pub modify_health: bool, pub modify_energy: bool, + /// Used to indicate to the frontend that there was an error in loading the + /// skillset from the database + pub persistence_load_error: Option, } impl Component for SkillSet { @@ -254,6 +257,7 @@ impl Default for SkillSet { skills: SkillSet::initial_skills(), modify_health: false, modify_energy: false, + persistence_load_error: None, }; // Insert default skill groups @@ -284,6 +288,7 @@ impl SkillSet { skills: SkillSet::initial_skills(), modify_health: true, modify_energy: true, + persistence_load_error: None, }; // Loops while checking the all_skills hashmap. For as long as it can find an @@ -297,15 +302,26 @@ impl SkillSet { { // Remove valid skill group kind from the hash map so that loop eventually // terminates. - if let Some(Ok(skills)) = all_skills.remove(&skill_group_kind) { - let backup_skillset = skillset.clone(); - // Iterate over all skills and make sure that unlocking them is successful. If - // any fail, fall back to skillset before unlocking any to allow a full respec - if !skills - .iter() - .all(|skill| skillset.unlock_skill(*skill).is_ok()) - { - skillset = backup_skillset; + if let Some(skills_result) = all_skills.remove(&skill_group_kind) { + match skills_result { + Ok(skills) => { + let backup_skillset = skillset.clone(); + // Iterate over all skills and make sure that unlocking them is successful. + // If any fail, fall back to skillset before + // unlocking any to allow a full respec + if !skills + .iter() + .all(|skill| skillset.unlock_skill(*skill).is_ok()) + { + skillset = backup_skillset; + // If unlocking failed, set persistence_load_error + skillset.persistence_load_error = + Some(SkillsPersistenceError::SkillsUnlockFailed) + } + }, + Err(persistence_error) => { + skillset.persistence_load_error = Some(persistence_error) + }, } } } @@ -549,9 +565,10 @@ pub enum SpRewardError { Overflow, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Clone, Copy, Deserialize, Serialize)] pub enum SkillsPersistenceError { HashMismatch, DeserializationFailure, SpentExpMismatch, + SkillsUnlockFailed, } diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 299bd3deb0..e38dd9c6aa 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -276,6 +276,11 @@ impl Sys { } => { presence.lossy_terrain_compression = lossy_terrain_compression; }, + ClientGeneral::AcknowledgePersistenceLoadError => { + skill_sets + .get_mut(entity) + .map(|mut skill_set| skill_set.persistence_load_error = None); + }, ClientGeneral::RequestCharacterList | ClientGeneral::CreateCharacter { .. } | ClientGeneral::EditCharacter { .. } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index f0d1bb3c47..0645d3a5e9 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -538,6 +538,7 @@ pub enum Event { ChangeAbility(usize, comp::ability::AuxiliaryAbility), SettingsChange(SettingsChange), + AcknowledgePersistenceLoadError, } // TODO: Are these the possible layouts we want? @@ -888,6 +889,7 @@ impl Show { pub struct PromptDialogSettings { message: String, affirmative_event: Event, + negative_option: bool, negative_event: Option, outcome_via_keypress: Option, } @@ -897,6 +899,7 @@ impl PromptDialogSettings { Self { message, affirmative_event, + negative_option: true, negative_event, outcome_via_keypress: None, } @@ -905,6 +908,11 @@ impl PromptDialogSettings { pub fn set_outcome_via_keypress(&mut self, outcome: bool) { self.outcome_via_keypress = Some(outcome); } + + pub fn with_no_negative_option(mut self) -> Self { + self.negative_option = false; + self + } } pub struct Floaters { @@ -1129,6 +1137,47 @@ impl Hud { let me = client.entity(); let poises = ecs.read_storage::(); + // Check if there was a persistence load error of the skillset, and if so + // display a dialog prompt + if self.show.prompt_dialog.is_none() { + if let Some(skill_set) = skill_sets.get(me) { + if let Some(persistence_error) = skill_set.persistence_load_error { + use comp::skillset::SkillsPersistenceError; + let persistence_error = match persistence_error { + SkillsPersistenceError::HashMismatch => { + "There was a difference detected in one of your skill groups since \ + you last played." + }, + SkillsPersistenceError::DeserializationFailure => { + "There was a error in loading some of your skills from the \ + database." + }, + SkillsPersistenceError::SpentExpMismatch => { + "The amount of free experience you had in one of your skill groups \ + differed from when you last played." + }, + SkillsPersistenceError::SkillsUnlockFailed => { + "Your skills were not able to be obtained in the same order you \ + acquired them. Prerequisites or costs may have changed." + }, + }; + let common_message = "At least one of your skill groups has been \ + invalidated. You will need to re-assign your skill \ + points to get skills."; + let persistence_error = + format!("{}\n{}", persistence_error, common_message); + let prompt_dialog = PromptDialogSettings::new( + persistence_error, + Event::AcknowledgePersistenceLoadError, + None, + ) + .with_no_negative_option(); + // self.set_prompt_dialog(prompt_dialog); + self.show.prompt_dialog = Some(prompt_dialog); + } + } + } + if (client.pending_trade().is_some() && !self.show.trade) || (client.pending_trade().is_none() && self.show.trade) { diff --git a/voxygen/src/hud/prompt_dialog.rs b/voxygen/src/hud/prompt_dialog.rs index 64a66a91d6..44e32b5b31 100644 --- a/voxygen/src/hud/prompt_dialog.rs +++ b/voxygen/src/hud/prompt_dialog.rs @@ -150,35 +150,37 @@ impl<'a> Widget for PromptDialog<'a> { .color(TEXT_COLOR) .set(state.ids.accept_txt, ui); - if Button::image(self.imgs.key_button) - .w_h(20.0, 20.0) - .hover_image(self.imgs.close_btn_hover) - .press_image(self.imgs.close_btn_press) - .label(&decline_key) - .image_color(UI_HIGHLIGHT_0) - .label_color(TEXT_COLOR) - .label_font_size(self.fonts.cyri.scale(16)) - .label_font_id(self.fonts.cyri.conrod_id) - .label_y(conrod_core::position::Relative::Scalar(2.5)) - .label_x(conrod_core::position::Relative::Scalar(0.5)) - .bottom_right_with_margins_on(state.ids.bot, 4.0, 6.0) - .set(state.ids.decline_key, ui) - .was_clicked() - || self - .prompt_dialog_settings - .outcome_via_keypress - .map_or(false, |outcome| !outcome) - { - event = Some(DialogOutcomeEvent::Negative( - self.prompt_dialog_settings.negative_event.as_ref().cloned(), - )); + if self.prompt_dialog_settings.negative_option { + if Button::image(self.imgs.key_button) + .w_h(20.0, 20.0) + .hover_image(self.imgs.close_btn_hover) + .press_image(self.imgs.close_btn_press) + .label(&decline_key) + .image_color(UI_HIGHLIGHT_0) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(16)) + .label_font_id(self.fonts.cyri.conrod_id) + .label_y(conrod_core::position::Relative::Scalar(2.5)) + .label_x(conrod_core::position::Relative::Scalar(0.5)) + .bottom_right_with_margins_on(state.ids.bot, 4.0, 6.0) + .set(state.ids.decline_key, ui) + .was_clicked() + || self + .prompt_dialog_settings + .outcome_via_keypress + .map_or(false, |outcome| !outcome) + { + event = Some(DialogOutcomeEvent::Negative( + self.prompt_dialog_settings.negative_event.as_ref().cloned(), + )); + } + Text::new("Decline") + .bottom_left_with_margins_on(state.ids.decline_key, 4.0, -65.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(18)) + .color(TEXT_COLOR) + .set(state.ids.decline_txt, ui); } - Text::new("Decline") - .bottom_left_with_margins_on(state.ids.decline_key, 4.0, -65.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(18)) - .color(TEXT_COLOR) - .set(state.ids.decline_txt, ui); // Prompt Description Text::new(&self.prompt_dialog_settings.message) diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 29553d520e..fba8c75c5b 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1428,6 +1428,11 @@ impl PlayState for SessionState { HudEvent::SettingsChange(settings_change) => { settings_change.process(global_state, self); }, + HudEvent::AcknowledgePersistenceLoadError => { + self.client + .borrow_mut() + .acknolwedge_persistence_load_error(); + }, } }