Save the selected character, deselect character when deleting, auto select newly created character

This commit is contained in:
Imbris 2020-11-14 14:32:39 -05:00
parent 2c2e4813fd
commit 4f2512f126
11 changed files with 293 additions and 159 deletions

View File

@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Persistent waypoints (start from the last camp fire you visited)
- NPCs use all three weapon skills in combat
- Speed stat to weapons which affects weapon attack speed
- Saving of the last selected character in the character selection screen
- Autoselecting the newly created character
- Deselecting when the selected character is deleted
### Changed

View File

@ -67,6 +67,7 @@ pub enum Event {
Notification(Notification),
SetViewDistance(u32),
Outcome(Outcome),
CharacterCreated(CharacterId),
}
pub struct Client {
@ -1422,7 +1423,11 @@ impl Client {
Ok(())
}
fn handle_server_character_screen_msg(&mut self, msg: ServerGeneral) -> Result<(), Error> {
fn handle_server_character_screen_msg(
&mut self,
events: &mut Vec<Event>,
msg: ServerGeneral,
) -> Result<(), Error> {
match msg {
ServerGeneral::CharacterListUpdate(character_list) => {
self.character_list.characters = character_list;
@ -1438,6 +1443,9 @@ impl Client {
self.clean_state();
self.character_list.error = Some(error);
},
ServerGeneral::CharacterCreated(character_id) => {
events.push(Event::CharacterCreated(character_id));
},
ServerGeneral::CharacterSuccess => {
debug!("client is now in ingame state on server");
if let Some(vd) = self.view_distance {
@ -1490,7 +1498,7 @@ impl Client {
self.handle_ping_msg(msg?)?;
}
if let Some(msg) = m3 {
self.handle_server_character_screen_msg(msg?)?;
self.handle_server_character_screen_msg(frontend_events, msg?)?;
}
if let Some(msg) = m4 {
self.handle_server_in_game_msg(frontend_events, msg?)?;

View File

@ -69,6 +69,8 @@ pub enum ServerGeneral {
CharacterListUpdate(Vec<CharacterItem>),
/// An error occurred while creating or deleting a character
CharacterActionError(String),
/// A new character was created
CharacterCreated(crate::character::CharacterId),
CharacterSuccess,
//Ingame related
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
@ -194,7 +196,8 @@ impl ServerMsg {
//Character Screen related
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_) => {
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_) => {
c_type != ClientType::ChatOnly && presence.is_none()
},
ServerGeneral::CharacterSuccess => {

View File

@ -73,6 +73,7 @@ impl Client {
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterSuccess => {
self.character_screen_stream.try_lock().unwrap().send(g)
},
@ -149,6 +150,7 @@ impl Client {
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterSuccess => {
PreparedMsg::new(1, &g, &self.character_screen_stream)
},

View File

@ -62,7 +62,7 @@ use futures_executor::block_on;
use metrics::{PhysicsMetrics, ServerMetrics, StateTickMetrics, TickMetrics};
use network::{Network, Pid, ProtocolAddr};
use persistence::{
character_loader::{CharacterLoader, CharacterLoaderResponseType},
character_loader::{CharacterLoader, CharacterLoaderResponseKind},
character_updater::CharacterUpdater,
};
use specs::{join::Join, Builder, Entity as EcsEntity, RunNow, SystemData, WorldExt};
@ -539,7 +539,7 @@ impl Server {
.read_resource::<persistence::character_loader::CharacterLoader>()
.messages()
.for_each(|query_result| match query_result.result {
CharacterLoaderResponseType::CharacterList(result) => match result {
CharacterLoaderResponseKind::CharacterList(result) => match result {
Ok(character_list_data) => self.notify_client(
query_result.entity,
ServerGeneral::CharacterListUpdate(character_list_data),
@ -549,7 +549,23 @@ impl Server {
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseType::CharacterData(result) => {
CharacterLoaderResponseKind::CharacterCreation(result) => match result {
Ok((character_id, list)) => {
self.notify_client(
query_result.entity,
ServerGeneral::CharacterListUpdate(list),
);
self.notify_client(
query_result.entity,
ServerGeneral::CharacterCreated(character_id),
);
},
Err(error) => self.notify_client(
query_result.entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterData(result) => {
let message = match *result {
Ok(character_data) => ServerEvent::UpdateCharacterData {
entity: query_result.entity,

View File

@ -17,7 +17,7 @@ use crate::{
convert_stats_from_database, convert_stats_to_database,
convert_waypoint_to_database_json,
},
character_loader::{CharacterDataResult, CharacterListResult},
character_loader::{CharacterCreationResult, CharacterDataResult, CharacterListResult},
error::Error::DatabaseError,
json_models::CharacterPosition,
PersistedComponents,
@ -170,7 +170,7 @@ pub fn create_character(
persisted_components: PersistedComponents,
connection: VelorenTransaction,
map: &AbilityMap,
) -> CharacterListResult {
) -> CharacterCreationResult {
use schema::item::dsl::*;
check_character_limit(uuid, connection)?;
@ -303,7 +303,7 @@ pub fn create_character(
)));
}
load_character_list(uuid, connection, map)
load_character_list(uuid, connection, map).map(|list| (character_id, list))
}
/// Delete a character. Returns the updated character list.

View File

@ -12,6 +12,7 @@ use std::path::Path;
use tracing::error;
pub(crate) type CharacterListResult = Result<Vec<CharacterItem>, Error>;
pub(crate) type CharacterCreationResult = Result<(CharacterId, Vec<CharacterItem>), Error>;
pub(crate) type CharacterDataResult = Result<PersistedComponents, Error>;
type CharacterLoaderRequest = (specs::Entity, CharacterLoaderRequestKind);
@ -38,16 +39,17 @@ enum CharacterLoaderRequestKind {
/// Wrapper for results for character actions. Can be a list of
/// characters, or component data belonging to an individual character
#[derive(Debug)]
pub enum CharacterLoaderResponseType {
pub enum CharacterLoaderResponseKind {
CharacterList(CharacterListResult),
CharacterData(Box<CharacterDataResult>),
CharacterCreation(CharacterCreationResult),
}
/// Common message format dispatched in response to an update request
#[derive(Debug)]
pub struct CharacterLoaderResponse {
pub entity: specs::Entity,
pub result: CharacterLoaderResponseType,
pub result: CharacterLoaderResponseKind,
}
/// A bi-directional messaging resource for making requests to modify or load
@ -80,45 +82,47 @@ impl CharacterLoader {
for request in internal_rx {
let (entity, kind) = request;
if let Err(e) = internal_tx.send(CharacterLoaderResponse {
entity,
result: match kind {
CharacterLoaderRequestKind::CreateCharacter {
player_uuid,
character_alias,
persisted_components,
} => CharacterLoaderResponseType::CharacterList(conn.transaction(|txn| {
create_character(
&player_uuid,
&character_alias,
if let Err(e) =
internal_tx.send(CharacterLoaderResponse {
entity,
result: match kind {
CharacterLoaderRequestKind::CreateCharacter {
player_uuid,
character_alias,
persisted_components,
txn,
&map,
)
})),
CharacterLoaderRequestKind::DeleteCharacter {
player_uuid,
character_id,
} => CharacterLoaderResponseType::CharacterList(conn.transaction(|txn| {
delete_character(&player_uuid, character_id, txn, &map)
})),
CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => {
CharacterLoaderResponseType::CharacterList(
} => CharacterLoaderResponseKind::CharacterCreation(conn.transaction(
|txn| {
create_character(
&player_uuid,
&character_alias,
persisted_components,
txn,
&map,
)
},
)),
CharacterLoaderRequestKind::DeleteCharacter {
player_uuid,
character_id,
} => CharacterLoaderResponseKind::CharacterList(conn.transaction(
|txn| delete_character(&player_uuid, character_id, txn, &map),
)),
CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => {
CharacterLoaderResponseKind::CharacterList(conn.transaction(
|txn| load_character_list(&player_uuid, txn, &map),
))
},
CharacterLoaderRequestKind::LoadCharacterData {
player_uuid,
character_id,
} => CharacterLoaderResponseKind::CharacterData(Box::new(
conn.transaction(|txn| {
load_character_list(&player_uuid, txn, &map)
load_character_data(player_uuid, character_id, txn, &map)
}),
)
)),
},
CharacterLoaderRequestKind::LoadCharacterData {
player_uuid,
character_id,
} => {
CharacterLoaderResponseType::CharacterData(Box::new(conn.transaction(
|txn| load_character_data(player_uuid, character_id, txn, &map),
)))
},
},
}) {
})
{
error!(?e, "Could not send send persistence request");
}
}

View File

@ -30,8 +30,10 @@ impl CharSelectionState {
Some("fixture.selection_bg"),
&*client.borrow(),
);
let char_selection_ui = CharSelectionUi::new(global_state, &*client.borrow());
Self {
char_selection_ui: CharSelectionUi::new(global_state),
char_selection_ui,
client,
scene,
}
@ -98,7 +100,7 @@ impl PlayState for CharSelectionState {
// Maintain the UI.
let events = self
.char_selection_ui
.maintain(global_state, &mut self.client.borrow_mut());
.maintain(global_state, &self.client.borrow());
for event in events {
match event {
@ -124,6 +126,15 @@ impl PlayState for CharSelectionState {
ui::Event::ClearCharacterListError => {
self.client.borrow_mut().character_list.error = None;
},
ui::Event::SelectCharacter(selected) => {
let client = self.client.borrow();
let server_name = &client.server_info.name;
// Select newly created character
global_state
.profile
.set_selected_character(server_name, selected);
global_state.profile.save_to_file_warn();
},
}
}
@ -179,6 +190,9 @@ impl PlayState for CharSelectionState {
);
return PlayStateResult::Pop;
},
client::Event::CharacterCreated(character_id) => {
self.char_selection_ui.select_character(character_id);
},
_ => {},
}
}

View File

@ -130,13 +130,12 @@ pub enum Event {
},
DeleteCharacter(CharacterId),
ClearCharacterListError,
SelectCharacter(Option<CharacterId>),
}
enum Mode {
Select {
info_content: Option<InfoContent>,
// Index of selected character
selected: Option<usize>,
characters_scroll: scrollable::State,
character_buttons: Vec<button::State>,
@ -168,7 +167,6 @@ impl Mode {
pub fn select(info_content: Option<InfoContent>) -> Self {
Self::Select {
info_content,
selected: None,
characters_scroll: Default::default(),
character_buttons: Vec::new(),
new_character_button: Default::default(),
@ -231,8 +229,9 @@ struct Controls {
tooltip_manager: TooltipManager,
// Zone for rotating the character with the mouse
mouse_detector: mouse_detector::State,
// enter: bool,
mode: Mode,
// Id of the selected character
selected: Option<CharacterId>,
}
#[derive(Clone)]
@ -240,7 +239,7 @@ enum Message {
Back,
Logout,
EnterWorld,
Select(usize),
Select(CharacterId),
Delete(usize),
NewCharacter,
CreateCharacter,
@ -265,7 +264,12 @@ enum Message {
}
impl Controls {
fn new(fonts: Fonts, imgs: Imgs, i18n: std::sync::Arc<Localization>) -> Self {
fn new(
fonts: Fonts,
imgs: Imgs,
i18n: std::sync::Arc<Localization>,
selected: Option<CharacterId>,
) -> Self {
let version = common::util::DISPLAY_VERSION_LONG.clone();
let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str());
@ -279,6 +283,7 @@ impl Controls {
tooltip_manager: TooltipManager::new(TOOLTIP_HOVER_DUR, TOOLTIP_FADE_DUR),
mouse_detector: Default::default(),
mode: Mode::select(Some(InfoContent::LoadingCharacters)),
selected,
}
}
@ -331,7 +336,6 @@ impl Controls {
let content = match &mut self.mode {
Mode::Select {
ref mut info_content,
selected,
ref mut characters_scroll,
ref mut character_buttons,
ref mut new_character_button,
@ -340,6 +344,24 @@ impl Controls {
ref mut yes_button,
ref mut no_button,
} => {
// If no character is selected then select the first one
// Note: we don't need to persist this because it is the default
if self.selected.is_none() {
self.selected = client
.character_list
.characters
.get(0)
.and_then(|i| i.character.id);
}
// Get the index of the selected character
let selected = self.selected.and_then(|id| {
client
.character_list
.characters
.iter()
.position(|i| i.character.id == Some(id))
});
if let Some(error) = &client.character_list.error {
// TODO: use more user friendly errors with suggestions on potential solutions
// instead of directly showing error message here
@ -386,75 +408,84 @@ impl Controls {
let mut characters = characters
.iter()
.zip(character_buttons.chunks_exact_mut(2))
.map(|(character, buttons)| {
.filter_map(|(character, buttons)| {
let mut buttons = buttons.iter_mut();
(
character,
(buttons.next().unwrap(), buttons.next().unwrap()),
)
// TODO: eliminate option in character id?
character.character.id.map(|id| {
(
id,
character,
(buttons.next().unwrap(), buttons.next().unwrap()),
)
})
})
.enumerate()
.map(|(i, (character, (select_button, delete_button)))| {
Overlay::new(
// Delete button
Button::new(
delete_button,
Space::new(Length::Units(16), Length::Units(16)),
)
.style(
style::button::Style::new(imgs.delete_button)
.hover_image(imgs.delete_button_hover)
.press_image(imgs.delete_button_press),
)
.on_press(Message::Delete(i))
.with_tooltip(
tooltip_manager,
move || {
tooltip::text(
i18n.get("char_selection.delete_permanently"),
tooltip_style,
)
},
),
// Select Button
AspectRatioContainer::new(
.map(
|(i, (character_id, character, (select_button, delete_button)))| {
Overlay::new(
// Delete button
Button::new(
select_button,
Column::with_children(vec![
Text::new(&character.character.alias).into(),
// TODO: only construct string once when characters are
// loaded
Text::new(
i18n.get("char_selection.level_fmt").replace(
"{level_nb}",
&character.level.to_string(),
),
)
.into(),
Text::new(i18n.get("char_selection.uncanny_valley"))
.into(),
]),
delete_button,
Space::new(Length::Units(16), Length::Units(16)),
)
.padding(10)
.style(
style::button::Style::new(if Some(i) == *selected {
imgs.selection_hover
} else {
imgs.selection
})
.hover_image(imgs.selection_hover)
.press_image(imgs.selection_press),
style::button::Style::new(imgs.delete_button)
.hover_image(imgs.delete_button_hover)
.press_image(imgs.delete_button_press),
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Select(i)),
.on_press(Message::Delete(i))
.with_tooltip(
tooltip_manager,
move || {
tooltip::text(
i18n.get("char_selection.delete_permanently"),
tooltip_style,
)
},
),
// Select Button
AspectRatioContainer::new(
Button::new(
select_button,
Column::with_children(vec![
Text::new(&character.character.alias).into(),
// TODO: only construct string once when characters
// are
// loaded
Text::new(
i18n.get("char_selection.level_fmt").replace(
"{level_nb}",
&character.level.to_string(),
),
)
.into(),
Text::new(
i18n.get("char_selection.uncanny_valley"),
)
.into(),
]),
)
.padding(10)
.style(
style::button::Style::new(if Some(i) == selected {
imgs.selection_hover
} else {
imgs.selection
})
.hover_image(imgs.selection_hover)
.press_image(imgs.selection_press),
)
.width(Length::Fill)
.height(Length::Fill)
.on_press(Message::Select(character_id)),
)
.ratio_of_image(imgs.selection),
)
.ratio_of_image(imgs.selection),
)
.padding(12)
.align_x(Align::End)
.into()
})
.padding(12)
.align_x(Align::End)
.into()
},
)
.collect::<Vec<_>>();
// Add create new character button
@ -1191,20 +1222,14 @@ impl Controls {
events.push(Event::Logout);
},
Message::EnterWorld => {
if let Mode::Select {
selected: Some(selected),
..
} = &self.mode
{
// TODO: eliminate option in character id?
if let Some(id) = characters.get(*selected).and_then(|i| i.character.id) {
events.push(Event::Play(id));
}
if let (Mode::Select { .. }, Some(selected)) = (&self.mode, self.selected) {
events.push(Event::Play(selected));
}
},
Message::Select(idx) => {
if let Mode::Select { selected, .. } = &mut self.mode {
*selected = Some(idx);
Message::Select(id) => {
if let Mode::Select { .. } = &mut self.mode {
self.selected = Some(id);
events.push(Event::SelectCharacter(Some(id)))
}
},
Message::Delete(idx) => {
@ -1276,6 +1301,11 @@ impl Controls {
if let Some(InfoContent::Deletion(idx)) = info_content {
if let Some(id) = characters.get(*idx).and_then(|i| i.character.id) {
events.push(Event::DeleteCharacter(id));
// Deselect if the selected character was deleted
if Some(id) == self.selected {
self.selected = None;
events.push(Event::SelectCharacter(None));
}
}
*info_content = Some(InfoContent::DeletingCharacter);
}
@ -1343,8 +1373,9 @@ impl Controls {
characters: &'a [CharacterItem],
) -> Option<(comp::Body, &'a comp::Loadout)> {
match &self.mode {
Mode::Select { selected, .. } => selected
.and_then(|idx| characters.get(idx))
Mode::Select { .. } => self
.selected
.and_then(|id| characters.iter().find(|i| i.character.id == Some(id)))
.map(|i| (i.body, &i.loadout)),
Mode::Create { loadout, body, .. } => Some((comp::Body::Humanoid(*body), loadout)),
}
@ -1355,10 +1386,15 @@ pub struct CharSelectionUi {
ui: Ui,
controls: Controls,
enter_pressed: bool,
select_character: Option<CharacterId>,
}
impl CharSelectionUi {
pub fn new(global_state: &mut GlobalState) -> Self {
pub fn new(global_state: &mut GlobalState, client: &Client) -> Self {
// Load up the last selected character for this server
let server_name = &client.server_info.name;
let selected_character = global_state.profile.get_selected_character(server_name);
// Load language
let i18n = Localization::load_expect(&i18n_asset_key(
&global_state.settings.language.selected_language,
@ -1390,12 +1426,14 @@ impl CharSelectionUi {
fonts,
Imgs::load(&mut ui).expect("Failed to load images"),
i18n,
selected_character,
);
Self {
ui,
controls,
enter_pressed: false,
select_character: None,
}
}
@ -1443,8 +1481,10 @@ impl CharSelectionUi {
self.ui.set_scaling_mode(scale_mode);
}
pub fn select_character(&mut self, id: CharacterId) { self.select_character = Some(id); }
// TODO: do we need whole client here or just character list?
pub fn maintain(&mut self, global_state: &mut GlobalState, client: &mut Client) -> Vec<Event> {
pub fn maintain(&mut self, global_state: &mut GlobalState, client: &Client) -> Vec<Event> {
let mut events = Vec::new();
let (mut messages, _) = self.ui.maintain(
@ -1457,6 +1497,10 @@ impl CharSelectionUi {
messages.push(Message::EnterWorld);
}
if let Some(id) = self.select_character.take() {
messages.push(Message::Select(id))
}
messages.into_iter().for_each(|message| {
self.controls.update(
message,

View File

@ -13,21 +13,23 @@ pub struct CharacterProfile {
pub hotbar_slots: [Option<hud::HotbarSlotContents>; 10],
}
const DEFAULT_SLOTS: [Option<hud::HotbarSlotContents>; 10] = [
None,
None,
None,
None,
None,
Some(hud::HotbarSlotContents::Inventory(0)),
Some(hud::HotbarSlotContents::Inventory(1)),
None,
None,
None,
];
impl Default for CharacterProfile {
fn default() -> Self {
CharacterProfile {
hotbar_slots: [
None,
None,
None,
None,
None,
Some(hud::HotbarSlotContents::Inventory(0)),
Some(hud::HotbarSlotContents::Inventory(1)),
None,
None,
None,
],
hotbar_slots: DEFAULT_SLOTS,
}
}
}
@ -38,12 +40,15 @@ impl Default for CharacterProfile {
pub struct ServerProfile {
/// A map of character's by id to their CharacterProfile.
pub characters: HashMap<CharacterId, CharacterProfile>,
// Selected character in the chararacter selection screen
pub selected_character: Option<CharacterId>,
}
impl Default for ServerProfile {
fn default() -> Self {
ServerProfile {
characters: HashMap::new(),
selected_character: None,
}
}
}
@ -106,26 +111,23 @@ impl Profile {
/// Get the hotbar_slots for the requested character_id.
///
/// if the server or character does not exist then the appropriate fields
/// will be initialised and default hotbar_slots (empty) returned.
/// If the server or character does not exist then the default hotbar_slots
/// (empty) is returned.
///
/// # Arguments
///
/// * server - current server the character is on.
/// * character_id - id of the character.
pub fn get_hotbar_slots(
&mut self,
&self,
server: &str,
character_id: CharacterId,
) -> [Option<hud::HotbarSlotContents>; 10] {
self.servers
.entry(server.to_string())
.or_insert(ServerProfile::default())
// Get or update the CharacterProfile.
.characters
.entry(character_id)
.or_insert(CharacterProfile::default())
.hotbar_slots
.get(server)
.and_then(|s| s.characters.get(&character_id))
.map(|c| c.hotbar_slots)
.unwrap_or(DEFAULT_SLOTS)
}
/// Set the hotbar_slots for the requested character_id.
@ -154,6 +156,41 @@ impl Profile {
.hotbar_slots = slots;
}
/// Get the selected_character for the provided server.
///
/// if the server does not exist then the default selected_character (None)
/// is returned.
///
/// # Arguments
///
/// * server - current server the character is on.
pub fn get_selected_character(&self, server: &str) -> Option<CharacterId> {
self.servers
.get(server)
.map(|s| s.selected_character)
.unwrap_or_default()
}
/// Set the selected_character for the provided server.
///
/// If the server does not exist then the appropriate fields
/// will be initialised and the selected_character added.
///
/// # Arguments
///
/// * server - current server the character is on.
/// * selected_character - option containing selected character ID
pub fn set_selected_character(
&mut self,
server: &str,
selected_character: Option<CharacterId>,
) {
self.servers
.entry(server.to_string())
.or_insert(ServerProfile::default())
.selected_character = selected_character;
}
/// Save the current profile to disk.
fn save_to_file(&self) -> std::io::Result<()> {
let path = Profile::get_path();
@ -188,7 +225,7 @@ mod tests {
#[test]
fn test_get_slots_with_empty_profile() {
let mut profile = Profile::default();
let profile = Profile::default();
let slots = profile.get_hotbar_slots("TestServer", 12345);
assert_eq!(slots, [
None,

View File

@ -178,6 +178,7 @@ impl SessionState {
global_state.settings.save_to_file_warn();
},
client::Event::Outcome(outcome) => outcomes.push(outcome),
client::Event::CharacterCreated(_) => {},
}
}
@ -928,7 +929,7 @@ impl PlayState for SessionState {
HudEvent::ChangeHotbarState(state) => {
let client = self.client.borrow();
let server = &client.server_info.name;
let server_name = &client.server_info.name;
// If we are changing the hotbar state this CANNOT be None.
let character_id = match client.presence().unwrap() {
PresenceKind::Character(id) => id,
@ -938,9 +939,11 @@ impl PlayState for SessionState {
};
// Get or update the ServerProfile.
global_state
.profile
.set_hotbar_slots(server, character_id, state.slots);
global_state.profile.set_hotbar_slots(
server_name,
character_id,
state.slots,
);
global_state.profile.save_to_file_warn();