Skip to content

Commit

Permalink
Handle illegal stack sizes in creative mode
Browse files Browse the repository at this point in the history
  • Loading branch information
AJ-Ferguson committed Sep 1, 2024
1 parent 63e60bc commit 0553905
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ public int getAmount() {
return isEmpty() ? 0 : amount;
}

public int maxStackSize() {
return getComponent(DataComponentType.MAX_STACK_SIZE, asItem().maxStackSize());
}

public @Nullable DataComponents getComponents() {
return isEmpty() ? null : components;
}
Expand Down Expand Up @@ -133,12 +137,20 @@ public int getNetId() {
return isEmpty() ? 0 : netId;
}

public void add(int add) {
public int capacity() {
return Math.max(maxStackSize() - amount, 0);
}

public int add(int add) {
add = Math.min(add, capacity());
amount += add;
return add;
}

public void sub(int sub) {
public int sub(int sub) {
sub = Math.min(sub, amount);
amount -= sub;
return sub;
}

public ItemStack getItemStack() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
package org.geysermc.geyser.translator.inventory;

import it.unimi.dsi.fastutil.ints.*;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
Expand Down Expand Up @@ -63,7 +63,7 @@

import java.util.*;

@AllArgsConstructor
@RequiredArgsConstructor
public abstract class InventoryTranslator {

public static final InventoryTranslator PLAYER_INVENTORY_TRANSLATOR = new PlayerInventoryTranslator();
Expand Down Expand Up @@ -108,6 +108,7 @@ public abstract class InventoryTranslator {
public static final int PLAYER_INVENTORY_OFFSET = 9;

public final int size;
protected boolean refreshPending;

public abstract boolean prepareInventory(GeyserSession session, Inventory inventory);
public abstract void openInventory(GeyserSession session, Inventory inventory);
Expand Down Expand Up @@ -157,7 +158,7 @@ protected ItemStackResponse translateSpecialRequest(GeyserSession session, Inven
}

public final void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
boolean refresh = false;
this.refreshPending = false;
ItemStackResponsePacket responsePacket = new ItemStackResponsePacket();
for (ItemStackRequest request : requests) {
ItemStackResponse response;
Expand All @@ -182,14 +183,14 @@ public final void translateRequests(GeyserSession session, Inventory inventory,

if (response.getResult() != ItemStackResponseStatus.OK) {
// Sync our copy of the inventory with Bedrock's to prevent desyncs
refresh = true;
this.refreshPending = true;
}

responsePacket.getEntries().add(response);
}
session.sendUpstreamPacket(responsePacket);

if (refresh) {
if (this.refreshPending) {
InventoryUtils.updateCursor(session);
updateInventory(session, inventory);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven

PlayerInventory playerInv = session.getPlayerInventory();
IntSet affectedSlots = new IntOpenHashSet();

actionLoop:
for (ItemStackRequestAction action : request.getActions()) {
switch (action.getType()) {
case TAKE, PLACE -> {
Expand All @@ -260,15 +262,21 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
if (playerInv.getCursor().isEmpty()) {
playerInv.setCursor(sourceItem.copy(0), session);
// Bypass stack limit for empty cursor
playerInv.setCursor(sourceItem.copy(transferAmount), session);
sourceItem.sub(transferAmount);
} else if (!InventoryUtils.canStack(sourceItem, playerInv.getCursor())) {
return rejectRequest(request);
} else {
transferAmount = playerInv.getCursor().add(transferAmount);
sourceItem.sub(transferAmount);
}

playerInv.getCursor().add(transferAmount);
sourceItem.sub(transferAmount);

affectedSlots.add(sourceSlot);
// Don't add to affectedSlots if nothing changed.
// Prevents sendCreativeAction from adjusting stack size.
if (transferAmount > 0) {
affectedSlots.add(sourceSlot);
}
} else if (isCursor(transferAction.getSource())) {
int destSlot = bedrockSlotToJava(transferAction.getDestination());
GeyserItemStack sourceItem = playerInv.getCursor();
Expand All @@ -278,10 +286,11 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request);
}

inventory.getItem(destSlot).add(transferAmount);
sourceItem.sub(transferAmount);

affectedSlots.add(destSlot);
transferAmount = inventory.getItem(destSlot).add(transferAmount);
if (transferAmount > 0) {
sourceItem.sub(transferAmount);
affectedSlots.add(destSlot);
}
} else {
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
int destSlot = bedrockSlotToJava(transferAction.getDestination());
Expand All @@ -292,11 +301,17 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request);
}

inventory.getItem(destSlot).add(transferAmount);
sourceItem.sub(transferAmount);
transferAmount = inventory.getItem(destSlot).add(transferAmount);
if (transferAmount > 0) {
sourceItem.sub(transferAmount);
affectedSlots.add(sourceSlot);
affectedSlots.add(destSlot);
}
}

affectedSlots.add(sourceSlot);
affectedSlots.add(destSlot);
if (transferAction.getCount() != transferAmount) {
this.refreshPending = true; // Fixes visual bug with cursor
break actionLoop; // Inventory is not what client expects right now
}
}
case SWAP -> {
Expand Down Expand Up @@ -361,10 +376,16 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven
return rejectRequest(request);
}

ServerboundSetCreativeModeSlotPacket creativeDropPacket = new ServerboundSetCreativeModeSlotPacket((short)-1, sourceItem.getItemStack(dropAction.getCount()));
int dropAmount = dropAction.getCount();
if (dropAmount > sourceItem.maxStackSize()) {
dropAmount = sourceItem.maxStackSize();
sourceItem.setAmount(dropAmount);
}

ServerboundSetCreativeModeSlotPacket creativeDropPacket = new ServerboundSetCreativeModeSlotPacket((short)-1, sourceItem.getItemStack(dropAmount));
session.sendDownstreamGamePacket(creativeDropPacket);

sourceItem.sub(dropAction.getCount());
sourceItem.sub(dropAmount);
}
case DESTROY -> {
// Only called when a creative client wants to destroy an item... I think - Camotoy
Expand Down Expand Up @@ -404,9 +425,12 @@ public ItemStackResponse translateRequest(GeyserSession session, Inventory inven

@Override
protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
ItemStack javaCreativeItem = null;
GeyserItemStack javaCreativeItem = null;
IntSet affectedSlots = new IntOpenHashSet();
CraftState craftState = CraftState.START;
boolean firstTransfer = true;

actionLoop:
for (ItemStackRequestAction action : request.getActions()) {
switch (action.getType()) {
case CRAFT_CREATIVE: {
Expand Down Expand Up @@ -456,26 +480,39 @@ protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inve
return rejectRequest(request);
}

int transferAmount = Math.min(transferAction.getCount(), javaCreativeItem.maxStackSize());
if (isCursor(transferAction.getDestination())) {
if (session.getPlayerInventory().getCursor().isEmpty()) {
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
newItemStack.setAmount(transferAction.getCount());
GeyserItemStack newItemStack = javaCreativeItem.copy(transferAmount);
session.getPlayerInventory().setCursor(newItemStack, session);
} else {
session.getPlayerInventory().getCursor().add(transferAction.getCount());
transferAmount = session.getPlayerInventory().getCursor().add(transferAmount);
}
//cursor is always included in response
} else {
int destSlot = bedrockSlotToJava(transferAction.getDestination());
if (inventory.getItem(destSlot).isEmpty()) {
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
newItemStack.setAmount(transferAction.getCount());
GeyserItemStack newItemStack = javaCreativeItem.copy(transferAmount);
inventory.setItem(destSlot, newItemStack, session);
} else {
inventory.getItem(destSlot).add(transferAction.getCount());
// If the player is shift clicking an item with a stack size greater than java edition,
// emulate the action ourselves instead.
if (firstTransfer && inventory.getItem(destSlot).capacity() < transferAmount) {
GeyserItemStack newItemStack = javaCreativeItem.copy(javaCreativeItem.maxStackSize());
emulateCreativeQuickMove(session, inventory, affectedSlots, newItemStack);
this.refreshPending = true;
break actionLoop; // Ignore the rest of the client's actions
}

transferAmount = inventory.getItem(destSlot).add(transferAmount);
}
affectedSlots.add(destSlot);
}

firstTransfer = false;
if (transferAmount != transferAction.getCount()) {
this.refreshPending = true;
}
break;
}
case DROP: {
Expand All @@ -489,14 +526,8 @@ protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inve
return rejectRequest(request);
}

ItemStack dropStack;
if (dropAction.getCount() == javaCreativeItem.getAmount()) {
dropStack = javaCreativeItem;
} else {
// Specify custom count
dropStack = new ItemStack(javaCreativeItem.getId(), dropAction.getCount(), javaCreativeItem.getDataComponents());
}
ServerboundSetCreativeModeSlotPacket creativeDropPacket = new ServerboundSetCreativeModeSlotPacket((short)-1, dropStack);
ItemStack dropItem = javaCreativeItem.getItemStack(Math.min(dropAction.getCount(), javaCreativeItem.maxStackSize()));
ServerboundSetCreativeModeSlotPacket creativeDropPacket = new ServerboundSetCreativeModeSlotPacket((short)-1, dropItem);
session.sendDownstreamGamePacket(creativeDropPacket);
break;
}
Expand All @@ -513,14 +544,47 @@ protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inve
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
}

private static void sendCreativeAction(GeyserSession session, Inventory inventory, int slot) {
private void sendCreativeAction(GeyserSession session, Inventory inventory, int slot) {
GeyserItemStack item = inventory.getItem(slot);

// This does not match java client behaviour, but the java server will ignore creative actions with illegal stack sizes
if (item.getAmount() > item.maxStackSize()) {
item.setAmount(item.maxStackSize());
this.refreshPending = true;
}

ItemStack itemStack = item.isEmpty() ? new ItemStack(-1, 0, null) : item.getItemStack();

ServerboundSetCreativeModeSlotPacket creativePacket = new ServerboundSetCreativeModeSlotPacket((short)slot, itemStack);
session.sendDownstreamGamePacket(creativePacket);
}

private static void emulateCreativeQuickMove(GeyserSession session, Inventory inventory, IntSet affectedSlots, GeyserItemStack creativeItem) {
int firstEmptySlot = -1; // Leftover stack is stored here

for (int i = 0; i < 36; i++) {
int slot = i < 9 ? i + 36 : i; // First iterate hotbar, then inventory
GeyserItemStack slotItem = inventory.getItem(slot);

if (firstEmptySlot == -1 && slotItem.isEmpty()) {
firstEmptySlot = slot;
}

if (InventoryUtils.canStack(slotItem, creativeItem) && slotItem.capacity() > 0) {
creativeItem.sub(slotItem.add(creativeItem.getAmount())); // Transfer as much as possible without passing stack capacity
affectedSlots.add(slot);
if (creativeItem.isEmpty()) {
return;
}
}
}

if (firstEmptySlot != -1) {
inventory.setItem(firstEmptySlot, creativeItem, session);
affectedSlots.add(firstEmptySlot);
}
}

private static boolean isCraftingGrid(ItemStackRequestSlotData slotInfoData) {
return slotInfoData.getContainer() == ContainerSlotType.CRAFTING_INPUT;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ public final class ItemTranslator {
private ItemTranslator() {
}

public static ItemStack translateToJava(GeyserSession session, ItemData data) {
public static GeyserItemStack translateToJava(GeyserSession session, ItemData data) {
if (data == null) {
return new ItemStack(Items.AIR_ID);
return GeyserItemStack.EMPTY;
}

ItemMapping bedrockItem = session.getItemMappings().getMapping(data);
Expand All @@ -119,7 +119,7 @@ public static ItemStack translateToJava(GeyserSession session, ItemData data) {
itemStack.setComponents(components);
}
}
return itemStack.getItemStack();
return itemStack;
}

public static ItemData.@NonNull Builder translateToBedrock(GeyserSession session, int javaId, int count, DataComponents components) {
Expand Down

0 comments on commit 0553905

Please sign in to comment.