Skip to content

Commit 775a428

Browse files
committed
Determine default keybinds from default_settings.json. Fallback on hardcoded values.
1 parent 8feb205 commit 775a428

3 files changed

Lines changed: 200 additions & 48 deletions

File tree

Intersect.Client.Framework/Input/BuiltinControlsProvider.cs

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,7 @@ namespace Intersect.Client.Framework.Input;
55

66
internal sealed class BuiltinControlsProvider : IControlsProvider
77
{
8-
private readonly Dictionary<Control, ControlMapping> _defaultMappings = new()
9-
{
10-
{ Control.MoveUp, new ControlMapping(new ControlBinding(Keys.None, Keys.Up), new ControlBinding(Keys.None, Keys.W)) },
11-
{ Control.MoveDown, new ControlMapping(new ControlBinding(Keys.None, Keys.Down), new ControlBinding(Keys.None, Keys.S)) },
12-
{ Control.MoveLeft, new ControlMapping(new ControlBinding(Keys.None, Keys.Left), new ControlBinding(Keys.None, Keys.A)) },
13-
{ Control.MoveRight, new ControlMapping(new ControlBinding(Keys.None, Keys.Right), new ControlBinding(Keys.None, Keys.D)) },
14-
{ Control.AttackInteract, new ControlMapping(new ControlBinding(Keys.None, Keys.E), new ControlBinding(Keys.None, Keys.LButton)) },
15-
{ Control.Block, new ControlMapping(new ControlBinding(Keys.None, Keys.Q), new ControlBinding(Keys.None, Keys.RButton)) },
16-
{ Control.AutoTarget, new ControlMapping(new ControlBinding(Keys.None, Keys.Tab), ControlBinding.Default) },
17-
{ Control.HoldToSoftRetargetOnSelfCast, new ControlMapping(new ControlBinding(Keys.None, Keys.LMenu), ControlBinding.Default) },
18-
{ Control.ToggleAutoSoftRetargetOnSelfCast, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
19-
{ Control.PickUp, new ControlMapping(new ControlBinding(Keys.None, Keys.Space), ControlBinding.Default) },
20-
{ Control.Enter, new ControlMapping(new ControlBinding(Keys.None, Keys.Enter), ControlBinding.Default) },
21-
{ Control.Screenshot, new ControlMapping(new ControlBinding(Keys.None, Keys.F12), ControlBinding.Default) },
22-
{ Control.OpenMenu, new ControlMapping(new ControlBinding(Keys.None, Keys.Escape), ControlBinding.Default) },
23-
{ Control.OpenInventory, new ControlMapping(new ControlBinding(Keys.None, Keys.I), ControlBinding.Default) },
24-
{ Control.OpenQuests, new ControlMapping(new ControlBinding(Keys.None, Keys.L), ControlBinding.Default) },
25-
{ Control.OpenCharacterInfo, new ControlMapping(new ControlBinding(Keys.None, Keys.C), ControlBinding.Default) },
26-
{ Control.OpenParties, new ControlMapping(new ControlBinding(Keys.None, Keys.P), ControlBinding.Default) },
27-
{ Control.OpenSpells, new ControlMapping(new ControlBinding(Keys.None, Keys.K), ControlBinding.Default) },
28-
{ Control.OpenFriends, new ControlMapping(new ControlBinding(Keys.None, Keys.F), ControlBinding.Default) },
29-
{ Control.OpenGuild, new ControlMapping(new ControlBinding(Keys.None, Keys.G), ControlBinding.Default) },
30-
{ Control.OpenSettings, new ControlMapping(new ControlBinding(Keys.None, Keys.O), ControlBinding.Default) },
31-
{ Control.OpenDebugger, new ControlMapping(new ControlBinding(Keys.None, Keys.F2), ControlBinding.Default) },
32-
{ Control.OpenAdminPanel, new ControlMapping(new ControlBinding(Keys.None, Keys.Insert), ControlBinding.Default) },
33-
{ Control.ToggleGui, new ControlMapping(new ControlBinding(Keys.None, Keys.F11), ControlBinding.Default) },
34-
{ Control.TurnAround, new ControlMapping(new ControlBinding(Keys.None, Keys.Control), ControlBinding.Default) },
35-
{ Control.ToggleZoomIn, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
36-
{ Control.ToggleZoomOut, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
37-
{ Control.HoldToZoomIn, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
38-
{ Control.HoldToZoomOut, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
39-
{ Control.ToggleFullscreen, new ControlMapping(new ControlBinding(Keys.Alt, Keys.Enter), ControlBinding.Default) },
40-
41-
// Hotkeys should be at the end of the list
42-
{ Control.Hotkey1, new ControlMapping(new ControlBinding(Keys.None, Keys.D1), ControlBinding.Default) },
43-
{ Control.Hotkey2, new ControlMapping(new ControlBinding(Keys.None, Keys.D2), ControlBinding.Default) },
44-
{ Control.Hotkey3, new ControlMapping(new ControlBinding(Keys.None, Keys.D3), ControlBinding.Default) },
45-
{ Control.Hotkey4, new ControlMapping(new ControlBinding(Keys.None, Keys.D4), ControlBinding.Default) },
46-
{ Control.Hotkey5, new ControlMapping(new ControlBinding(Keys.None, Keys.D5), ControlBinding.Default) },
47-
{ Control.Hotkey6, new ControlMapping(new ControlBinding(Keys.None, Keys.D6), ControlBinding.Default) },
48-
{ Control.Hotkey7, new ControlMapping(new ControlBinding(Keys.None, Keys.D7), ControlBinding.Default) },
49-
{ Control.Hotkey8, new ControlMapping(new ControlBinding(Keys.None, Keys.D8), ControlBinding.Default) },
50-
{ Control.Hotkey9, new ControlMapping(new ControlBinding(Keys.None, Keys.D9), ControlBinding.Default) },
51-
{ Control.Hotkey10, new ControlMapping(new ControlBinding(Keys.None, Keys.D0), ControlBinding.Default) },
52-
};
8+
private readonly Dictionary<Control, ControlMapping> _defaultMappings = LoadDefaultMappings();
539

5410
public Control[] Controls { get; } = Enum.GetValues<Control>().Where(control => control.IsValid()).ToArray();
5511

@@ -67,5 +23,79 @@ public bool TryGetDefaultMapping(Control control, [NotNullWhen(true)] out Contro
6723

6824
public void ReloadFromOptions(Options? options)
6925
{
26+
// Builtin controls don't change based on options
27+
// Defaults are loaded once from default_settings.json at initialization
28+
}
29+
30+
/// <summary>
31+
/// Loads default mappings by starting with hardcoded defaults and then merging overrides from default_settings.json if present.
32+
/// </summary>
33+
private static Dictionary<Control, ControlMapping> LoadDefaultMappings()
34+
{
35+
// Start with all hardcoded defaults
36+
var defaults = GetHardcodedDefaults();
37+
38+
// Load file overrides
39+
var fileDefaults = DefaultSettingsLoader.LoadDefaultsFromFile(defaults.Keys);
40+
41+
// Merge: file overrides hardcoded defaults
42+
foreach (var (control, mapping) in fileDefaults)
43+
{
44+
defaults[control] = mapping;
45+
}
46+
47+
return defaults;
48+
}
49+
50+
/// <summary>
51+
/// Returns the hardcoded default control mappings.
52+
/// </summary>
53+
private static Dictionary<Control, ControlMapping> GetHardcodedDefaults()
54+
{
55+
return new Dictionary<Control, ControlMapping>
56+
{
57+
{ Control.MoveUp, new ControlMapping(new ControlBinding(Keys.None, Keys.Up), new ControlBinding(Keys.None, Keys.W)) },
58+
{ Control.MoveDown, new ControlMapping(new ControlBinding(Keys.None, Keys.Down), new ControlBinding(Keys.None, Keys.S)) },
59+
{ Control.MoveLeft, new ControlMapping(new ControlBinding(Keys.None, Keys.Left), new ControlBinding(Keys.None, Keys.A)) },
60+
{ Control.MoveRight, new ControlMapping(new ControlBinding(Keys.None, Keys.Right), new ControlBinding(Keys.None, Keys.D)) },
61+
{ Control.AttackInteract, new ControlMapping(new ControlBinding(Keys.None, Keys.E), new ControlBinding(Keys.None, Keys.LButton)) },
62+
{ Control.Block, new ControlMapping(new ControlBinding(Keys.None, Keys.Q), new ControlBinding(Keys.None, Keys.RButton)) },
63+
{ Control.AutoTarget, new ControlMapping(new ControlBinding(Keys.None, Keys.Tab), ControlBinding.Default) },
64+
{ Control.HoldToSoftRetargetOnSelfCast, new ControlMapping(new ControlBinding(Keys.None, Keys.LMenu), ControlBinding.Default) },
65+
{ Control.ToggleAutoSoftRetargetOnSelfCast, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
66+
{ Control.PickUp, new ControlMapping(new ControlBinding(Keys.None, Keys.Space), ControlBinding.Default) },
67+
{ Control.Enter, new ControlMapping(new ControlBinding(Keys.None, Keys.Enter), ControlBinding.Default) },
68+
{ Control.Screenshot, new ControlMapping(new ControlBinding(Keys.None, Keys.F12), ControlBinding.Default) },
69+
{ Control.OpenMenu, new ControlMapping(new ControlBinding(Keys.None, Keys.Escape), ControlBinding.Default) },
70+
{ Control.OpenInventory, new ControlMapping(new ControlBinding(Keys.None, Keys.I), ControlBinding.Default) },
71+
{ Control.OpenQuests, new ControlMapping(new ControlBinding(Keys.None, Keys.L), ControlBinding.Default) },
72+
{ Control.OpenCharacterInfo, new ControlMapping(new ControlBinding(Keys.None, Keys.C), ControlBinding.Default) },
73+
{ Control.OpenParties, new ControlMapping(new ControlBinding(Keys.None, Keys.P), ControlBinding.Default) },
74+
{ Control.OpenSpells, new ControlMapping(new ControlBinding(Keys.None, Keys.K), ControlBinding.Default) },
75+
{ Control.OpenFriends, new ControlMapping(new ControlBinding(Keys.None, Keys.F), ControlBinding.Default) },
76+
{ Control.OpenGuild, new ControlMapping(new ControlBinding(Keys.None, Keys.G), ControlBinding.Default) },
77+
{ Control.OpenSettings, new ControlMapping(new ControlBinding(Keys.None, Keys.O), ControlBinding.Default) },
78+
{ Control.OpenDebugger, new ControlMapping(new ControlBinding(Keys.None, Keys.F2), ControlBinding.Default) },
79+
{ Control.OpenAdminPanel, new ControlMapping(new ControlBinding(Keys.None, Keys.Insert), ControlBinding.Default) },
80+
{ Control.ToggleGui, new ControlMapping(new ControlBinding(Keys.None, Keys.F11), ControlBinding.Default) },
81+
{ Control.TurnAround, new ControlMapping(new ControlBinding(Keys.None, Keys.Control), ControlBinding.Default) },
82+
{ Control.ToggleZoomIn, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
83+
{ Control.ToggleZoomOut, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
84+
{ Control.HoldToZoomIn, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
85+
{ Control.HoldToZoomOut, new ControlMapping(ControlBinding.Default, ControlBinding.Default) },
86+
{ Control.ToggleFullscreen, new ControlMapping(new ControlBinding(Keys.Alt, Keys.Enter), ControlBinding.Default) },
87+
88+
// Hotkeys should be at the end of the list
89+
{ Control.Hotkey1, new ControlMapping(new ControlBinding(Keys.None, Keys.D1), ControlBinding.Default) },
90+
{ Control.Hotkey2, new ControlMapping(new ControlBinding(Keys.None, Keys.D2), ControlBinding.Default) },
91+
{ Control.Hotkey3, new ControlMapping(new ControlBinding(Keys.None, Keys.D3), ControlBinding.Default) },
92+
{ Control.Hotkey4, new ControlMapping(new ControlBinding(Keys.None, Keys.D4), ControlBinding.Default) },
93+
{ Control.Hotkey5, new ControlMapping(new ControlBinding(Keys.None, Keys.D5), ControlBinding.Default) },
94+
{ Control.Hotkey6, new ControlMapping(new ControlBinding(Keys.None, Keys.D6), ControlBinding.Default) },
95+
{ Control.Hotkey7, new ControlMapping(new ControlBinding(Keys.None, Keys.D7), ControlBinding.Default) },
96+
{ Control.Hotkey8, new ControlMapping(new ControlBinding(Keys.None, Keys.D8), ControlBinding.Default) },
97+
{ Control.Hotkey9, new ControlMapping(new ControlBinding(Keys.None, Keys.D9), ControlBinding.Default) },
98+
{ Control.Hotkey10, new ControlMapping(new ControlBinding(Keys.None, Keys.D0), ControlBinding.Default) },
99+
};
70100
}
71101
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Intersect.Client.Framework.GenericClasses;
3+
using Intersect.Core;
4+
using Microsoft.Extensions.Logging;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Linq;
7+
8+
namespace Intersect.Client.Framework.Input;
9+
10+
/// <summary>
11+
/// Helper class for loading default control mappings from resources/default_settings.json
12+
/// </summary>
13+
internal static class DefaultSettingsLoader
14+
{
15+
/// <summary>
16+
/// Loads default control mappings from resources/default_settings.json if the file exists.
17+
/// </summary>
18+
/// <param name="controlsToLoad">The controls to attempt to load from the file</param>
19+
/// <returns>Dictionary of controls with their mappings from the file (empty if file doesn't exist)</returns>
20+
public static Dictionary<Control, ControlMapping> LoadDefaultsFromFile(IEnumerable<Control> controlsToLoad)
21+
{
22+
var defaults = new Dictionary<Control, ControlMapping>();
23+
24+
var defaultSettingsPath = Path.Combine("resources", "default_settings.json");
25+
if (!File.Exists(defaultSettingsPath))
26+
{
27+
// File doesn't exist - this is expected for most games
28+
return defaults;
29+
}
30+
31+
try
32+
{
33+
var json = File.ReadAllText(defaultSettingsPath);
34+
var settings = JObject.Parse(json);
35+
36+
foreach (var control in controlsToLoad)
37+
{
38+
if (TryLoadControlMapping(settings, control, out var mapping))
39+
{
40+
defaults[control] = mapping;
41+
}
42+
}
43+
}
44+
catch (Exception exception)
45+
{
46+
ApplicationContext.Context.Value?.Logger.LogError(
47+
exception,
48+
"Failed to load default_settings.json"
49+
);
50+
}
51+
52+
return defaults;
53+
}
54+
55+
/// <summary>
56+
/// Attempts to load a single control's mapping from the JSON settings.
57+
/// </summary>
58+
/// <param name="settings">The parsed JSON object containing the settings</param>
59+
/// <param name="control">The control to load</param>
60+
/// <param name="mapping">The loaded control mapping, if successful</param>
61+
/// <returns>True if at least one valid binding was loaded, false otherwise</returns>
62+
private static bool TryLoadControlMapping(
63+
JObject settings,
64+
Control control,
65+
[NotNullWhen(true)] out ControlMapping? mapping)
66+
{
67+
var controlId = control.GetControlId();
68+
var bindings = new List<ControlBinding>();
69+
70+
// Try to load binding0 and binding1
71+
for (int i = 0; i < 2; i++)
72+
{
73+
var key = $"{controlId}_binding{i}";
74+
if (settings.TryGetValue(key, out var token) && token.Type == JTokenType.String)
75+
{
76+
try
77+
{
78+
var binding = JsonConvert.DeserializeObject<ControlBinding>(token.Value<string>());
79+
if (binding != null)
80+
{
81+
bindings.Add(binding);
82+
}
83+
}
84+
catch (Exception ex)
85+
{
86+
ApplicationContext.Context.Value?.Logger.LogWarning(
87+
ex,
88+
"Failed to deserialize binding {Key} from default_settings.json",
89+
key
90+
);
91+
}
92+
}
93+
}
94+
95+
// Need at least one valid binding
96+
if (bindings.Count > 0)
97+
{
98+
// Pad to 2 bindings (ControlMapping requires at least 2)
99+
while (bindings.Count < 2)
100+
{
101+
bindings.Add(ControlBinding.Default);
102+
}
103+
104+
mapping = new ControlMapping(bindings[0], bindings.Skip(1).ToArray());
105+
return true;
106+
}
107+
108+
mapping = null;
109+
return false;
110+
}
111+
}

Intersect.Client.Framework/Input/HotkeyControlsProvider.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@ namespace Intersect.Client.Framework.Input;
55

66
internal sealed class HotkeyControlsProvider : IControlsProvider
77
{
8+
private Dictionary<Control, ControlMapping>? _defaultMappings = null;
9+
810
public HotkeyControlsProvider() => Controls = ControlsFromOptions(Options.Instance);
911

1012
public Control[] Controls { get; private set; }
1113

1214
public bool TryGetDefaultMapping(Control control, [NotNullWhen(true)] out ControlMapping? defaultMapping)
1315
{
14-
defaultMapping = null;
15-
return false;
16+
// Lazy-load on first call
17+
if (_defaultMappings == null)
18+
{
19+
_defaultMappings = DefaultSettingsLoader.LoadDefaultsFromFile(Controls);
20+
}
21+
22+
return _defaultMappings.TryGetValue(control, out defaultMapping);
1623
}
1724

1825
private static Control[] ControlsFromOptions(Options? options) =>
@@ -21,5 +28,9 @@ private static Control[] ControlsFromOptions(Options? options) =>
2128
.Select(hotkeyValue => Control.HotkeyOffset + hotkeyValue)
2229
.ToArray();
2330

24-
public void ReloadFromOptions(Options? options) => Controls = ControlsFromOptions(options);
31+
public void ReloadFromOptions(Options? options)
32+
{
33+
Controls = ControlsFromOptions(options);
34+
_defaultMappings = null; // Force reload with new hotbar slot count
35+
}
2536
}

0 commit comments

Comments
 (0)