diff --git a/CHANGELOG.md b/CHANGELOG.md index 89cde4526b..c02676c7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added daily Mac builds - Allow spawning individual pet species, not just generic body kinds. - Configurable fonts +- Configurable keybindings from the Controls menu - Tanslation status tracking - Added gamma setting - Added new orc hairstyles diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index d1ca9fb2cc..368f02145b 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -250,90 +250,7 @@ Enjoy your stay in the World of Veloren."#, "hud.settings.sound_effect_volume": "Sound Effects Volume", "hud.settings.audio_device": "Audio Device", - // Control list - "hud.settings.control_names": r#"Free Cursor -Toggle Help Window -Toggle Interface -Toggle FPS and Debug Info -Take Screenshot -Toggle Nametags -Toggle Fullscreen - - -Move Forward -Move Left -Move Right -Move Backwards - -Jump - -Glider - -Dodge - -Roll - -Climb - -Climb down - -Auto Walk - -Sheathe/Draw Weapons - -Put on/Remove Helmet - -Sit - -Mount - -Interact - - -Basic Attack -Secondary Attack/Block/Aim - - -Skillbar Slot 1 -Skillbar Slot 2 -Skillbar Slot 3 -Skillbar Slot 4 -Skillbar Slot 5 -Skillbar Slot 6 -Skillbar Slot 7 -Skillbar Slot 8 -Skillbar Slot 9 -Skillbar Slot 10 - - -Pause Menu -Settings -Social -Map -Spellbook -Character -Questlog -Bag - - - -Send Chat Message -Scroll Chat - - -Free look - - -Chat commands: - -/alias [Name] - Change your Chat Name -/tp [Name] - Teleports you to another player -/jump - Offset your position -/goto - Teleport to a position -/kill - Kill yourself -/pig - Spawn pig NPC -/wolf - Spawn wolf NPC -/help - Display chat commands"#, + "hud.settings.awaitingkey": "Press a key...", "hud.social": "Social", "hud.social.online": "Online", @@ -348,6 +265,48 @@ Chat commands: /// End HUD section + /// Start GameInput section + + "gameinput.primary": "Basic Attack", + "gameinput.secondary": "Secondary Attack/Block/Aim", + "gameinput.ability3": "Hotbar Slot 1", + "gameinput.swaploadout": "Swap Loadout", + "gameinput.togglecursor": "Free Cursor", + "gameinput.help": "Toggle Help Window", + "gameinput.toggleinterface": "Toggle Interface", + "gameinput.toggledebug": "Toggle FPS and Debug Info", + "gameinput.screenshot": "Take Screenshot", + "gameinput.toggleingameui": "Toggle Nametags", + "gameinput.fullscreen": "Toggle Fullscreen", + "gameinput.moveforward": "Move Forward", + "gameinput.moveleft": "Move Left", + "gameinput.moveright": "Move Right", + "gameinput.moveback": "Move Backwards", + "gameinput.jump": "Jump", + "gameinput.glide": "Glider", + "gameinput.roll": "Roll", + "gameinput.climb": "Climb", + "gameinput.climbdown": "Climb Down", + "gameinput.wallleap": "Wall Leap", + "gameinput.mount": "Mount", + "gameinput.enter": "Enter", + "gameinput.command": "Command", + "gameinput.escape": "Escape", + "gameinput.map": "Map", + "gameinput.bag": "Bag", + "gameinput.social": "Social", + "gameinput.sit": "Sit", + "gameinput.spellbook": "Spell Book", + "gameinput.settings": "Settings", + "gameinput.respawn": "Respawn", + "gameinput.charge": "Charge", + "gameinput.togglewield": "Toggle Wield", + "gameinput.interact": "Interact", + "gameinput.freelook": "Free look behavior", + + /// End GameInput section + + /// Start chracter selection section "char_selection.delete_permanently": "Permanently delete this Character?", "char_selection.change_server": "Change Server", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index fe58075ddb..ebe1d3f163 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -237,6 +237,7 @@ pub enum Event { Logout, Quit, ChangeLanguage(LanguageMetadata), + ChangeBinding(GameInput), ChangeFreeLookBehavior(PressBehavior), } @@ -1494,66 +1495,70 @@ impl Hud { .set(self.ids.num_figures, ui_widgets); // Help Window - Text::new( - &self - .voxygen_i18n - .get("hud.press_key_to_toggle_keybindings_fmt") - .replace( - "{key}", - &format!("{:?}", global_state.settings.controls.help), - ), - ) - .color(TEXT_COLOR) - .down_from(self.ids.num_figures, 5.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(14)) - .set(self.ids.help_info, ui_widgets); + if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) { + Text::new( + &self + .voxygen_i18n + .get("hud.press_key_to_toggle_keybindings_fmt") + .replace("{key}", help_key.to_string().as_str()), + ) + .color(TEXT_COLOR) + .down_from(self.ids.num_figures, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .set(self.ids.help_info, ui_widgets); + } // Info about Debug Shortcut - Text::new( - &self - .voxygen_i18n - .get("hud.press_key_to_toggle_debug_info_fmt") - .replace( - "{key}", - &format!("{:?}", global_state.settings.controls.toggle_debug), - ), - ) - .color(TEXT_COLOR) - .down_from(self.ids.help_info, 5.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(14)) - .set(self.ids.debug_info, ui_widgets); + if let Some(toggle_debug_key) = global_state + .settings + .controls + .get_binding(GameInput::ToggleDebug) + { + Text::new( + &self + .voxygen_i18n + .get("hud.press_key_to_toggle_debug_info_fmt") + .replace("{key}", toggle_debug_key.to_string().as_str()), + ) + .color(TEXT_COLOR) + .down_from(self.ids.help_info, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .set(self.ids.debug_info, ui_widgets); + } } else { // Help Window - Text::new( - &self - .voxygen_i18n - .get("hud.press_key_to_show_keybindings_fmt") - .replace( - "{key}", - &format!("{:?}", global_state.settings.controls.help), - ), - ) - .color(TEXT_COLOR) - .top_left_with_margins_on(ui_widgets.window, 5.0, 5.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(16)) - .set(self.ids.help_info, ui_widgets); + if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) { + Text::new( + &self + .voxygen_i18n + .get("hud.press_key_to_show_keybindings_fmt") + .replace("{key}", help_key.to_string().as_str()), + ) + .color(TEXT_COLOR) + .top_left_with_margins_on(ui_widgets.window, 5.0, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(16)) + .set(self.ids.help_info, ui_widgets); + } // Info about Debug Shortcut - Text::new( - &self - .voxygen_i18n - .get("hud.press_key_to_show_debug_info_fmt") - .replace( - "{key}", - &format!("{:?}", global_state.settings.controls.toggle_debug), - ), - ) - .color(TEXT_COLOR) - .down_from(self.ids.help_info, 5.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(12)) - .set(self.ids.debug_info, ui_widgets); + if let Some(toggle_debug_key) = global_state + .settings + .controls + .get_binding(GameInput::ToggleDebug) + { + Text::new( + &self + .voxygen_i18n + .get("hud.press_key_to_show_debug_info_fmt") + .replace("{key}", toggle_debug_key.to_string().as_str()), + ) + .color(TEXT_COLOR) + .down_from(self.ids.help_info, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(12)) + .set(self.ids.debug_info, ui_widgets); + } } // Help Text @@ -1822,6 +1827,9 @@ impl Hud { settings_window::Event::AdjustWindowSize(new_size) => { events.push(Event::AdjustWindowSize(new_size)); }, + settings_window::Event::ChangeBinding(game_input) => { + events.push(Event::ChangeBinding(game_input)); + }, settings_window::Event::ChangeFreeLookBehavior(behavior) => { events.push(Event::ChangeFreeLookBehavior(behavior)); }, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index d8a3db3109..8ead5fb68d 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -6,12 +6,15 @@ use crate::{ i18n::{list_localizations, LanguageMetadata, VoxygenLocalization}, render::{AaMode, CloudMode, FluidMode}, ui::{fonts::ConrodVoxygenFonts, ImageSlider, ScaleMode, ToggleButton}, + window::GameInput, GlobalState, }; use conrod_core::{ color, + position::Relative, widget::{self, Button, DropDownList, Image, Rectangle, Scrollbar, Text}, - widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, + widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, + WidgetCommon, }; const FPS_CHOICES: [u32; 11] = [15, 30, 40, 50, 60, 90, 120, 144, 240, 300, 500]; @@ -27,8 +30,9 @@ widget_ids! { settings_r, settings_l, settings_scrollbar, - controls_text, - controls_controls, + controls_texts[], + controls_buttons[], + controls_alignment_rectangle, button_help, button_help2, show_help_label, @@ -221,6 +225,7 @@ pub enum Event { SctPlayerBatch(bool), SctDamageBatch(bool), ChangeLanguage(LanguageMetadata), + ChangeBinding(GameInput), ChangeFreeLookBehavior(PressBehavior), } @@ -1328,142 +1333,86 @@ impl<'a> Widget for SettingsWindow<'a> { // Contents if let SettingsTab::Controls = self.show.settings_tab { let controls = &self.global_state.settings.controls; - Text::new(&self.localized_strings.get("hud.settings.control_names")) - .color(TEXT_COLOR) - .top_left_with_margins_on(state.ids.settings_content, 5.0, 5.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(18)) - .set(state.ids.controls_text, ui); - // TODO: Replace with buttons that show actual keybinds and allow the user to - // change them. - #[rustfmt::skip] - Text::new(&format!( - "{}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - \n\ - \n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - {}\n\ - \n\ - \n\ - {}\n\ - {}\n\ - \n\ - \n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - \n\ - \n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - {}\n\ - \n\ - \n\ - \n\ - {}\n\ - {}\n\ - \n\ - \n\ - {}\n\ - \n\ - \n\ - \n\ - \n\ - ", - controls.toggle_cursor, - controls.help, - controls.toggle_interface, - controls.toggle_debug, - controls.screenshot, - controls.toggle_ingame_ui, - controls.fullscreen, - controls.move_forward, - controls.move_left, - controls.move_back, - controls.move_right, - controls.jump, - controls.glide, - "??", // Dodge - controls.roll, - controls.climb, - controls.climb_down, - "??", // Auto Walk - controls.toggle_wield, - "??", // Put on/Remove Helmet - controls.sit, - controls.mount, - controls.interact, - controls.primary, - controls.secondary, - "1", // Skillbar Slot 1 - "2", // Skillbar Slot 2 - "3", // Skillbar Slot 3 - "4", // Skillbar Slot 4 - "5", // Skillbar Slot 5 - "6", // Skillbar Slot 6 - "7", // Skillbar Slot 7 - "8", // Skillbar Slot 8 - "9", // Skillbar Slot 9 - "0", // Skillbar Slot 10 - controls.escape, - controls.settings, - controls.social, - controls.map, - controls.spellbook, - //controls.character_window, - //controls.quest_log, - controls.bag, - controls.enter, - "Mouse Wheel", // Scroll chat - controls.free_look - )) - .color(TEXT_COLOR) - .right_from(state.ids.controls_text, 0.0) - .font_id(self.fonts.cyri.conrod_id) - .font_size(self.fonts.cyri.scale(18)) - .set(state.ids.controls_controls, ui); + if controls.keybindings.len() > state.ids.controls_texts.len() + || controls.keybindings.len() > state.ids.controls_buttons.len() + { + state.update(|s| { + s.ids + .controls_texts + .resize(controls.keybindings.len(), &mut ui.widget_id_generator()); + s.ids + .controls_buttons + .resize(controls.keybindings.len(), &mut ui.widget_id_generator()); + }); + } + // Used for sequential placement in a flow-down pattern + let mut previous_text_id = None; + let mut keybindings_vec: Vec<&GameInput> = controls.keybindings.keys().collect(); + keybindings_vec.sort(); + // Loop all existing keybindings and the ids for text and button widgets + for (game_input, (&text_id, &button_id)) in keybindings_vec.into_iter().zip( + state + .ids + .controls_texts + .iter() + .zip(state.ids.controls_buttons.iter()), + ) { + if let Some(key) = controls.get_binding(*game_input) { + let loc_key = self + .localized_strings + .get(game_input.get_localization_key()); + let key_string = match self.global_state.window.remapping_keybindings { + Some(game_input_binding) => { + if *game_input == game_input_binding { + String::from(self.localized_strings.get("hud.settings.awaitingkey")) + } else { + key.to_string() + } + }, + None => key.to_string(), + }; + + let text_widget = Text::new(loc_key) + .color(TEXT_COLOR) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(18)); + let button_widget = Button::new() + .label(&key_string) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(15)) + .w(150.0) + .rgba(0.0, 0.0, 0.0, 0.0) + .border_rgba(0.0, 0.0, 0.0, 255.0) + .label_y(Relative::Scalar(3.0)); + // Place top-left if it's the first text, else under the previous one + let text_widget = match previous_text_id { + None => text_widget.top_left_with_margins_on( + state.ids.settings_content, + 10.0, + 5.0, + ), + Some(prev_id) => text_widget.down_from(prev_id, 10.0), + }; + let text_width = text_widget.get_w(ui).unwrap_or(0.0); + text_widget.set(text_id, ui); + if button_widget + .right_from(text_id, 350.0 - text_width) + .set(button_id, ui) + .was_clicked() + { + events.push(Event::ChangeBinding(*game_input)); + } + // Set the previous id to the current one for the next cycle + previous_text_id = Some(text_id); + } + } + // Add an empty text widget to simulate some bottom margin, because conrod sucks + if let Some(prev_id) = previous_text_id { + Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT) + .down_from(prev_id, 10.0) + .set(state.ids.controls_alignment_rectangle, ui); + } } // 4) Video Tab ----------------------------------- diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 2c1f3136dd..036cbc8302 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -5,6 +5,7 @@ use super::{ use crate::{ i18n::{i18n_asset_key, VoxygenLocalization}, ui::fonts::ConrodVoxygenFonts, + window::GameInput, GlobalState, }; use common::{ @@ -300,36 +301,45 @@ impl<'a> Widget for Skillbar<'a> { .set(state.ids.level_down, ui); // Death message if self.stats.is_dead { - Text::new(&localized_strings.get("hud.you_died")) - .middle_of(ui.window) - .font_size(self.fonts.cyri.scale(50)) + if let Some(key) = self + .global_state + .settings + .controls + .get_binding(GameInput::Respawn) + { + Text::new(&localized_strings.get("hud.you_died")) + .middle_of(ui.window) + .font_size(self.fonts.cyri.scale(50)) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.death_message_1_bg, ui); + Text::new( + &localized_strings + .get("hud.press_key_to_respawn") + .replace("{key}", key.to_string().as_str()), + ) + .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0) + .font_size(self.fonts.cyri.scale(30)) .font_id(self.fonts.cyri.conrod_id) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.death_message_1_bg, ui); - Text::new(&localized_strings.get("hud.press_key_to_respawn").replace( - "{key}", - &format!("{:?}", self.global_state.settings.controls.respawn), - )) - .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0) - .font_size(self.fonts.cyri.scale(30)) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) - .set(state.ids.death_message_2_bg, ui); - Text::new(&localized_strings.get("hud.you_died")) - .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0) - .font_size(self.fonts.cyri.scale(50)) + .set(state.ids.death_message_2_bg, ui); + Text::new(&localized_strings.get("hud.you_died")) + .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0) + .font_size(self.fonts.cyri.scale(50)) + .font_id(self.fonts.cyri.conrod_id) + .color(CRITICAL_HP_COLOR) + .set(state.ids.death_message_1, ui); + Text::new( + &localized_strings + .get("hud.press_key_to_respawn") + .replace("{key}", key.to_string().as_str()), + ) + .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0) + .font_size(self.fonts.cyri.scale(30)) .font_id(self.fonts.cyri.conrod_id) .color(CRITICAL_HP_COLOR) - .set(state.ids.death_message_1, ui); - Text::new(&localized_strings.get("hud.press_key_to_respawn").replace( - "{key}", - &format!("{:?}", self.global_state.settings.controls.respawn), - )) - .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0) - .font_size(self.fonts.cyri.scale(30)) - .font_id(self.fonts.cyri.conrod_id) - .color(CRITICAL_HP_COLOR) - .set(state.ids.death_message_2, ui); + .set(state.ids.death_message_2, ui); + } } // Experience-Bar match self.global_state.settings.gameplay.xp_bar { diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 4fa2b2b833..61bfcd3d92 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -713,6 +713,9 @@ impl PlayState for SessionState { global_state.settings.graphics.window_size = new_size; global_state.settings.save_to_file_warn(); }, + HudEvent::ChangeBinding(game_input) => { + global_state.window.set_keybinding_mode(game_input); + }, HudEvent::ChangeFreeLookBehavior(behavior) => { global_state.settings.gameplay.free_look_behavior = behavior; }, diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index fddf225fb1..c062ae420f 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -3,7 +3,7 @@ use crate::{ i18n, render::{AaMode, CloudMode, FluidMode}, ui::ScaleMode, - window::KeyMouse, + window::{GameInput, KeyMouse}, }; use directories::{ProjectDirs, UserDirs}; use glutin::{MouseButton, VirtualKeyCode}; @@ -12,46 +12,47 @@ use log::warn; use serde_derive::{Deserialize, Serialize}; use std::{fs, io::prelude::*, path::PathBuf}; +// ControlSetting-like struct used by Serde, to handle not serializing/building +// post-deserializing the inverse_keybindings hashmap +#[derive(Serialize, Deserialize)] +struct ControlSettingsSerde { + keybindings: HashMap, +} + +impl From for ControlSettingsSerde { + fn from(control_settings: ControlSettings) -> Self { + let mut user_bindings: HashMap = HashMap::new(); + // Do a delta between default() ControlSettings and the argument, and let + // keybindings be only the custom keybindings chosen by the user. + for (k, v) in control_settings.keybindings { + if ControlSettings::default_binding(k) != v { + // Keybinding chosen by the user + user_bindings.insert(k, v); + } + } + ControlSettingsSerde { + keybindings: user_bindings, + } + } +} + /// `ControlSettings` contains keybindings. #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(default)] +#[serde(from = "ControlSettingsSerde", into = "ControlSettingsSerde")] pub struct ControlSettings { - pub primary: KeyMouse, - pub secondary: KeyMouse, - pub ability3: KeyMouse, - pub toggle_cursor: KeyMouse, - pub escape: KeyMouse, - pub enter: KeyMouse, - pub command: KeyMouse, - pub move_forward: KeyMouse, - pub move_left: KeyMouse, - pub move_back: KeyMouse, - pub move_right: KeyMouse, - pub jump: KeyMouse, - pub sit: KeyMouse, - pub glide: KeyMouse, - pub climb: KeyMouse, - pub climb_down: KeyMouse, - pub wall_leap: KeyMouse, - pub mount: KeyMouse, - pub map: KeyMouse, - pub bag: KeyMouse, - pub social: KeyMouse, - pub spellbook: KeyMouse, - pub settings: KeyMouse, - pub help: KeyMouse, - pub toggle_interface: KeyMouse, - pub toggle_debug: KeyMouse, - pub fullscreen: KeyMouse, - pub screenshot: KeyMouse, - pub toggle_ingame_ui: KeyMouse, - pub roll: KeyMouse, - pub respawn: KeyMouse, - pub interact: KeyMouse, - pub toggle_wield: KeyMouse, - pub swap_loadout: KeyMouse, - pub charge: KeyMouse, - pub free_look: KeyMouse, + pub keybindings: HashMap, + pub inverse_keybindings: HashMap>, // used in event loop +} + +impl From for ControlSettings { + fn from(control_serde: ControlSettingsSerde) -> Self { + let user_keybindings = control_serde.keybindings; + let mut control_settings = ControlSettings::default(); + for (k, v) in user_keybindings { + control_settings.modify_binding(k, v); + } + control_settings + } } /// Since Macbook trackpads lack middle click, on OS X we default to LShift @@ -64,46 +65,135 @@ const MIDDLE_CLICK_KEY: KeyMouse = KeyMouse::Key(VirtualKeyCode::LShift); #[cfg(not(target_os = "macos"))] const MIDDLE_CLICK_KEY: KeyMouse = KeyMouse::Mouse(MouseButton::Middle); +impl ControlSettings { + pub fn get_binding(&self, game_input: GameInput) -> Option { + self.keybindings.get(&game_input).copied() + } + + pub fn get_associated_game_inputs(&self, key_mouse: &KeyMouse) -> Option<&HashSet> { + self.inverse_keybindings.get(key_mouse) + } + + pub fn insert_binding(&mut self, game_input: GameInput, key_mouse: KeyMouse) { + self.keybindings.insert(game_input, key_mouse); + self.inverse_keybindings + .entry(key_mouse) + .or_default() + .insert(game_input); + } + + pub fn modify_binding(&mut self, game_input: GameInput, key_mouse: KeyMouse) { + // For the KeyMouse->GameInput hashmap, we first need to remove the GameInput + // from the old binding + if let Some(old_binding) = self.get_binding(game_input) { + self.inverse_keybindings + .entry(old_binding) + .or_default() + .remove(&game_input); + } + // then we add the GameInput to the proper key + self.inverse_keybindings + .entry(key_mouse) + .or_default() + .insert(game_input); + // For the GameInput->KeyMouse hashmap, just overwrite the value + self.keybindings.insert(game_input, key_mouse); + } + + pub fn default_binding(game_input: GameInput) -> KeyMouse { + // If a new GameInput is added, be sure to update ControlSettings::default() + // too! + match game_input { + GameInput::Primary => KeyMouse::Mouse(MouseButton::Left), + GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right), + GameInput::ToggleCursor => KeyMouse::Key(VirtualKeyCode::Tab), + GameInput::Escape => KeyMouse::Key(VirtualKeyCode::Escape), + GameInput::Enter => KeyMouse::Key(VirtualKeyCode::Return), + GameInput::Command => KeyMouse::Key(VirtualKeyCode::Slash), + GameInput::MoveForward => KeyMouse::Key(VirtualKeyCode::W), + GameInput::MoveLeft => KeyMouse::Key(VirtualKeyCode::A), + GameInput::MoveBack => KeyMouse::Key(VirtualKeyCode::S), + GameInput::MoveRight => KeyMouse::Key(VirtualKeyCode::D), + GameInput::Jump => KeyMouse::Key(VirtualKeyCode::Space), + GameInput::Sit => KeyMouse::Key(VirtualKeyCode::K), + GameInput::Glide => KeyMouse::Key(VirtualKeyCode::LShift), + GameInput::Climb => KeyMouse::Key(VirtualKeyCode::Space), + GameInput::ClimbDown => KeyMouse::Key(VirtualKeyCode::LControl), + GameInput::WallLeap => MIDDLE_CLICK_KEY, + GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F), + GameInput::Map => KeyMouse::Key(VirtualKeyCode::M), + GameInput::Bag => KeyMouse::Key(VirtualKeyCode::B), + GameInput::Social => KeyMouse::Key(VirtualKeyCode::O), + GameInput::Spellbook => KeyMouse::Key(VirtualKeyCode::P), + GameInput::Settings => KeyMouse::Key(VirtualKeyCode::N), + GameInput::Help => KeyMouse::Key(VirtualKeyCode::F1), + GameInput::ToggleInterface => KeyMouse::Key(VirtualKeyCode::F2), + GameInput::ToggleDebug => KeyMouse::Key(VirtualKeyCode::F3), + GameInput::Fullscreen => KeyMouse::Key(VirtualKeyCode::F11), + GameInput::Screenshot => KeyMouse::Key(VirtualKeyCode::F4), + GameInput::ToggleIngameUi => KeyMouse::Key(VirtualKeyCode::F6), + GameInput::Roll => MIDDLE_CLICK_KEY, + GameInput::Respawn => KeyMouse::Key(VirtualKeyCode::Space), + GameInput::Interact => KeyMouse::Mouse(MouseButton::Right), + GameInput::ToggleWield => KeyMouse::Key(VirtualKeyCode::T), + GameInput::Charge => KeyMouse::Key(VirtualKeyCode::Key1), + GameInput::FreeLook => KeyMouse::Key(VirtualKeyCode::L), + GameInput::Ability3 => KeyMouse::Key(VirtualKeyCode::Key1), + GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::LAlt), + } + } +} impl Default for ControlSettings { fn default() -> Self { - Self { - primary: KeyMouse::Mouse(MouseButton::Left), - secondary: KeyMouse::Mouse(MouseButton::Right), - ability3: KeyMouse::Key(VirtualKeyCode::Key1), - toggle_cursor: KeyMouse::Key(VirtualKeyCode::Tab), - escape: KeyMouse::Key(VirtualKeyCode::Escape), - enter: KeyMouse::Key(VirtualKeyCode::Return), - command: KeyMouse::Key(VirtualKeyCode::Slash), - move_forward: KeyMouse::Key(VirtualKeyCode::W), - move_left: KeyMouse::Key(VirtualKeyCode::A), - move_back: KeyMouse::Key(VirtualKeyCode::S), - move_right: KeyMouse::Key(VirtualKeyCode::D), - jump: KeyMouse::Key(VirtualKeyCode::Space), - sit: KeyMouse::Key(VirtualKeyCode::K), - glide: KeyMouse::Key(VirtualKeyCode::LShift), - climb: KeyMouse::Key(VirtualKeyCode::Space), - climb_down: KeyMouse::Key(VirtualKeyCode::LControl), - wall_leap: MIDDLE_CLICK_KEY, - mount: KeyMouse::Key(VirtualKeyCode::F), - map: KeyMouse::Key(VirtualKeyCode::M), - bag: KeyMouse::Key(VirtualKeyCode::B), - social: KeyMouse::Key(VirtualKeyCode::O), - spellbook: KeyMouse::Key(VirtualKeyCode::P), - settings: KeyMouse::Key(VirtualKeyCode::N), - help: KeyMouse::Key(VirtualKeyCode::F1), - toggle_interface: KeyMouse::Key(VirtualKeyCode::F2), - toggle_debug: KeyMouse::Key(VirtualKeyCode::F3), - fullscreen: KeyMouse::Key(VirtualKeyCode::F11), - screenshot: KeyMouse::Key(VirtualKeyCode::F4), - toggle_ingame_ui: KeyMouse::Key(VirtualKeyCode::F6), - roll: MIDDLE_CLICK_KEY, - respawn: KeyMouse::Key(VirtualKeyCode::Space), - interact: KeyMouse::Mouse(MouseButton::Right), - toggle_wield: KeyMouse::Key(VirtualKeyCode::T), - swap_loadout: KeyMouse::Key(VirtualKeyCode::Q), - charge: KeyMouse::Key(VirtualKeyCode::Key1), - free_look: KeyMouse::Key(VirtualKeyCode::L), + let mut new_settings = Self { + keybindings: HashMap::new(), + inverse_keybindings: HashMap::new(), + }; + // Sets the initial keybindings for those GameInputs. If a new one is created in + // future, you'll have to update default_binding, and you should update this vec + // too. + let game_inputs = vec![ + GameInput::Primary, + GameInput::Secondary, + GameInput::ToggleCursor, + GameInput::MoveForward, + GameInput::MoveBack, + GameInput::MoveLeft, + GameInput::MoveRight, + GameInput::Jump, + GameInput::Sit, + GameInput::Glide, + GameInput::Climb, + GameInput::ClimbDown, + GameInput::WallLeap, + GameInput::Mount, + GameInput::Enter, + GameInput::Command, + GameInput::Escape, + GameInput::Map, + GameInput::Bag, + GameInput::Social, + GameInput::Spellbook, + GameInput::Settings, + GameInput::ToggleInterface, + GameInput::Help, + GameInput::ToggleDebug, + GameInput::Fullscreen, + GameInput::Screenshot, + GameInput::ToggleIngameUi, + GameInput::Roll, + GameInput::Respawn, + GameInput::Interact, + GameInput::ToggleWield, + GameInput::Charge, + GameInput::FreeLook, + GameInput::Ability3, + GameInput::SwapLoadout, + ]; + for game_input in game_inputs { + new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input)); } + new_settings } } diff --git a/voxygen/src/window.rs b/voxygen/src/window.rs index f54e0eabf1..9b80288f37 100644 --- a/voxygen/src/window.rs +++ b/voxygen/src/window.rs @@ -1,7 +1,7 @@ use crate::{ controller::*, render::{Renderer, WinColorFmt, WinDepthFmt}, - settings::Settings, + settings::{ControlSettings, Settings}, ui, Error, }; use gilrs::{EventType, Gilrs}; @@ -12,7 +12,7 @@ use std::fmt; use vek::*; /// Represents a key that the game recognises after input mapping. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] pub enum GameInput { Primary, Secondary, @@ -34,8 +34,6 @@ pub enum GameInput { Escape, Map, Bag, - QuestLog, - CharacterWindow, Social, Spellbook, Settings, @@ -54,6 +52,49 @@ pub enum GameInput { FreeLook, } +impl GameInput { + pub fn get_localization_key(&self) -> &str { + match *self { + GameInput::Primary => "gameinput.primary", + GameInput::Secondary => "gameinput.secondary", + GameInput::ToggleCursor => "gameinput.togglecursor", + GameInput::MoveForward => "gameinput.moveforward", + GameInput::MoveLeft => "gameinput.moveleft", + GameInput::MoveRight => "gameinput.moveright", + GameInput::MoveBack => "gameinput.moveback", + GameInput::Jump => "gameinput.jump", + GameInput::Sit => "gameinput.sit", + GameInput::Glide => "gameinput.glide", + GameInput::Climb => "gameinput.climb", + GameInput::ClimbDown => "gameinput.climbdown", + GameInput::WallLeap => "gameinput.wallleap", + GameInput::Mount => "gameinput.mount", + GameInput::Enter => "gameinput.enter", + GameInput::Command => "gameinput.command", + GameInput::Escape => "gameinput.escape", + GameInput::Map => "gameinput.map", + GameInput::Bag => "gameinput.bag", + GameInput::Social => "gameinput.social", + GameInput::Spellbook => "gameinput.spellbook", + GameInput::Settings => "gameinput.settings", + GameInput::ToggleInterface => "gameinput.toggleinterface", + GameInput::Help => "gameinput.help", + GameInput::ToggleDebug => "gameinput.toggledebug", + GameInput::Fullscreen => "gameinput.fullscreen", + GameInput::Screenshot => "gameinput.screenshot", + GameInput::ToggleIngameUi => "gameinput.toggleingameui", + GameInput::Roll => "gameinput.roll", + GameInput::Respawn => "gameinput.respawn", + GameInput::Interact => "gameinput.interact", + GameInput::ToggleWield => "gameinput.togglewield", + GameInput::Charge => "gameinput.charge", + GameInput::FreeLook => "gameinput.freelook", + GameInput::Ability3 => "gameinput.ability3", + GameInput::SwapLoadout => "gameinput.swaploadout", + } + } +} + /// Represents a key that the game menus recognise after input mapping #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] pub enum MenuInput { @@ -319,8 +360,8 @@ pub struct Window { pub mouse_y_inversion: bool, fullscreen: bool, needs_refresh_resize: bool, - key_map: HashMap>, keypress_map: HashMap, + pub remapping_keybindings: Option, supplement_events: Vec, focused: bool, gilrs: Option, @@ -355,116 +396,6 @@ impl Window { ) .map_err(|err| Error::BackendError(Box::new(err)))?; - let mut map: HashMap<_, Vec<_>> = HashMap::new(); - map.entry(settings.controls.primary) - .or_default() - .push(GameInput::Primary); - map.entry(settings.controls.secondary) - .or_default() - .push(GameInput::Secondary); - map.entry(settings.controls.ability3) - .or_default() - .push(GameInput::Ability3); - map.entry(settings.controls.toggle_cursor) - .or_default() - .push(GameInput::ToggleCursor); - map.entry(settings.controls.escape) - .or_default() - .push(GameInput::Escape); - map.entry(settings.controls.enter) - .or_default() - .push(GameInput::Enter); - map.entry(settings.controls.command) - .or_default() - .push(GameInput::Command); - map.entry(settings.controls.move_forward) - .or_default() - .push(GameInput::MoveForward); - map.entry(settings.controls.move_left) - .or_default() - .push(GameInput::MoveLeft); - map.entry(settings.controls.move_back) - .or_default() - .push(GameInput::MoveBack); - map.entry(settings.controls.move_right) - .or_default() - .push(GameInput::MoveRight); - map.entry(settings.controls.jump) - .or_default() - .push(GameInput::Jump); - map.entry(settings.controls.sit) - .or_default() - .push(GameInput::Sit); - map.entry(settings.controls.glide) - .or_default() - .push(GameInput::Glide); - map.entry(settings.controls.climb) - .or_default() - .push(GameInput::Climb); - map.entry(settings.controls.climb_down) - .or_default() - .push(GameInput::ClimbDown); - map.entry(settings.controls.wall_leap) - .or_default() - .push(GameInput::WallLeap); - map.entry(settings.controls.mount) - .or_default() - .push(GameInput::Mount); - map.entry(settings.controls.map) - .or_default() - .push(GameInput::Map); - map.entry(settings.controls.bag) - .or_default() - .push(GameInput::Bag); - map.entry(settings.controls.social) - .or_default() - .push(GameInput::Social); - map.entry(settings.controls.spellbook) - .or_default() - .push(GameInput::Spellbook); - map.entry(settings.controls.settings) - .or_default() - .push(GameInput::Settings); - map.entry(settings.controls.help) - .or_default() - .push(GameInput::Help); - map.entry(settings.controls.toggle_interface) - .or_default() - .push(GameInput::ToggleInterface); - map.entry(settings.controls.toggle_debug) - .or_default() - .push(GameInput::ToggleDebug); - map.entry(settings.controls.fullscreen) - .or_default() - .push(GameInput::Fullscreen); - map.entry(settings.controls.screenshot) - .or_default() - .push(GameInput::Screenshot); - map.entry(settings.controls.toggle_ingame_ui) - .or_default() - .push(GameInput::ToggleIngameUi); - map.entry(settings.controls.roll) - .or_default() - .push(GameInput::Roll); - map.entry(settings.controls.respawn) - .or_default() - .push(GameInput::Respawn); - map.entry(settings.controls.interact) - .or_default() - .push(GameInput::Interact); - map.entry(settings.controls.toggle_wield) - .or_default() - .push(GameInput::ToggleWield); - map.entry(settings.controls.swap_loadout) - .or_default() - .push(GameInput::SwapLoadout); - map.entry(settings.controls.charge) - .or_default() - .push(GameInput::Charge); - map.entry(settings.controls.free_look) - .or_default() - .push(GameInput::FreeLook); - let keypress_map = HashMap::new(); let gilrs = match Gilrs::new() { @@ -510,8 +441,8 @@ impl Window { mouse_y_inversion: settings.gameplay.mouse_y_inversion, fullscreen: false, needs_refresh_resize: false, - key_map: map, keypress_map, + remapping_keybindings: None, supplement_events: vec![], focused: true, gilrs, @@ -543,8 +474,9 @@ impl Window { let cursor_grabbed = self.cursor_grabbed; let renderer = &mut self.renderer; let window = &mut self.window; + let remapping_keybindings = &mut self.remapping_keybindings; let focused = &mut self.focused; - let key_map = &self.key_map; + let controls = &mut settings.controls; let keypress_map = &mut self.keypress_map; let pan_sensitivity = self.pan_sensitivity; let zoom_sensitivity = self.zoom_sensitivity; @@ -577,23 +509,27 @@ impl Window { }, glutin::WindowEvent::ReceivedCharacter(c) => events.push(Event::Char(c)), glutin::WindowEvent::MouseInput { button, state, .. } => { - if let (true, Some(game_inputs)) = - (cursor_grabbed, key_map.get(&KeyMouse::Mouse(button))) - { + if let Some(game_inputs) = Window::map_input( + KeyMouse::Mouse(button), + controls, + remapping_keybindings, + ) { for game_input in game_inputs { events.push(Event::InputUpdate( *game_input, state == glutin::ElementState::Pressed, )); } + events.push(Event::MouseButton(button, state)); } - events.push(Event::MouseButton(button, state)); }, - glutin::WindowEvent::KeyboardInput { input, .. } => match input.virtual_keycode - { - Some(key) => { - let game_inputs = key_map.get(&KeyMouse::Key(key)); - if let Some(game_inputs) = game_inputs { + glutin::WindowEvent::KeyboardInput { input, .. } => { + if let Some(key) = input.virtual_keycode { + if let Some(game_inputs) = Window::map_input( + KeyMouse::Key(key), + controls, + remapping_keybindings, + ) { for game_input in game_inputs { match game_input { GameInput::Fullscreen => { @@ -631,9 +567,9 @@ impl Window { } } } - }, - _ => {}, + } }, + glutin::WindowEvent::Focused(state) => { *focused = state; events.push(Event::Focused(state)); @@ -1000,4 +936,33 @@ impl Window { ) { map.insert(input, state); } + + // Function used to handle Mouse and Key events. It first checks if we're in + // remapping mode for a specific GameInput. If we are, we modify the binding + // of that GameInput with the KeyMouse passed. Else, we return an iterator of + // the GameInputs for that KeyMouse. + fn map_input<'a>( + key_mouse: KeyMouse, + controls: &'a mut ControlSettings, + remapping: &mut Option, + ) -> Option> { + match *remapping { + Some(game_input) => { + controls.modify_binding(game_input, key_mouse); + *remapping = None; + None + }, + None => { + if let Some(game_inputs) = controls.get_associated_game_inputs(&key_mouse) { + Some(game_inputs.iter()) + } else { + None + } + }, + } + } + + pub fn set_keybinding_mode(&mut self, game_input: GameInput) { + self.remapping_keybindings = Some(game_input); + } }