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); } }