diff --git a/Source/Data/Controls.cs b/Source/Data/Controls.cs
index 6c014e7e..7a1ecf8e 100644
--- a/Source/Data/Controls.cs
+++ b/Source/Data/Controls.cs
@@ -1,34 +1,134 @@
using Celeste64.Mod;
+using System.Reflection;
namespace Celeste64;
+[DisallowHooks]
public static class Controls
{
- public static readonly VirtualStick Move = new("Move", VirtualAxis.Overlaps.TakeNewer, 0.35f);
- public static readonly VirtualStick Menu = new("Menu", VirtualAxis.Overlaps.TakeNewer, 0.35f);
- public static readonly VirtualStick Camera = new("Camera", VirtualAxis.Overlaps.TakeNewer, 0.35f);
- public static readonly VirtualButton Jump = new("Jump", .1f);
- public static readonly VirtualButton Dash = new("Dash", .1f);
- public static readonly VirtualButton Climb = new("Climb");
- public static readonly VirtualButton Confirm = new("Confirm");
- public static readonly VirtualButton Cancel = new("Cancel");
- public static readonly VirtualButton Pause = new("Pause");
- public static readonly VirtualButton CopyFile = new("CopyFile");
- public static readonly VirtualButton DeleteFile = new("DeleteFile");
- public static readonly VirtualButton CreateFile = new("CreateFile");
- public static readonly VirtualButton ResetBindings = new("ResetBindings");
- public static readonly VirtualButton ClearBindings = new("ClearBindings");
- public static readonly VirtualButton DebugMenu = new("DebugMenu");
- public static readonly VirtualButton Restart = new("Restart");
- public static readonly VirtualButton FullScreen = new("FullScreen");
- public static readonly VirtualButton ReloadAssets = new("ReloadAssets");
+ #region Default Controls
+ [DefaultStickBinding(StickDirection.Up, Keys.Up)]
+ [DefaultStickBinding(StickDirection.Up, Buttons.Up)]
+ [DefaultStickBinding(StickDirection.Up, Axes.LeftY, 0.0f, true)]
+ [DefaultStickBinding(StickDirection.Down, Keys.Down)]
+ [DefaultStickBinding(StickDirection.Down, Buttons.Down)]
+ [DefaultStickBinding(StickDirection.Down, Axes.LeftY, 0.0f, false)]
+ [DefaultStickBinding(StickDirection.Left, Keys.Left)]
+ [DefaultStickBinding(StickDirection.Left, Buttons.Left)]
+ [DefaultStickBinding(StickDirection.Left, Axes.LeftX, 0.0f, true)]
+ [DefaultStickBinding(StickDirection.Right, Keys.Right)]
+ [DefaultStickBinding(StickDirection.Right, Buttons.Right)]
+ [DefaultStickBinding(StickDirection.Right, Axes.LeftX, 0.0f, false)]
+ public static VirtualStick Move { get; private set; } = new("Move", VirtualAxis.Overlaps.TakeNewer, 0.35f);
+
+ [DefaultStickBinding(StickDirection.Up, Keys.Up)]
+ [DefaultStickBinding(StickDirection.Up, Buttons.Up)]
+ [DefaultStickBinding(StickDirection.Up, Axes.LeftY, 0.50f, true)]
+ [DefaultStickBinding(StickDirection.Down, Keys.Down)]
+ [DefaultStickBinding(StickDirection.Down, Buttons.Down)]
+ [DefaultStickBinding(StickDirection.Down, Axes.LeftY, 0.50f, false)]
+ [DefaultStickBinding(StickDirection.Left, Keys.Left)]
+ [DefaultStickBinding(StickDirection.Left, Buttons.Left)]
+ [DefaultStickBinding(StickDirection.Left, Axes.LeftX, 0.50f, true)]
+ [DefaultStickBinding(StickDirection.Right, Keys.Right)]
+ [DefaultStickBinding(StickDirection.Right, Buttons.Right)]
+ [DefaultStickBinding(StickDirection.Right, Axes.LeftX, 0.50f, false)]
+ public static VirtualStick Menu { get; private set; } = new("Menu", VirtualAxis.Overlaps.TakeNewer, 0.35f);
+
+ [DefaultStickBinding(StickDirection.Up, Keys.W)]
+ [DefaultStickBinding(StickDirection.Up, Axes.RightY, 0.0f, true)]
+ [DefaultStickBinding(StickDirection.Down, Keys.S)]
+ [DefaultStickBinding(StickDirection.Down, Axes.RightY, 0.0f, false)]
+ [DefaultStickBinding(StickDirection.Left, Keys.A)]
+ [DefaultStickBinding(StickDirection.Left, Axes.RightX, 0.0f, true)]
+ [DefaultStickBinding(StickDirection.Right, Keys.D)]
+ [DefaultStickBinding(StickDirection.Right, Axes.RightX, 0.0f, false)]
+ public static VirtualStick Camera { get; private set; } = new("Camera", VirtualAxis.Overlaps.TakeNewer, 0.35f);
+
+ [DefaultBinding(Keys.C)]
+ [DefaultBinding(Buttons.South)]
+ [DefaultBinding(Buttons.North)]
+ public static VirtualButton Jump { get; private set; } = new("Jump", .1f);
+
+ [DefaultBinding(Keys.X)]
+ [DefaultBinding(Buttons.West)]
+ [DefaultBinding(Buttons.East)]
+ public static VirtualButton Dash { get; private set; } = new("Dash", .1f);
+
+ [DefaultBinding(Keys.Z)]
+ [DefaultBinding(Keys.V)]
+ [DefaultBinding(Keys.LeftShift)]
+ [DefaultBinding(Keys.RightShift)]
+ [DefaultBinding(Buttons.LeftShoulder)]
+ [DefaultBinding(Buttons.RightShoulder)]
+ [DefaultBinding(Axes.LeftTrigger, 0.4f, false)]
+ [DefaultBinding(Axes.RightTrigger, 0.4f, false)]
+ public static VirtualButton Climb { get; private set; } = new("Climb");
+
+ [DefaultBinding(Keys.C)]
+ [DefaultBinding(Buttons.South, [Gamepads.DualSense, Gamepads.DualShock4, Gamepads.Xbox])]
+ [DefaultBinding(Buttons.East, [Gamepads.Nintendo])]
+ public static VirtualButton Confirm { get; private set; } = new("Confirm");
+
+ [DefaultBinding(Keys.X)]
+ [DefaultBinding(Buttons.East, [Gamepads.DualSense, Gamepads.DualShock4, Gamepads.Xbox])]
+ [DefaultBinding(Buttons.South, [Gamepads.Nintendo])]
+ public static VirtualButton Cancel { get; private set; } = new("Cancel");
+
+ [DefaultBinding(Keys.Escape)]
+ [DefaultBinding(Keys.Enter)]
+ [DefaultBinding(Buttons.Start)]
+ [DefaultBinding(Buttons.Select)]
+ [DefaultBinding(Buttons.Back)]
+ public static VirtualButton Pause { get; private set; } = new("Pause");
+
+ [DefaultBinding(Keys.V)]
+ [DefaultBinding(Buttons.LeftShoulder)]
+ public static VirtualButton CopyFile { get; private set; } = new("CopyFile");
+
+ [DefaultBinding(Keys.B)]
+ [DefaultBinding(Buttons.North)]
+ public static VirtualButton DeleteFile { get; private set; } = new("DeleteFile");
+
+ [DefaultBinding(Keys.N)]
+ [DefaultBinding(Buttons.RightShoulder)]
+ public static VirtualButton CreateFile { get; private set; } = new("CreateFile");
+
+ [DefaultBinding(Keys.V)]
+ [DefaultBinding(Buttons.LeftShoulder)]
+ public static VirtualButton ResetBindings { get; private set; } = new("ResetBindings");
+
+ [DefaultBinding(Keys.B)]
+ [DefaultBinding(Buttons.North)]
+ public static VirtualButton ClearBindings { get; private set; } = new("ClearBindings");
+
+ [DefaultBinding(Keys.F6)]
+ public static VirtualButton DebugMenu { get; private set; } = new("DebugMenu");
+
+ [DefaultBinding(Keys.R)]
+ public static VirtualButton Restart { get; private set; } = new("Restart");
+
+ [DefaultBinding(Keys.F4)]
+ public static VirtualButton FullScreen { get; private set; } = new("FullScreen");
+
+ [DefaultBinding(Keys.F5)]
+ public static VirtualButton ReloadAssets { get; private set; } = new("ReloadAssets");
+ #endregion
+
+ #region Additional Properties
public static ControlsConfig_V01 Instance = new();
public const string DefaultFileName = "controls.json";
+ #endregion
- [DisallowHooks]
- internal static void LoadControlsByFileName(string file_name)
+ #region Saving and Loading
+
+ ///
+ /// Load Controls from a file
+ ///
+ ///
+ internal static void LoadControlsFromFile(string file_name)
{
if (file_name == string.Empty) file_name = DefaultFileName;
var controlsFile = Path.Join(App.UserPath, file_name);
@@ -50,38 +150,187 @@ internal static void LoadControlsByFileName(string file_name)
{
controls = new ControlsConfig_V01();
}
+ Instance = controls;
+
+ // Add any missing default sticks or buttons, if they weren't loaded from the file
+ AddMissingBindings(Instance, typeof(Controls));
+
+ LoadBindings(Instance, typeof(Controls));
+ }
+
+ ///
+ /// Save control bindings to a file
+ ///
+ internal static void SaveToFile()
+ {
+ var savePath = Path.Join(App.UserPath, DefaultFileName);
+ var tempPath = Path.Join(App.UserPath, DefaultFileName + ".backup");
+
+ // first save to a temporary file
+ {
+ using var stream = File.Create(tempPath);
+ Instance.Serialize(stream, Instance);
+ stream.Flush();
+ }
- // Add any default actions and sticks that are missing.
- foreach (var defaultAction in ControlsConfig_V01.Defaults.Actions)
+ // validate that the temp path worked, and overwrite existing if it did.
+ if (File.Exists(tempPath) &&
+ Instance.Deserialize(File.ReadAllText(tempPath)) != null)
{
- if (!controls.Actions.ContainsKey(defaultAction.Key))
+ File.Copy(tempPath, savePath, true);
+ }
+ }
+ #endregion
+
+ ///
+ /// Add any default bindings that are not currently set up.
+ ///
+ ///
+ ///
+ ///
+ private static void AddMissingBindings(ControlsConfig_V01 config, Type controlsType, object? controlsInstance = null)
+ {
+ foreach (var prop in controlsType.GetProperties())
+ {
+ if (prop.PropertyType == typeof(VirtualButton))
+ {
+ var vb = prop.GetValue(controlsInstance) as VirtualButton;
+ if (vb != null && !config.Actions.ContainsKey(vb.Name))
+ {
+ config.Actions[vb.Name] = GetDefaultActionBindings(vb, controlsType, controlsInstance).ToList();
+ }
+ }
+ else if (prop.PropertyType == typeof(VirtualStick))
{
- controls.Actions[defaultAction.Key] = [.. defaultAction.Value];
+ var vs = prop.GetValue(controlsInstance) as VirtualStick;
+ if (vs != null && !config.Sticks.ContainsKey(vs.Name))
+ {
+ config.Sticks[vs.Name] = new ControlsConfigStick()
+ {
+ Up = GetDefaultStickBindings(vs, controlsType, StickDirection.Up, controlsInstance).ToList(),
+ Down = GetDefaultStickBindings(vs, controlsType, StickDirection.Down, controlsInstance).ToList(),
+ Left = GetDefaultStickBindings(vs, controlsType, StickDirection.Left, controlsInstance).ToList(),
+ Right = GetDefaultStickBindings(vs, controlsType, StickDirection.Right, controlsInstance).ToList(),
+ Deadzone = vs.CircularDeadzone
+ };
+ }
}
}
- foreach (var defaultStick in ControlsConfig_V01.Defaults.Sticks)
+ }
+
+ ///
+ /// Get default bindings for a virtual button, based on attributes.
+ /// If button is from a virtual stick, get bindings from that instead.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static IEnumerable GetDefaultActionBindings(VirtualButton virtualButton, Type instanceType, object? instance = null)
+ {
+ string[] nameParts = virtualButton.Name.Split("/");
+
+ if (nameParts.Length == 3)
{
- if (!controls.Sticks.ContainsKey(defaultStick.Key))
+ StickDirection stickDir = StickDirection.Down;
+ if (nameParts[1] == "Horizontal" && nameParts[2] == "Positive")
{
- controls.Sticks[defaultStick.Key] = new ControlsConfigStick()
- {
- Up = [.. defaultStick.Value.Up],
- Down = [.. defaultStick.Value.Down],
- Left = [.. defaultStick.Value.Left],
- Right = [.. defaultStick.Value.Right],
- Deadzone = defaultStick.Value.Deadzone
- };
+ stickDir = StickDirection.Right;
+ }
+ else if (nameParts[1] == "Horizontal" && nameParts[2] == "Negative")
+ {
+ stickDir = StickDirection.Left;
+ }
+ else if (nameParts[1] == "Vertical" && nameParts[2] == "Negative")
+ {
+ stickDir = StickDirection.Up;
+ }
+ else if (nameParts[1] == "Vertical" && nameParts[2] == "Positive")
+ {
+ stickDir = StickDirection.Down;
+ }
+
+ var stickAttributes = instanceType
+ .GetProperties()
+ .FirstOrDefault(p => p.PropertyType == typeof(VirtualStick) && p.GetValue(instance) is VirtualStick vs && vs.Name == nameParts[0])?
+ .GetCustomAttributes(typeof(DefaultStickBindingAttribute));
+ if (stickAttributes != null)
+ {
+ return stickAttributes
+ .Where(a => ((DefaultStickBindingAttribute)a).Direction == stickDir)
+ .Select(a => ((DefaultStickBindingAttribute)a).Binding);
+ }
+
+ }
+ else
+ {
+ var attributes = instanceType
+ .GetProperties()
+ .FirstOrDefault(p => p.PropertyType == typeof(VirtualButton) && p.GetValue(instance) is VirtualButton vb && vb.Name == virtualButton.Name)?
+ .GetCustomAttributes(typeof(DefaultBindingAttribute));
+ if (attributes != null)
+ {
+ var gamepad = Input.Controllers[0];
+
+ return attributes
+ .Where(a => a is DefaultBindingAttribute attr && (attr.Binding.ForGamepads == null || attr.Binding.ForGamepads.Contains(gamepad.Gamepad)))
+ .Select(a => ((DefaultBindingAttribute)a).Binding);
}
}
+ return Enumerable.Empty();
+ }
- Instance = controls;
- LoadConfig(Instance);
+ ///
+ /// Get default bindings for a virtual stick.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static IEnumerable GetDefaultStickBindings(VirtualStick virtualStick, Type instanceType, StickDirection? stickDirection = null, object? instance = null)
+ {
+ var attributes = instanceType
+ .GetProperties()
+ .FirstOrDefault(p => p.PropertyType == typeof(VirtualStick) && p.GetValue(instance) is VirtualStick vs && vs.Name == virtualStick.Name)?
+ .GetCustomAttributes(typeof(DefaultStickBindingAttribute));
+ if (attributes != null)
+ {
+ return attributes
+ .Where(a => ((DefaultStickBindingAttribute)a).Direction == stickDirection)
+ .Select(a => ((DefaultStickBindingAttribute)a).Binding);
+ }
+ return Enumerable.Empty();
+ }
+
+ ///
+ /// Load bindings from a mod config.
+ ///
+ ///
+ internal static void LoadModConfig(GameMod mod)
+ {
+ if (mod.SettingsType != null)
+ {
+ AddMissingBindings(mod.ModSettingsData.ModControlBindings, mod.SettingsType, mod.Settings);
+
+ LoadBindings(mod.ModSettingsData.ModControlBindings, mod.SettingsType, mod.Settings);
+ }
}
- [DisallowHooks]
- internal static void AddBinding(VirtualButton virtualButton, Keys key)
+ ///
+ /// Bind a new Key to a virtual button.
+ ///
+ ///
+ ///
+ ///
+ internal static void AddBinding(VirtualButton virtualButton, Keys key, ControlsConfig_V01? config = null)
{
- var bindings = GetButtonBindings(Instance, virtualButton);
+ if (config == null)
+ {
+ config = Instance;
+ }
+
+ var bindings = GetButtonBindings(config, virtualButton);
if (bindings != null && !bindings.Any(a => a.Key == key))
{
bindings.Add(new(key));
@@ -90,10 +339,20 @@ internal static void AddBinding(VirtualButton virtualButton, Keys key)
Consume();
}
- [DisallowHooks]
- internal static void AddBinding(VirtualButton virtualButton, Buttons button)
+ ///
+ /// Bind a new Controller button to a virtual button.
+ ///
+ ///
+ ///
+ ///
+ internal static void AddBinding(VirtualButton virtualButton, Buttons button, ControlsConfig_V01? config = null)
{
- var bindings = GetButtonBindings(Instance, virtualButton);
+ if (config == null)
+ {
+ config = Instance;
+ }
+
+ var bindings = GetButtonBindings(config, virtualButton);
if (bindings != null && !bindings.Any(a => a.Button == button))
{
bindings.Add(new(button));
@@ -102,10 +361,20 @@ internal static void AddBinding(VirtualButton virtualButton, Buttons button)
Consume();
}
- [DisallowHooks]
- internal static void AddBinding(VirtualButton virtualButton, MouseButtons mouseButton)
+ ///
+ /// Bind a new Mouse button to a virtual button.
+ ///
+ ///
+ ///
+ ///
+ internal static void AddBinding(VirtualButton virtualButton, MouseButtons mouseButton, ControlsConfig_V01? config = null)
{
- var bindings = GetButtonBindings(Instance, virtualButton);
+ if (config == null)
+ {
+ config = Instance;
+ }
+
+ var bindings = GetButtonBindings(config, virtualButton);
if (bindings != null && !bindings.Any(a => a.MouseButton == mouseButton))
{
bindings.Add(new(mouseButton));
@@ -114,10 +383,22 @@ internal static void AddBinding(VirtualButton virtualButton, MouseButtons mouseB
Consume();
}
- [DisallowHooks]
- internal static void AddBinding(VirtualButton virtualButton, Axes axis, bool inverted, float deadzone = 0.0f)
+ ///
+ /// Bind a new axis value to a virtual button.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static void AddBinding(VirtualButton virtualButton, Axes axis, bool inverted, float deadzone = 0.0f, ControlsConfig_V01? config = null)
{
- var bindings = GetButtonBindings(Instance, virtualButton);
+ if (config == null)
+ {
+ config = Instance;
+ }
+
+ var bindings = GetButtonBindings(config, virtualButton);
if (bindings != null && !bindings.Any(a => a.Axis == axis && a.AxisInverted == inverted))
{
bindings.Add(new(axis, deadzone, inverted));
@@ -126,10 +407,21 @@ internal static void AddBinding(VirtualButton virtualButton, Axes axis, bool inv
Consume();
}
- [DisallowHooks]
- internal static void ClearBinding(VirtualButton virtualButton, bool forController, bool requiresBinding = false)
+ ///
+ /// Removes all bindings from a virtual button, unless requiresBinding is true, in which case it will keep the last one.
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static void ClearBinding(VirtualButton virtualButton, bool forController, bool requiresBinding = false, ControlsConfig_V01? config = null)
{
- var bindings = GetButtonBindings(Instance, virtualButton);
+ if (config == null)
+ {
+ config = Instance;
+ }
+
+ var bindings = GetButtonBindings(config, virtualButton);
if (bindings != null)
{
virtualButton.Clear();
@@ -156,11 +448,29 @@ internal static void ClearBinding(VirtualButton virtualButton, bool forControlle
}
}
- [DisallowHooks]
- internal static void ResetBinding(VirtualButton virtualButton, bool forController)
+ ///
+ /// Resets bindings for a single virtual button to their default values.
+ ///
+ ///
+ ///
+ ///
+ internal static void ResetBinding(VirtualButton virtualButton, bool forController, GameMod? mod = null)
{
- var bindings = GetButtonBindings(Instance, virtualButton);
- var defaultBindings = GetButtonBindings(ControlsConfig_V01.Defaults, virtualButton);
+ ControlsConfig_V01 config;
+ IEnumerable defaultBindings;
+ if (mod != null && mod.SettingsType != null)
+ {
+ config = mod.ModSettingsData.ModControlBindings;
+ defaultBindings = GetDefaultActionBindings(virtualButton, mod.SettingsType, mod.Settings);
+ }
+ else
+ {
+ config = Instance;
+ defaultBindings = GetDefaultActionBindings(virtualButton, typeof(Controls));
+ }
+
+ var bindings = GetButtonBindings(config, virtualButton);
+
if (bindings != null && defaultBindings != null)
{
virtualButton.Clear();
@@ -185,168 +495,189 @@ internal static void ResetBinding(VirtualButton virtualButton, bool forControlle
}
}
- [DisallowHooks]
- internal static void ResetAllBindings(bool forController)
+ ///
+ /// Resets all bindings for this control type to their default values.
+ ///
+ ///
+ ///
+ internal static void ResetAllBindings(bool forController, GameMod? mod = null)
{
+ ControlsConfig_V01 config;
+ IEnumerable defaultBindings;
+ Type settingsType;
+ object? settingsObject;
+
+ if (mod != null && mod.SettingsType != null)
+ {
+ config = mod.ModSettingsData.ModControlBindings;
+ settingsType = mod.SettingsType;
+ settingsObject = mod.Settings;
+ }
+ else
+ {
+ config = Instance;
+ settingsType = typeof(Controls);
+ settingsObject = Instance;
+ }
+
// Remove all bindings for this control type
- foreach (var action in Instance.Actions)
+ foreach (var action in config.Actions)
{
- Instance.Actions[action.Key].RemoveAll(x => x.IsForController() == forController);
+ config.Actions[action.Key].RemoveAll(x => x.IsForController() == forController);
}
- foreach (var action in Instance.Sticks)
+ foreach (var action in config.Sticks)
{
- Instance.Sticks[action.Key].Up.RemoveAll(x => x.IsForController() == forController);
- Instance.Sticks[action.Key].Down.RemoveAll(x => x.IsForController() == forController);
- Instance.Sticks[action.Key].Left.RemoveAll(x => x.IsForController() == forController);
- Instance.Sticks[action.Key].Right.RemoveAll(x => x.IsForController() == forController);
+ config.Sticks[action.Key].Up.RemoveAll(x => x.IsForController() == forController);
+ config.Sticks[action.Key].Down.RemoveAll(x => x.IsForController() == forController);
+ config.Sticks[action.Key].Left.RemoveAll(x => x.IsForController() == forController);
+ config.Sticks[action.Key].Right.RemoveAll(x => x.IsForController() == forController);
}
// Readd default bindings for this control type.
- foreach (var action in ControlsConfig_V01.Defaults.Actions)
+ foreach (var prop in settingsType.GetProperties())
{
- foreach (var binding in action.Value)
+ if (prop.PropertyType == typeof(VirtualButton))
{
- if (binding.IsForController() == forController)
+ var vb = prop.GetValue(settingsObject) as VirtualButton;
+ if (vb != null)
{
- Instance.Actions[action.Key].Add(binding);
+ config.Actions[vb.Name].AddRange(GetDefaultActionBindings(vb, settingsType, settingsObject).Where(b => b.IsForController() == forController));
+ }
+ }
+ else if (prop.PropertyType == typeof(VirtualStick))
+ {
+ var vs = prop.GetValue(settingsObject) as VirtualStick;
+ if (vs != null)
+ {
+ config.Sticks[vs.Name].Up.AddRange(GetDefaultStickBindings(vs, settingsType, StickDirection.Up, settingsObject).Where(b => b.IsForController() == forController));
+ config.Sticks[vs.Name].Down.AddRange(GetDefaultStickBindings(vs, settingsType, StickDirection.Down, settingsObject).Where(b => b.IsForController() == forController));
+ config.Sticks[vs.Name].Left.AddRange(GetDefaultStickBindings(vs, settingsType, StickDirection.Left, settingsObject).Where(b => b.IsForController() == forController));
+ config.Sticks[vs.Name].Right.AddRange(GetDefaultStickBindings(vs, settingsType, StickDirection.Right, settingsObject).Where(b => b.IsForController() == forController));
+
+ if (forController)
+ {
+ config.Sticks[vs.Name].Deadzone = vs.CircularDeadzone;
+ }
}
}
- }
- foreach (var stick in ControlsConfig_V01.Defaults.Sticks)
- {
- Instance.Sticks[stick.Key].Up.AddRange(stick.Value.Up.Where(b => b.IsForController() == forController));
- Instance.Sticks[stick.Key].Down.AddRange(stick.Value.Down.Where(b => b.IsForController() == forController));
- Instance.Sticks[stick.Key].Left.AddRange(stick.Value.Left.Where(b => b.IsForController() == forController));
- Instance.Sticks[stick.Key].Right.AddRange(stick.Value.Right.Where(b => b.IsForController() == forController));
- Instance.Sticks[stick.Key].Deadzone = stick.Value.Deadzone;
}
// Reload bindings
- LoadConfig(Instance);
+ LoadBindings(config, settingsType, settingsObject);
}
- [DisallowHooks]
- public static void SaveToFile()
+ ///
+ /// Actually bind things to the Virtual Buttons.
+ ///
+ ///
+ ///
+ ///
+ internal static void LoadBindings(ControlsConfig_V01 config, Type controlsType, object? controlsInstance = null)
{
- var savePath = Path.Join(App.UserPath, DefaultFileName);
- var tempPath = Path.Join(App.UserPath, DefaultFileName + ".backup");
+ ClearBindingsForType(controlsType, controlsInstance);
- // first save to a temporary file
+ foreach (var prop in controlsType.GetProperties())
{
- using var stream = File.Create(tempPath);
- Instance.Serialize(stream, Instance);
- stream.Flush();
+ if (prop.PropertyType == typeof(VirtualButton))
+ {
+ var vb = prop.GetValue(controlsInstance) as VirtualButton;
+ if (vb != null)
+ {
+ foreach (var it in FindAction(config, vb.Name))
+ it.BindTo(vb);
+ }
+ }
+ else if (prop.PropertyType == typeof(VirtualStick))
+ {
+ var vs = prop.GetValue(controlsInstance) as VirtualStick;
+ if (vs != null)
+ {
+ FindStick(config, vs.Name).BindTo(vs);
+ }
+ }
}
+ }
- // validate that the temp path worked, and overwrite existing if it did.
- if (File.Exists(tempPath) &&
- Instance.Deserialize(File.ReadAllText(tempPath)) != null)
- {
- File.Copy(tempPath, savePath, true);
- }
+ ///
+ /// Find stick from its name
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static ControlsConfigStick FindStick(ControlsConfig_V01? config, string name)
+ {
+ if (config != null && config.Sticks.TryGetValue(name, out var stick))
+ return stick;
+ throw new Exception($"Missing Stick Binding for '{name}'");
}
- [DisallowHooks]
- internal static void LoadConfig(ControlsConfig_V01? config = null)
- {
- static ControlsConfigStick FindStick(ControlsConfig_V01? config, string name)
- {
- if (config != null && config.Sticks.TryGetValue(name, out var stick))
- return stick;
- if (ControlsConfig_V01.Defaults.Sticks.TryGetValue(name, out stick))
- return stick;
- throw new Exception($"Missing Stick Binding for '{name}'");
- }
-
- static List FindAction(ControlsConfig_V01? config, string name)
- {
- if (config != null && config.Actions.TryGetValue(name, out var action))
- return action;
- if (ControlsConfig_V01.Defaults.Actions.TryGetValue(name, out action))
- return action;
- throw new Exception($"Missing Action Binding for '{name}'");
- }
-
- Clear();
-
- FindStick(config, "Move").BindTo(Move);
- FindStick(config, "Camera").BindTo(Camera);
- FindStick(config, "Menu").BindTo(Menu);
-
- foreach (var it in FindAction(config, "Jump"))
- it.BindTo(Jump);
- foreach (var it in FindAction(config, "Dash"))
- it.BindTo(Dash);
- foreach (var it in FindAction(config, "Climb"))
- it.BindTo(Climb);
- foreach (var it in FindAction(config, "Confirm"))
- it.BindTo(Confirm);
- foreach (var it in FindAction(config, "Cancel"))
- it.BindTo(Cancel);
- foreach (var it in FindAction(config, "Pause"))
- it.BindTo(Pause);
- foreach (var it in FindAction(config, "CopyFile"))
- it.BindTo(CopyFile);
- foreach (var it in FindAction(config, "DeleteFile"))
- it.BindTo(DeleteFile);
- foreach (var it in FindAction(config, "CreateFile"))
- it.BindTo(CreateFile);
- foreach (var it in FindAction(config, "ResetBindings"))
- it.BindTo(ResetBindings);
- foreach (var it in FindAction(config, "ClearBindings"))
- it.BindTo(ClearBindings);
- foreach (var it in FindAction(config, "DebugMenu"))
- it.BindTo(DebugMenu);
- foreach (var it in FindAction(config, "Restart"))
- it.BindTo(Restart);
- foreach (var it in FindAction(config, "FullScreen"))
- it.BindTo(FullScreen);
- foreach (var it in FindAction(config, "ReloadAssets"))
- it.BindTo(ReloadAssets);
- }
-
- public static void Clear()
- {
- Move.Clear();
- Camera.Clear();
- Jump.Clear();
- Dash.Clear();
- Climb.Clear();
- Menu.Clear();
- Confirm.Clear();
- Cancel.Clear();
- Pause.Clear();
- CopyFile.Clear();
- DeleteFile.Clear();
- CreateFile.Clear();
- ResetBindings.Clear();
- ClearBindings.Clear();
- DebugMenu.Clear();
- Restart.Clear();
- FullScreen.Clear();
- ReloadAssets.Clear();
+ ///
+ /// Find an action binding from its name
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static List FindAction(ControlsConfig_V01? config, string name)
+ {
+ if (config != null && config.Actions.TryGetValue(name, out var action))
+ return action;
+ throw new Exception($"Missing Action Binding for '{name}'");
}
+ ///
+ /// Reset virtual button bindings.
+ ///
+ ///
+ ///
+ internal static void ClearBindingsForType(Type controlsType, object? controlsInstance = null)
+ {
+ foreach (var prop in controlsType.GetProperties())
+ {
+ if (prop.PropertyType == typeof(VirtualButton))
+ {
+ var vb = prop.GetValue(controlsInstance) as VirtualButton;
+ if (vb != null)
+ {
+ vb.Clear();
+ }
+ }
+ else if (prop.PropertyType == typeof(VirtualStick))
+ {
+ var vs = prop.GetValue(controlsInstance) as VirtualStick;
+ if (vs != null)
+ {
+ vs.Clear();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Consume virtual button presses.
+ ///
public static void Consume()
{
- Move.Consume();
- Menu.Consume();
- Camera.Consume();
- Jump.Consume();
- Dash.Consume();
- Climb.Consume();
- Confirm.Consume();
- Cancel.Consume();
- Pause.Consume();
- CopyFile.Consume();
- DeleteFile.Consume();
- CreateFile.Consume();
- ResetBindings.Consume();
- ClearBindings.Consume();
- DebugMenu.Consume();
- Restart.Consume();
- FullScreen.Consume();
- ReloadAssets.Consume();
+ foreach (var prop in typeof(Controls).GetProperties())
+ {
+ if (prop.PropertyType == typeof(VirtualButton))
+ {
+ var vb = prop.GetValue(null) as VirtualButton;
+ if (vb != null)
+ {
+ vb.Consume();
+ }
+ }
+ else if (prop.PropertyType == typeof(VirtualStick))
+ {
+ var vs = prop.GetValue(null) as VirtualStick;
+ if (vs != null)
+ {
+ vs.Consume();
+ }
+ }
+ }
}
private static readonly Dictionary> prompts = [];
@@ -356,9 +687,14 @@ public static void Consume()
///
/// Virtual button to get an icon for.
///
- public static Subtexture GetPrompt(VirtualButton button)
+ public static Subtexture GetPrompt(VirtualButton button, ControlsConfig_V01? config = null)
{
- return Assets.Subtextures.GetValueOrDefault(GetPromptLocations(button, Input.Controllers.Any() && Input.Controllers[0].Connected).FirstOrDefault(""));
+ if (config == null)
+ {
+ config = Instance;
+ }
+
+ return Assets.Subtextures.GetValueOrDefault(GetPromptLocations(button, Input.Controllers.Any() && Input.Controllers[0].Connected, config).FirstOrDefault(""));
}
///
@@ -367,26 +703,36 @@ public static Subtexture GetPrompt(VirtualButton button)
/// Virtual button to get icons for.
/// True if we should show controller icons. False to show keyboard icons.
///
- public static List GetPrompts(VirtualButton button, bool isForController)
+ public static List GetPrompts(VirtualButton button, bool isForController, ControlsConfig_V01? config = null)
{
+ if (config == null)
+ {
+ config = Instance;
+ }
+
List subtextures = [];
- foreach (var location in GetPromptLocations(button, isForController))
+ foreach (var location in GetPromptLocations(button, isForController, config))
subtextures.Add(Assets.Subtextures.GetValueOrDefault(location));
return subtextures;
}
- private static List GetPromptLocations(VirtualButton button, bool isForController)
+ private static List GetPromptLocations(VirtualButton button, bool isForController, ControlsConfig_V01? config = null)
{
+ if (config == null)
+ {
+ config = Instance;
+ }
+
List locations = [];
var gamepad = Input.Controllers[0];
var deviceTypeName =
gamepad.Connected ? GetControllerName(gamepad.Gamepad) : "PC";
- var config = GetButtonBindings(Instance, button);
+ var bindings = GetButtonBindings(config, button);
- if (config != null)
+ if (bindings != null)
{
- foreach (var binding in config)
+ foreach (var binding in bindings)
{
if (binding.IsForController() != isForController)
{
@@ -406,7 +752,7 @@ private static List GetPromptLocations(VirtualButton button, bool isForC
if (!list.TryGetValue(buttonName, out var lookup))
list[buttonName] = lookup = $"Controls/{promptDeviceTypeName}/{buttonName}";
- if (Gamepads.Nintendo.Equals(binding.NotFor) || !Gamepads.Nintendo.Equals(binding.OnlyFor) || (binding.NotFor == null && binding.OnlyFor == null)) //only non switch prompts atm
+ if (binding.ForGamepads is not Gamepads[] gamepads || !gamepads.Any() || gamepads.Contains(gamepad.Gamepad))
locations.Add(lookup);
}
}
@@ -558,3 +904,81 @@ private static string GetButtonOverrides(ControlsConfigBinding binding, string b
return buttonName;
}
}
+
+[DisallowHooks]
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+public class DefaultBindingAttribute : Attribute
+{
+ public ControlsConfigBinding Binding;
+
+ public DefaultBindingAttribute(Keys key)
+ {
+ Binding = new ControlsConfigBinding(key);
+ }
+
+ public DefaultBindingAttribute(Buttons button)
+ {
+ Binding = new ControlsConfigBinding(button);
+ }
+
+ public DefaultBindingAttribute(Buttons button, Gamepads[] forGamepads)
+ {
+ Binding = new ControlsConfigBinding(button) { ForGamepads = forGamepads };
+ }
+
+ public DefaultBindingAttribute(MouseButtons mouseButton)
+ {
+ Binding = new ControlsConfigBinding(mouseButton);
+ }
+
+ public DefaultBindingAttribute(Axes axis, float deadzone, bool inverted)
+ {
+ Binding = new ControlsConfigBinding(axis, deadzone, inverted);
+ }
+}
+
+public enum StickDirection
+{
+ Up, Down, Left, Right
+}
+
+[DisallowHooks]
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+public class DefaultStickBindingAttribute : Attribute
+{
+ public StickDirection Direction;
+ public ControlsConfigBinding Binding;
+
+ public Gamepads? onlyFor;
+ public Gamepads? notFor;
+
+ public DefaultStickBindingAttribute(StickDirection direction, Keys key)
+ {
+ this.Direction = direction;
+ Binding = new ControlsConfigBinding(key);
+ }
+
+ public DefaultStickBindingAttribute(StickDirection direction, Buttons button)
+ {
+ this.Direction = direction;
+ Binding = new ControlsConfigBinding(button);
+ }
+
+ public DefaultStickBindingAttribute(StickDirection direction, Buttons button, Gamepads[] forGamepads)
+ {
+ this.Direction = direction;
+ Binding = new ControlsConfigBinding(button) { ForGamepads = forGamepads };
+ }
+
+ public DefaultStickBindingAttribute(StickDirection direction, MouseButtons mouseButton)
+ {
+ this.Direction = direction;
+ Binding = new ControlsConfigBinding(mouseButton);
+ }
+
+ public DefaultStickBindingAttribute(StickDirection direction, Axes axis, float deadzone, bool inverted)
+ {
+ this.Direction = direction;
+ Binding = new ControlsConfigBinding(axis, deadzone, inverted);
+ }
+}
diff --git a/Source/Data/PersistedData/ControlsConfigBinding.cs b/Source/Data/PersistedData/ControlsConfigBinding.cs
index 2eec032b..73a32358 100644
--- a/Source/Data/PersistedData/ControlsConfigBinding.cs
+++ b/Source/Data/PersistedData/ControlsConfigBinding.cs
@@ -11,8 +11,7 @@ public sealed class ControlsConfigBinding
public Axes? Axis { get; set; }
public float AxisDeadzone { get; set; }
public bool AxisInverted { get; set; }
- public Gamepads? OnlyFor { get; set; }
- public Gamepads? NotFor { get; set; }
+ public Gamepads[]? ForGamepads { get; set; }
public ControlsConfigBinding() { }
public ControlsConfigBinding(Keys input) => Key = input;
@@ -27,7 +26,7 @@ public ControlsConfigBinding(Axes input, float deadzone, bool inverted)
private bool Condition(VirtualButton vb, VirtualButton.IBinding binding)
{
- if (!OnlyFor.HasValue && !NotFor.HasValue)
+ if (ForGamepads == null || !ForGamepads.Any())
return true;
int index;
@@ -38,10 +37,8 @@ private bool Condition(VirtualButton vb, VirtualButton.IBinding binding)
else
return true;
- if (OnlyFor.HasValue && Input.Controllers[index].Gamepad != OnlyFor.Value)
- return false;
-
- if (NotFor.HasValue && Input.Controllers[index].Gamepad == NotFor.Value)
+ Gamepads gamepad = Input.Controllers.Any() ? Input.Controllers[index].Gamepad : Gamepads.Xbox;
+ if (!ForGamepads.Contains(gamepad))
return false;
return true;
diff --git a/Source/Data/PersistedData/ControlsConfig_V01.cs b/Source/Data/PersistedData/ControlsConfig_V01.cs
index c8375f75..a4b2b07b 100644
--- a/Source/Data/PersistedData/ControlsConfig_V01.cs
+++ b/Source/Data/PersistedData/ControlsConfig_V01.cs
@@ -10,154 +10,6 @@ public class ControlsConfig_V01 : PersistedData
public Dictionary> Actions { get; set; } = [];
public Dictionary Sticks { get; set; } = [];
- public static ControlsConfig_V01 Defaults = new()
- {
- Actions = new()
- {
- ["Jump"] = [
- new(Keys.C),
- new(Buttons.South),
- new(Buttons.North),
- ],
- ["Dash"] = [
- new(Keys.X),
- new(Buttons.West),
- new(Buttons.East),
- ],
- ["Climb"] = [
- new(Keys.Z),
- new(Keys.V),
- new(Keys.LeftShift),
- new(Keys.RightShift),
- new(Buttons.LeftShoulder),
- new(Buttons.RightShoulder),
- new(Axes.LeftTrigger, 0.4f, false),
- new(Axes.RightTrigger, 0.4f, false),
- ],
- ["Confirm"] = [
- new(Keys.C),
- new(Buttons.South) { NotFor = Gamepads.Nintendo },
- new(Buttons.East) { OnlyFor = Gamepads.Nintendo },
- ],
- ["Cancel"] = [
- new(Keys.X),
- new(Buttons.East) { NotFor = Gamepads.Nintendo },
- new(Buttons.South) { OnlyFor = Gamepads.Nintendo },
- ],
- ["Pause"] = [
- new(Keys.Escape),
- new(Keys.Enter),
- new(Buttons.Start),
- new(Buttons.Select),
- new(Buttons.Back)
- ],
- ["CopyFile"] = [
- new(Keys.V),
- new(Buttons.LeftShoulder)
- ],
- ["DeleteFile"] = [
- new(Keys.N),
- new(Buttons.RightShoulder)
- ],
- ["CreateFile"] = [
- new(Keys.B),
- new(Buttons.North)
- ],
- ["ResetBindings"] = [
- new(Keys.V),
- new(Buttons.LeftShoulder)
- ],
- ["ClearBindings"] = [
- new(Keys.B),
- new(Buttons.North)
- ],
- ["FullScreen"] = [
- new(Keys.F4),
- ],
- ["ReloadAssets"] = [
- new(Keys.F5),
- ],
- ["DebugMenu"] = [
- new(Keys.F6),
- ],
- ["Restart"] = [
- new(Keys.R),
- ],
- },
-
- Sticks = new()
- {
- ["Move"] = new()
- {
- Deadzone = 0.35f,
- Left = [
- new(Keys.Left),
- new(Buttons.Left),
- new(Axes.LeftX, 0.0f, true)
- ],
- Right = [
- new(Keys.Right),
- new(Buttons.Right),
- new(Axes.LeftX, 0.0f, false)
- ],
- Up = [
- new(Keys.Up),
- new(Buttons.Up),
- new(Axes.LeftY, 0.0f, true)
- ],
- Down = [
- new(Keys.Down),
- new(Buttons.Down),
- new(Axes.LeftY, 0.0f, false)
- ],
- },
- ["Camera"] = new()
- {
- Deadzone = 0.35f,
- Left = [
- new(Keys.A),
- new(Axes.RightX, 0.0f, true)
- ],
- Right = [
- new(Keys.D),
- new(Axes.RightX, 0.0f, false)
- ],
- Up = [
- new(Keys.W),
- new(Axes.RightY, 0.0f, true)
- ],
- Down = [
- new(Keys.S),
- new(Axes.RightY, 0.0f, false)
- ],
- },
- ["Menu"] = new()
- {
- Deadzone = 0.35f,
- Left = [
- new(Keys.Left),
- new(Buttons.Left),
- new(Axes.LeftX, 0.50f, true)
- ],
- Right = [
- new(Keys.Right),
- new(Buttons.Right),
- new(Axes.LeftX, 0.50f, false)
- ],
- Up = [
- new(Keys.Up),
- new(Buttons.Up),
- new(Axes.LeftY, 0.50f, true)
- ],
- Down = [
- new(Keys.Down),
- new(Buttons.Down),
- new(Axes.LeftY, 0.50f, false)
- ],
- },
- }
- };
-
public override JsonTypeInfo GetTypeInfo()
{
return ControlsConfig_V01Context.Default.ControlsConfig_V01;
diff --git a/Source/Data/PersistedData/ModSettingsRecord_V01.cs b/Source/Data/PersistedData/ModSettingsRecord_V01.cs
index 5f3ccc2d..94be2ca7 100644
--- a/Source/Data/PersistedData/ModSettingsRecord_V01.cs
+++ b/Source/Data/PersistedData/ModSettingsRecord_V01.cs
@@ -18,6 +18,8 @@ public sealed class ModSettingsRecord_V01 : PersistedData
public Dictionary SettingsFloatData { get; set; } = [];
public Dictionary SettingsBoolData { get; set; } = [];
+ public ControlsConfig_V01 ModControlBindings { get; set; } = new ControlsConfig_V01();
+
public string GetStringSetting(string name, string defaultValue = "")
=> SettingsStringData.TryGetValue(name, out string? value) ? value : defaultValue;
diff --git a/Source/Game.cs b/Source/Game.cs
index 70a743d2..72ce43dc 100644
--- a/Source/Game.cs
+++ b/Source/Game.cs
@@ -349,6 +349,11 @@ public override void Update()
// update top scene
try
{
+ if (!(scene is GameErrorMessage))
+ {
+ ModManager.Instance.PreUpdate(Time.Delta);
+ }
+
if (scene != null)
{
var pausing =
diff --git a/Source/Helpers/Menu.cs b/Source/Helpers/Menu.cs
index 1d52901e..218a7244 100644
--- a/Source/Helpers/Menu.cs
+++ b/Source/Helpers/Menu.cs
@@ -1,3 +1,5 @@
+using Celeste64.Mod;
+
namespace Celeste64;
public class Menu
@@ -49,23 +51,24 @@ public Item Describe(Loc.Localized description)
/// Virtual button we are trying to bind to
/// Root menu for the menu we are currently in
/// True if this is a controller binding, false for keyboard
- public class InputBind(Loc.Localized locString, VirtualButton button, Menu? rootMenu, bool isForController) : Item
+ public class InputBind(Loc.Localized locString, VirtualButton button, Menu? rootMenu, bool isForController, GameMod? mod = null) : Item
{
public override Loc.Localized? LocString => locString;
public float DeadZone;
public bool RequiresBinding;
public bool IsForController => isForController;
+ public GameMod? Mod => mod;
public override bool Pressed()
{
Audio.Play(Sfx.ui_select);
- rootMenu?.PushSubMenu(new BindControlMenu(rootMenu, button, locString, isForController, DeadZone));
+ rootMenu?.PushSubMenu(new BindControlMenu(rootMenu, button, locString, isForController, DeadZone, mod));
return true;
}
public virtual List GetTextures()
{
- return Controls.GetPrompts(button, isForController);
+ return Controls.GetPrompts(button, isForController, Mod?.ModSettingsData?.ModControlBindings);
}
public VirtualButton GetButton()
@@ -478,12 +481,12 @@ protected virtual void HandleInput()
if (Controls.ResetBindings.ConsumePress())
{
// Reset current binding to it's default value
- Controls.ResetBinding(bind.GetButton(), bind.IsForController);
+ Controls.ResetBinding(bind.GetButton(), bind.IsForController, bind.Mod);
}
else if (Controls.ClearBindings.ConsumePress())
{
// Clear current binding so there are no bindings (Or 1 binding if RequiredBinding flag is true, where it will keep just the last binding)
- Controls.ClearBinding(bind.GetButton(), bind.IsForController, bind.RequiresBinding);
+ Controls.ClearBinding(bind.GetButton(), bind.IsForController, bind.RequiresBinding, bind.Mod?.ModSettingsData?.ModControlBindings);
}
}
}
diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs
index 3de2c2ae..03053167 100644
--- a/Source/Mod/Core/GameMod.cs
+++ b/Source/Mod/Core/GameMod.cs
@@ -214,6 +214,8 @@ public bool LoadSettings()
try
{
+ Controls.LoadModConfig(this);
+
return LoadSettingsForType("Settings.", SettingsType, Settings);
}
catch (Exception e)
@@ -587,6 +589,13 @@ public virtual void OnModLoaded() { }
///
public virtual void OnModUnloaded() { }
+
+ ///
+ /// Called once at the beginning of every frame, before game logic
+ ///
+ /// How much time passed since the previous update
+ public virtual void PreUpdate(float deltaTime) { }
+
///
/// Called once every frame
///
diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs
index 77a10635..02dff481 100644
--- a/Source/Mod/Core/ModManager.cs
+++ b/Source/Mod/Core/ModManager.cs
@@ -133,6 +133,14 @@ internal void OnModFileChanged(ModFileChangedCtx ctx)
if (Settings.EnableAutoReload) Game.Instance.ReloadAssets(false);
}
+ internal void PreUpdate(float deltaTime)
+ {
+ foreach (var mod in EnabledMods)
+ {
+ mod.PreUpdate(deltaTime);
+ }
+ }
+
internal void Update(float deltaTime)
{
foreach (var mod in EnabledMods)
diff --git a/Source/Mod/Menu/BindControlMenu.cs b/Source/Mod/Menu/BindControlMenu.cs
index 12ab2237..a17a5827 100644
--- a/Source/Mod/Menu/BindControlMenu.cs
+++ b/Source/Mod/Menu/BindControlMenu.cs
@@ -1,3 +1,5 @@
+using Celeste64.Mod;
+
namespace Celeste64;
public class BindControlMenu : Menu
@@ -7,15 +9,17 @@ public class BindControlMenu : Menu
private const float waitBeforeClosingTime = 5;
private VirtualButton button;
private bool isForController;
+ private GameMod? mod;
private float deadZone;
- public BindControlMenu(Menu? rootMenu, VirtualButton button, string buttonName, bool isForController, float deadZone = 0)
+ public BindControlMenu(Menu? rootMenu, VirtualButton button, string buttonName, bool isForController, float deadZone = 0, GameMod? mod = null)
{
RootMenu = rootMenu;
this.button = button;
this.deadZone = deadZone;
this.isForController = isForController;
+ this.mod = mod;
Title = string.Format(Loc.Str("PressToBind"), buttonName);
TitleScale = 1;
@@ -30,7 +34,7 @@ protected override void HandleInput()
Keys? pressed = Input.Keyboard.FirstPressed();
if (pressed != null && pressed != Keys.Unknown && pressed != Keys.Application && pressed != Keys.ScrollLock) // We don't have icons for these currently, so don't allow them
{
- Controls.AddBinding(button, (Keys)pressed);
+ Controls.AddBinding(button, (Keys)pressed, mod?.ModSettingsData?.ModControlBindings);
RootMenu?.PopSubMenu();
return;
}
@@ -40,7 +44,7 @@ protected override void HandleInput()
{
if (Input.Mouse.Pressed((MouseButtons)button))
{
- Controls.AddBinding(this.button, (MouseButtons)button);
+ Controls.AddBinding(this.button, (MouseButtons)button, mod?.ModSettingsData?.ModControlBindings);
RootMenu?.PopSubMenu();
return;
}
@@ -56,7 +60,7 @@ protected override void HandleInput()
{
if (controller.Pressed((Buttons)button))
{
- Controls.AddBinding(this.button, (Buttons)button);
+ Controls.AddBinding(this.button, (Buttons)button, mod?.ModSettingsData?.ModControlBindings);
RootMenu?.PopSubMenu();
return;
}
@@ -66,7 +70,7 @@ protected override void HandleInput()
{
if (Math.Abs(controller.Axis((Axes)axis)) > 0.5f)
{
- Controls.AddBinding(this.button, (Axes)axis, controller.Axis((Axes)axis) < -0.5f, deadZone);
+ Controls.AddBinding(this.button, (Axes)axis, controller.Axis((Axes)axis) < -0.5f, deadZone, mod?.ModSettingsData?.ModControlBindings);
RootMenu?.PopSubMenu();
return;
}
diff --git a/Source/Mod/Menu/ControlsMenu.cs b/Source/Mod/Menu/ControlsMenu.cs
index 32c9cd4b..a4b10ff2 100644
--- a/Source/Mod/Menu/ControlsMenu.cs
+++ b/Source/Mod/Menu/ControlsMenu.cs
@@ -92,10 +92,10 @@ protected override void RenderItems(Batcher batch)
UI.Prompt(batch, Controls.Confirm, Loc.Str("Bind"), at, out width, 1.0f);
at.X -= width + 8 * Game.RelativeScale;
- UI.Prompt(batch, Controls.CreateFile, Loc.Str("Clear"), at, out width, 1.0f);
+ UI.Prompt(batch, Controls.ClearBindings, Loc.Str("Clear"), at, out width, 1.0f);
at.X -= width + 8 * Game.RelativeScale;
- UI.Prompt(batch, Controls.CopyFile, Loc.Str("Reset"), at, out width, 1.0f);
+ UI.Prompt(batch, Controls.ResetBindings, Loc.Str("Reset"), at, out width, 1.0f);
batch.PopMatrix();
}
}
diff --git a/Source/Mod/Menu/ModControlsMenu.cs b/Source/Mod/Menu/ModControlsMenu.cs
new file mode 100644
index 00000000..c039ca8c
--- /dev/null
+++ b/Source/Mod/Menu/ModControlsMenu.cs
@@ -0,0 +1,129 @@
+using Celeste64.Mod;
+using System.Reflection;
+
+namespace Celeste64;
+
+public class ModControlsMenu : Menu
+{
+ private Target GameTarget;
+
+ //Items list always includes the Back item and reset to default, so check if it's more than 2 to know if we should display it.
+ internal bool ShouldDisplay => items.Count > 2;
+
+ private GameMod? Mod;
+
+ public override void Closed()
+ {
+ base.Closed();
+ if (Mod != null)
+ {
+ Mod.SaveSettings();
+ }
+ }
+
+ public ModControlsMenu(Menu? rootMenu)
+ {
+ RootMenu = rootMenu;
+ GameTarget = new Target(Game.Width, Game.Height);
+ Game.OnResolutionChanged += () => GameTarget = new Target(Game.Width, Game.Height);
+ }
+
+ public void AddItems(GameMod mod, bool isForController)
+ {
+ Mod = mod;
+ items.Clear();
+ if (mod.SettingsType != null)
+ {
+ foreach (var prop in mod.SettingsType.GetProperties())
+ {
+ var propType = prop.PropertyType;
+
+ if (prop.GetCustomAttribute() != null)
+ continue;
+
+ string propName = prop.Name;
+ string? nameAttibute = prop.GetCustomAttribute()?.Name;
+ if (!string.IsNullOrEmpty(nameAttibute))
+ {
+ propName = Loc.TryGetModString(mod, nameAttibute, out string localizedName) ?
+ localizedName :
+ nameAttibute;
+ }
+
+ Menu.Item? newItem = null;
+
+ if (prop.GetCustomAttribute() != null)
+ {
+ Add(new Spacer());
+ }
+
+ bool changingNeedsReload = prop.GetCustomAttribute() != null;
+
+ string? subheader = prop.GetCustomAttribute()?.SubHeader;
+ if (!string.IsNullOrEmpty(subheader))
+ {
+ string subHeader = Loc.TryGetModString(mod, subheader, out string localizedSubHeader) ?
+ localizedSubHeader :
+ subheader;
+ Add(new SubHeader((Loc.Unlocalized)subHeader));
+ }
+
+ if (prop.PropertyType == typeof(VirtualButton))
+ {
+ VirtualButton? vb = prop.GetValue(mod.Settings) as VirtualButton;
+ if (vb != null)
+ {
+ Add(new InputBind((Loc.Unlocalized)(propName), vb, RootMenu, isForController, mod));
+ }
+ }
+ if (prop.PropertyType == typeof(VirtualStick))
+ {
+ VirtualStick? vs = prop.GetValue(mod.Settings) as VirtualStick;
+ if (vs != null)
+ {
+ Add(new InputBind((Loc.Unlocalized)(propName + " Up"), vs.Vertical.Negative, RootMenu, isForController, mod));
+ Add(new InputBind((Loc.Unlocalized)(propName + " Down"), vs.Vertical.Positive, RootMenu, isForController, mod));
+ Add(new InputBind((Loc.Unlocalized)(propName + " Left"), vs.Horizontal.Negative, RootMenu, isForController, mod));
+ Add(new InputBind((Loc.Unlocalized)(propName + " Right"), vs.Horizontal.Positive, RootMenu, isForController, mod));
+ }
+ }
+ }
+ }
+
+ // Reset all bindings for this control type to their default value.
+ Add(new Option("ControlsResetToDefault", () =>
+ {
+ Controls.ResetAllBindings(isForController, mod);
+ }));
+
+ Add(new Option("Exit", () =>
+ {
+ PopRootSubMenu();
+ }));
+ }
+
+ ///
+ /// Render bindings and input legend
+ ///
+ ///
+ protected override void RenderItems(Batcher batch)
+ {
+ batch.PushMatrix(new Vec2(GameTarget.Bounds.Center.X, GameTarget.Bounds.Center.Y - 16f), false);
+ base.RenderItems(batch);
+ batch.PopMatrix();
+
+ batch.PushMatrix(Vec2.Zero, false);
+ var at = GameTarget.Bounds.BottomRight + new Vec2(-32, -4) * Game.RelativeScale + new Vec2(0, -UI.PromptSize);
+ UI.Prompt(batch, Controls.Cancel, Loc.Str("Back"), at, out var width, 1.0f);
+ at.X -= width + 8 * Game.RelativeScale;
+
+ UI.Prompt(batch, Controls.Confirm, Loc.Str("Bind"), at, out width, 1.0f);
+ at.X -= width + 8 * Game.RelativeScale;
+
+ UI.Prompt(batch, Controls.ClearBindings, Loc.Str("Clear"), at, out width, 1.0f);
+ at.X -= width + 8 * Game.RelativeScale;
+
+ UI.Prompt(batch, Controls.ResetBindings, Loc.Str("Reset"), at, out width, 1.0f);
+ batch.PopMatrix();
+ }
+}
diff --git a/Source/Mod/Menu/ModInfoMenu.cs b/Source/Mod/Menu/ModInfoMenu.cs
index 4ef7d6b8..f486f086 100644
--- a/Source/Mod/Menu/ModInfoMenu.cs
+++ b/Source/Mod/Menu/ModInfoMenu.cs
@@ -13,6 +13,8 @@ public class ModInfoMenu : Menu
public Menu? depWarningMenu;
public Menu? safeDisableErrorMenu;
public ModOptionsMenu modOptionsMenu;
+ public ModControlsMenu modKeyboardBindingMenu;
+ public ModControlsMenu modControllerBindingMenu;
internal ModInfoMenu(Menu? rootMenu)
{
@@ -25,6 +27,8 @@ internal ModInfoMenu(Menu? rootMenu)
strawberryImage = Assets.Subtextures["icon_strawberry"];
modOptionsMenu = new ModOptionsMenu(rootMenu);
+ modKeyboardBindingMenu = new ModControlsMenu(rootMenu);
+ modControllerBindingMenu = new ModControlsMenu(rootMenu);
InitItems();
}
@@ -109,6 +113,17 @@ private void InitItems()
{
Add(new Submenu("ModOptions", RootMenu, modOptionsMenu));
}
+
+ if (modKeyboardBindingMenu.ShouldDisplay)
+ {
+ Add(new Submenu("KeyboardConfig", RootMenu, modKeyboardBindingMenu));
+ }
+
+ if (modControllerBindingMenu.ShouldDisplay)
+ {
+ Add(new Submenu("ControllerConfig", RootMenu, modControllerBindingMenu));
+ }
+
Add(new Option("Back", () =>
{
if (RootMenu != null)
@@ -122,6 +137,8 @@ internal void SetMod(GameMod mod)
{
Mod = mod;
modOptionsMenu.SetMod(mod);
+ modKeyboardBindingMenu.AddItems(mod, false);
+ modControllerBindingMenu.AddItems(mod, true);
InitItems();
}
@@ -157,7 +174,7 @@ protected override void RenderItems(Batcher batch)
batch.Image(stampImage, (stampPos + new Vec2(imgSizeMin, imgSizeMin) * imgScale * 0.05f) * Game.RelativeScale, stampImageSize * imgScale * Game.RelativeScale, stampImageSize * imgScale * 1.3f * Game.RelativeScale, 0, Color.White);
batch.Image(image, (pos + new Vec2(imgSizeMin, imgSizeMin) * imgScale * 0.05f) * Game.RelativeScale, imageSize * imgScale * Game.RelativeScale, imageSize * imgScale * Game.RelativeScale, 0, Color.White);
- batch.PushMatrix(Matrix3x2.CreateScale(1.0f) * Matrix3x2.CreateTranslation(bounds.TopLeft + new Vec2(size.X / 6.8f, -size.Y / 20) * Game.RelativeScale));
+ batch.PushMatrix(new Vec2(Game.Width * 0.72f, (Game.Height * 0.4f) + (Size.Y / 2)), false);
base.RenderItems(batch);
batch.PopMatrix();
}
diff --git a/Source/Scenes/Startup.cs b/Source/Scenes/Startup.cs
index e7822a69..c61be919 100644
--- a/Source/Scenes/Startup.cs
+++ b/Source/Scenes/Startup.cs
@@ -48,7 +48,7 @@ public Startup()
// try to load controls, or overwrite with defaults if they don't exist
{
- Controls.LoadControlsByFileName(Controls.DefaultFileName);
+ Controls.LoadControlsFromFile(Controls.DefaultFileName);
}
}