From e785d797cb183af71596a7745c18ea564e4f4e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Burgt?= <209150+thatdutchguy@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:29:48 -0700 Subject: [PATCH 1/2] add test for recipe book navigation --- .../mixin/RecipeBookPageAccessor.java | 20 +++ .../tests/RecipeBookNavigationTests.java | 117 ++++++++++++++++++ .../resources/controlify_test.mixins.json | 1 + src/gametest/resources/fabric.mod.json | 1 + 4 files changed, 139 insertions(+) create mode 100644 src/gametest/java/dev/isxander/controlify/gametest/mixin/RecipeBookPageAccessor.java create mode 100644 src/gametest/java/dev/isxander/controlify/gametest/tests/RecipeBookNavigationTests.java diff --git a/src/gametest/java/dev/isxander/controlify/gametest/mixin/RecipeBookPageAccessor.java b/src/gametest/java/dev/isxander/controlify/gametest/mixin/RecipeBookPageAccessor.java new file mode 100644 index 000000000..04e9cbbc6 --- /dev/null +++ b/src/gametest/java/dev/isxander/controlify/gametest/mixin/RecipeBookPageAccessor.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.gametest.mixin; + +import net.minecraft.client.gui.screens.recipebook.RecipeBookPage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(RecipeBookPage.class) +public interface RecipeBookPageAccessor { + @Accessor("currentPage") + int controlify_test$getCurrentPage(); + + @Accessor("totalPages") + int controlify_test$getTotalPages(); +} diff --git a/src/gametest/java/dev/isxander/controlify/gametest/tests/RecipeBookNavigationTests.java b/src/gametest/java/dev/isxander/controlify/gametest/tests/RecipeBookNavigationTests.java new file mode 100644 index 000000000..4cc635bef --- /dev/null +++ b/src/gametest/java/dev/isxander/controlify/gametest/tests/RecipeBookNavigationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.gametest.tests; + +import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.InputMode; +import dev.isxander.controlify.gametest.framework.TestUnitWorldContext; +import dev.isxander.controlify.gametest.framework.controller.ControlifyGameTestContext; +import dev.isxander.controlify.gametest.mixin.RecipeBookPageAccessor; +import dev.isxander.controlify.mixins.feature.virtualmouse.snapping.RecipeBookComponentAccessor; +import dev.isxander.controlify.screenop.compat.vanilla.RecipeBookScreenProcessor; +import dev.isxander.controlify.utils.MinecraftUtil; +import dev.isxander.sdl.SdlGamepad; +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; +import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.core.BlockPos; +import net.minecraft.world.inventory.RecipeBookType; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Blocks; + +import java.util.function.BooleanSupplier; + +/// Exercises the simulated mouse clicks used by recipe-book controller navigation. +@SuppressWarnings("UnstableApiUsage") +public class RecipeBookNavigationTests implements FabricClientGameTest { + @Override + public void runTest(ClientGameTestContext context) { + var controlify = new ControlifyGameTestContext(context); + controlify.resetSettings(); + + try (var controller = controlify.virtualControllerBuilder().withXbox().attach(); + var world = new TestUnitWorldContext(context); + var region = world.allocateRegion(3, 3, 3)) { + var tablePos = new BlockPos(1, 1, 2); + region.setBlock(tablePos, Blocks.CRAFTING_TABLE); + region.runOnServer((level, player) -> { + player.setGameMode(GameType.SURVIVAL); + player.awardRecipes(level.getServer().getRecipeManager().getRecipes()); + }); + + context.runOnClient(client -> { + client.player.getRecipeBook().setOpen(RecipeBookType.CRAFTING, true); + client.player.getRecipeBook().setFiltering(RecipeBookType.CRAFTING, false); + Controlify.instance().setCurrentController(controller.getControllerEntity(), true); + Controlify.instance().setInputMode(InputMode.CONTROLLER); + }); + region.runOnServer((level, player) -> { + var pos = region.absolutePos(tablePos); + player.openMenu(level.getBlockState(pos).getMenuProvider(level, pos)); + }); + context.waitForScreen(CraftingScreen.class); + + var recipeBook = context.computeOnClient(_ -> + ((RecipeBookScreenProcessor.RecipeBookScreenAccessor) MinecraftUtil.getScreen()) + .controlify$getRecipeBookComponent()); + var component = (RecipeBookComponentAccessor) recipeBook; + var page = context.computeOnClient(_ -> + (RecipeBookPageAccessor) component.controlify$getRecipeBookPage()); + + // Require real navigation targets so empty recipes or a closed book + // cannot produce a misleading navigation failure. + context.waitFor(_ -> recipeBook.isVisible() && page.controlify_test$getTotalPages() > 1); + var tabs = context.computeOnClient(_ -> component.controlify$getTabButtons().stream() + .filter(tab -> tab.visible).toList()); + context.runOnClient(_ -> { + if (tabs.size() < 2 || component.controlify$getSelectedTab() != tabs.getFirst() + || page.controlify_test$getCurrentPage() != 0) { + throw new AssertionError("Expected the first recipe category and page, with multiple visible categories"); + } + if (!Controlify.instance().virtualMouseHandler().isVirtualMouseEnabled()) { + throw new AssertionError("Crafting screen must have virtual mouse enabled for recipe navigation"); + } + }); + + pressAndWait(context, + () -> controller.holdButton(SdlGamepad.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), + () -> controller.releaseButton(SdlGamepad.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), + () -> page.controlify_test$getCurrentPage() == 1, + "Right shoulder should advance the recipe page from 0 to 1"); + pressAndWait(context, + () -> controller.holdButton(SdlGamepad.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), + () -> controller.releaseButton(SdlGamepad.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), + () -> page.controlify_test$getCurrentPage() == 0, + "Left shoulder should return the recipe page from 1 to 0"); + pressAndWait(context, + () -> controller.holdAxis(SdlGamepad.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1f), + () -> controller.releaseAxis(SdlGamepad.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), + () -> component.controlify$getSelectedTab() == tabs.get(1), + "Right trigger should select the next recipe category"); + pressAndWait(context, + () -> controller.holdAxis(SdlGamepad.SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1f), + () -> controller.releaseAxis(SdlGamepad.SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + () -> component.controlify$getSelectedTab() == tabs.getFirst(), + "Left trigger should select the previous recipe category"); + } finally { + controlify.resetSettings(); + } + } + + private static void pressAndWait(ClientGameTestContext context, Runnable press, Runnable release, + BooleanSupplier condition, String message) { + press.run(); + try { + context.waitFor(_ -> condition.getAsBoolean(), 20); + } catch (AssertionError failure) { + throw new AssertionError(message, failure); + } finally { + release.run(); + context.waitTick(); + } + } +} diff --git a/src/gametest/resources/controlify_test.mixins.json b/src/gametest/resources/controlify_test.mixins.json index d18f67e9a..008de9104 100644 --- a/src/gametest/resources/controlify_test.mixins.json +++ b/src/gametest/resources/controlify_test.mixins.json @@ -18,6 +18,7 @@ "SDLControllerManagerMixin" ], "client": [ + "RecipeBookPageAccessor", "SDLEventHandlerAccessor", "SystemToastMixin", "ToastInstanceAccessor", diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json index a6af3326e..469ed2fda 100644 --- a/src/gametest/resources/fabric.mod.json +++ b/src/gametest/resources/fabric.mod.json @@ -16,6 +16,7 @@ "dev.isxander.controlify.gametest.tests.ManyHotplugTests", "dev.isxander.controlify.gametest.tests.ResetControlifySettingsTests", "dev.isxander.controlify.gametest.tests.VirtualControllerInputTests", + "dev.isxander.controlify.gametest.tests.RecipeBookNavigationTests", "dev.isxander.controlify.gametest.tests.KeyboardLayoutTests", "dev.isxander.controlify.gametest.tests.RumbleTests", "dev.isxander.controlify.gametest.tests.AdaptiveTriggerTests", From b666d6770d3fe39bf3860a76c00db88341a94549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Burgt?= <209150+thatdutchguy@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:34:43 -0700 Subject: [PATCH 2/2] fix: recipe book paging --- .../screenop/compat/vanilla/RecipeBookScreenProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/RecipeBookScreenProcessor.java b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/RecipeBookScreenProcessor.java index c5d374e76..61187d063 100644 --- a/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/RecipeBookScreenProcessor.java +++ b/src/main/java/dev/isxander/controlify/screenop/compat/vanilla/RecipeBookScreenProcessor.java @@ -6,6 +6,7 @@ */ package dev.isxander.controlify.screenop.compat.vanilla; +import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.controlify.bindings.ControlifyBindings; import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.mixins.feature.virtualmouse.snapping.RecipeBookComponentAccessor; @@ -76,7 +77,7 @@ protected void handleScreenVMouse(ControllerEntity controller, VirtualMouseHandl if (button != null) { recipeBookComponent.mouseClicked(new MouseButtonEvent( button.getX(), button.getY(), - new MouseButtonInfo(0, 0)), false); + new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT, 0)), false); } }