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.awaitingkey": "Press a key...",
"hud.settings.unbound": "None",
"hud.settings.reset_keybinds": "Reset to Defaults",
"hud.social": "Social",
"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 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_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 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);
@ -282,6 +283,7 @@ pub enum Event {
Quit,
ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput),
ResetBindings,
ChangeFreeLookBehavior(PressBehavior),
ChangeAutoWalkBehavior(PressBehavior),
ChangeStopAutoWalkOnInput(bool),
@ -1744,6 +1746,9 @@ impl Hud {
settings_window::Event::ChangeBinding(game_input) => {
events.push(Event::ChangeBinding(game_input));
},
settings_window::Event::ResetBindings => {
events.push(Event::ResetBindings);
},
settings_window::Event::ChangeFreeLookBehavior(behavior) => {
events.push(Event::ChangeFreeLookBehavior(behavior));
},

View File

@ -1,6 +1,6 @@
use super::{
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar, MENU_BG,
TEXT_COLOR,
img_ids::Imgs, BarNumbers, CrosshairType, PressBehavior, ShortcutNumbers, Show, XpBar,
ERROR_COLOR, MENU_BG, TEXT_BIND_CONFLICT_COLOR, TEXT_COLOR,
};
use crate::{
i18n::{list_localizations, LanguageMetadata, VoxygenLocalization},
@ -32,6 +32,7 @@ widget_ids! {
settings_scrollbar,
controls_texts[],
controls_buttons[],
reset_controls_button,
controls_alignment_rectangle,
button_help,
button_help2,
@ -250,6 +251,7 @@ pub enum Event {
SpeechBubbleIcon(bool),
ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput),
ResetBindings,
ChangeFreeLookBehavior(PressBehavior),
ChangeAutoWalkBehavior(PressBehavior),
ChangeStopAutoWalkOnInput(bool),
@ -1512,23 +1514,25 @@ impl<'a> Widget for SettingsWindow<'a> {
// Contents
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;
if controls.keybindings.len() > state.ids.controls_texts.len()
|| controls.keybindings.len() > state.ids.controls_buttons.len()
if keybindings_vec.len() > state.ids.controls_texts.len()
|| keybindings_vec.len() > state.ids.controls_buttons.len()
{
state.update(|s| {
s.ids
.controls_texts
.resize(controls.keybindings.len(), &mut ui.widget_id_generator());
.resize(keybindings_vec.len(), &mut ui.widget_id_generator());
s.ids
.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
for (game_input, (&text_id, &button_id)) in keybindings_vec.into_iter().zip(
state
@ -1537,58 +1541,82 @@ impl<'a> Widget for SettingsWindow<'a> {
.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"))
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 {
key.to_string()
}
},
None => key.to_string(),
TEXT_COLOR
},
)
} else {
(
String::from(self.localized_strings.get("hud.settings.unbound")),
ERROR_COLOR,
)
};
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);
let loc_key = self
.localized_strings
.get(game_input.get_localization_key());
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(key_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_element_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_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
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)
.down_from(prev_id, 10.0)
.set(state.ids.controls_alignment_rectangle, ui);

View File

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

View File

@ -100,9 +100,23 @@ impl ControlSettings {
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 {
// If a new GameInput is added, be sure to update ControlSettings::default()
// too!
// If a new GameInput is added, be sure to update GameInput::iterator() too!
match game_input {
GameInput::Primary => KeyMouse::Mouse(MouseButton::Left),
GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right),
@ -157,68 +171,14 @@ impl ControlSettings {
}
}
}
impl Default for ControlSettings {
fn default() -> Self {
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::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 {
for game_input in GameInput::iterator() {
new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
}
new_settings

View File

@ -123,6 +123,85 @@ impl GameInput {
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