Prevent GameInputs from being bound to multiple keys unless explicitly allowed. Add a Reset to Defaults button for controls.

This commit is contained in:
Kai 2020-07-12 11:59:18 -07:00
parent 930e0028bc
commit 8e523364ac
6 changed files with 194 additions and 116 deletions

View File

@ -303,6 +303,8 @@ magically infused items?"#,
"hud.settings.audio_device": "Audio Device", "hud.settings.audio_device": "Audio Device",
"hud.settings.awaitingkey": "Press a key...", "hud.settings.awaitingkey": "Press a key...",
"hud.settings.unbound": "None",
"hud.settings.reset_keybinds": "Reset to Defaults",
"hud.social": "Social", "hud.social": "Social",
"hud.social.online": "Online", "hud.social.online": "Online",

View File

@ -67,6 +67,7 @@ const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4); const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4);
//const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); //const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1); const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1);
const TEXT_BIND_CONFLICT_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
//const BG_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 0.8); //const BG_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 0.8);
const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0); const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0);
@ -282,6 +283,7 @@ pub enum Event {
Quit, Quit,
ChangeLanguage(LanguageMetadata), ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput), ChangeBinding(GameInput),
ResetBindings,
ChangeFreeLookBehavior(PressBehavior), ChangeFreeLookBehavior(PressBehavior),
ChangeAutoWalkBehavior(PressBehavior), ChangeAutoWalkBehavior(PressBehavior),
ChangeStopAutoWalkOnInput(bool), ChangeStopAutoWalkOnInput(bool),
@ -1744,6 +1746,9 @@ impl Hud {
settings_window::Event::ChangeBinding(game_input) => { settings_window::Event::ChangeBinding(game_input) => {
events.push(Event::ChangeBinding(game_input)); events.push(Event::ChangeBinding(game_input));
}, },
settings_window::Event::ResetBindings => {
events.push(Event::ResetBindings);
},
settings_window::Event::ChangeFreeLookBehavior(behavior) => { settings_window::Event::ChangeFreeLookBehavior(behavior) => {
events.push(Event::ChangeFreeLookBehavior(behavior)); events.push(Event::ChangeFreeLookBehavior(behavior));
}, },

View File

@ -1,6 +1,6 @@
use super::{ use super::{
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar, MENU_BG, img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar,
TEXT_COLOR, ERROR_COLOR, MENU_BG, TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR,
}; };
use crate::{ use crate::{
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization}, i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
@ -32,6 +32,7 @@ widget_ids! {
settings_scrollbar, settings_scrollbar,
controls_texts[], controls_texts[],
controls_buttons[], controls_buttons[],
reset_controls_button,
controls_alignment_rectangle, controls_alignment_rectangle,
button_help, button_help,
button_help2, button_help2,
@ -250,6 +251,7 @@ pub enum Event {
SpeechBubbleIcon(bool), SpeechBubbleIcon(bool),
ChangeLanguage(LanguageMetadata), ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput), ChangeBinding(GameInput),
ResetBindings,
ChangeFreeLookBehavior(PressBehavior), ChangeFreeLookBehavior(PressBehavior),
ChangeAutoWalkBehavior(PressBehavior), ChangeAutoWalkBehavior(PressBehavior),
ChangeStopAutoWalkOnInput(bool), ChangeStopAutoWalkOnInput(bool),
@ -1512,23 +1514,25 @@ impl<'a> Widget for SettingsWindow<'a> {
// Contents // Contents
if let SettingsTab::Controls = self.show.settings_tab { if let SettingsTab::Controls = self.show.settings_tab {
// Used for sequential placement in a flow-down pattern
let mut previous_element_id = None;
let mut keybindings_vec: Vec<GameInput> = GameInput::iterator().collect();
keybindings_vec.sort();
let controls = &self.global_state.settings.controls; let controls = &self.global_state.settings.controls;
if controls.keybindings.len() > state.ids.controls_texts.len() if keybindings_vec.len() > state.ids.controls_texts.len()
|| controls.keybindings.len() > state.ids.controls_buttons.len() || keybindings_vec.len() > state.ids.controls_buttons.len()
{ {
state.update(|s| { state.update(|s| {
s.ids s.ids
.controls_texts .controls_texts
.resize(controls.keybindings.len(), &mut ui.widget_id_generator()); .resize(keybindings_vec.len(), &mut ui.widget_id_generator());
s.ids s.ids
.controls_buttons .controls_buttons
.resize(controls.keybindings.len(), &mut ui.widget_id_generator()); .resize(keybindings_vec.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 // 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( for (game_input, (&text_id, &button_id)) in keybindings_vec.into_iter().zip(
state state
@ -1537,28 +1541,37 @@ impl<'a> Widget for SettingsWindow<'a> {
.iter() .iter()
.zip(state.ids.controls_buttons.iter()), .zip(state.ids.controls_buttons.iter()),
) { ) {
if let Some(key) = controls.get_binding(*game_input) { let (key_string, key_color) =
if self.global_state.window.remapping_keybindings == Some(game_input) {
(
String::from(self.localized_strings.get("hud.settings.awaitingkey")),
TEXT_COLOR,
)
} else if let Some(key) = controls.get_binding(game_input) {
(
key.to_string(),
if controls.has_conflicting_bindings(key) {
TEXT_BIND_CONFLICT_COLOR
} else {
TEXT_COLOR
},
)
} else {
(
String::from(self.localized_strings.get("hud.settings.unbound")),
ERROR_COLOR,
)
};
let loc_key = self let loc_key = self
.localized_strings .localized_strings
.get(game_input.get_localization_key()); .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) let text_widget = Text::new(loc_key)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(18)); .font_size(self.fonts.cyri.scale(18));
let button_widget = Button::new() let button_widget = Button::new()
.label(&key_string) .label(&key_string)
.label_color(TEXT_COLOR) .label_color(key_color)
.label_font_id(self.fonts.cyri.conrod_id) .label_font_id(self.fonts.cyri.conrod_id)
.label_font_size(self.fonts.cyri.scale(15)) .label_font_size(self.fonts.cyri.scale(15))
.w(150.0) .w(150.0)
@ -1566,12 +1579,10 @@ impl<'a> Widget for SettingsWindow<'a> {
.border_rgba(0.0, 0.0, 0.0, 255.0) .border_rgba(0.0, 0.0, 0.0, 255.0)
.label_y(Relative::Scalar(3.0)); .label_y(Relative::Scalar(3.0));
// Place top-left if it's the first text, else under the previous one // Place top-left if it's the first text, else under the previous one
let text_widget = match previous_text_id { let text_widget = match previous_element_id {
None => text_widget.top_left_with_margins_on( None => {
state.ids.settings_content, text_widget.top_left_with_margins_on(state.ids.settings_content, 10.0, 5.0)
10.0, },
5.0,
),
Some(prev_id) => text_widget.down_from(prev_id, 10.0), Some(prev_id) => text_widget.down_from(prev_id, 10.0),
}; };
let text_width = text_widget.get_w(ui).unwrap_or(0.0); let text_width = text_widget.get_w(ui).unwrap_or(0.0);
@ -1581,14 +1592,31 @@ impl<'a> Widget for SettingsWindow<'a> {
.set(button_id, ui) .set(button_id, ui)
.was_clicked() .was_clicked()
{ {
events.push(Event::ChangeBinding(*game_input)); events.push(Event::ChangeBinding(game_input));
} }
// Set the previous id to the current one for the next cycle // Set the previous id to the current one for the next cycle
previous_text_id = Some(text_id); previous_element_id = Some(text_id);
} }
if let Some(prev_id) = previous_element_id {
let key_string = self.localized_strings.get("hud.settings.reset_keybinds");
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(18))
.down_from(prev_id, 20.0)
.w(200.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))
.set(state.ids.reset_controls_button, ui);
if button_widget.was_clicked() {
events.push(Event::ResetBindings);
}
previous_element_id = Some(state.ids.reset_controls_button)
} }
// Add an empty text widget to simulate some bottom margin, because conrod sucks // Add an empty text widget to simulate some bottom margin, because conrod sucks
if let Some(prev_id) = previous_text_id { if let Some(prev_id) = previous_element_id {
Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT) Rectangle::fill_with([1.0, 1.0], color::TRANSPARENT)
.down_from(prev_id, 10.0) .down_from(prev_id, 10.0)
.set(state.ids.controls_alignment_rectangle, ui); .set(state.ids.controls_alignment_rectangle, ui);

View File

@ -6,7 +6,7 @@ use crate::{
key_state::KeyState, key_state::KeyState,
menu::char_selection::CharSelectionState, menu::char_selection::CharSelectionState,
scene::{camera, Scene, SceneData}, scene::{camera, Scene, SceneData},
settings::AudioOutput, settings::{AudioOutput, ControlSettings},
window::{AnalogGameInput, Event, GameInput}, window::{AnalogGameInput, Event, GameInput},
Direction, Error, GlobalState, PlayState, PlayStateResult, Direction, Error, GlobalState, PlayState, PlayStateResult,
}; };
@ -922,6 +922,10 @@ impl PlayState for SessionState {
HudEvent::ChangeBinding(game_input) => { HudEvent::ChangeBinding(game_input) => {
global_state.window.set_keybinding_mode(game_input); global_state.window.set_keybinding_mode(game_input);
}, },
HudEvent::ResetBindings => {
global_state.settings.controls = ControlSettings::default();
global_state.settings.save_to_file_warn();
},
HudEvent::ChangeFreeLookBehavior(behavior) => { HudEvent::ChangeFreeLookBehavior(behavior) => {
global_state.settings.gameplay.free_look_behavior = behavior; global_state.settings.gameplay.free_look_behavior = behavior;
}, },

View File

@ -100,9 +100,23 @@ impl ControlSettings {
self.keybindings.insert(game_input, key_mouse); self.keybindings.insert(game_input, key_mouse);
} }
/// Return true if this key is used for multiple GameInputs that aren't
/// expected to be safe to have bound to the same key at the same time
pub fn has_conflicting_bindings(&self, key_mouse: KeyMouse) -> bool {
if let Some(game_inputs) = self.inverse_keybindings.get(&key_mouse) {
for a in game_inputs.iter() {
for b in game_inputs.iter() {
if !GameInput::can_share_bindings(*a, *b) {
return true;
}
}
}
}
false
}
pub fn default_binding(game_input: GameInput) -> KeyMouse { pub fn default_binding(game_input: GameInput) -> KeyMouse {
// If a new GameInput is added, be sure to update ControlSettings::default() // If a new GameInput is added, be sure to update GameInput::iterator() too!
// too!
match game_input { match game_input {
GameInput::Primary => KeyMouse::Mouse(MouseButton::Left), GameInput::Primary => KeyMouse::Mouse(MouseButton::Left),
GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right), GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right),
@ -157,68 +171,14 @@ impl ControlSettings {
} }
} }
} }
impl Default for ControlSettings { impl Default for ControlSettings {
fn default() -> Self { fn default() -> Self {
let mut new_settings = Self { let mut new_settings = Self {
keybindings: HashMap::new(), keybindings: HashMap::new(),
inverse_keybindings: HashMap::new(), inverse_keybindings: HashMap::new(),
}; };
// Sets the initial keybindings for those GameInputs. If a new one is created in for game_input in GameInput::iterator() {
// 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::Dance,
GameInput::Glide,
GameInput::Climb,
GameInput::ClimbDown,
GameInput::Swim,
//GameInput::WallLeap,
GameInput::ToggleLantern,
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::AutoWalk,
GameInput::CycleCamera,
GameInput::Slot1,
GameInput::Slot2,
GameInput::Slot3,
GameInput::Slot4,
GameInput::Slot5,
GameInput::Slot6,
GameInput::Slot7,
GameInput::Slot8,
GameInput::Slot9,
GameInput::Slot10,
GameInput::SwapLoadout,
];
for game_input in game_inputs {
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input)); new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
} }
new_settings new_settings

View File

@ -123,6 +123,85 @@ impl GameInput {
GameInput::SwapLoadout => "gameinput.swaploadout", GameInput::SwapLoadout => "gameinput.swaploadout",
} }
} }
pub fn iterator() -> impl Iterator<Item = GameInput> {
[
GameInput::Primary,
GameInput::Secondary,
GameInput::ToggleCursor,
GameInput::MoveForward,
GameInput::MoveLeft,
GameInput::MoveRight,
GameInput::MoveBack,
GameInput::Jump,
GameInput::Sit,
GameInput::Dance,
GameInput::Glide,
GameInput::Climb,
GameInput::ClimbDown,
GameInput::Swim,
GameInput::ToggleLantern,
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::FreeLook,
GameInput::AutoWalk,
GameInput::Slot1,
GameInput::Slot2,
GameInput::Slot3,
GameInput::Slot4,
GameInput::Slot5,
GameInput::Slot6,
GameInput::Slot7,
GameInput::Slot8,
GameInput::Slot9,
GameInput::Slot10,
GameInput::SwapLoadout,
]
.iter()
.copied()
}
/// Return true if `a` and `b` are able to be bound to the same key at the
/// same time without conflict. For example, the player can't jump and climb
/// at the same time, so these can be bound to the same key.
pub fn can_share_bindings(a: GameInput, b: GameInput) -> bool {
a.get_representative_binding() == b.get_representative_binding()
}
/// If two GameInputs are able to be bound at the same time, then they will
/// return the same value from this function (the representative value for
/// that set). This models the Find operation of a disjoint-set data
/// structure.
fn get_representative_binding(&self) -> GameInput {
match self {
GameInput::Jump => GameInput::Jump,
GameInput::Climb => GameInput::Jump,
GameInput::Swim => GameInput::Jump,
GameInput::Respawn => GameInput::Jump,
GameInput::FreeLook => GameInput::FreeLook,
GameInput::AutoWalk => GameInput::FreeLook,
_ => *self,
}
}
} }
/// Represents a key that the game menus recognise after input mapping /// Represents a key that the game menus recognise after input mapping