Skip to content

Commit 3e92818

Browse files
feat: LEFT_SHIFT implementation and handling within stonecutters (#5195) (#6222)
* feat: LEFT_SHIFT implementation and handling within stonecutters (fixes #5195) chore: early return if there are no remaining items readability! chore: add a smol comment to clarify this variable because why the hell not? * cleanup: reduce redundant code & (hopefully) fix touchscreen compat * Fix touchscreen and ensure only one shift click is sent (#1) <3 you, AJ! --------- Co-authored-by: AJ Ferguson <AJ-Ferguson@users.noreply.github.com>
1 parent bf93cc3 commit 3e92818

2 files changed

Lines changed: 128 additions & 8 deletions

File tree

core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,13 @@ public void execute(boolean refresh) {
157157
}
158158

159159
ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
160-
inventory.getJavaId(),
161-
stateId,
162-
action.slot,
163-
action.click.actionType,
164-
action.click.action,
165-
DataComponentHashers.hashStack(session, clickedItemStack),
166-
changedHashedItems
160+
inventory.getJavaId(),
161+
stateId,
162+
action.slot,
163+
action.click.actionType,
164+
action.click.action,
165+
DataComponentHashers.hashStack(session, clickedItemStack),
166+
changedHashedItems
167167
);
168168

169169
session.sendDownstreamGamePacket(clickPacket);
@@ -397,7 +397,40 @@ private void simulateAction(ClickAction action) {
397397
swap(action.slot, inventory.getOffsetForHotbar(8), clicked);
398398
break;
399399
case LEFT_SHIFT:
400-
//TODO
400+
if (clicked.isEmpty()) {
401+
break;
402+
}
403+
404+
int remaining = clicked.getAmount();
405+
// are we shift-clicking items from the player inventory, or from the container?
406+
boolean sourceInPlayerInventory = action.slot >= translator.size;
407+
408+
if (!sourceInPlayerInventory) {
409+
int hotbarOffset = inventory.getOffsetForHotbar(0);
410+
int mainStart = hotbarOffset - 27;
411+
int mainEnd = hotbarOffset - 1;
412+
int hotbarEnd = hotbarOffset + 8;
413+
414+
// from what I understand, java has this priority for shift clicks:
415+
// 1. existing partial stacks in main inventory, top rows first
416+
// 2. existing partial stacks in hotbar
417+
// 3. empty slots in main inventory
418+
// 4. empty slots in hotbar
419+
420+
// fill partial stacks in main inventory, then opt for hotbar afterwards
421+
remaining = fillPartialStacks(action.slot, clicked, remaining, mainStart, mainEnd);
422+
remaining = fillPartialStacks(action.slot, clicked, remaining, hotbarOffset, hotbarEnd);
423+
424+
// fill empty slots in main inventory, then opt for hotbar afterwards
425+
remaining = fillEmptySlots(action.slot, clicked, remaining, mainStart, mainEnd);
426+
fillEmptySlots(action.slot, clicked, remaining, hotbarOffset, hotbarEnd); // remaining is no longer used
427+
} else { // the other way around. the source is the player inventory, and we're shift clicking into the target container
428+
// fill partial stacks in top inventory, then opt for empty slots
429+
// this isn't a player's inventory, so we do not need the hotbar/main-inventory logic and vice versa :P
430+
remaining = fillPartialStacks(action.slot, clicked, remaining, 0, translator.size - 1);
431+
fillEmptySlots(action.slot, clicked, remaining, 0, translator.size - 1);
432+
}
433+
401434
break;
402435
case DROP_ONE:
403436
if (!clicked.isEmpty()) {
@@ -501,6 +534,47 @@ private void reduceCraftingGrid(boolean makeAll) {
501534
}
502535
}
503536

537+
/**
538+
* Utility method for LEFT_SHIFT. Java has some specific logic where it will try fill partial stacks first, so this is
539+
* just a util method to reduce code duplication.
540+
*
541+
* @return The remaining item count
542+
*/
543+
private int fillPartialStacks(int sourceSlot, GeyserItemStack source, int remaining, int rangeStart, int rangeEnd) {
544+
if (remaining <= 0) return 0; // we already return early within the underlying loop, this is a sanity check and/or readability to avoid confusion
545+
for (int i = rangeStart; i <= rangeEnd && remaining > 0; i++) {
546+
if (!canStack(i, source)) continue;
547+
548+
GeyserItemStack dest = getItem(i);
549+
int canAdd = Math.min(remaining, dest.getMaxStackSize() - dest.getAmount());
550+
if (canAdd <= 0) continue;
551+
552+
add(i, dest, canAdd);
553+
sub(sourceSlot, source, canAdd);
554+
remaining -= canAdd;
555+
}
556+
return remaining;
557+
}
558+
559+
/**
560+
* Utility method for LEFT_SHIFT. Java has some specific logic where it will fill empty slots AFTER attempting to fill partial slots...
561+
*
562+
* @return The remaining item count
563+
*/
564+
private int fillEmptySlots(int sourceSlot, GeyserItemStack source, int remaining, int rangeStart, int rangeEnd) {
565+
if (remaining <= 0) return 0; // we already return early within the underlying loop, this is a sanity check and/or readability to avoid confusion
566+
for (int i = rangeStart; i <= rangeEnd && remaining > 0; i++) {
567+
if (!isEmpty(i)) continue;
568+
569+
int toMove = Math.min(remaining, source.getMaxStackSize());
570+
571+
setItem(i, source.copy(toMove));
572+
sub(sourceSlot, source, toMove);
573+
remaining -= toMove;
574+
}
575+
return remaining;
576+
}
577+
504578
/**
505579
* @return a new set of all affected slots.
506580
*/

core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@
2525

2626
package org.geysermc.geyser.translator.inventory;
2727

28+
import it.unimi.dsi.fastutil.ints.IntSet;
2829
import org.checkerframework.checker.nullness.qual.Nullable;
2930
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
3031
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
3132
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
3233
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.CraftRecipeAction;
3334
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestAction;
3435
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestActionType;
36+
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.TransferItemStackRequestAction;
3537
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse;
3638
import org.geysermc.geyser.inventory.*;
39+
import org.geysermc.geyser.inventory.click.Click;
40+
import org.geysermc.geyser.inventory.click.ClickPlan;
3741
import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
3842
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
3943
import org.geysermc.geyser.level.block.Blocks;
@@ -79,6 +83,48 @@ protected ItemStackResponse translateSpecialRequest(GeyserSession session, Stone
7983
container.setItem(1, GeyserItemStack.from(session, javaOutput), session);
8084
}
8185

86+
// The client might send multiple ItemStackRequests for one shift click,
87+
// so check if input is empty, meaning we already sent one
88+
GeyserItemStack input = container.getItem(0);
89+
if (input.isEmpty()) {
90+
return rejectRequest(request, false);
91+
}
92+
93+
// support for quick move on the output.
94+
// CRAFT_RECIPE is always at index 0 so we do instanceof checks for obvious reasons
95+
if (data.getNumberOfRequestedCrafts() > 1) {
96+
for (ItemStackRequestAction action : request.getActions()) {
97+
if (action instanceof TransferItemStackRequestAction transfer) {
98+
if (transfer.getSource().getContainerName().getContainer() == ContainerSlotType.CREATED_OUTPUT) {
99+
ContainerSlotType destContainer = transfer.getDestination().getContainerName().getContainer();
100+
101+
if (destContainer == ContainerSlotType.HOTBAR
102+
|| destContainer == ContainerSlotType.HOTBAR_AND_INVENTORY
103+
|| destContainer == ContainerSlotType.INVENTORY) {
104+
105+
// shift click of the result into the inventory
106+
ClickPlan plan = new ClickPlan(session, this, container);
107+
plan.add(Click.LEFT_SHIFT, 1);
108+
plan.execute(true);
109+
110+
// slot 0 = stonecutter input, special logic here as getGridSize is non existent! yay!
111+
// from my testing, this seems to cause ClickPlan#reduceCraftingGrid to return early which is
112+
// why we have to handle this logic ourselves...
113+
// to do, check in to the mental asylum
114+
container.setItem(0, GeyserItemStack.EMPTY, session); // assume that all input was used. server will resync
115+
116+
// we need to ALWAYS update slot 0 as well so that the client knows the new input count
117+
IntSet reportedSlots = plan.getAffectedSlots();
118+
reportedSlots.add(0);
119+
120+
return acceptRequest(request, makeContainerEntries(session, container, reportedSlots));
121+
}
122+
}
123+
}
124+
}
125+
}
126+
127+
// translate the request normally as it's a plain single craft. no shift-click transfer actions found
82128
return translateRequest(session, container, request);
83129
}
84130

0 commit comments

Comments
 (0)