From 102b5d92be89f076d4e490dc0f4f6e9d19381130 Mon Sep 17 00:00:00 2001 From: Gaetarra Date: Sat, 29 Aug 2026 22:09:03 +0200 Subject: [PATCH] Give Click Slot the three clicks a player actually makes Click Slot always sent ClickType.PICKUP, so shift-clicking a stack into the other container or swapping it to the offhand with F had to be built out of Move Item or the raw packet fabricator in UI Utils. Both are more work than the click they stand in for, and neither matches what the server sees from a real player. Click Slot gains three modes, mapping to what the vanilla screen sends for the same input: Left Click is PICKUP and stays the default, Shift Click is QUICK_MOVE, and F is SWAP on button 40, the offhand's button index in AbstractContainerScreen. Two supporting changes, both consequences of a node gaining modes at all: - Click Slot is tagged RENDER_INLINE_PARAMETERS. NodeGraph.rendersInline Parameters gates the parameter strip and the mode selector is drawn inside it, so without the tag the modes would exist but not be selectable. The tag also makes the Slot number editable on the node, which it was not before: the node declared the parameter but rendered no field for it, so the only way to name a slot was to attach an Inventory Slot node to the Selection slot. - The Slot parameter moves from a type parameter to a mode parameter on each of the three modes. NodeCatalog.initializeParameters reads mode parameters instead of type parameters once a node has modes, so a mode without it would silently drop the value. Keeping the id and label identical is what lets presets saved before this restore their slot unchanged; they load with no mode and take the constructor default of CLICK_SLOT_LEFT, which is the click they were already sending. Mode display names get lang entries, since NodeMode.getDisplayName ignores the strings in the enum constructor and builds "pathmind.node.mode." as a translation key. Verified: Fabric compiles on 26.2 and NeoForge on 26.1.2. Not verified locally: :common:test and the 1.21.x targets, which need a JDK 21 toolchain; CI covers both, and NodeClickSlotTest pins the mode-to-click mapping and the parameter round-trip. The editor behaviour, in particular the mode selector and the now-visible Slot field, has not been clicked through in a dev client. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JNco3sHnsGRnCojqqhMZ53 --- .../java/com/pathmind/nodes/NodeCatalog.java | 11 +++- .../nodes/NodeInventoryCommandExecutor.java | 25 +++++++++- .../java/com/pathmind/nodes/NodeMode.java | 9 ++++ .../resources/assets/pathmind/lang/en_us.json | 6 +++ .../com/pathmind/nodes/NodeClickSlotTest.java | 50 +++++++++++++++++++ 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 common/src/test/java/com/pathmind/nodes/NodeClickSlotTest.java diff --git a/common/src/main/java/com/pathmind/nodes/NodeCatalog.java b/common/src/main/java/com/pathmind/nodes/NodeCatalog.java index c5997502..9d38b489 100644 --- a/common/src/main/java/com/pathmind/nodes/NodeCatalog.java +++ b/common/src/main/java/com/pathmind/nodes/NodeCatalog.java @@ -440,7 +440,11 @@ public final class NodeCatalog { NodeType.OPEN_INVENTORY, NodeType.CLOSE_GUI); + // This flag is what makes a node draw its parameter strip, and the mode selector lives + // inside that strip (NodeRenderer.renderInlineParameterContent). A node with modes but + // without this tag has no way to reach them in the editor. tag(NodeFlag.RENDER_INLINE_PARAMETERS, + NodeType.CLICK_SLOT, NodeType.UI_UTILS, NodeType.SENSOR_FABRIC_EVENT, NodeType.SENSOR_ATTRIBUTE_DETECTION, @@ -903,6 +907,12 @@ public final class NodeCatalog { modeParameters(NodeMode.FARM_WAYPOINT, of("Waypoint", ParameterType.STRING, "farm"), of("Range", ParameterType.INTEGER, "10")); + // Click Slot's three modes take the same Slot number; they differ only in the click the + // node sends. Keeping the id and label identical to the old type parameter is what lets + // presets saved before the modes existed restore their slot unchanged. + modeParameters(NodeMode.CLICK_SLOT_LEFT, of("click_slot_index", "Slot", ParameterType.INTEGER, "0")); + modeParameters(NodeMode.CLICK_SLOT_SHIFT, of("click_slot_index", "Slot", ParameterType.INTEGER, "0")); + modeParameters(NodeMode.CLICK_SLOT_SWAP_OFFHAND, of("click_slot_index", "Slot", ParameterType.INTEGER, "0")); modeParameters(NodeMode.WAIT_SECONDS, of("Duration", ParameterType.DOUBLE, "")); modeParameters(NodeMode.WAIT_TICKS, of("Duration", ParameterType.DOUBLE, "")); modeParameters(NodeMode.WAIT_MINUTES, of("Duration", ParameterType.DOUBLE, "")); @@ -948,7 +958,6 @@ public final class NodeCatalog { of("drop_slot_index", "Slot", ParameterType.INTEGER, "0"), of("Count", ParameterType.INTEGER, "0"), of("EntireStack", ParameterType.BOOLEAN, "true")); - typeParameters(NodeType.CLICK_SLOT, of("click_slot_index", "Slot", ParameterType.INTEGER, "0")); typeParameters(NodeType.CLICK_SCREEN, of("X", ParameterType.INTEGER, "0"), of("Y", ParameterType.INTEGER, "0")); diff --git a/common/src/main/java/com/pathmind/nodes/NodeInventoryCommandExecutor.java b/common/src/main/java/com/pathmind/nodes/NodeInventoryCommandExecutor.java index fa3d5f0f..68212789 100644 --- a/common/src/main/java/com/pathmind/nodes/NodeInventoryCommandExecutor.java +++ b/common/src/main/java/com/pathmind/nodes/NodeInventoryCommandExecutor.java @@ -31,6 +31,8 @@ import net.minecraft.world.item.Items; final class NodeInventoryCommandExecutor { + private static final int OFFHAND_SWAP_BUTTON = 40; + private final Node owner; private final NodeType type; private final NodeRuntimeState runtimeState; @@ -197,11 +199,12 @@ void executeClickSlotCommand(CompletableFuture future) { return; } + NodeMode mode = owner.getMode(); interactionManager.handleInventoryMouseClick( handler.containerId, resolution.handlerSlotIndex, - 0, - ClickType.PICKUP, + clickSlotButton(mode), + clickSlotClickType(mode), client.player ); @@ -210,6 +213,24 @@ void executeClickSlotCommand(CompletableFuture future) { future.complete(null); } + // The three Click Slot modes send exactly what the vanilla screen sends for the same input: + // a plain left click is PICKUP, shift-click is QUICK_MOVE, and F is SWAP on button 40, which + // is the offhand's button index in AbstractContainerScreen. A node saved before the modes + // existed loads with the default CLICK_SLOT_LEFT, and a null mode is the same plain click. + static ClickType clickSlotClickType(NodeMode mode) { + if (mode == NodeMode.CLICK_SLOT_SHIFT) { + return ClickType.QUICK_MOVE; + } + if (mode == NodeMode.CLICK_SLOT_SWAP_OFFHAND) { + return ClickType.SWAP; + } + return ClickType.PICKUP; + } + + static int clickSlotButton(NodeMode mode) { + return mode == NodeMode.CLICK_SLOT_SWAP_OFFHAND ? OFFHAND_SWAP_BUTTON : 0; + } + void executeClickScreenCommand(CompletableFuture future) { net.minecraft.client.Minecraft client = net.minecraft.client.Minecraft.getInstance(); if (client == null || client.getWindow() == null) { diff --git a/common/src/main/java/com/pathmind/nodes/NodeMode.java b/common/src/main/java/com/pathmind/nodes/NodeMode.java index 870219d6..560af44a 100644 --- a/common/src/main/java/com/pathmind/nodes/NodeMode.java +++ b/common/src/main/java/com/pathmind/nodes/NodeMode.java @@ -8,6 +8,11 @@ * Each mode corresponds to a specific behavior within a generalized node type. */ public enum NodeMode { + // CLICK_SLOT modes + CLICK_SLOT_LEFT("Left Click", "Pick up or place the stack, as a plain left click does"), + CLICK_SLOT_SHIFT("Shift Click", "Move the stack to the other container, as shift-clicking does"), + CLICK_SLOT_SWAP_OFFHAND("F", "Swap the stack with the offhand, as pressing F does"), + // GOTO modes GOTO_XYZ("Go to XYZ", "Go to specific X, Y, Z coordinates"), GOTO_XZ("Go to XZ", "Go to X, Z coordinates (Y defaults to surface)"), @@ -176,6 +181,9 @@ public static NodeMode[] getModesForNodeType(NodeType nodeType) { case SENSOR_LOOK_DIRECTION -> new NodeMode[]{ SENSOR_LOOK_YAW, SENSOR_LOOK_PITCH, SENSOR_LOOK_ROTATION }; + case CLICK_SLOT -> new NodeMode[]{ + CLICK_SLOT_LEFT, CLICK_SLOT_SHIFT, CLICK_SLOT_SWAP_OFFHAND + }; default -> new NodeMode[0]; }; } @@ -200,6 +208,7 @@ public static NodeMode getDefaultModeForNodeType(NodeType nodeType) { case WAIT, PARAM_DURATION -> WAIT_SECONDS; case SENSOR_POSITION_OF -> SENSOR_POSITION_XYZ; case SENSOR_LOOK_DIRECTION -> SENSOR_LOOK_ROTATION; + case CLICK_SLOT -> CLICK_SLOT_LEFT; default -> null; }; } diff --git a/common/src/main/resources/assets/pathmind/lang/en_us.json b/common/src/main/resources/assets/pathmind/lang/en_us.json index a12e7843..828d375d 100644 --- a/common/src/main/resources/assets/pathmind/lang/en_us.json +++ b/common/src/main/resources/assets/pathmind/lang/en_us.json @@ -283,6 +283,12 @@ "pathmind.node.mode.goal_xz.desc": "Set goal to X, Z coordinates (Y defaults to surface)", "pathmind.node.mode.goal_y": "Set Goal Y", "pathmind.node.mode.goal_y.desc": "Set goal to specific Y level", + "pathmind.node.mode.click_slot_left": "Left Click", + "pathmind.node.mode.click_slot_left.desc": "Pick up or place the stack, as a plain left click does", + "pathmind.node.mode.click_slot_shift": "Shift Click", + "pathmind.node.mode.click_slot_shift.desc": "Move the stack to the other container, as shift-clicking does", + "pathmind.node.mode.click_slot_swap_offhand": "F", + "pathmind.node.mode.click_slot_swap_offhand.desc": "Swap the stack with the offhand, as pressing F does", "pathmind.node.mode.goto_block": "Go to Block", "pathmind.node.mode.goto_block.desc": "Go to nearest block of specified type", "pathmind.node.mode.goto_xyz": "Go to XYZ", diff --git a/common/src/test/java/com/pathmind/nodes/NodeClickSlotTest.java b/common/src/test/java/com/pathmind/nodes/NodeClickSlotTest.java new file mode 100644 index 00000000..ad0eada5 --- /dev/null +++ b/common/src/test/java/com/pathmind/nodes/NodeClickSlotTest.java @@ -0,0 +1,50 @@ +package com.pathmind.nodes; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.minecraft.world.inventory.ClickType; +import org.junit.jupiter.api.Test; + +class NodeClickSlotTest { + + @Test + void eachModeSendsTheClickVanillaSendsForTheSameInput() { + assertEquals(ClickType.PICKUP, + NodeInventoryCommandExecutor.clickSlotClickType(NodeMode.CLICK_SLOT_LEFT)); + assertEquals(ClickType.QUICK_MOVE, + NodeInventoryCommandExecutor.clickSlotClickType(NodeMode.CLICK_SLOT_SHIFT)); + assertEquals(ClickType.SWAP, + NodeInventoryCommandExecutor.clickSlotClickType(NodeMode.CLICK_SLOT_SWAP_OFFHAND)); + + // Only the offhand swap carries a button; SWAP with any other button hits a hotbar slot. + assertEquals(0, NodeInventoryCommandExecutor.clickSlotButton(NodeMode.CLICK_SLOT_LEFT)); + assertEquals(0, NodeInventoryCommandExecutor.clickSlotButton(NodeMode.CLICK_SLOT_SHIFT)); + assertEquals(40, NodeInventoryCommandExecutor.clickSlotButton(NodeMode.CLICK_SLOT_SWAP_OFFHAND)); + } + + @Test + void aNodeSavedBeforeTheModesExistedStillSendsAPlainLeftClick() { + assertEquals(ClickType.PICKUP, NodeInventoryCommandExecutor.clickSlotClickType(null)); + assertEquals(0, NodeInventoryCommandExecutor.clickSlotButton(null)); + } + + @Test + void clickSlotOffersItsThreeModesWithLeftClickAsTheDefault() { + assertEquals(NodeMode.CLICK_SLOT_LEFT, NodeMode.getDefaultModeForNodeType(NodeType.CLICK_SLOT)); + assertEquals(3, NodeMode.getModesForNodeType(NodeType.CLICK_SLOT).length); + } + + @Test + void everyModeKeepsTheSlotParameterSoOldPresetsRestoreIt() { + // initializeParameters reads mode parameters instead of type parameters once a node has + // modes, so a mode missing the Slot definition would silently drop the saved value. + for (NodeMode mode : NodeMode.getModesForNodeType(NodeType.CLICK_SLOT)) { + java.util.List parameters = new java.util.ArrayList<>(); + NodeCatalog.initializeParameters(parameters, NodeType.CLICK_SLOT, mode); + assertEquals(1, parameters.size(), mode.name()); + assertEquals("Slot", parameters.get(0).getName(), mode.name()); + assertEquals("click_slot_index", parameters.get(0).getId(), mode.name()); + assertEquals("0", parameters.get(0).getStringValue(), mode.name()); + } + } +}