@@ -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 */
0 commit comments