From 03b3c77adbf6650b3c41317cf457b21c0c31695f Mon Sep 17 00:00:00 2001 From: m4-used-rollout Date: Thu, 14 Dec 2023 06:36:46 +0000 Subject: [PATCH 1/3] Added DS, 3DS, (S)NES, N64, GameCube, and Switch button profiles --- TPP.Core/ButtonProfiles.cs | 67 ++++++- TPP.Inputting/InputParserBuilder.cs | 74 ++++++- .../Parsing/ButtonMappingTest.cs | 189 ++++++++++++++++++ .../TPP.Inputting.Tests.csproj | 1 + 4 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 tests/TPP.Inputting.Tests/Parsing/ButtonMappingTest.cs diff --git a/TPP.Core/ButtonProfiles.cs b/TPP.Core/ButtonProfiles.cs index a82780ef..fe8ba7ed 100644 --- a/TPP.Core/ButtonProfiles.cs +++ b/TPP.Core/ButtonProfiles.cs @@ -8,7 +8,20 @@ public enum ButtonProfile { [EnumMember(Value = "gb")] GameBoy, [EnumMember(Value = "gba")] GameBoyAdvance, + [EnumMember(Value = "nds")] NintendoDS, + [EnumMember(Value = "3ds")] Nintendo3DS, + + [EnumMember(Value = "nes")] NES, + [EnumMember(Value = "snes")] SNES, + [EnumMember(Value = "n64")] N64, + [EnumMember(Value = "gc")] GameCube, + [EnumMember(Value = "switch")] Switch, + [EnumMember(Value = "dualgb")] DualGameBoy, + [EnumMember(Value = "dualnes")] DualNES, + [EnumMember(Value = "dualsnes")] DualSNES, + [EnumMember(Value = "dualn64")] DualN64, + [EnumMember(Value = "dualgc")] DualGameCube, } public static class ButtonProfileExtensions @@ -19,14 +32,60 @@ public static InputParserBuilder ToInputParserBuilder(this ButtonProfile profile ButtonProfile.GameBoy => InputParserBuilder.FromBare() .Buttons("a", "b", "start", "select") .DPad() - .RemappedDPad(up: "n", down: "s", left: "w", right: "e", mapsToPrefix: "") - .RemappedDPad(up: "north", down: "south", left: "west", right: "east", mapsToPrefix: "") - .LengthRestrictions(maxSetLength: 2, maxSequenceLength: 1) - .HoldEnabled(true), + // Max set length of 3 is too short to perform Soft Reset (A+B+Start+Select) + .LengthRestrictions(maxSetLength: 3, maxSequenceLength: 1), ButtonProfile.GameBoyAdvance => ButtonProfile.GameBoy.ToInputParserBuilder() .Buttons("l", "r"), + ButtonProfile.NintendoDS => ButtonProfile.SNES.ToInputParserBuilder() + .Touchscreen(width: 256, height: 192, multitouch: false, allowDrag: true), + ButtonProfile.Nintendo3DS => ButtonProfile.SNES.ToInputParserBuilder() + .SimpleAliasedDPad("d", "") + .AnalogStick("c", true) + .Touchscreen(width: 320, height: 240, multitouch: false, allowDrag: true) + // Prevent Soft Reset in 3DS Pokemon Games (L+R+Start/Select) as well as Luma3DS and NTR menu shortcuts + .Conflicts(("l", "select"), ("l", "start")), + + ButtonProfile.NES => ButtonProfile.GameBoy.ToInputParserBuilder(), + ButtonProfile.SNES => ButtonProfile.GameBoyAdvance.ToInputParserBuilder() + .Buttons("x", "y"), + ButtonProfile.N64 => InputParserBuilder.FromBare() + .Buttons("a", "b", "start", "l", "r", "z") + .DPad() + .SimpleAliasedDPad("d", "") + .DPad("c") + .AnalogStick("a", true) + .SimpleAliasedAnalogStick("l", "a", true) + .LengthRestrictions(maxSetLength: 4, maxSequenceLength: 1), + ButtonProfile.GameCube => InputParserBuilder.FromBare() + .Buttons("a", "b", "x", "y", "l", "r", "z", "start") + .AliasedButtons(("pause", "start")) + .DPad() + .SimpleAliasedDPad("d", "") + .AnalogStick("l", true) + .AnalogStick("r", true) + .SimpleAliasedAnalogStick("c", "r", true) + .Conflicts(("x", "start")) // Prevent Soft Reset in Pokemon XD (B+X+Start) + .LengthRestrictions(maxSetLength: 4, maxSequenceLength: 1), + ButtonProfile.Switch => InputParserBuilder.FromBare() + .Buttons("a", "b", "x", "y", "l", "r", "zl", "zr", "lstick", "rstick", "plus", "minus") // Capture and Home buttons omitted on purpose + .AliasedButtons(("start", "plus"), ("select", "minus"), ("+", "plus"), ("-", "minus"), ("l2", "zl"), ("r2", "zr"), ("l3", "lstick"), ("r3", "rstick")) + .DPad() + .SimpleAliasedDPad("d", "") + .AnalogStick("l", true) + .AnalogStick("r", true) + .SimpleAliasedAnalogStick("c", "r", true) + .LengthRestrictions(maxSetLength: 4, maxSequenceLength: 1), + ButtonProfile.DualGameBoy => ButtonProfile.GameBoy.ToInputParserBuilder() .LeftRightSidesEnabled(true), + ButtonProfile.DualNES => ButtonProfile.NES.ToInputParserBuilder() + .LeftRightSidesEnabled(true), + ButtonProfile.DualSNES => ButtonProfile.SNES.ToInputParserBuilder() + .LeftRightSidesEnabled(true), + ButtonProfile.DualN64 => ButtonProfile.DualN64.ToInputParserBuilder() + .LeftRightSidesEnabled(true), + ButtonProfile.DualGameCube => ButtonProfile.GameCube.ToInputParserBuilder() + .LeftRightSidesEnabled(true), }; } } diff --git a/TPP.Inputting/InputParserBuilder.cs b/TPP.Inputting/InputParserBuilder.cs index 9f6a5925..dbcbe74e 100644 --- a/TPP.Inputting/InputParserBuilder.cs +++ b/TPP.Inputting/InputParserBuilder.cs @@ -245,9 +245,26 @@ public InputParserBuilder AnalogStick(string prefix, bool allowSpin) Conflicts((spinl, up), (spinl, down), (spinl, left), (spinl, right)); Conflicts((spinr, up), (spinr, down), (spinr, left), (spinr, right)); } - return this; + return this.CardinalAnalogStickMapping(prefix); } + /// + /// Adds an aliased analog stick, automatically building a stick for an aliased prefix + /// + /// prefix for the aliased analog stick + /// prefix for the analog stick that the alias names map to + /// if enabled, additional prefixed "spinl" and "spinr" buttons are added + public InputParserBuilder SimpleAliasedAnalogStick(string aliasPrefix, string mapsToPrefix, bool allowSpin) => + AliasedAnalogStick( + up: aliasPrefix + "up", + down: aliasPrefix + "down", + left: aliasPrefix + "left", + right: aliasPrefix + "right", + spinl: allowSpin ? aliasPrefix + "spinl" : null, + spinr: allowSpin ? aliasPrefix + "spinr" : null, + mapsToPrefix + ).CardinalAnalogStickAliases(aliasPrefix, mapsToPrefix); + /// /// Adds an aliased analog stick. This is a shortcut for adding aliased analog inputs for /// "up"/"down"/"left"/"right", plus configuring respective conflicts, like inputting opposing directions. @@ -321,6 +338,7 @@ public InputParserBuilder RemappedAnalogStick( /// /// Add a D-pad. This is a shortcut for adding buttons for "up"/"down"/"left"/"right", /// plus configuring respective conflicts, like inputting opposing directions. + /// Also automatically adds N E W S and North East West South remappings for the pad. /// /// prefix for the D-pad, which will get prepended to "up"/"down"/"left"/"right" public InputParserBuilder DPad(string prefix = "") @@ -331,9 +349,23 @@ public InputParserBuilder DPad(string prefix = "") string right = prefix + "right"; Buttons(up, down, left, right); Conflicts((up, down), (left, right)); - return this; + return this.CardinalDPadMapping(prefix); } + /// + /// Adds an aliased D-pad, automatically building up/down/left/right buttons for an aliased prefix + /// + /// prefix for the aliased D-pad + /// prefix for the D-pad that the alias names map to + public InputParserBuilder SimpleAliasedDPad(string aliasPrefix, string mapsToPrefix) => + AliasedDPad( + up: aliasPrefix + "up", + down: aliasPrefix + "down", + left: aliasPrefix + "left", + right: aliasPrefix + "right", + mapsToPrefix + ).CardinalDPadAliases(aliasPrefix, mapsToPrefix); + /// /// Add an aliased D-pad. This is a shortcut for adding aliased buttons for "up"/"down"/"left"/"right", /// plus configuring respective conflicts, like inputting opposing directions. @@ -376,6 +408,44 @@ public InputParserBuilder RemappedDPad(string up, string down, string left, stri return this; } + /// + /// Add N E W S and North East West South remapped D-pad. + /// + /// prefix for the D-pad that the remapping names map to, + /// which will get prepended to "n"/"s"/"w"/"e" + public InputParserBuilder CardinalDPadMapping(string prefix) => + RemappedDPad(up: prefix + "n", down: prefix + "s", left: prefix + "w", right: prefix + "e", prefix) + .RemappedDPad(up: prefix + "north", down: prefix + "south", left: prefix + "west", right: prefix + "east", prefix); + + /// + /// Add N E W S and North East West South remapped Analog Stick. + /// + /// prefix for the stick that the remapping names map to, + /// which will get prepended to "n"/"s"/"w"/"e" + public InputParserBuilder CardinalAnalogStickMapping(string prefix) => + RemappedAnalogStick(up: prefix + "n", down: prefix + "s", left: prefix + "w", right: prefix + "e", spinl: null, spinr: null, prefix) + .RemappedAnalogStick(up: prefix + "north", down: prefix + "south", left: prefix + "west", right: prefix + "east", spinl: null, spinr: null, prefix); + + /// + /// Add N E W S and North East West South D-pad aliases. + /// + /// prefix for the aliased D-pad, + /// prefix that the aliased D-pad maps to + public InputParserBuilder CardinalDPadAliases(string prefix, string mapsToPrefix) => + AliasedDPad(up: prefix + "n", down: prefix + "s", left: prefix + "w", right: prefix + "e", mapsToPrefix) + .AliasedDPad(up: prefix + "north", down: prefix + "south", left: prefix + "west", right: prefix + "east", mapsToPrefix); + + + /// + /// Add N E W S and North East West South Analog Stick aliases. + /// + /// prefix for the aliased stick, + /// prefix that the aliased stick maps to + public InputParserBuilder CardinalAnalogStickAliases(string prefix, string mapsToPrefix) => + AliasedAnalogStick(up: prefix + "n", down: prefix + "s", left: prefix + "w", right: prefix + "e", spinl: null, spinr: null, mapsToPrefix) + .AliasedAnalogStick(up: prefix + "north", down: prefix + "south", left: prefix + "west", right: prefix + "east", spinl: null, spinr: null, mapsToPrefix); + + public InputParserBuilder LeftRightSidesEnabled(bool enabled) { _leftRightSides = enabled; diff --git a/tests/TPP.Inputting.Tests/Parsing/ButtonMappingTest.cs b/tests/TPP.Inputting.Tests/Parsing/ButtonMappingTest.cs new file mode 100644 index 00000000..8daf2835 --- /dev/null +++ b/tests/TPP.Inputting.Tests/Parsing/ButtonMappingTest.cs @@ -0,0 +1,189 @@ +using NUnit.Framework; +using NUnit.Framework.Internal; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TPP.ArgsParsing.TypeParsers; +using TPP.Core; +using TPP.Inputting.Inputs; +using TPP.Inputting.Parsing; + +namespace TPP.Inputting.Tests.Parsing; +public class ButtonMappingTest +{ + private static readonly IInputMapper _inputMapper = new DefaultTppInputMapper(); + + private IInputParser _inputParser = null!; + + private InputSet ParseInput(string inputStr) + { + InputSequence? inputSequence = _inputParser.Parse(inputStr); + return inputSequence?.InputSets[0] ?? new InputSet(ImmutableList.Empty); + } + + private void AssertMapped(string rawInput, params string[] buttons) + { + var mappedInputs = _inputMapper.Map(new TimedInputSet(ParseInput(rawInput), 1, 2)); + foreach (var button in buttons) + { + Assert.IsTrue(mappedInputs.ContainsKey(button), $"Output should contain {button}."); + } + } + + private void AssertEmptyMap(string rawInput, string? message = null) + { + var mappedInputs = _inputMapper.Map(new TimedInputSet(ParseInput(rawInput), 1, 2)); + Assert.IsTrue(mappedInputs.Keys.Count == 2, message ?? "Mapped output should contain no buttons."); + } + + [Test] + public void TestGameBoy() + { + _inputParser = ButtonProfile.GameBoy.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("n", "Up"); + AssertMapped("south", "Down"); + + // bad cases + AssertEmptyMap("x"); // not a button + AssertEmptyMap("a+b+start+select", "Soft Reset should be blocked."); // Soft Reset + } + + [Test] + public void TestGameBoyAdvance() + { + _inputParser = ButtonProfile.GameBoyAdvance.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("l+r", "L", "R"); + AssertMapped("n", "Up"); + AssertMapped("south", "Down"); + + // bad cases + AssertEmptyMap("x"); // not a button + AssertEmptyMap("a+b+start+select", "Soft Reset should be blocked."); + } + + [Test] + public void TestNintendoDS() + { + _inputParser = ButtonProfile.NintendoDS.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("l+r", "L", "R"); + AssertMapped("x+y", "X", "Y"); + AssertMapped("n", "Up"); + AssertMapped("south", "Down"); + AssertMapped("10,100>80,120", "Touch_Screen_X", "Touch_Screen_Y", "Touch_Screen_X2", "Touch_Screen_Y2"); + + // bad cases + AssertEmptyMap("300,200", "Out of bounds touch should be blocked."); + AssertEmptyMap("l+r+start+select", "Soft Reset should be blocked."); + } + + [Test] + public void TestNintendo3DS() + { + _inputParser = ButtonProfile.Nintendo3DS.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("l+r", "L", "R"); + AssertMapped("x+y", "X", "Y"); + AssertMapped("n", "Up"); + AssertMapped("dn", "Up"); + AssertMapped("south", "Down"); + AssertMapped("cn", "Cup"); + AssertMapped("cspinl", "Cspinl"); + AssertMapped("10,100>80,120", "Touch_Screen_X", "Touch_Screen_Y", "Touch_Screen_X2", "Touch_Screen_Y2"); + + // bad cases + AssertEmptyMap("400,200", "Out of bounds touch should be blocked."); + AssertEmptyMap("l+r+start", "Soft Reset should be blocked."); + AssertEmptyMap("l+r+select", "Soft Reset should be blocked."); + AssertEmptyMap("l+s+select", "Luma3DS menu shortcut should be blocked."); + AssertEmptyMap("l+start", "NTR menu shortcut should be blocked."); + } + + [Test] + public void TestN64() + { + _inputParser = ButtonProfile.N64.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("l+r+z", "L", "R", "Z"); + AssertMapped("n", "Up"); + AssertMapped("aw.5", "Aleft"); + AssertMapped("ln+cs+cright", "Aup", "Cdown", "Cright"); + AssertMapped("south", "Down"); + + // bad cases + AssertEmptyMap("x+y"); + AssertEmptyMap("b+x+start", "XD Soft Reset went through"); + } + + [Test] + public void TestGameCube() + { + _inputParser = ButtonProfile.GameCube.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("l+r", "L", "R"); + AssertMapped("x+y", "X", "Y"); + AssertMapped("n", "Up"); + AssertMapped("ln.5", "Lup"); + AssertMapped("ln+cs+rright", "Lup", "Rdown", "Rright"); + AssertMapped("south", "Down"); + + // bad cases + AssertEmptyMap("b+x+start", "XD Soft Reset went through"); + } + + [Test] + public void TestSwitch() + { + _inputParser = ButtonProfile.Switch.ToInputParserBuilder().Build(); + + // good cases + AssertMapped("a+b", "A", "B"); + AssertMapped("l2+zr+r3", "Zl", "Zr", "Rstick"); + AssertMapped("x+y", "X", "Y"); + AssertMapped("+", "Plus"); + AssertMapped("start+minus", "Plus", "Minus"); + AssertMapped("n", "Up"); + AssertMapped("dn", "Up"); + AssertMapped("ln.5", "Lup"); + AssertMapped("ln+cs+rright", "Lup", "Rdown", "Rright"); + AssertMapped("lspinl", "Lspinl"); + AssertMapped("lsouth", "Ldown"); + + // bad cases + AssertEmptyMap("capture+home", "Capture and Home buttons should not be mapped."); + } + + [Test] + public void TestDualSNES() + { + _inputParser = ButtonProfile.DualSNES.ToInputParserBuilder().Build(); + if (_inputParser is SidedInputParser and not null) + { + ((SidedInputParser)_inputParser).AllowDirectedInputs = true; + } + + // good cases + AssertMapped("lstart", "P1 Start"); + AssertMapped("r:b+n", "P2 B", "P2 Up"); + + // bad cases + AssertEmptyMap("l:a+b+start+select", "Soft Reset should be blocked."); + } +} diff --git a/tests/TPP.Inputting.Tests/TPP.Inputting.Tests.csproj b/tests/TPP.Inputting.Tests/TPP.Inputting.Tests.csproj index 9c0590ac..56d48819 100644 --- a/tests/TPP.Inputting.Tests/TPP.Inputting.Tests.csproj +++ b/tests/TPP.Inputting.Tests/TPP.Inputting.Tests.csproj @@ -11,6 +11,7 @@ + From 39a42fac605f177d00bfb733f20e78a3f9ce504d Mon Sep 17 00:00:00 2001 From: m4-used-rollout Date: Thu, 14 Dec 2023 06:58:58 +0000 Subject: [PATCH 2/3] Regenerated JSON schemas --- TPP.Core/config.runmode.schema.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/TPP.Core/config.runmode.schema.json b/TPP.Core/config.runmode.schema.json index 9d86f9dd..e0aaa181 100644 --- a/TPP.Core/config.runmode.schema.json +++ b/TPP.Core/config.runmode.schema.json @@ -12,7 +12,18 @@ "enum": [ "gb", "gba", - "dualgb" + "nds", + "3ds", + "nes", + "snes", + "n64", + "gc", + "switch", + "dualgb", + "dualnes", + "dualsnes", + "dualn64", + "dualgc" ] }, "FramesPerSecond": { From 42fee3a1a1501106c1f9a4579485daf58fd61a96 Mon Sep 17 00:00:00 2001 From: M4 Date: Thu, 14 Dec 2023 07:01:48 +0000 Subject: [PATCH 3/3] Clarified JSON Schema check failure message --- .github/workflows/json-schemas.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/json-schemas.yml b/.github/workflows/json-schemas.yml index 2231d031..7687ad8c 100644 --- a/.github/workflows/json-schemas.yml +++ b/.github/workflows/json-schemas.yml @@ -22,4 +22,4 @@ jobs: git add config.matchmode.schema.json git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git diff-index --quiet HEAD || { echo 'Please update the json schemas by running `dotnet run -- regenjsonschemas`'; exit 1; } + git diff-index --quiet HEAD || { echo 'The configuration schema has changed. Please update the json schema files by running `dotnet run -- regenjsonschemas` locally and committing the output.'; exit 1; }