diff --git a/SimLoop/src/main/java/org/horizon36596/simloop/opmode/GamepadScript.java b/SimLoop/src/main/java/org/horizon36596/simloop/opmode/GamepadScript.java index 0cb8a1d..b13b34f 100644 --- a/SimLoop/src/main/java/org/horizon36596/simloop/opmode/GamepadScript.java +++ b/SimLoop/src/main/java/org/horizon36596/simloop/opmode/GamepadScript.java @@ -9,7 +9,9 @@ * *

The two gamepads passed in are the same objects on every tick, so a button stays held until the script * releases it, exactly like a real one. Write only fields ({@code gamepad1.left_stick_y = -1f}, - * {@code gamepad1.a = true}); do not keep references to them outside the call. + * {@code gamepad1.a = true}); do not keep references to them outside the call. A button can be pressed by either + * of its names ({@code a} or {@code cross}, {@code y} or {@code triangle}, {@code back} or {@code share}, ...): the + * OpMode sees both names pressed, as it would on the robot. * *

A script must be a pure function of its arguments (and of state it owns) for a run to be deterministic. */ diff --git a/SimLoop/src/main/java/org/horizon36596/simloop/opmode/OpModeContext.java b/SimLoop/src/main/java/org/horizon36596/simloop/opmode/OpModeContext.java index 32352be..1c6582f 100644 --- a/SimLoop/src/main/java/org/horizon36596/simloop/opmode/OpModeContext.java +++ b/SimLoop/src/main/java/org/horizon36596/simloop/opmode/OpModeContext.java @@ -32,6 +32,9 @@ final class OpModeContext { /** The operator's gamepad as the Driver Station holds it. */ final Gamepad driverStationGamepad2 = new Gamepad(); + /** Where a script's gamepad is staged on its way to the OpMode, with its PlayStation names folded in. */ + private final Gamepad staged = new Gamepad(); + /** Sim time, in seconds, at which {@code getRuntime()} reads zero. */ private double runtimeOriginSeconds; @@ -73,6 +76,26 @@ TelemetryRewiring inject(HardwareMap hardwareMap, RecordingTelemetry telemetry, return rewiring; } + /** + * Copies a script's gamepad into the OpMode's the way the SDK delivers Driver Station data, with the buttons + * the script pressed by their PlayStation names counted too. {@code Gamepad.copy} carries only the Xbox names + * ({@code toByteArray}) and sets the PlayStation ones from them ({@code updateButtonAliases}), so on its own it + * drops {@code triangle = true}: a real Driver Station has one button there, sent under its Xbox name. Each + * PlayStation name is ORed into its Xbox twin on a staging copy, never on the script's gamepad, so a button + * the script releases is released. The pairs are the SDK's ({@code updateButtonAliases}, RobotCore 12.0.0). + */ + private void deliver(Gamepad script, Gamepad toOpMode) { + staged.copy(script); + staged.a |= script.cross; + staged.b |= script.circle; + staged.x |= script.square; + staged.y |= script.triangle; + staged.back |= script.share; + staged.start |= script.options; + staged.guide |= script.ps; + toOpMode.copy(staged); + } + /** Makes {@code getRuntime()} and {@code time} count from the sim clock's current reading: INIT. */ void startRuntimeHere() { runtimeOriginSeconds = timer.time(); @@ -96,8 +119,8 @@ void markStopRequested() { * SDK's edge detection), and {@code time} and {@code getRuntime()} are set to the sim runtime. */ void beforeUserCode() { - opMode.gamepad1.copy(driverStationGamepad1); - opMode.gamepad2.copy(driverStationGamepad2); + deliver(driverStationGamepad1, opMode.gamepad1); + deliver(driverStationGamepad2, opMode.gamepad2); double runtimeSeconds = timer.time() - runtimeOriginSeconds; opMode.time = runtimeSeconds; opMode.resetRuntime(); diff --git a/SimLoop/src/test/java/org/horizon36596/simloop/opmode/IterativeOpModeRunnerTest.java b/SimLoop/src/test/java/org/horizon36596/simloop/opmode/IterativeOpModeRunnerTest.java index af01e4a..858f99b 100644 --- a/SimLoop/src/test/java/org/horizon36596/simloop/opmode/IterativeOpModeRunnerTest.java +++ b/SimLoop/src/test/java/org/horizon36596/simloop/opmode/IterativeOpModeRunnerTest.java @@ -192,6 +192,48 @@ void theScriptDrivesTheGamepadsTickByTickAndEdgeDetectionSeesOnePressPerPress() assertEquals(2, opMode.presses, "aWasPressed() fires once per press, not once per tick held"); } + /** Records each tick what the OpMode sees of one button, under both its names. */ + static class TriangleReader extends OpMode { + final List seen = new ArrayList<>(); + int trianglePresses, yPresses, sharePresses; + + @Override public void init() { } + + @Override + public void loop() { + seen.add((gamepad1.triangle ? "T" : "-") + (gamepad1.y ? "Y" : "-") + (gamepad1.back ? "B" : "-") + + (gamepad1.share ? "S" : "-")); + if (gamepad1.triangleWasPressed()) { + trianglePresses++; + } + if (gamepad1.yWasPressed()) { + yPresses++; + } + if (gamepad1.shareWasPressed()) { + sharePresses++; + } + } + } + + @Test + void aButtonPressedByItsPlayStationNameReachesTheOpModeUnderBothNamesAndReleases() { + TriangleReader opMode = new TriangleReader(); + GamepadScript script = (phase, tick, gamepad1, gamepad2) -> { + if (phase != OpModePhase.ACTIVE) { + return; + } + gamepad1.triangle = tick >= 1 && tick < 3; // pressed by its PlayStation name for two ticks + gamepad1.share = tick == 5; // and share tapped once + }; + runner(opMode, new FakeTimer()).activeTicks(7).gamepads(script).run(); + + assertEquals(Arrays.asList("----", "TY--", "TY--", "----", "----", "--BS", "----"), opMode.seen, + "triangle and y are one button, and so are share and back; released when the script lets go"); + assertEquals(1, opMode.trianglePresses, "triangleWasPressed() once"); + assertEquals(1, opMode.yPresses, "yWasPressed() once: it is the same button"); + assertEquals(1, opMode.sharePresses, "shareWasPressed() once"); + } + @Test void requestOpModeStopRunsStopAtOnceAndNothingAfterIt() { FakeTimer timer = new FakeTimer();