feat: input remapping screen with conflict detection and config persistence

This commit is contained in:
2026-03-19 03:39:19 +01:00
parent 47600be867
commit dc46769c11
3 changed files with 213 additions and 21 deletions

205
scripts/UI/InputSettings.cs Normal file
View File

@@ -0,0 +1,205 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class InputSettings : Control
{
[Export] public VBoxContainer ActionsContainer { get; set; }
[Export] public Button ResetButton { get; set; }
[Export] public Control InputSettingsControl { get; set; }
[Export] public PackedScene InputButtonScene { get; set; }
private static readonly string[] RemappableActions =
["left", "right", "jump", "attack", "pause", "show_marketplace"];
private UIManager UIManager => UIManager.Instance;
private ConfigFileHandler ConfigFileHandler => ConfigFileHandler.Instance;
private string _rebindingAction;
private Button _rebindingButton;
private Label _rebindingLabel;
public override void _Ready()
{
PopulateActions();
ResetButton.FocusMode = FocusModeEnum.All;
ResetButton.Pressed += OnResetPressed;
if (InputDeviceManager.Instance != null)
InputDeviceManager.Instance.DeviceChanged += OnDeviceChanged;
InputSettingsControl.VisibilityChanged += OnVisibilityChanged;
}
public override void _ExitTree()
{
if (InputDeviceManager.Instance != null)
InputDeviceManager.Instance.DeviceChanged -= OnDeviceChanged;
InputSettingsControl.VisibilityChanged -= OnVisibilityChanged;
}
public override void _UnhandledInput(InputEvent @event)
{
if (_rebindingAction != null)
{
if (@event is InputEventKey keyEvent && keyEvent.Pressed && !keyEvent.Echo)
{
GetViewport().SetInputAsHandled();
TryBind(_rebindingAction, _rebindingButton, _rebindingLabel, keyEvent);
}
else if (@event.IsActionReleased("ui_cancel"))
{
GetViewport().SetInputAsHandled();
CancelRebind();
}
return;
}
if (!@event.IsActionReleased("ui_cancel")) return;
if (!UIManager.IsScreenOnTop(InputSettingsControl)) return;
UIManager.PopScreen();
}
private void PopulateActions()
{
foreach (Node child in ActionsContainer.GetChildren())
child.QueueFree();
bool first = true;
foreach (var action in RemappableActions)
{
var row = InputButtonScene.Instantiate<Button>();
ActionsContainer.AddChild(row);
var labelAction = row.GetNode<Label>("MarginContainer/HBoxContainer/LabelAction");
var labelInput = row.GetNode<Label>("MarginContainer/HBoxContainer/LabelInput");
labelAction.Text = HumanizeAction(action);
labelInput.Text = GetKeyName(action);
row.FocusMode = FocusModeEnum.All;
var capturedAction = action;
var capturedLabel = labelInput;
var capturedRow = row;
row.Pressed += () => OnRowPressed(capturedAction, capturedRow, capturedLabel);
if (first)
{
first = false;
}
}
}
private void OnRowPressed(string action, Button row, Label labelInput)
{
if (_rebindingAction != null)
CancelRebind();
_rebindingAction = action;
_rebindingButton = row;
_rebindingLabel = labelInput;
labelInput.Text = "Press a key...";
}
private void TryBind(string action, Button row, Label labelInput, InputEventKey keyEvent)
{
var newKey = keyEvent.Keycode != Key.None ? keyEvent.Keycode : keyEvent.PhysicalKeycode;
// Conflict check
foreach (var other in RemappableActions)
{
if (other == action) continue;
var existing = GetCurrentKey(other);
if (existing == newKey)
{
labelInput.Text = $"Already used by {HumanizeAction(other)}!";
_rebindingAction = null;
_rebindingButton = null;
_rebindingLabel = null;
var capturedAction = action;
var capturedLabel = labelInput;
GetTree().CreateTimer(2.0).Timeout += () =>
{
if (IsInstanceValid(capturedLabel))
capturedLabel.Text = GetKeyName(capturedAction);
};
return;
}
}
// Apply
foreach (var ev in InputMap.ActionGetEvents(action))
{
if (ev is InputEventKey)
InputMap.ActionEraseEvent(action, ev);
}
InputMap.ActionAddEvent(action, keyEvent);
// Persist
ConfigFileHandler.SettingsConfig.SetValue("input_settings", action, (long)newKey);
ConfigFileHandler.SettingsConfig.Save(ConfigFileHandler.SettingsPath);
labelInput.Text = OS.GetKeycodeString(newKey);
_rebindingAction = null;
_rebindingButton = null;
_rebindingLabel = null;
}
private void CancelRebind()
{
if (_rebindingLabel != null)
_rebindingLabel.Text = GetKeyName(_rebindingAction);
_rebindingAction = null;
_rebindingButton = null;
_rebindingLabel = null;
}
private void OnResetPressed()
{
InputMap.LoadFromProjectSettings();
ConfigFileHandler.SettingsConfig.EraseSection("input_settings");
ConfigFileHandler.SettingsConfig.Save(ConfigFileHandler.SettingsPath);
PopulateActions();
}
private void OnVisibilityChanged()
{
if (InputSettingsControl.IsVisibleInTree() && InputDeviceManager.Instance?.IsPointerless == true)
GrabFirstFocus();
}
private void OnDeviceChanged(int device)
{
var d = (InputDeviceManager.InputDevice)device;
if (d != InputDeviceManager.InputDevice.Mouse && InputSettingsControl.IsVisibleInTree())
GrabFirstFocus();
}
private void GrabFirstFocus()
{
var children = ActionsContainer.GetChildren();
if (children.Count > 0 && children[0] is Button btn)
btn.GrabFocus();
}
private static Key GetCurrentKey(string action)
{
foreach (var ev in InputMap.ActionGetEvents(action))
{
if (ev is InputEventKey key)
return key.Keycode != Key.None ? key.Keycode : key.PhysicalKeycode;
}
return Key.None;
}
private static string GetKeyName(string action)
{
var key = GetCurrentKey(action);
return key == Key.None ? "---" : OS.GetKeycodeString(key);
}
private static string HumanizeAction(string action)
=> action.Replace("_", " ").ToUpper();
}