aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--game/addons/keychain/Keychain.gd212
-rw-r--r--game/addons/keychain/LICENSE21
-rw-r--r--game/addons/keychain/ShortcutEdit.gd416
-rw-r--r--game/addons/keychain/ShortcutEdit.tscn113
-rw-r--r--game/addons/keychain/ShortcutProfile.gd38
-rw-r--r--game/addons/keychain/ShortcutSelectorDialog.gd199
-rw-r--r--game/addons/keychain/ShortcutSelectorDialog.tscn46
-rw-r--r--game/addons/keychain/assets/add.svg1
-rw-r--r--game/addons/keychain/assets/add.svg.import37
-rw-r--r--game/addons/keychain/assets/close.svg1
-rw-r--r--game/addons/keychain/assets/close.svg.import37
-rw-r--r--game/addons/keychain/assets/edit.svg1
-rw-r--r--game/addons/keychain/assets/edit.svg.import37
-rw-r--r--game/addons/keychain/assets/folder.svg1
-rw-r--r--game/addons/keychain/assets/folder.svg.import37
-rw-r--r--game/addons/keychain/assets/joy_axis.svg1
-rw-r--r--game/addons/keychain/assets/joy_axis.svg.import37
-rw-r--r--game/addons/keychain/assets/joy_button.svg1
-rw-r--r--game/addons/keychain/assets/joy_button.svg.import37
-rw-r--r--game/addons/keychain/assets/keyboard.svg1
-rw-r--r--game/addons/keychain/assets/keyboard.svg.import37
-rw-r--r--game/addons/keychain/assets/keyboard_physical.svg1
-rw-r--r--game/addons/keychain/assets/keyboard_physical.svg.import37
-rw-r--r--game/addons/keychain/assets/mouse.svg1
-rw-r--r--game/addons/keychain/assets/mouse.svg.import37
-rw-r--r--game/addons/keychain/assets/shortcut.svg1
-rw-r--r--game/addons/keychain/assets/shortcut.svg.import37
-rw-r--r--game/addons/keychain/plugin.cfg7
-rw-r--r--game/addons/keychain/plugin.gd10
-rw-r--r--game/addons/keychain/profiles/default.tres9
-rw-r--r--game/addons/keychain/translations/README.md4
-rw-r--r--game/addons/keychain/translations/Translations.pot209
-rw-r--r--game/addons/keychain/translations/el_GR.po215
-rw-r--r--game/project.godot11
-rw-r--r--game/src/Autoload/Events.gd3
-rw-r--r--game/src/Autoload/Events/Options.gd22
-rw-r--r--game/src/Autoload/Resolution.gd55
-rw-r--r--game/src/GameMenu.gd2
-rw-r--r--game/src/LocaleButton.gd15
-rw-r--r--game/src/OptionMenu/ControlsTab.tscn14
-rw-r--r--game/src/OptionMenu/MonitorDisplaySelector.gd6
-rw-r--r--game/src/OptionMenu/OptionsMenu.gd48
-rw-r--r--game/src/OptionMenu/OptionsMenu.tscn130
-rw-r--r--game/src/OptionMenu/OtherTab.tscn18
-rw-r--r--game/src/OptionMenu/ResolutionSelector.gd62
-rw-r--r--game/src/OptionMenu/ScreenModeSelector.gd6
-rw-r--r--game/src/OptionMenu/SettingNodes/SettingHSlider.gd7
-rw-r--r--game/src/OptionMenu/SettingNodes/SettingOptionButton.gd51
-rw-r--r--game/src/OptionMenu/SoundTab.gd4
-rw-r--r--game/src/OptionMenu/SoundTab.tscn34
-rw-r--r--game/src/OptionMenu/VideoTab.tscn73
51 files changed, 2240 insertions, 200 deletions
diff --git a/game/addons/keychain/Keychain.gd b/game/addons/keychain/Keychain.gd
new file mode 100644
index 0000000..ff939f3
--- /dev/null
+++ b/game/addons/keychain/Keychain.gd
@@ -0,0 +1,212 @@
+extends Node
+
+const TRANSLATIONS_PATH := "res://addons/keychain/translations"
+const PROFILES_PATH := "user://shortcut_profiles"
+
+# Change these settings
+var profiles := [preload("profiles/default.tres")]
+var selected_profile: ShortcutProfile = profiles[0]
+var profile_index := 0
+# Syntax: "action_name": InputAction.new("Action Display Name", "Group", true)
+# Note that "action_name" must already exist in the Project's Input Map.
+var actions := {}
+# Syntax: "Group Name": InputGroup.new("Parent Group Name")
+var groups := {}
+var ignore_actions := []
+var ignore_ui_actions := true
+var changeable_types := [true, true, true, true]
+var multiple_menu_accelerators := false
+var config_path := "user://cache.ini"
+var config_file: ConfigFile
+
+
+class InputAction:
+ var display_name := ""
+ var group := ""
+ var global := true
+
+ func _init(_display_name := "",_group := "",_global := true):
+ display_name = _display_name
+ group = _group
+ global = _global
+
+ func update_node(_action: String) -> void:
+ pass
+
+ func handle_input(_event: InputEvent, _action: String) -> bool:
+ return false
+
+
+# This class is useful for the accelerators of PopupMenu items
+# It's possible for PopupMenu items to have multiple shortcuts by using
+# set_item_shortcut(), but we have no control over the accelerator text that appears.
+# Thus, we are stuck with using accelerators instead of shortcuts.
+# If Godot ever receives the ability to change the accelerator text of the items,
+# we could in theory remove this class.
+# If you don't care about PopupMenus in the same scene as ShortcutEdit
+# such as projects like Pixelorama where everything is in the same scene,
+# then you can ignore this class.
+class MenuInputAction:
+ extends InputAction
+ var node_path := ""
+ var node: PopupMenu
+ var menu_item_id := 0
+ var echo := false
+
+ func _init(
+ _display_name := "",
+ _group := "",
+ _global := true,
+ _node_path := "",
+ _menu_item_id := 0,
+ _echo := false
+ ) -> void:
+ super._init(_display_name, _group, _global)
+ node_path = _node_path
+ menu_item_id = _menu_item_id
+ echo = _echo
+
+ func get_node(root: Node) -> void:
+ var temp_node = root.get_node(node_path)
+ if temp_node is PopupMenu:
+ node = node
+ elif temp_node is MenuButton:
+ node = temp_node.get_popup()
+
+ func update_node(action: String) -> void:
+ if !node:
+ return
+ var first_key: InputEventKey = Keychain.action_get_first_key(action)
+ var accel := first_key.get_keycode_with_modifiers() if first_key else 0
+ node.set_item_accelerator(menu_item_id, accel)
+
+ func handle_input(event: InputEvent, action: String) -> bool:
+ if not node:
+ return false
+ if event.is_action_pressed(action, false, true):
+ if event is InputEventKey:
+ var acc: int = node.get_item_accelerator(menu_item_id)
+ # If the event is the same as the menu item's accelerator, skip
+ if acc == event.get_keycode_with_modifiers():
+ return true
+ node.emit_signal("id_pressed", menu_item_id)
+ return true
+ if event.is_action(action, true) and echo:
+ if event.is_echo():
+ node.emit_signal("id_pressed", menu_item_id)
+ return true
+
+ return false
+
+
+class InputGroup:
+ var parent_group := ""
+ var folded := true
+ var tree_item: TreeItem
+
+ func _init(_parent_group := "", _folded := true) -> void:
+ parent_group = _parent_group
+ folded = _folded
+
+
+func _ready() -> void:
+ if !config_file:
+ config_file = ConfigFile.new()
+ if !config_path.is_empty():
+ config_file.load(config_path)
+
+ set_process_input(multiple_menu_accelerators)
+
+ # Load shortcut profiles
+ DirAccess.make_dir_recursive_absolute(PROFILES_PATH)
+ var profile_dir := DirAccess.open(PROFILES_PATH)
+ profile_dir.make_dir(PROFILES_PATH)
+ profile_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
+ var file_name = profile_dir.get_next()
+ while file_name != "":
+ if !profile_dir.current_is_dir():
+ if file_name.get_extension() == "tres":
+ var file = load(PROFILES_PATH.path_join(file_name))
+ if file is ShortcutProfile:
+ profiles.append(file)
+ file_name = profile_dir.get_next()
+
+ # If there are no profiles besides the default, create one custom
+ if profiles.size() == 1:
+ var profile := ShortcutProfile.new()
+ profile.name = "Custom"
+ profile.resource_path = PROFILES_PATH.path_join("custom.tres")
+ var saved := profile.save()
+ if saved:
+ profiles.append(profile)
+
+ for profile in profiles:
+ profile.fill_bindings()
+
+ var l18n_dir := DirAccess.open(TRANSLATIONS_PATH)
+ l18n_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
+ file_name = l18n_dir.get_next()
+ while file_name != "":
+ if !l18n_dir.current_is_dir():
+ if file_name.get_extension() == "po":
+ var t: Translation = load(TRANSLATIONS_PATH.path_join(file_name))
+ TranslationServer.add_translation(t)
+ file_name = l18n_dir.get_next()
+
+ profile_index = config_file.get_value("shortcuts", "shortcuts_profile", 0)
+ change_profile(profile_index)
+
+ for action in actions:
+ var input_action: InputAction = actions[action]
+ if input_action is MenuInputAction:
+ input_action.get_node(get_tree().current_scene)
+
+
+func _input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ return
+
+ for action in actions:
+ var input_action: InputAction = actions[action]
+ var done: bool = input_action.handle_input(event, action)
+ if done:
+ return
+
+
+func change_profile(index: int) -> void:
+ if index >= profiles.size():
+ index = profiles.size() - 1
+ profile_index = index
+ selected_profile = profiles[index]
+ for action in selected_profile.bindings:
+ action_erase_events(action)
+ for event in selected_profile.bindings[action]:
+ action_add_event(action, event)
+
+
+func action_add_event(action: String, event: InputEvent) -> void:
+ InputMap.action_add_event(action, event)
+ if action in actions:
+ actions[action].update_node(action)
+
+
+func action_erase_event(action: String, event: InputEvent) -> void:
+ InputMap.action_erase_event(action, event)
+ if action in actions:
+ actions[action].update_node(action)
+
+
+func action_erase_events(action: String) -> void:
+ InputMap.action_erase_events(action)
+ if action in actions:
+ actions[action].update_node(action)
+
+
+func action_get_first_key(action: String) -> InputEventKey:
+ var first_key: InputEventKey = null
+ var events := InputMap.action_get_events(action)
+ for event in events:
+ if event is InputEventKey:
+ first_key = event
+ break
+ return first_key
diff --git a/game/addons/keychain/LICENSE b/game/addons/keychain/LICENSE
new file mode 100644
index 0000000..5deb9a7
--- /dev/null
+++ b/game/addons/keychain/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Orama Interactive
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/game/addons/keychain/ShortcutEdit.gd b/game/addons/keychain/ShortcutEdit.gd
new file mode 100644
index 0000000..fe4e69f
--- /dev/null
+++ b/game/addons/keychain/ShortcutEdit.gd
@@ -0,0 +1,416 @@
+extends Control
+
+enum { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS }
+
+const MOUSE_BUTTON_NAMES := [
+ "Left Button",
+ "Right Button",
+ "Middle Button",
+ "Wheel Up Button",
+ "Wheel Down Button",
+ "Wheel Left Button",
+ "Wheel Right Button",
+ "X Button 1",
+ "X Button 2",
+]
+
+const JOY_BUTTON_NAMES := [
+ "DualShock Cross, Xbox A, Nintendo B",
+ "DualShock Circle, Xbox B, Nintendo A",
+ "DualShock Square, Xbox X, Nintendo Y",
+ "DualShock Triangle, Xbox Y, Nintendo X",
+ "L, L1",
+ "R, R1",
+ "L2",
+ "R2",
+ "L3",
+ "R3",
+ "Select, DualShock Share, Nintendo -",
+ "Start, DualShock Options, Nintendo +",
+ "D-Pad Up",
+ "D-Pad Down",
+ "D-Pad Left",
+ "D-Pad Right",
+ "Home, DualShock PS, Guide",
+ "Xbox Share, PS5 Microphone, Nintendo Capture",
+ "Xbox Paddle 1",
+ "Xbox Paddle 2",
+ "Xbox Paddle 3",
+ "Xbox Paddle 4",
+ "PS4/5 Touchpad",
+]
+
+const JOY_AXIS_NAMES := [
+ "(Left Stick Left)",
+ "(Left Stick Right)",
+ "(Left Stick Up)",
+ "(Left Stick Down)",
+ "(Right Stick Left)",
+ "(Right Stick Right)",
+ "(Right Stick Up)",
+ "(Right Stick Down)",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "(L2)",
+ "",
+ "(R2)",
+ "",
+ "",
+ "",
+ "",
+]
+
+var currently_editing_tree_item: TreeItem
+var is_editing := false
+# Textures taken from Godot https://github.com/godotengine/godot/tree/master/editor/icons
+var add_tex: Texture2D = preload("assets/add.svg")
+var edit_tex: Texture2D = preload("assets/edit.svg")
+var delete_tex: Texture2D = preload("assets/close.svg")
+var joy_axis_tex: Texture2D = preload("assets/joy_axis.svg")
+var joy_button_tex: Texture2D = preload("assets/joy_button.svg")
+var key_tex: Texture2D = preload("assets/keyboard.svg")
+var key_phys_tex: Texture2D = preload("assets/keyboard_physical.svg")
+var mouse_tex: Texture2D = preload("assets/mouse.svg")
+var shortcut_tex: Texture2D = preload("assets/shortcut.svg")
+var folder_tex: Texture2D = preload("assets/folder.svg")
+
+@onready var tree: Tree = $VBoxContainer/ShortcutTree
+@onready var profile_option_button: OptionButton = find_child("ProfileOptionButton")
+@onready var rename_profile_button: Button = find_child("RenameProfile")
+@onready var delete_profile_button: Button = find_child("DeleteProfile")
+@onready var shortcut_type_menu: PopupMenu = $ShortcutTypeMenu
+@onready var keyboard_shortcut_selector: ConfirmationDialog = $KeyboardShortcutSelectorDialog
+@onready var mouse_shortcut_selector: ConfirmationDialog = $MouseShortcutSelectorDialog
+@onready var joy_key_shortcut_selector: ConfirmationDialog = $JoyKeyShortcutSelectorDialog
+@onready var joy_axis_shortcut_selector: ConfirmationDialog = $JoyAxisShortcutSelectorDialog
+@onready var profile_settings: ConfirmationDialog = $ProfileSettings
+@onready var profile_name: LineEdit = $ProfileSettings/ProfileName
+@onready var delete_confirmation: ConfirmationDialog = $DeleteConfirmation
+
+
+func _ready() -> void:
+ for profile in Keychain.profiles:
+ profile_option_button.add_item(profile.name)
+
+ _fill_selector_options()
+
+ # Remove input types that are not changeable
+ var i := 0
+ for type in Keychain.changeable_types:
+ if !type:
+ shortcut_type_menu.remove_item(i)
+ else:
+ i += 1
+
+ profile_option_button.select(Keychain.profile_index)
+ _on_ProfileOptionButton_item_selected(Keychain.profile_index)
+ if OS.get_name() == "HTML5":
+ $VBoxContainer/HBoxContainer/OpenProfileFolder.queue_free()
+
+
+func _construct_tree() -> void:
+ var buttons_disabled := false if Keychain.selected_profile.customizable else true
+ var tree_root: TreeItem = tree.create_item()
+ for group in Keychain.groups: # Create groups
+ var input_group: Keychain.InputGroup = Keychain.groups[group]
+ _create_group_tree_item(input_group, group)
+
+ for action in InputMap.get_actions(): # Fill the tree with actions and their events
+ if action in Keychain.ignore_actions:
+ continue
+ if Keychain.ignore_ui_actions and (action as String).begins_with("ui_"):
+ continue
+
+ var display_name := get_action_name(action)
+ var group_name := ""
+ if action in Keychain.actions:
+ var input_action: Keychain.InputAction = Keychain.actions[action]
+ group_name = input_action.group
+
+ var tree_item: TreeItem
+ if not group_name.is_empty() and group_name in Keychain.groups:
+ var input_group: Keychain.InputGroup = Keychain.groups[group_name]
+ var group_root: TreeItem = input_group.tree_item
+ tree_item = tree.create_item(group_root)
+
+ else:
+ tree_item = tree.create_item(tree_root)
+
+ tree_item.set_text(0, display_name)
+ tree_item.set_metadata(0, action)
+ tree_item.set_icon(0, shortcut_tex)
+ for event in InputMap.action_get_events(action):
+ add_event_tree_item(event, tree_item)
+
+ tree_item.add_button(0, add_tex, 0, buttons_disabled, "Add")
+ tree_item.add_button(0, delete_tex, 1, buttons_disabled, "Delete")
+ tree_item.collapsed = true
+
+
+func _fill_selector_options() -> void:
+ keyboard_shortcut_selector.entered_shortcut.visible = true
+ keyboard_shortcut_selector.option_button.visible = false
+ mouse_shortcut_selector.input_type_l.text = "Mouse Button Index:"
+ joy_key_shortcut_selector.input_type_l.text = "Joypad Button Index:"
+ joy_axis_shortcut_selector.input_type_l.text = "Joypad Axis Index:"
+
+ var mouse_option_button: OptionButton = mouse_shortcut_selector.option_button
+ for option in MOUSE_BUTTON_NAMES:
+ mouse_option_button.add_item(option)
+
+ var joy_key_option_button: OptionButton = joy_key_shortcut_selector.option_button
+ for i in JOY_BUTTON_MAX:
+ var text: String = tr("Button") + " %s" % i
+ if i < JOY_BUTTON_NAMES.size():
+ text += " (%s)" % tr(JOY_BUTTON_NAMES[i])
+ joy_key_option_button.add_item(text)
+
+ var joy_axis_option_button: OptionButton = joy_axis_shortcut_selector.option_button
+ var i := 0.0
+ for option in JOY_AXIS_NAMES:
+ var sign_symbol = "+" if floor(i) != i else "-"
+ var text: String = tr("Axis") + " %s %s %s" % [floor(i), sign_symbol, tr(option)]
+ joy_axis_option_button.add_item(text)
+ i += 0.5
+
+
+func _create_group_tree_item(group: Keychain.InputGroup, group_name: String) -> void:
+ if group.tree_item:
+ return
+
+ var group_root: TreeItem
+ if group.parent_group:
+ var parent_group: Keychain.InputGroup = Keychain.groups[group.parent_group]
+ _create_group_tree_item(parent_group, group.parent_group)
+ group_root = tree.create_item(parent_group.tree_item)
+ else:
+ group_root = tree.create_item(tree.get_root())
+ group_root.set_text(0, group_name)
+ group_root.set_icon(0, folder_tex)
+ group.tree_item = group_root
+ if group.folded:
+ group_root.collapsed = true
+
+
+func get_action_name(action: String) -> String:
+ var display_name := ""
+ if action in Keychain.actions:
+ display_name = Keychain.actions[action].display_name
+
+ if display_name.is_empty():
+ display_name = _humanize_snake_case(action)
+ return display_name
+
+
+func _humanize_snake_case(text: String) -> String:
+ text = text.replace("_", " ")
+ var first_letter := text.left(1)
+ first_letter = first_letter.capitalize()
+ text = text.right(-1)
+ text = text.insert(0, first_letter)
+ return text
+
+
+func add_event_tree_item(event: InputEvent, action_tree_item: TreeItem) -> void:
+ var event_class := event.get_class()
+ match event_class:
+ "InputEventKey":
+ if !Keychain.changeable_types[0]:
+ return
+ "InputEventMouseButton":
+ if !Keychain.changeable_types[1]:
+ return
+ "InputEventJoypadButton":
+ if !Keychain.changeable_types[2]:
+ return
+ "InputEventJoypadMotion":
+ if !Keychain.changeable_types[3]:
+ return
+
+ var buttons_disabled := false if Keychain.selected_profile.customizable else true
+ var event_tree_item: TreeItem = tree.create_item(action_tree_item)
+ event_tree_item.set_text(0, event_to_str(event))
+ event_tree_item.set_metadata(0, event)
+ match event_class:
+ "InputEventKey":
+ var scancode: int = event.get_keycode_with_modifiers()
+ if scancode > 0:
+ event_tree_item.set_icon(0, key_tex)
+ else:
+ event_tree_item.set_icon(0, key_phys_tex)
+ "InputEventMouseButton":
+ event_tree_item.set_icon(0, mouse_tex)
+ "InputEventJoypadButton":
+ event_tree_item.set_icon(0, joy_button_tex)
+ "InputEventJoypadMotion":
+ event_tree_item.set_icon(0, joy_axis_tex)
+ event_tree_item.add_button(0, edit_tex, 0, buttons_disabled, "Edit")
+ event_tree_item.add_button(0, delete_tex, 1, buttons_disabled, "Delete")
+
+
+func event_to_str(event: InputEvent) -> String:
+ var output := ""
+ if event is InputEventKey:
+ var scancode: int = 0
+ if event.keycode != 0:
+ scancode = event.get_keycode_with_modifiers()
+ var physical_str := ""
+ if scancode == 0:
+ scancode = event.get_physical_keycode_with_modifiers()
+ physical_str = " " + tr("(Physical)")
+ output = OS.get_keycode_string(scancode) + physical_str
+
+ elif event is InputEventMouseButton:
+ output = tr(MOUSE_BUTTON_NAMES[event.button_index - 1])
+
+ elif event is InputEventJoypadButton:
+ var button_index: int = event.button_index
+ output = tr("Button")
+ if button_index >= JOY_BUTTON_NAMES.size():
+ output += " %s" % button_index
+ else:
+ output += " %s (%s)" % [button_index, tr(JOY_BUTTON_NAMES[button_index])]
+
+ elif event is InputEventJoypadMotion:
+ var positive_axis: bool = event.axis_value > 0
+ var axis_value: int = event.axis * 2 + int(positive_axis)
+ var sign_symbol = "+" if positive_axis else "-"
+ output = tr("Axis")
+ output += " %s %s %s" % [event.axis, sign_symbol, tr(JOY_AXIS_NAMES[axis_value])]
+ return output
+
+
+func _on_shortcut_tree_button_clicked(item: TreeItem, _column: int, id: int, _mbi: int) -> void:
+ var action = item.get_metadata(0)
+ currently_editing_tree_item = item
+ if action is StringName:
+ if id == 0: # Add
+ var rect: Rect2 = tree.get_item_area_rect(item, 0)
+ rect.position.x = rect.end.x - 42
+ rect.position.y += 42 - tree.get_scroll().y
+ rect.position += global_position
+ rect.size = Vector2(110, 23 * shortcut_type_menu.get_item_count())
+ shortcut_type_menu.popup(rect)
+ elif id == 1: # Delete
+ Keychain.action_erase_events(action)
+ Keychain.selected_profile.change_action(action)
+ for child in item.get_children():
+ child.free()
+
+ elif action is InputEvent:
+ var parent_action = item.get_parent().get_metadata(0)
+ if id == 0: # Edit
+ if action is InputEventKey:
+ keyboard_shortcut_selector.popup_centered()
+ elif action is InputEventMouseButton:
+ mouse_shortcut_selector.popup_centered()
+ elif action is InputEventJoypadButton:
+ joy_key_shortcut_selector.popup_centered()
+ elif action is InputEventJoypadMotion:
+ joy_axis_shortcut_selector.popup_centered()
+ elif id == 1: # Delete
+ if not parent_action is StringName:
+ return
+ Keychain.action_erase_event(parent_action, action)
+ Keychain.selected_profile.change_action(parent_action)
+ item.free()
+
+
+func _on_ShortcutTree_item_activated() -> void:
+ var selected_item: TreeItem = tree.get_selected()
+ if selected_item.get_button_count(0) > 0 and !selected_item.is_button_disabled(0, 0):
+ _on_shortcut_tree_button_clicked(tree.get_selected(), 0, 0, 0)
+
+
+func _on_ShortcutTypeMenu_id_pressed(id: int) -> void:
+ if id == KEYBOARD:
+ keyboard_shortcut_selector.popup_centered()
+ elif id == MOUSE:
+ mouse_shortcut_selector.popup_centered()
+ elif id == JOY_BUTTON:
+ joy_key_shortcut_selector.popup_centered()
+ elif id == JOY_AXIS:
+ joy_axis_shortcut_selector.popup_centered()
+
+
+func _on_ProfileOptionButton_item_selected(index: int) -> void:
+ Keychain.change_profile(index)
+ rename_profile_button.disabled = false if Keychain.selected_profile.customizable else true
+ delete_profile_button.disabled = false if Keychain.selected_profile.customizable else true
+
+ # Re-construct the tree
+ for group in Keychain.groups:
+ Keychain.groups[group].tree_item = null
+ tree.clear()
+ _construct_tree()
+ Keychain.config_file.set_value("shortcuts", "shortcuts_profile", index)
+ Keychain.config_file.save(Keychain.config_path)
+
+
+func _on_NewProfile_pressed() -> void:
+ is_editing = false
+ profile_name.text = "New Shortcut Profile"
+ profile_settings.title = "New Shortcut Profile"
+ profile_settings.popup_centered()
+
+
+func _on_RenameProfile_pressed() -> void:
+ is_editing = true
+ profile_name.text = Keychain.selected_profile.name
+ profile_settings.title = "Rename Shortcut Profile"
+ profile_settings.popup_centered()
+
+
+func _on_DeleteProfile_pressed() -> void:
+ delete_confirmation.popup_centered()
+
+
+func _on_OpenProfileFolder_pressed() -> void:
+ OS.shell_open(ProjectSettings.globalize_path(Keychain.PROFILES_PATH))
+
+
+func _on_ProfileSettings_confirmed() -> void:
+ var file_name := profile_name.text + ".tres"
+ var profile := ShortcutProfile.new()
+ profile.name = profile_name.text
+ profile.resource_path = Keychain.PROFILES_PATH.path_join(file_name)
+ profile.fill_bindings()
+ var saved := profile.save()
+ if not saved:
+ return
+
+ if is_editing:
+ var old_file_name: String = Keychain.selected_profile.resource_path
+ if old_file_name != file_name:
+ _delete_profile_file(old_file_name)
+ Keychain.profiles[Keychain.profile_index] = profile
+ profile_option_button.set_item_text(Keychain.profile_index, profile.name)
+ else: # Add new shortcut profile
+ Keychain.profiles.append(profile)
+ profile_option_button.add_item(profile.name)
+ Keychain.profile_index = Keychain.profiles.size() - 1
+ profile_option_button.select(Keychain.profile_index)
+ _on_ProfileOptionButton_item_selected(Keychain.profile_index)
+
+
+func _delete_profile_file(file_name: String) -> void:
+ var dir := DirAccess.open(file_name.get_base_dir())
+ var err := dir.get_open_error()
+ if err != OK:
+ print("Error deleting shortcut profile %s. Error code: %s" % [file_name, err])
+ return
+ dir.remove(file_name)
+
+
+func _on_DeleteConfirmation_confirmed() -> void:
+ _delete_profile_file(Keychain.selected_profile.resource_path)
+ profile_option_button.remove_item(Keychain.profile_index)
+ Keychain.profiles.remove_at(Keychain.profile_index)
+ Keychain.profile_index -= 1
+ if Keychain.profile_index < 0:
+ Keychain.profile_index = 0
+ profile_option_button.select(Keychain.profile_index)
+ _on_ProfileOptionButton_item_selected(Keychain.profile_index)
diff --git a/game/addons/keychain/ShortcutEdit.tscn b/game/addons/keychain/ShortcutEdit.tscn
new file mode 100644
index 0000000..daeb0b8
--- /dev/null
+++ b/game/addons/keychain/ShortcutEdit.tscn
@@ -0,0 +1,113 @@
+[gd_scene load_steps=7 format=3 uid="uid://bq7ibhm0txl5p"]
+
+[ext_resource type="Script" path="res://addons/keychain/ShortcutEdit.gd" id="1"]
+[ext_resource type="Texture2D" uid="uid://ca58ufal2ufd8" path="res://addons/keychain/assets/joy_button.svg" id="2"]
+[ext_resource type="Texture2D" uid="uid://c2s5rm4nec5yh" path="res://addons/keychain/assets/keyboard.svg" id="3"]
+[ext_resource type="Texture2D" uid="uid://bb6q6om3d08cm" path="res://addons/keychain/assets/joy_axis.svg" id="4"]
+[ext_resource type="Texture2D" uid="uid://bma7xj2rqqcr8" path="res://addons/keychain/assets/mouse.svg" id="5"]
+[ext_resource type="PackedScene" uid="uid://bfjcafe2kvx7n" path="res://addons/keychain/ShortcutSelectorDialog.tscn" id="6"]
+
+[node name="ShortcutEdit" type="VBoxContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_vertical = 3
+script = ExtResource("1")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+
+[node name="ProfileLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+text = "Shortcut profile:"
+
+[node name="ProfileOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+
+[node name="NewProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "New"
+
+[node name="RenameProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "Rename"
+
+[node name="DeleteProfile" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "Delete"
+
+[node name="OpenProfileFolder" type="Button" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+mouse_default_cursor_shape = 2
+text = "Open Folder"
+
+[node name="ShortcutTree" type="Tree" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+hide_root = true
+
+[node name="ShortcutTypeMenu" type="PopupMenu" parent="."]
+size = Vector2i(154, 116)
+item_count = 4
+item_0/text = "Key"
+item_0/icon = ExtResource("3")
+item_0/id = 0
+item_1/text = "Mouse Button"
+item_1/icon = ExtResource("5")
+item_1/id = 1
+item_2/text = "Joy Button"
+item_2/icon = ExtResource("2")
+item_2/id = 2
+item_3/text = "Joy Axis"
+item_3/icon = ExtResource("4")
+item_3/id = 3
+
+[node name="KeyboardShortcutSelectorDialog" parent="." instance=ExtResource("6")]
+size = Vector2i(417, 134)
+
+[node name="MouseShortcutSelectorDialog" parent="." instance=ExtResource("6")]
+size = Vector2i(417, 134)
+input_type = 1
+
+[node name="JoyKeyShortcutSelectorDialog" parent="." instance=ExtResource("6")]
+size = Vector2i(417, 134)
+input_type = 2
+
+[node name="JoyAxisShortcutSelectorDialog" parent="." instance=ExtResource("6")]
+size = Vector2i(417, 134)
+input_type = 3
+
+[node name="ProfileSettings" type="ConfirmationDialog" parent="."]
+
+[node name="ProfileName" type="LineEdit" parent="ProfileSettings"]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 192.0
+offset_bottom = 51.0
+caret_blink = true
+caret_blink_interval = 0.5
+
+[node name="DeleteConfirmation" type="ConfirmationDialog" parent="."]
+size = Vector2i(427, 100)
+dialog_text = "Are you sure you want to delete this shortcut profile?"
+
+[connection signal="item_selected" from="VBoxContainer/HBoxContainer/ProfileOptionButton" to="." method="_on_ProfileOptionButton_item_selected"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer/NewProfile" to="." method="_on_NewProfile_pressed"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer/RenameProfile" to="." method="_on_RenameProfile_pressed"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer/DeleteProfile" to="." method="_on_DeleteProfile_pressed"]
+[connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenProfileFolder" to="." method="_on_OpenProfileFolder_pressed"]
+[connection signal="button_clicked" from="VBoxContainer/ShortcutTree" to="." method="_on_shortcut_tree_button_clicked"]
+[connection signal="item_activated" from="VBoxContainer/ShortcutTree" to="." method="_on_ShortcutTree_item_activated"]
+[connection signal="id_pressed" from="ShortcutTypeMenu" to="." method="_on_ShortcutTypeMenu_id_pressed"]
+[connection signal="confirmed" from="ProfileSettings" to="." method="_on_ProfileSettings_confirmed"]
+[connection signal="confirmed" from="DeleteConfirmation" to="." method="_on_DeleteConfirmation_confirmed"]
diff --git a/game/addons/keychain/ShortcutProfile.gd b/game/addons/keychain/ShortcutProfile.gd
new file mode 100644
index 0000000..0fb269a
--- /dev/null
+++ b/game/addons/keychain/ShortcutProfile.gd
@@ -0,0 +1,38 @@
+class_name ShortcutProfile
+extends Resource
+
+@export var name := ""
+@export var customizable := true
+@export var bindings := {}
+
+
+func _init() -> void:
+ bindings = bindings.duplicate(true)
+
+
+func fill_bindings() -> void:
+ var unnecessary_actions = bindings.duplicate() # Checks if the profile has any unused actions
+ for action in InputMap.get_actions():
+ if not action in bindings:
+ bindings[action] = InputMap.action_get_events(action)
+ unnecessary_actions.erase(action)
+ for action in unnecessary_actions:
+ bindings.erase(action)
+ save()
+
+
+func change_action(action: String) -> void:
+ if not customizable:
+ return
+ bindings[action] = InputMap.action_get_events(action)
+ save()
+
+
+func save() -> bool:
+ if !customizable:
+ return false
+ var err := ResourceSaver.save(self, resource_path)
+ if err != OK:
+ print("Error saving shortcut profile %s. Error code: %s" % [resource_path, err])
+ return false
+ return true
diff --git a/game/addons/keychain/ShortcutSelectorDialog.gd b/game/addons/keychain/ShortcutSelectorDialog.gd
new file mode 100644
index 0000000..38cb555
--- /dev/null
+++ b/game/addons/keychain/ShortcutSelectorDialog.gd
@@ -0,0 +1,199 @@
+extends ConfirmationDialog
+
+enum InputTypes { KEYBOARD, MOUSE, JOY_BUTTON, JOY_AXIS }
+
+@export var input_type: InputTypes = InputTypes.KEYBOARD
+var listened_input: InputEvent
+
+@onready var root: Node = get_parent()
+@onready var input_type_l: Label = $VBoxContainer/InputTypeLabel
+@onready var entered_shortcut: LineEdit = $VBoxContainer/EnteredShortcut
+@onready var option_button: OptionButton = $VBoxContainer/OptionButton
+@onready var already_exists: Label = $VBoxContainer/AlreadyExistsLabel
+
+
+func _ready() -> void:
+ set_process_input(false)
+ if input_type == InputTypes.KEYBOARD:
+ get_ok_button().focus_neighbor_top = entered_shortcut.get_path()
+ get_cancel_button().focus_neighbor_top = entered_shortcut.get_path()
+ entered_shortcut.focus_neighbor_bottom = get_ok_button().get_path()
+ else:
+ get_ok_button().focus_neighbor_top = option_button.get_path()
+ get_cancel_button().focus_neighbor_top = option_button.get_path()
+ option_button.focus_neighbor_bottom = get_ok_button().get_path()
+
+# get_close_button().focus_mode = Control.FOCUS_NONE
+
+
+func _input(event: InputEvent) -> void:
+ if not event is InputEventKey:
+ return
+ if event.pressed:
+ listened_input = event
+ entered_shortcut.text = OS.get_keycode_string(event.get_keycode_with_modifiers())
+ _show_assigned_state(event)
+
+
+func _show_assigned_state(event: InputEvent) -> void:
+ var metadata = root.currently_editing_tree_item.get_metadata(0)
+ var action := ""
+ if metadata is InputEvent: # Editing an input event
+ action = root.currently_editing_tree_item.get_parent().get_metadata(0)
+ elif metadata is StringName: # Adding a new input event to an action
+ action = metadata
+
+ var matching_pair: Array = _find_matching_event_in_map(action, event)
+ if matching_pair:
+ already_exists.text = tr("Already assigned to: %s") % root.get_action_name(matching_pair[0])
+ else:
+ already_exists.text = ""
+
+
+func _on_ShortcutSelectorDialog_confirmed() -> void:
+ if listened_input == null:
+ return
+ _apply_shortcut_change(listened_input)
+
+
+func _apply_shortcut_change(input_event: InputEvent) -> void:
+ var metadata = root.currently_editing_tree_item.get_metadata(0)
+ if metadata is InputEvent: # Editing an input event
+ var parent_metadata = root.currently_editing_tree_item.get_parent().get_metadata(0)
+ var changed: bool = _set_shortcut(parent_metadata, metadata, input_event)
+ if !changed:
+ return
+ root.currently_editing_tree_item.set_metadata(0, input_event)
+ root.currently_editing_tree_item.set_text(0, root.event_to_str(input_event))
+ elif metadata is StringName: # Adding a new input event to an action
+ var changed: bool = _set_shortcut(metadata, null, input_event)
+ if !changed:
+ return
+ root.add_event_tree_item(input_event, root.currently_editing_tree_item)
+
+
+func _set_shortcut(action: StringName, old_event: InputEvent, new_event: InputEvent) -> bool:
+ if InputMap.action_has_event(action, new_event): # If the current action already has that event
+ return false
+ if old_event:
+ Keychain.action_erase_event(action, old_event)
+
+ # Loop through other actions to see if the event exists there, to re-assign it
+ var matching_pair := _find_matching_event_in_map(action, new_event)
+
+ if matching_pair:
+ var group := ""
+ if action in Keychain.actions:
+ group = Keychain.actions[action].group
+
+ var action_to_replace: StringName = matching_pair[0]
+ var input_to_replace: InputEvent = matching_pair[1]
+ Keychain.action_erase_event(action_to_replace, input_to_replace)
+ Keychain.selected_profile.change_action(action_to_replace)
+ var tree_item: TreeItem = root.tree.get_root()
+ var prev_tree_item: TreeItem
+ while tree_item != null: # Loop through Tree's TreeItems...
+ var metadata = tree_item.get_metadata(0)
+ if metadata is InputEvent:
+ if input_to_replace.is_match(metadata):
+ var map_action: StringName = tree_item.get_parent().get_metadata(0)
+ if map_action == action_to_replace:
+ tree_item.free()
+ break
+
+ tree_item = _get_next_tree_item(tree_item)
+
+ Keychain.action_add_event(action, new_event)
+ Keychain.selected_profile.change_action(action)
+ return true
+
+
+# Based on https://github.com/godotengine/godot/blob/master/scene/gui/tree.cpp#L685
+func _get_next_tree_item(current: TreeItem) -> TreeItem:
+ if current.get_first_child():
+ current = current.get_first_child()
+ elif current.get_next():
+ current = current.get_next()
+ else:
+ while current and !current.get_next():
+ current = current.get_parent()
+ if current:
+ current = current.get_next()
+ return current
+
+
+func _find_matching_event_in_map(action: StringName, event: InputEvent) -> Array:
+ var group := ""
+ if action in Keychain.actions:
+ group = Keychain.actions[action].group
+
+ for map_action in InputMap.get_actions():
+ if map_action in Keychain.ignore_actions:
+ continue
+ if Keychain.ignore_ui_actions and (map_action as String).begins_with("ui_"):
+ continue
+ for map_event in InputMap.action_get_events(map_action):
+ if !event.is_match(map_event):
+ continue
+
+ if map_action in Keychain.actions:
+ # If it's local, check if it's the same group, otherwise ignore
+ if !Keychain.actions[action].global or !Keychain.actions[map_action].global:
+ if Keychain.actions[map_action].group != group:
+ continue
+
+ return [map_action, map_event]
+
+ return []
+
+
+func _on_ShortcutSelectorDialog_about_to_show() -> void:
+ if input_type == InputTypes.KEYBOARD:
+ listened_input = null
+ already_exists.text = ""
+ entered_shortcut.text = ""
+ await get_tree().process_frame
+ entered_shortcut.grab_focus()
+ else:
+ var metadata = root.currently_editing_tree_item.get_metadata(0)
+ if metadata is InputEvent: # Editing an input event
+ var index := 0
+ if metadata is InputEventMouseButton:
+ index = metadata.button_index - 1
+ elif metadata is InputEventJoypadButton:
+ index = metadata.button_index
+ elif metadata is InputEventJoypadMotion:
+ index = metadata.axis * 2
+ index += round(metadata.axis_value) / 2.0 + 0.5
+ option_button.select(index)
+ _on_OptionButton_item_selected(index)
+
+ elif metadata is StringName: # Adding a new input event to an action
+ option_button.select(0)
+ _on_OptionButton_item_selected(0)
+
+
+func _on_ShortcutSelectorDialog_popup_hide() -> void:
+ set_process_input(false)
+
+
+func _on_OptionButton_item_selected(index: int) -> void:
+ if input_type == InputTypes.MOUSE:
+ listened_input = InputEventMouseButton.new()
+ listened_input.button_index = index + 1
+ elif input_type == InputTypes.JOY_BUTTON:
+ listened_input = InputEventJoypadButton.new()
+ listened_input.button_index = index
+ elif input_type == InputTypes.JOY_AXIS:
+ listened_input = InputEventJoypadMotion.new()
+ listened_input.axis = index / 2
+ listened_input.axis_value = -1.0 if index % 2 == 0 else 1.0
+ _show_assigned_state(listened_input)
+
+
+func _on_EnteredShortcut_focus_entered() -> void:
+ set_process_input(true)
+
+
+func _on_EnteredShortcut_focus_exited() -> void:
+ set_process_input(false)
diff --git a/game/addons/keychain/ShortcutSelectorDialog.tscn b/game/addons/keychain/ShortcutSelectorDialog.tscn
new file mode 100644
index 0000000..d8f528f
--- /dev/null
+++ b/game/addons/keychain/ShortcutSelectorDialog.tscn
@@ -0,0 +1,46 @@
+[gd_scene load_steps=2 format=3 uid="uid://bfjcafe2kvx7n"]
+
+[ext_resource type="Script" path="res://addons/keychain/ShortcutSelectorDialog.gd" id="1"]
+
+[node name="ShortcutSelectorDialog" type="ConfirmationDialog"]
+script = ExtResource("1")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 341.0
+offset_bottom = 64.0
+
+[node name="InputTypeLabel" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+offset_right = 333.0
+offset_bottom = 14.0
+text = "Press a key or a key combination to set the shortcut"
+
+[node name="EnteredShortcut" type="LineEdit" parent="VBoxContainer"]
+visible = false
+layout_mode = 2
+offset_top = 18.0
+offset_right = 333.0
+offset_bottom = 32.0
+editable = false
+virtual_keyboard_enabled = false
+
+[node name="OptionButton" type="OptionButton" parent="VBoxContainer"]
+layout_mode = 2
+offset_top = 18.0
+offset_right = 333.0
+offset_bottom = 38.0
+mouse_default_cursor_shape = 2
+
+[node name="AlreadyExistsLabel" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+offset_top = 42.0
+offset_right = 333.0
+offset_bottom = 56.0
+
+[connection signal="about_to_popup" from="." to="." method="_on_ShortcutSelectorDialog_about_to_show"]
+[connection signal="confirmed" from="." to="." method="_on_ShortcutSelectorDialog_confirmed"]
+[connection signal="focus_entered" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_entered"]
+[connection signal="focus_exited" from="VBoxContainer/EnteredShortcut" to="." method="_on_EnteredShortcut_focus_exited"]
+[connection signal="item_selected" from="VBoxContainer/OptionButton" to="." method="_on_OptionButton_item_selected"]
diff --git a/game/addons/keychain/assets/add.svg b/game/addons/keychain/assets/add.svg
new file mode 100644
index 0000000..afad08a
--- /dev/null
+++ b/game/addons/keychain/assets/add.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1v6h-6v2h6v6h2v-6h6v-2h-6v-6z" fill="#e0e0e0"/></svg>
diff --git a/game/addons/keychain/assets/add.svg.import b/game/addons/keychain/assets/add.svg.import
new file mode 100644
index 0000000..25442bd
--- /dev/null
+++ b/game/addons/keychain/assets/add.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c4433pa25cxp2"
+path="res://.godot/imported/add.svg-4084e314648c872072757f9b0f544cf9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/add.svg"
+dest_files=["res://.godot/imported/add.svg-4084e314648c872072757f9b0f544cf9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/close.svg b/game/addons/keychain/assets/close.svg
new file mode 100644
index 0000000..331727a
--- /dev/null
+++ b/game/addons/keychain/assets/close.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m3.7578 2.3438-1.4141 1.4141 4.2422 4.2422-4.2422 4.2422 1.4141 1.4141 4.2422-4.2422 4.2422 4.2422 1.4141-1.4141-4.2422-4.2422 4.2422-4.2422-1.4141-1.4141-4.2422 4.2422z" fill="#e0e0e0"/></svg>
diff --git a/game/addons/keychain/assets/close.svg.import b/game/addons/keychain/assets/close.svg.import
new file mode 100644
index 0000000..d1bc742
--- /dev/null
+++ b/game/addons/keychain/assets/close.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://f1gcylay6c6f"
+path="res://.godot/imported/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/close.svg"
+dest_files=["res://.godot/imported/close.svg-12d57fa7e5a34826f312eed6ba1feb1a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/edit.svg b/game/addons/keychain/assets/edit.svg
new file mode 100644
index 0000000..6fc7ae0
--- /dev/null
+++ b/game/addons/keychain/assets/edit.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1c-.554 0-1 .446-1 1v2h4v-2c0-.554-.446-1-1-1zm-1 4v7l2 3 2-3v-7zm1 1h1v5h-1z" fill="#e0e0e0"/></svg>
diff --git a/game/addons/keychain/assets/edit.svg.import b/game/addons/keychain/assets/edit.svg.import
new file mode 100644
index 0000000..73ee367
--- /dev/null
+++ b/game/addons/keychain/assets/edit.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://df8gl3u2nqpl3"
+path="res://.godot/imported/edit.svg-cd9834545a8696f1e8611efa12a48f33.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/edit.svg"
+dest_files=["res://.godot/imported/edit.svg-cd9834545a8696f1e8611efa12a48f33.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/folder.svg b/game/addons/keychain/assets/folder.svg
new file mode 100644
index 0000000..c2def25
--- /dev/null
+++ b/game/addons/keychain/assets/folder.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 2a1 1 0 0 0 -1 1v2 6 2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-7a1 1 0 0 0 -1-1h-4a1 1 0 0 1 -1-1v-1a1 1 0 0 0 -1-1z" fill="#e0e0e0"/></svg>
diff --git a/game/addons/keychain/assets/folder.svg.import b/game/addons/keychain/assets/folder.svg.import
new file mode 100644
index 0000000..d2eece2
--- /dev/null
+++ b/game/addons/keychain/assets/folder.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b0gbmkb8xwksb"
+path="res://.godot/imported/folder.svg-490bb7e2d2aa4425de998b1d382cf534.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/folder.svg"
+dest_files=["res://.godot/imported/folder.svg-490bb7e2d2aa4425de998b1d382cf534.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/joy_axis.svg b/game/addons/keychain/assets/joy_axis.svg
new file mode 100644
index 0000000..1ab65f0
--- /dev/null
+++ b/game/addons/keychain/assets/joy_axis.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -1036.4)"><path d="m27 1038.4h7v14h-7z" fill="#fff" fill-opacity=".99608"/><g fill="#e0e0e0"><path d="m3 1a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h12v-14zm4 2h2a1 1 0 0 1 1 1v2h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1-1v-2h-2a1 1 0 0 1 -1-1v-2a1 1 0 0 1 1-1h2v-2a1 1 0 0 1 1-1z" fill-opacity=".99608" transform="translate(0 1036.4)"/><circle cx="8" cy="1044.4" r="1"/></g></g></svg>
diff --git a/game/addons/keychain/assets/joy_axis.svg.import b/game/addons/keychain/assets/joy_axis.svg.import
new file mode 100644
index 0000000..f517dae
--- /dev/null
+++ b/game/addons/keychain/assets/joy_axis.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bb6q6om3d08cm"
+path="res://.godot/imported/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/joy_axis.svg"
+dest_files=["res://.godot/imported/joy_axis.svg-9e1156700cfe46afb1ca622536a9a2e1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/joy_button.svg b/game/addons/keychain/assets/joy_button.svg
new file mode 100644
index 0000000..080d91a
--- /dev/null
+++ b/game/addons/keychain/assets/joy_button.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill-opacity=".99608" transform="translate(0 -1036.4)"><path d="m27 1038.4h7v14h-7z" fill="#fff"/><path d="m1 1v14h12c1.1046 0 2-.8954 2-2v-10c0-1.1046-.89543-2-2-2zm7 1a2 2 0 0 1 2 2 2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2zm-4 4a2 2 0 0 1 2 2 2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2zm8 0a2 2 0 0 1 2 2 2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2zm-4 4a2 2 0 0 1 2 2 2 2 0 0 1 -2 2 2 2 0 0 1 -2-2 2 2 0 0 1 2-2z" fill="#e0e0e0" transform="translate(0 1036.4)"/></g></svg>
diff --git a/game/addons/keychain/assets/joy_button.svg.import b/game/addons/keychain/assets/joy_button.svg.import
new file mode 100644
index 0000000..8963b8b
--- /dev/null
+++ b/game/addons/keychain/assets/joy_button.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ca58ufal2ufd8"
+path="res://.godot/imported/joy_button.svg-df5663c6f296cab556e81b9c770699d0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/joy_button.svg"
+dest_files=["res://.godot/imported/joy_button.svg-df5663c6f296cab556e81b9c770699d0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/keyboard.svg b/game/addons/keychain/assets/keyboard.svg
new file mode 100644
index 0000000..b9dfab7
--- /dev/null
+++ b/game/addons/keychain/assets/keyboard.svg
@@ -0,0 +1 @@
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill-opacity=".996"><path d="m4 2a1 1 0 0 0 -1 1v9.084c0 .506.448.916 1 .916h8c.552 0 1-.41 1-.916v-9.084a1 1 0 0 0 -1-1zm1.543 1.139h1.393l1.834 4.199h1.295v.437c.708.052 1.246.239 1.61.559.368.316.55.747.55 1.295 0 .552-.182.99-.55 1.314-.368.32-.906.505-1.61.553v.467h-1.294v-.473c-.708-.06-1.247-.248-1.615-.564-.364-.316-.545-.75-.545-1.297 0-.548.181-.977.545-1.29.368-.315.907-.504 1.615-.564v-.437h-1.464l-.282-.733h-1.595l-.284.733h-1.439l1.836-4.2zm.684 1.39-.409 1.057h.817zm3.84 4.338v1.526c.28-.04.483-.12.607-.24.124-.125.185-.302.185-.53 0-.224-.063-.396-.191-.516-.124-.12-.326-.2-.602-.24zm-1.296.006c-.284.04-.487.12-.61.24-.12.116-.182.288-.182.516 0 .22.065.392.193.512.132.12.331.202.6.246v-1.514z" fill="#e0e0e0"/><path d="m27 2h7v14h-7z" fill="#fff"/><path d="m1 4v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9z" fill="#e0e0e0"/></g></svg>
diff --git a/game/addons/keychain/assets/keyboard.svg.import b/game/addons/keychain/assets/keyboard.svg.import
new file mode 100644
index 0000000..0294680
--- /dev/null
+++ b/game/addons/keychain/assets/keyboard.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c2s5rm4nec5yh"
+path="res://.godot/imported/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/keyboard.svg"
+dest_files=["res://.godot/imported/keyboard.svg-fac365b6f70899f1dfa71ce4b4761ac6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/keyboard_physical.svg b/game/addons/keychain/assets/keyboard_physical.svg
new file mode 100644
index 0000000..4364e0b
--- /dev/null
+++ b/game/addons/keychain/assets/keyboard_physical.svg
@@ -0,0 +1 @@
+<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill-opacity=".996"><path d="m4 2a1 1 0 0 0 -1 1v9.084c0 .506.448.916 1 .916h8c.552 0 1-.41 1-.916v-9.084a1 1 0 0 0 -1-1zm2.762 1.768h2.476l3.264 7.464h-2.604l-.502-1.3h-2.835l-.502 1.3h-2.561zm1.217 2.474-.725 1.878h1.45z" fill="#e0e0e0"/><path d="m27 2h7v14h-7z" fill="#fff"/><path d="m1 4v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9z" fill="#e0e0e0"/></g></svg>
diff --git a/game/addons/keychain/assets/keyboard_physical.svg.import b/game/addons/keychain/assets/keyboard_physical.svg.import
new file mode 100644
index 0000000..86e2b3e
--- /dev/null
+++ b/game/addons/keychain/assets/keyboard_physical.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cmh8eaibhn5y8"
+path="res://.godot/imported/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/keyboard_physical.svg"
+dest_files=["res://.godot/imported/keyboard_physical.svg-f50c796569ade32b57ece1ba0bd7dfbb.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/mouse.svg b/game/addons/keychain/assets/mouse.svg
new file mode 100644
index 0000000..2175120
--- /dev/null
+++ b/game/addons/keychain/assets/mouse.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1.1016a5 5 0 0 0 -4 4.8984h4zm2 .0039063v4.8945h4a5 5 0 0 0 -4-4.8945zm-6 6.8945v2a5 5 0 0 0 5 5 5 5 0 0 0 5-5v-2z" fill="#e0e0e0"/></svg>
diff --git a/game/addons/keychain/assets/mouse.svg.import b/game/addons/keychain/assets/mouse.svg.import
new file mode 100644
index 0000000..21c2216
--- /dev/null
+++ b/game/addons/keychain/assets/mouse.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bma7xj2rqqcr8"
+path="res://.godot/imported/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/mouse.svg"
+dest_files=["res://.godot/imported/mouse.svg-559695787f3bb55c16dc66bd6a9b9032.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/assets/shortcut.svg b/game/addons/keychain/assets/shortcut.svg
new file mode 100644
index 0000000..4ef16f0
--- /dev/null
+++ b/game/addons/keychain/assets/shortcut.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m4 2c-.55228 0-1 .4477-1 1v9.084c.0004015.506.448.91602 1 .91602h8c.552 0 .9996-.41002 1-.91602v-9.084c0-.5523-.44772-1-1-1zm-3 2v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9h-1v9a.99998.99998 0 0 1 -1 1h-10a1 1 0 0 1 -1-1v-9zm6 0h3l-1 3h2l-4 4 1-3h-2z" fill="#e0e0e0" fill-opacity=".99608"/></svg>
diff --git a/game/addons/keychain/assets/shortcut.svg.import b/game/addons/keychain/assets/shortcut.svg.import
new file mode 100644
index 0000000..ae3bd49
--- /dev/null
+++ b/game/addons/keychain/assets/shortcut.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bmi24jp4fqi7k"
+path="res://.godot/imported/shortcut.svg-401daac18a817142e2b29e5801e00053.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/keychain/assets/shortcut.svg"
+dest_files=["res://.godot/imported/shortcut.svg-401daac18a817142e2b29e5801e00053.ctex"]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/bptc_ldr=0
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/game/addons/keychain/plugin.cfg b/game/addons/keychain/plugin.cfg
new file mode 100644
index 0000000..095c8ee
--- /dev/null
+++ b/game/addons/keychain/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Keychain"
+description="A plugin for the Godot Engine that aims to give the player full control over the input actions of the game."
+author="Orama Interactive"
+version="2.0"
+script="plugin.gd"
diff --git a/game/addons/keychain/plugin.gd b/game/addons/keychain/plugin.gd
new file mode 100644
index 0000000..1eb1d3c
--- /dev/null
+++ b/game/addons/keychain/plugin.gd
@@ -0,0 +1,10 @@
+@tool
+extends EditorPlugin
+
+
+func _enter_tree() -> void:
+ add_autoload_singleton("Keychain", "res://addons/keychain/Keychain.gd")
+
+
+func _exit_tree() -> void:
+ remove_autoload_singleton("Keychain")
diff --git a/game/addons/keychain/profiles/default.tres b/game/addons/keychain/profiles/default.tres
new file mode 100644
index 0000000..b952157
--- /dev/null
+++ b/game/addons/keychain/profiles/default.tres
@@ -0,0 +1,9 @@
+[gd_resource type="Resource" script_class="ShortcutProfile" load_steps=2 format=3 uid="uid://df04uev1epmmo"]
+
+[ext_resource type="Script" path="res://addons/keychain/ShortcutProfile.gd" id="1"]
+
+[resource]
+script = ExtResource("1")
+name = "Default"
+customizable = false
+bindings = {}
diff --git a/game/addons/keychain/translations/README.md b/game/addons/keychain/translations/README.md
new file mode 100644
index 0000000..086527b
--- /dev/null
+++ b/game/addons/keychain/translations/README.md
@@ -0,0 +1,4 @@
+# Localization files for Keychain
+Keychain uses .po files to handle localization. More information in the Godot documentation: [Localization using gettext](https://docs.godotengine.org/en/stable/tutorials/i18n/localization_using_gettext.html)
+
+Simply add a .po file of the language you want to provide localization for, and Keychain will automatically add it to the TranslationServer when the project runs.
diff --git a/game/addons/keychain/translations/Translations.pot b/game/addons/keychain/translations/Translations.pot
new file mode 100644
index 0000000..1f10c77
--- /dev/null
+++ b/game/addons/keychain/translations/Translations.pot
@@ -0,0 +1,209 @@
+msgid ""
+msgstr ""
+
+msgid "OK"
+msgstr ""
+
+msgid "Cancel"
+msgstr ""
+
+msgid "Add"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "Shortcut profile:"
+msgstr ""
+
+msgid "Default"
+msgstr ""
+
+msgid "Custom"
+msgstr ""
+
+msgid "New"
+msgstr ""
+
+msgid "Rename"
+msgstr ""
+
+msgid "Open Folder"
+msgstr ""
+
+msgid "New Shortcut Profile"
+msgstr ""
+
+msgid "Rename Shortcut Profile"
+msgstr ""
+
+msgid "Are you sure you want to delete this shortcut profile?"
+msgstr ""
+
+msgid "Key"
+msgstr ""
+
+msgid "Mouse Button"
+msgstr ""
+
+msgid "Joy Button"
+msgstr ""
+
+msgid "Joy Axis"
+msgstr ""
+
+msgid "Button"
+msgstr ""
+
+msgid "Axis"
+msgstr ""
+
+msgid "(Physical)"
+msgstr ""
+
+msgid "Set the shortcut"
+msgstr ""
+
+msgid "Press a key or a key combination to set the shortcut"
+msgstr ""
+
+msgid "Mouse Button Index:"
+msgstr ""
+
+msgid "Joypad Button Index:"
+msgstr ""
+
+msgid "Joypad Axis Index:"
+msgstr ""
+
+msgid "Already assigned to: %s"
+msgstr ""
+
+msgid "Left Button"
+msgstr ""
+
+msgid "Right Button"
+msgstr ""
+
+msgid "Middle Button"
+msgstr ""
+
+msgid "Wheel Up Button"
+msgstr ""
+
+msgid "Wheel Down Button"
+msgstr ""
+
+msgid "Wheel Left Button"
+msgstr ""
+
+msgid "Wheel Right Button"
+msgstr ""
+
+msgid "X Button 1"
+msgstr ""
+
+msgid "X Button 2"
+msgstr ""
+
+msgid "DualShock Cross, Xbox A, Nintendo B"
+msgstr ""
+
+msgid "DualShock Circle, Xbox B, Nintendo A"
+msgstr ""
+
+msgid "DualShock Square, Xbox X, Nintendo Y"
+msgstr ""
+
+msgid "DualShock Triangle, Xbox Y, Nintendo X"
+msgstr ""
+
+msgid "L, L1"
+msgstr ""
+
+msgid "R, R1"
+msgstr ""
+
+msgid "L2"
+msgstr ""
+
+msgid "R2"
+msgstr ""
+
+msgid "L3"
+msgstr ""
+
+msgid "R3"
+msgstr ""
+
+msgid "Select, DualShock Share, Nintendo -"
+msgstr ""
+
+msgid "Start, DualShock Options, Nintendo +"
+msgstr ""
+
+msgid "D-Pad Up"
+msgstr ""
+
+msgid "D-Pad Down"
+msgstr ""
+
+msgid "D-Pad Left"
+msgstr ""
+
+msgid "D-Pad Right"
+msgstr ""
+
+msgid "Home, DualShock PS, Guide"
+msgstr ""
+
+msgid "Xbox Share, PS5 Microphone, Nintendo Capture"
+msgstr ""
+
+msgid "Xbox Paddle 1"
+msgstr ""
+
+msgid "Xbox Paddle 2"
+msgstr ""
+
+msgid "Xbox Paddle 3"
+msgstr ""
+
+msgid "Xbox Paddle 4"
+msgstr ""
+
+msgid "PS4/5 Touchpad"
+msgstr ""
+
+msgid "(Left Stick Left)"
+msgstr ""
+
+msgid "(Left Stick Right)"
+msgstr ""
+
+msgid "(Left Stick Up)"
+msgstr ""
+
+msgid "(Left Stick Down)"
+msgstr ""
+
+msgid "(Right Stick Left)"
+msgstr ""
+
+msgid "(Right Stick Right)"
+msgstr ""
+
+msgid "(Right Stick Up)"
+msgstr ""
+
+msgid "(Right Stick Down)"
+msgstr ""
+
+msgid "(L2)"
+msgstr ""
+
+msgid "(R2)"
+msgstr ""
diff --git a/game/addons/keychain/translations/el_GR.po b/game/addons/keychain/translations/el_GR.po
new file mode 100644
index 0000000..4b6ce8b
--- /dev/null
+++ b/game/addons/keychain/translations/el_GR.po
@@ -0,0 +1,215 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Keychain\n"
+"Last-Translator: Overloaded\n"
+"Language-Team: none\n"
+"Language: el_GR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+msgid "OK"
+msgstr "Εντάξει"
+
+msgid "Cancel"
+msgstr "Άκυρο"
+
+msgid "Add"
+msgstr "Προσθήκη"
+
+msgid "Edit"
+msgstr "Επεξεργασία"
+
+msgid "Delete"
+msgstr "Διαγραφή"
+
+msgid "Shortcut profile:"
+msgstr "Προφίλ συντομέυσεων:"
+
+msgid "Default"
+msgstr "Προεπιλογή"
+
+msgid "Custom"
+msgstr "Προσαρμοσμένο"
+
+msgid "New"
+msgstr "Νέο"
+
+msgid "Rename"
+msgstr "Μετονομασία"
+
+msgid "Open Folder"
+msgstr "Άνοιγμα φακέλου"
+
+msgid "New Shortcut Profile"
+msgstr "Νέο προφίλ συντομέυσεων"
+
+msgid "Rename Shortcut Profile"
+msgstr "Μετονομασία προφίλ συντομεύσεων"
+
+msgid "Are you sure you want to delete this shortcut profile?"
+msgstr "Είστε σίγουροι πως θέλετε να διαγραφτεί αυτό το προφίλ συντομέυσεων;"
+
+msgid "Key"
+msgstr "Πλήκτρο"
+
+msgid "Mouse Button"
+msgstr "Κουμπί ποντικιού"
+
+msgid "Joy Button"
+msgstr "Κουμπί χειριστηρίου"
+
+msgid "Joy Axis"
+msgstr "Άξονας χειριστηρίου"
+
+msgid "Button"
+msgstr "Κουμπί"
+
+msgid "Axis"
+msgstr "Άξονας"
+
+msgid "(Physical)"
+msgstr "(Φυσικό)"
+
+msgid "Set the shortcut"
+msgstr "Επιλέξτε μια συντόμευση"
+
+msgid "Press a key or a key combination to set the shortcut"
+msgstr "Πατήστε ένα πλήκτρο ή συνδυασμό πλήκτρων για να ορίσετε τη συντόμευση"
+
+msgid "Mouse Button Index:"
+msgstr "Δείκτης κουμπιού ποντικού"
+
+msgid "Joypad Button Index:"
+msgstr "Δείκτης κουμπιού χειριστηρίου"
+
+msgid "Joypad Axis Index:"
+msgstr "Δείκτης άξονα χειριστηρίου"
+
+msgid "Already assigned to: %s"
+msgstr "Έχει ήδη εκχωρηθεί σε: %s"
+
+msgid "Left Button"
+msgstr "Αριστερό κουμπί"
+
+msgid "Right Button"
+msgstr "Δεξί κουμπί"
+
+msgid "Middle Button"
+msgstr "Μεσαίο κουμπί"
+
+msgid "Wheel Up Button"
+msgstr "Τρόχος πάνω"
+
+msgid "Wheel Down Button"
+msgstr "Τρόχος κάτω"
+
+msgid "Wheel Left Button"
+msgstr "Τρόχος αριστερά"
+
+msgid "Wheel Right Button"
+msgstr "Τρόχος δεξιά"
+
+msgid "X Button 1"
+msgstr "Κουμπί X 1"
+
+msgid "X Button 2"
+msgstr "Κουμπί X 2"
+
+msgid "DualShock Cross, Xbox A, Nintendo B"
+msgstr "DualShock Σταυρός, Xbox A, Nintendo B"
+
+msgid "DualShock Circle, Xbox B, Nintendo A"
+msgstr "DualShock Κύκλος, Xbox B, Nintendo A"
+
+msgid "DualShock Square, Xbox X, Nintendo Y"
+msgstr "DualShock Τετράγωνο, Xbox X, Nintendo Y"
+
+msgid "DualShock Triangle, Xbox Y, Nintendo X"
+msgstr "DualShock Τρίγωνο, Xbox Y, Nintendo X"
+
+msgid "L, L1"
+msgstr "L, L1"
+
+msgid "R, R1"
+msgstr "R, R1"
+
+msgid "L2"
+msgstr "L2"
+
+msgid "R2"
+msgstr "R2"
+
+msgid "L3"
+msgstr "L3"
+
+msgid "R3"
+msgstr "R3"
+
+msgid "Select, DualShock Share, Nintendo -"
+msgstr ""
+
+msgid "Start, DualShock Options, Nintendo +"
+msgstr ""
+
+msgid "D-Pad Up"
+msgstr "D-Pad Πάνω"
+
+msgid "D-Pad Down"
+msgstr "D-Pad Κάτω"
+
+msgid "D-Pad Left"
+msgstr "D-Pad Αριστερά"
+
+msgid "D-Pad Right"
+msgstr "D-Pad Δεξιά"
+
+msgid "Home, DualShock PS, Guide"
+msgstr ""
+
+msgid "Xbox Share, PS5 Microphone, Nintendo Capture"
+msgstr "Xbox Share, PS5 Μικρόφωνο, Nintendo Capture"
+
+msgid "Xbox Paddle 1"
+msgstr ""
+
+msgid "Xbox Paddle 2"
+msgstr ""
+
+msgid "Xbox Paddle 3"
+msgstr ""
+
+msgid "Xbox Paddle 4"
+msgstr ""
+
+msgid "PS4/5 Touchpad"
+msgstr "PS4/5 επιφάνεια αφής"
+
+msgid "(Left Stick Left)"
+msgstr "(Αριστερό Stick Αριστερά)"
+
+msgid "(Left Stick Right)"
+msgstr "(Αριστερό Stick Δεξιά)"
+
+msgid "(Left Stick Up)"
+msgstr "(Αριστερό Stick Πάνω)"
+
+msgid "(Left Stick Down)"
+msgstr "(Αριστερό Stick Κάτω)"
+
+msgid "(Right Stick Left)"
+msgstr "(Δεξί Stick Αριστερά)"
+
+msgid "(Right Stick Right)"
+msgstr "(Δεξί Stick Δεξιά)"
+
+msgid "(Right Stick Up)"
+msgstr "(Δεξί Stick Πάνω)"
+
+msgid "(Right Stick Down)"
+msgstr "(Δεξί Stick Κάτω)"
+
+msgid "(L2)"
+msgstr "(L2)"
+
+msgid "(R2)"
+msgstr "(R2)"
diff --git a/game/project.godot b/game/project.godot
index 34d4267..35b92f6 100644
--- a/game/project.godot
+++ b/game/project.godot
@@ -14,10 +14,13 @@ config/name="OpenVic2"
run/main_scene="res://src/GameMenu.tscn"
config/features=PackedStringArray("4.0", "Forward Plus")
config/icon="res://icon.svg"
+config/project_settings_override.template="user://settings.cfg"
[autoload]
+Events="*res://src/Autoload/Events.gd"
Resolution="*res://src/Autoload/Resolution.gd"
+Keychain="*res://addons/keychain/Keychain.gd"
[display]
@@ -28,6 +31,10 @@ window/size/resizable=false
window/stretch/mode="canvas_items"
window/stretch/aspect="ignore"
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/keychain/plugin.cfg")
+
[internationalization]
locale/translation_remaps={}
@@ -41,6 +48,10 @@ locale/country_short_name={
limits/message_queue/max_size_kb=16384
+[openvic2]
+
+settings/settings_file_path="user://settings.cfg"
+
[rendering]
textures/lossless_compression/force_png=true
diff --git a/game/src/Autoload/Events.gd b/game/src/Autoload/Events.gd
new file mode 100644
index 0000000..f0f60b7
--- /dev/null
+++ b/game/src/Autoload/Events.gd
@@ -0,0 +1,3 @@
+extends Node
+
+var Options = preload("Events/Options.gd").new()
diff --git a/game/src/Autoload/Events/Options.gd b/game/src/Autoload/Events/Options.gd
new file mode 100644
index 0000000..0acaa63
--- /dev/null
+++ b/game/src/Autoload/Events/Options.gd
@@ -0,0 +1,22 @@
+extends RefCounted
+
+signal save_settings(save_file: ConfigFile)
+signal load_settings(load_file: ConfigFile)
+signal reset_settings()
+
+func load_settings_from_file() -> void:
+ load_settings.emit(_settings_file)
+
+func save_settings_from_file() -> void:
+ save_settings.emit(_settings_file)
+ _settings_file.save(_settings_file_path)
+
+func try_reset_settings() -> void:
+ reset_settings.emit()
+
+var _settings_file_path := ProjectSettings.get_setting("openvic2/settings/settings_file_path", "user://settings.cfg") as String
+var _settings_file := ConfigFile.new()
+
+func _init():
+ if FileAccess.file_exists(_settings_file_path):
+ _settings_file.load(_settings_file_path)
diff --git a/game/src/Autoload/Resolution.gd b/game/src/Autoload/Resolution.gd
index cde46f5..ac54c0d 100644
--- a/game/src/Autoload/Resolution.gd
+++ b/game/src/Autoload/Resolution.gd
@@ -1,28 +1,38 @@
extends Node
-const _resolutions := {
- &"3840x2160": Vector2i(3840,2160),
- &"2560x1440": Vector2i(2560,1080),
- &"1920x1080": Vector2i(1920,1080),
- &"1366x768": Vector2i(1366,768),
- &"1536x864": Vector2i(1536,864),
- &"1280x720": Vector2i(1280,720),
- &"1440x900": Vector2i(1440,900),
- &"1600x900": Vector2i(1600,900),
- &"1024x600": Vector2i(1024,600),
- &"800x600": Vector2i(800,600)
-}
+var _resolutions : Array[Dictionary]= [
+ { "name": &"", "value": Vector2i(3840,2160) },
+ { "name": &"", "value": Vector2i(2560,1080) },
+ { "name": &"", "value": Vector2i(1920,1080) },
+ { "name": &"", "value": Vector2i(1366,768) },
+ { "name": &"", "value": Vector2i(1536,864) },
+ { "name": &"", "value": Vector2i(1280,720) },
+ { "name": &"", "value": Vector2i(1440,900) },
+ { "name": &"", "value": Vector2i(1600,900) },
+ { "name": &"", "value": Vector2i(1024,600) },
+ { "name": &"", "value": Vector2i(800,600) }
+]
+
+func _ready():
+ for resolution in _resolutions:
+ resolution["tag"] = _get_name_of_resolution(resolution["name"], resolution["value"])
func has_resolution(resolution_name : StringName) -> bool:
return resolution_name in _resolutions
func get_resolution(resolution_name : StringName, default : Vector2i = Vector2i(1920, 1080)) -> Vector2i:
- return _resolutions.get(resolution_name, default)
+ var resolution := _get_resolution_by_name(resolution_name)
+ if resolution.x < 0 and resolution.y < 0:
+ return default
+ return resolution
-func get_resolution_name_list() -> Array:
- return _resolutions.keys()
+func get_resolution_name_list() -> Array[StringName]:
+ var result : Array[StringName] = []
+ for resolution in _resolutions:
+ result.append(resolution["tag"])
+ return result
-func get_current_resolution() -> Vector2:
+func get_current_resolution() -> Vector2i:
var window := get_viewport().get_window()
match window.mode:
Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN:
@@ -30,7 +40,7 @@ func get_current_resolution() -> Vector2:
_:
return window.size
-func set_resolution(resolution : Vector2) -> void:
+func set_resolution(resolution : Vector2i) -> void:
var window := get_viewport().get_window()
match window.mode:
Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN:
@@ -41,3 +51,14 @@ func set_resolution(resolution : Vector2) -> void:
func reset_resolution() -> void:
set_resolution(get_current_resolution())
+
+func _get_name_of_resolution(resolution_name : StringName, resolution_value : Vector2i) -> StringName:
+ if resolution_name != null and not resolution_name.is_empty():
+ return "%s (%sx%s)" % [resolution_name, resolution_value.x, resolution_value.y]
+ return "%sx%s" % [resolution_value.x, resolution_value.y]
+
+func _get_resolution_by_name(resolution_name : StringName) -> Vector2i:
+ for resolution in _resolutions:
+ if resolution["name"] == resolution_name or resolution["tag"] == resolution_name:
+ return resolution["value"]
+ return Vector2i(-1, -1)
diff --git a/game/src/GameMenu.gd b/game/src/GameMenu.gd
index 1f5a77e..c6174bb 100644
--- a/game/src/GameMenu.gd
+++ b/game/src/GameMenu.gd
@@ -1,5 +1,7 @@
extends Control
+func _ready():
+ Events.Options.load_settings_from_file()
func _on_main_menu_options_button_pressed():
$OptionsMenu.toggle_locale_button_visibility(false)
diff --git a/game/src/LocaleButton.gd b/game/src/LocaleButton.gd
index eed815e..32807d0 100644
--- a/game/src/LocaleButton.gd
+++ b/game/src/LocaleButton.gd
@@ -4,8 +4,6 @@ var _locales_country_rename : Dictionary
var _locales_list : Array[String]
func _ready():
- print("Loading locale button")
-
_locales_country_rename = ProjectSettings.get_setting("internationalization/locale/country_short_name", {})
_locales_list = [TranslationServer.get_locale()]
@@ -20,7 +18,18 @@ func _ready():
add_item("%s, %s" % [locale_first_part, locale_second_part])
+ Events.Options.load_settings.connect(load_setting)
+ Events.Options.save_settings.connect(save_setting)
+
+
+func load_setting(file : ConfigFile):
+ var locale_index := _locales_list.find(file.get_value("Localization", "Locale", "") as String)
+ if locale_index != -1:
+ selected = locale_index
+
+func save_setting(file : ConfigFile):
+ file.set_value("Localization", "Locale", _locales_list[selected])
func _on_item_selected(index):
- print("Selected locale " + _locales_list[index])
TranslationServer.set_locale(_locales_list[index])
+ Events.Options.save_settings_from_file.call_deferred()
diff --git a/game/src/OptionMenu/ControlsTab.tscn b/game/src/OptionMenu/ControlsTab.tscn
new file mode 100644
index 0000000..b84dc85
--- /dev/null
+++ b/game/src/OptionMenu/ControlsTab.tscn
@@ -0,0 +1,14 @@
+[gd_scene load_steps=2 format=3 uid="uid://cdwymd51i4b2f"]
+
+[ext_resource type="PackedScene" uid="uid://by4gggse2nsdx" path="res://addons/keychain/ShortcutEdit.tscn" id="1_fv8sh"]
+
+[node name="Controls" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="ShortcutEdit" parent="." instance=ExtResource("1_fv8sh")]
+layout_mode = 1
diff --git a/game/src/OptionMenu/MonitorDisplaySelector.gd b/game/src/OptionMenu/MonitorDisplaySelector.gd
index 600b296..f2f0dc8 100644
--- a/game/src/OptionMenu/MonitorDisplaySelector.gd
+++ b/game/src/OptionMenu/MonitorDisplaySelector.gd
@@ -1,16 +1,14 @@
extends SettingOptionButton
-
-func _ready():
+func _setup_button():
clear()
for screen_index in range(DisplayServer.get_screen_count()):
add_item("Monitor %d" % (screen_index + 1))
+ default_selected = get_viewport().get_window().current_screen
func _on_item_selected(index):
- print("Selected index: %d" % index)
var window := get_viewport().get_window()
var mode := window.mode
window.mode = Window.MODE_WINDOWED
get_viewport().get_window().set_current_screen(index)
window.mode = mode
- print(get_viewport().get_window().current_screen)
diff --git a/game/src/OptionMenu/OptionsMenu.gd b/game/src/OptionMenu/OptionsMenu.gd
index e3c8433..5aba7f2 100644
--- a/game/src/OptionMenu/OptionsMenu.gd
+++ b/game/src/OptionMenu/OptionsMenu.gd
@@ -1,27 +1,9 @@
extends Control
-@export
-var user_settings_file_path : String = "settings.cfg"
-
signal back_button_pressed
-signal save_settings(save_file: ConfigFile)
-signal load_settings(load_file: ConfigFile)
-signal reset_settings()
-
-@onready
-var _settings_file_path := "user://" + user_settings_file_path
-var _settings_file := ConfigFile.new()
-
func _ready():
# Prepare options menu before loading user settings
-
- print("TODO: Load user settings!")
-
- if FileAccess.file_exists(_settings_file_path):
- _settings_file.load(_settings_file_path)
- load_settings.emit(_settings_file)
-
var tab_bar : TabBar = $Margin/Tab.get_child(0, true)
# This ends up easier to manage then trying to manually recreate the TabContainer's behavior
@@ -33,7 +15,7 @@ func _ready():
var reset_button := Button.new()
reset_button.text = "R"
- reset_button.pressed.connect(func(): reset_settings.emit())
+ reset_button.pressed.connect(Events.Options.try_reset_settings)
button_list.add_child(reset_button)
var back_button := Button.new()
@@ -42,6 +24,8 @@ func _ready():
button_list.add_child(back_button)
get_viewport().get_window().close_requested.connect(_on_window_close_requested)
+ _save_overrides.call_deferred()
+ Events.Options.save_settings.connect(func(_f): self._save_overrides.call_deferred())
func _notification(what):
match what:
@@ -51,22 +35,24 @@ func _notification(what):
# Could pass the LocaleButton between the MainMenu and OptionsMenu
# but that seems a bit excessive
func toggle_locale_button_visibility(locale_visible : bool):
- print("Toggling locale button: %s" % locale_visible)
$LocaleVBox/LocaleHBox/LocaleButton.visible = locale_visible
-func _on_ear_exploder_toggled(button_pressed):
- print("KABOOM!!!" if button_pressed else "DEFUSED!!!")
-
-
func _on_back_button_pressed():
- save_settings.emit(_settings_file)
- _settings_file.save(_settings_file_path)
+ Events.Options.save_settings_from_file()
back_button_pressed.emit()
-
-func _on_spin_box_value_changed(value):
- print("Spinbox: %d" % value)
-
func _on_window_close_requested() -> void:
if visible:
- _on_back_button_pressed()
+ Events.Options.save_settings_from_file()
+
+func _save_overrides() -> void:
+ var override_path := ProjectSettings.get_setting("application/config/project_settings_override") as String
+ if override_path == null or override_path.is_empty():
+ override_path = ProjectSettings.get_setting("openvic2/settings/settings_file_path") as String
+ var file := ConfigFile.new()
+ file.load(override_path)
+ file.set_value("display", "window/size/mode", get_viewport().get_window().mode)
+ var resolution : Vector2i = Resolution.get_current_resolution()
+ file.set_value("display", "window/size/viewport_width", resolution.x)
+ file.set_value("display", "window/size/viewport_height", resolution.y)
+ file.save(override_path)
diff --git a/game/src/OptionMenu/OptionsMenu.tscn b/game/src/OptionMenu/OptionsMenu.tscn
index 0ed531f..eafe37f 100644
--- a/game/src/OptionMenu/OptionsMenu.tscn
+++ b/game/src/OptionMenu/OptionsMenu.tscn
@@ -2,10 +2,10 @@
[ext_resource type="Script" path="res://src/OptionMenu/OptionsMenu.gd" id="1_tlein"]
[ext_resource type="PackedScene" uid="uid://b7oncobnacxmt" path="res://src/LocaleButton.tscn" id="2_d7wvq"]
-[ext_resource type="Script" path="res://src/OptionMenu/ResolutionSelector.gd" id="2_jk1ey"]
-[ext_resource type="Script" path="res://src/OptionMenu/ScreenModeSelector.gd" id="3_hsicf"]
-[ext_resource type="Script" path="res://src/OptionMenu/MonitorDisplaySelector.gd" id="3_q1cm3"]
-[ext_resource type="PackedScene" uid="uid://dy4si8comamnv" path="res://src/OptionMenu/VolumeGrid.tscn" id="4_n4oqr"]
+[ext_resource type="PackedScene" uid="uid://bq3awxxjn1tuw" path="res://src/OptionMenu/VideoTab.tscn" id="2_ji8xr"]
+[ext_resource type="PackedScene" uid="uid://cbtgwpx2wxi33" path="res://src/OptionMenu/SoundTab.tscn" id="3_4w35t"]
+[ext_resource type="PackedScene" uid="uid://bq7ibhm0txl5p" path="res://addons/keychain/ShortcutEdit.tscn" id="4_vdhjp"]
+[ext_resource type="PackedScene" uid="uid://dp2grvybtecqu" path="res://src/OptionMenu/OtherTab.tscn" id="5_ahefp"]
[node name="OptionsMenu" type="Control"]
layout_mode = 3
@@ -34,114 +34,18 @@ size_flags_vertical = 3
tab_alignment = 1
use_hidden_tabs_for_min_size = true
-[node name="Video" type="HBoxContainer" parent="Margin/Tab"]
+[node name="Video" parent="Margin/Tab" instance=ExtResource("2_ji8xr")]
layout_mode = 2
-tooltip_text = "This is my cool and very nice tooltip"
-alignment = 1
-[node name="VBoxContainer" type="VBoxContainer" parent="Margin/Tab/Video"]
-layout_mode = 2
-
-[node name="Control" type="Control" parent="Margin/Tab/Video/VBoxContainer"]
-layout_mode = 2
-size_flags_vertical = 3
-size_flags_stretch_ratio = 0.1
-
-[node name="GridContainer" type="GridContainer" parent="Margin/Tab/Video/VBoxContainer"]
-layout_mode = 2
-size_flags_vertical = 3
-columns = 2
-
-[node name="ResolutionLabel" type="Label" parent="Margin/Tab/Video/VBoxContainer/GridContainer"]
-layout_mode = 2
-text = "Resolution"
-
-[node name="ResolutionSelector" type="OptionButton" parent="Margin/Tab/Video/VBoxContainer/GridContainer"]
-layout_mode = 2
-item_count = 1
-selected = 0
-popup/item_0/text = "MISSING"
-popup/item_0/id = 0
-script = ExtResource("2_jk1ey")
-section_name = "Video"
-setting_name = "Resolution"
-default_value = -1
-
-[node name="ScreenModeLabel" type="Label" parent="Margin/Tab/Video/VBoxContainer/GridContainer"]
-layout_mode = 2
-text = "Screen Mode"
-
-[node name="ScreenModeSelector" type="OptionButton" parent="Margin/Tab/Video/VBoxContainer/GridContainer"]
-layout_mode = 2
-item_count = 3
-selected = 0
-popup/item_0/text = "Fullscreen"
-popup/item_0/id = 0
-popup/item_1/text = "Borderless"
-popup/item_1/id = 1
-popup/item_2/text = "Windowed"
-popup/item_2/id = 2
-script = ExtResource("3_hsicf")
-section_name = "Video"
-setting_name = "Mode Selected"
-
-[node name="MonitorSelectionLabel" type="Label" parent="Margin/Tab/Video/VBoxContainer/GridContainer"]
-layout_mode = 2
-text = "Monitor Selection"
-
-[node name="MonitorDisplaySelector" type="OptionButton" parent="Margin/Tab/Video/VBoxContainer/GridContainer"]
-layout_mode = 2
-item_count = 1
-selected = 0
-popup/item_0/text = "MISSING"
-popup/item_0/id = 0
-script = ExtResource("3_q1cm3")
-section_name = "Video"
-setting_name = "Current Screen"
-
-[node name="Sound" type="HBoxContainer" parent="Margin/Tab"]
+[node name="Sound" parent="Margin/Tab" instance=ExtResource("3_4w35t")]
visible = false
layout_mode = 2
-alignment = 1
-
-[node name="VBoxContainer" type="VBoxContainer" parent="Margin/Tab/Sound"]
-layout_mode = 2
-
-[node name="Control" type="Control" parent="Margin/Tab/Sound/VBoxContainer"]
-layout_mode = 2
-size_flags_vertical = 3
-size_flags_stretch_ratio = 0.1
-
-[node name="VolumeGrid" parent="Margin/Tab/Sound/VBoxContainer" instance=ExtResource("4_n4oqr")]
-layout_mode = 2
-
-[node name="ButtonGrid" type="GridContainer" parent="Margin/Tab/Sound/VBoxContainer"]
-layout_mode = 2
-size_flags_vertical = 2
-columns = 2
-
-[node name="Spacer" type="Control" parent="Margin/Tab/Sound/VBoxContainer/ButtonGrid"]
-layout_mode = 2
-size_flags_horizontal = 3
-
-[node name="EarExploder" type="CheckButton" parent="Margin/Tab/Sound/VBoxContainer/ButtonGrid"]
-layout_mode = 2
-text = "Explode Eardrums on Startup?"
-[node name="Other" type="Control" parent="Margin/Tab"]
+[node name="Controls" parent="Margin/Tab" instance=ExtResource("4_vdhjp")]
visible = false
layout_mode = 2
-[node name="HBoxContainer" type="HBoxContainer" parent="Margin/Tab/Other"]
-layout_mode = 0
-offset_right = 40.0
-offset_bottom = 40.0
-
-[node name="Label" type="Label" parent="Margin/Tab/Other/HBoxContainer"]
-layout_mode = 2
-text = "Spinbox Example :)"
-
-[node name="SpinBox" type="SpinBox" parent="Margin/Tab/Other/HBoxContainer"]
+[node name="Other" parent="Margin/Tab" instance=ExtResource("5_ahefp")]
layout_mode = 2
[node name="LocaleVBox" type="VBoxContainer" parent="."]
@@ -161,21 +65,3 @@ alignment = 2
[node name="LocaleButton" parent="LocaleVBox/LocaleHBox" instance=ExtResource("2_d7wvq")]
layout_mode = 2
-
-[connection signal="load_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/ResolutionSelector" method="load_setting"]
-[connection signal="load_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/ScreenModeSelector" method="load_setting"]
-[connection signal="load_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/MonitorDisplaySelector" method="load_setting"]
-[connection signal="load_settings" from="." to="Margin/Tab/Sound/VBoxContainer/VolumeGrid" method="_on_options_menu_load_settings"]
-[connection signal="reset_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/ResolutionSelector" method="reset_setting"]
-[connection signal="reset_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/ScreenModeSelector" method="reset_setting"]
-[connection signal="reset_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/MonitorDisplaySelector" method="reset_setting"]
-[connection signal="reset_settings" from="." to="Margin/Tab/Sound/VBoxContainer/VolumeGrid" method="_on_options_menu_reset_settings"]
-[connection signal="save_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/ResolutionSelector" method="save_setting"]
-[connection signal="save_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/ScreenModeSelector" method="save_setting"]
-[connection signal="save_settings" from="." to="Margin/Tab/Video/VBoxContainer/GridContainer/MonitorDisplaySelector" method="save_setting"]
-[connection signal="save_settings" from="." to="Margin/Tab/Sound/VBoxContainer/VolumeGrid" method="_on_options_menu_save_settings"]
-[connection signal="item_selected" from="Margin/Tab/Video/VBoxContainer/GridContainer/ResolutionSelector" to="Margin/Tab/Video/VBoxContainer/GridContainer/ResolutionSelector" method="_on_item_selected"]
-[connection signal="item_selected" from="Margin/Tab/Video/VBoxContainer/GridContainer/ScreenModeSelector" to="Margin/Tab/Video/VBoxContainer/GridContainer/ScreenModeSelector" method="_on_item_selected"]
-[connection signal="item_selected" from="Margin/Tab/Video/VBoxContainer/GridContainer/MonitorDisplaySelector" to="Margin/Tab/Video/VBoxContainer/GridContainer/MonitorDisplaySelector" method="_on_item_selected"]
-[connection signal="toggled" from="Margin/Tab/Sound/VBoxContainer/ButtonGrid/EarExploder" to="." method="_on_ear_exploder_toggled"]
-[connection signal="value_changed" from="Margin/Tab/Other/HBoxContainer/SpinBox" to="." method="_on_spin_box_value_changed"]
diff --git a/game/src/OptionMenu/OtherTab.tscn b/game/src/OptionMenu/OtherTab.tscn
new file mode 100644
index 0000000..0ffc92d
--- /dev/null
+++ b/game/src/OptionMenu/OtherTab.tscn
@@ -0,0 +1,18 @@
+[gd_scene format=3 uid="uid://dp2grvybtecqu"]
+
+[node name="Other" type="Control"]
+visible = false
+layout_mode = 3
+anchors_preset = 0
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 0
+offset_right = 40.0
+offset_bottom = 40.0
+
+[node name="Label" type="Label" parent="HBoxContainer"]
+layout_mode = 2
+text = "Spinbox Example :)"
+
+[node name="SpinBox" type="SpinBox" parent="HBoxContainer"]
+layout_mode = 2
diff --git a/game/src/OptionMenu/ResolutionSelector.gd b/game/src/OptionMenu/ResolutionSelector.gd
index ef1a0ff..e602bab 100644
--- a/game/src/OptionMenu/ResolutionSelector.gd
+++ b/game/src/OptionMenu/ResolutionSelector.gd
@@ -1,24 +1,58 @@
extends SettingOptionButton
-func _ready():
- print("Resolution selector ready")
+@export
+var default_value : Vector2i = Vector2i(-1, -1)
+
+func add_resolution(value : Vector2i, selection_name : String = "") -> void:
+ if selection_name.is_empty():
+ selection_name = "%sx%s" % [value.x, value.y]
+ add_item(selection_name)
+ set_item_metadata(item_count - 1, value)
+
+func find_resolution_value(value : Vector2i) -> int:
+ for item_index in range(item_count):
+ if get_item_metadata(item_index) == value:
+ return item_index
+ return -1
+
+func _setup_button():
+ if default_value.x < 0:
+ default_value.x = ProjectSettings.get_setting("display/window/size/viewport_width")
+
+ if default_value.y < 0:
+ default_value.y = ProjectSettings.get_setting("display/window/size/viewport_height")
clear()
- var resolution_index := 0
+ default_selected = -1
+ selected = -1
for resolution in Resolution.get_resolution_name_list():
- add_item(resolution)
+ var resolution_value := Resolution.get_resolution(resolution)
+ add_resolution(resolution_value, resolution)
- if Vector2(Resolution.get_resolution(resolution)) == Resolution.get_current_resolution():
- if default_value == -1:
- default_value = resolution_index
- _select_int(resolution_index)
- print(resolution)
+ if resolution_value == default_value:
+ default_selected = item_count - 1
- resolution_index += 1
+ if resolution_value == Resolution.get_current_resolution():
+ selected = item_count - 1
+ if default_selected == -1:
+ add_resolution(default_value)
+ default_selected = item_count - 1
-func _on_item_selected(index):
- print("Selected index: %d" % index)
+ if selected == -1:
+ selected = default_selected
+
+func _get_value_for_file(select_value : int):
+ return get_item_metadata(select_value)
- var resolution_size : Vector2i = Resolution.get_resolution(get_item_text(index))
- Resolution.set_resolution(resolution_size)
+func _set_value_from_file(load_value):
+ var resolution_value := load_value as Vector2i
+ selected = find_resolution_value(resolution_value)
+ if selected == -1:
+ if add_nonstandard_value:
+ add_resolution(resolution_value)
+ selected = item_count - 1
+ else: push_error("Setting value '%s' invalid for setting [%s] %s" % [load_value, section_name, setting_name])
+
+func _on_item_selected(index):
+ Resolution.set_resolution(get_item_metadata(index))
diff --git a/game/src/OptionMenu/ScreenModeSelector.gd b/game/src/OptionMenu/ScreenModeSelector.gd
index fae0771..92c5d60 100644
--- a/game/src/OptionMenu/ScreenModeSelector.gd
+++ b/game/src/OptionMenu/ScreenModeSelector.gd
@@ -24,9 +24,11 @@ func get_window_mode_from_screen_mode(screen_mode : int) -> Window.Mode:
_:
return Window.MODE_EXCLUSIVE_FULLSCREEN
-func _on_item_selected(index : int):
- print("Selected index: %d" % index)
+func _setup_button():
+ default_selected = get_screen_mode_from_window_mode(get_viewport().get_window().mode)
+ selected = default_selected
+func _on_item_selected(index : int):
var window := get_viewport().get_window()
var current_resolution := Resolution.get_current_resolution()
window.mode = get_window_mode_from_screen_mode(index)
diff --git a/game/src/OptionMenu/SettingNodes/SettingHSlider.gd b/game/src/OptionMenu/SettingNodes/SettingHSlider.gd
index da9348f..cf2adf4 100644
--- a/game/src/OptionMenu/SettingNodes/SettingHSlider.gd
+++ b/game/src/OptionMenu/SettingNodes/SettingHSlider.gd
@@ -10,10 +10,17 @@ var setting_name : String = "SettingHSlider"
@export
var default_value : float = 0
+func _ready():
+ Events.Options.load_settings.connect(load_setting)
+ Events.Options.save_settings.connect(save_setting)
+ Events.Options.reset_settings.connect(reset_setting)
+
func load_setting(file : ConfigFile):
+ if file == null: return
value = file.get_value(section_name, setting_name, default_value)
func save_setting(file : ConfigFile):
+ if file == null: return
file.set_value(section_name, setting_name, value)
func reset_setting():
diff --git a/game/src/OptionMenu/SettingNodes/SettingOptionButton.gd b/game/src/OptionMenu/SettingNodes/SettingOptionButton.gd
index 46fc825..3a5c979 100644
--- a/game/src/OptionMenu/SettingNodes/SettingOptionButton.gd
+++ b/game/src/OptionMenu/SettingNodes/SettingOptionButton.gd
@@ -8,15 +8,52 @@ var section_name : String = "Setting"
var setting_name : String = "SettingOptionMenu"
@export
-var default_value : int = 0
+var default_selected : int = -1:
+ get: return default_selected
+ set(v):
+ if v == -1:
+ default_selected = -1
+ return
+ default_selected = v % item_count
-func load_setting(file : ConfigFile):
- selected = file.get_value(section_name, setting_name, default_value)
+@export
+var add_nonstandard_value := false
+
+func _get_value_for_file(select_value : int):
+ if select_value > -1:
+ return get_item_text(select_value)
+ else:
+ return null
+
+func _set_value_from_file(load_value) -> void:
+ selected = -1
+ for item_index in range(item_count):
+ if load_value == get_item_text(item_index):
+ selected = item_index
+ if selected == -1:
+ if add_nonstandard_value:
+ add_item(load_value)
+ selected = item_count - 1
+ else: push_error("Setting value '%s' invalid for setting [%s] \"%s\"" % [load_value, section_name, setting_name])
+
+func _setup_button() -> void:
+ pass
+
+func _ready():
+ Events.Options.load_settings.connect(load_setting)
+ Events.Options.save_settings.connect(save_setting)
+ Events.Options.reset_settings.connect(reset_setting)
+ _setup_button()
+
+func load_setting(file : ConfigFile) -> void:
+ if file == null: return
+ _set_value_from_file(file.get_value(section_name, setting_name, _get_value_for_file(default_selected)))
item_selected.emit(selected)
-func save_setting(file : ConfigFile):
- file.set_value(section_name, setting_name, selected)
+func save_setting(file : ConfigFile) -> void:
+ if file == null: return
+ file.set_value(section_name, setting_name, _get_value_for_file(selected))
-func reset_setting():
- selected = default_value
+func reset_setting() -> void:
+ selected = default_selected
item_selected.emit(selected)
diff --git a/game/src/OptionMenu/SoundTab.gd b/game/src/OptionMenu/SoundTab.gd
new file mode 100644
index 0000000..c707605
--- /dev/null
+++ b/game/src/OptionMenu/SoundTab.gd
@@ -0,0 +1,4 @@
+extends HBoxContainer
+
+func _on_ear_exploder_toggled(button_pressed):
+ print("KABOOM!!!" if button_pressed else "DEFUSED!!!")
diff --git a/game/src/OptionMenu/SoundTab.tscn b/game/src/OptionMenu/SoundTab.tscn
new file mode 100644
index 0000000..10d7f10
--- /dev/null
+++ b/game/src/OptionMenu/SoundTab.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=3 format=3 uid="uid://cbtgwpx2wxi33"]
+
+[ext_resource type="Script" path="res://src/OptionMenu/SoundTab.gd" id="1_a7k0s"]
+[ext_resource type="PackedScene" uid="uid://dy4si8comamnv" path="res://src/OptionMenu/VolumeGrid.tscn" id="1_okpft"]
+
+[node name="Sound" type="HBoxContainer"]
+alignment = 1
+script = ExtResource("1_a7k0s")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Control" type="Control" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.1
+
+[node name="VolumeGrid" parent="VBoxContainer" instance=ExtResource("1_okpft")]
+layout_mode = 2
+
+[node name="ButtonGrid" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 2
+columns = 2
+
+[node name="Spacer" type="Control" parent="VBoxContainer/ButtonGrid"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="EarExploder" type="CheckButton" parent="VBoxContainer/ButtonGrid"]
+layout_mode = 2
+text = "Explode Eardrums on Startup?"
+
+[connection signal="toggled" from="VBoxContainer/ButtonGrid/EarExploder" to="." method="_on_ear_exploder_toggled"]
diff --git a/game/src/OptionMenu/VideoTab.tscn b/game/src/OptionMenu/VideoTab.tscn
new file mode 100644
index 0000000..d46f056
--- /dev/null
+++ b/game/src/OptionMenu/VideoTab.tscn
@@ -0,0 +1,73 @@
+[gd_scene load_steps=4 format=3 uid="uid://bq3awxxjn1tuw"]
+
+[ext_resource type="Script" path="res://src/OptionMenu/ResolutionSelector.gd" id="1_i8nro"]
+[ext_resource type="Script" path="res://src/OptionMenu/ScreenModeSelector.gd" id="2_wa7vw"]
+[ext_resource type="Script" path="res://src/OptionMenu/MonitorDisplaySelector.gd" id="3_y6lyb"]
+
+[node name="Video" type="HBoxContainer"]
+tooltip_text = "This is my cool and very nice tooltip"
+alignment = 1
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Control" type="Control" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+size_flags_stretch_ratio = 0.1
+
+[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+columns = 2
+
+[node name="ResolutionLabel" type="Label" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+text = "Resolution"
+
+[node name="ResolutionSelector" type="OptionButton" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+item_count = 1
+selected = 0
+popup/item_0/text = "MISSING"
+popup/item_0/id = 0
+script = ExtResource("1_i8nro")
+section_name = "Video"
+setting_name = "Resolution"
+add_nonstandard_value = true
+
+[node name="ScreenModeLabel" type="Label" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+text = "Screen Mode"
+
+[node name="ScreenModeSelector" type="OptionButton" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+item_count = 3
+selected = 0
+popup/item_0/text = "Fullscreen"
+popup/item_0/id = 0
+popup/item_1/text = "Borderless"
+popup/item_1/id = 1
+popup/item_2/text = "Windowed"
+popup/item_2/id = 2
+script = ExtResource("2_wa7vw")
+section_name = "Video"
+setting_name = "Mode Selected"
+
+[node name="MonitorSelectionLabel" type="Label" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+text = "Monitor Selection"
+
+[node name="MonitorDisplaySelector" type="OptionButton" parent="VBoxContainer/GridContainer"]
+layout_mode = 2
+item_count = 1
+selected = 0
+popup/item_0/text = "MISSING"
+popup/item_0/id = 0
+script = ExtResource("3_y6lyb")
+section_name = "Video"
+setting_name = "Current Screen"
+
+[connection signal="item_selected" from="VBoxContainer/GridContainer/ResolutionSelector" to="VBoxContainer/GridContainer/ResolutionSelector" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/GridContainer/ScreenModeSelector" to="VBoxContainer/GridContainer/ScreenModeSelector" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/GridContainer/MonitorDisplaySelector" to="VBoxContainer/GridContainer/MonitorDisplaySelector" method="_on_item_selected"]