diff --git a/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java b/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java index 2147b070..24ec4a3c 100644 --- a/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/Inventory.java @@ -52,7 +52,6 @@ public class Inventory { @Setter protected String title; - @Getter @Setter protected ItemStack[] items; @@ -82,4 +81,10 @@ public class Inventory { public ItemStack getItem(int slot) { return items[slot]; } + + public void setItem(int slot, ItemStack item) { + if (item != null && (item.getId() == 0 || item.getAmount() < 1)) + item = null; + items[slot] = item; + } } diff --git a/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java b/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java index 4fd3bcd8..a11ce856 100644 --- a/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java +++ b/connector/src/main/java/org/geysermc/connector/inventory/PlayerInventory.java @@ -44,7 +44,7 @@ public class PlayerInventory extends Inventory { } public void setCursor(ItemStack stack) { - if (stack != null && stack.getId() == 0) + if (stack != null && (stack.getId() == 0 || stack.getAmount() < 1)) stack = null; cursor = stack; } diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index da8aaa20..1e410134 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -105,11 +105,9 @@ public class GeyserSession implements Player { @Setter private GameMode gameMode = GameMode.SURVIVAL; - @Getter @Setter - private int lastClickedSlot; + private int craftSlot = 0; - @Getter @Setter private int reopeningWindow = -1; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java index 535faf5c..ba7e3a97 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java @@ -99,6 +99,7 @@ public class TranslatorsInit { Registry.registerJava(ServerRespawnPacket.class, new JavaRespawnTranslator()); Registry.registerJava(ServerSpawnPositionPacket.class, new JavaSpawnPositionTranslator()); Registry.registerJava(ServerDifficultyPacket.class, new JavaDifficultyTranslator()); + Registry.registerJava(ServerDeclareRecipesPacket.class, new JavaDeclareRecipesTranslator()); Registry.registerJava(ServerEntityAnimationPacket.class, new JavaEntityAnimationTranslator()); Registry.registerJava(ServerEntityPositionPacket.class, new JavaEntityPositionTranslator()); @@ -181,7 +182,7 @@ public class TranslatorsInit { inventoryTranslators.put(WindowType.GENERIC_3X3, new BlockInventoryTranslator(9, 23 << 4, ContainerType.DISPENSER)); inventoryTranslators.put(WindowType.HOPPER, new BlockInventoryTranslator(5, 154 << 4, ContainerType.HOPPER)); - inventoryTranslators.put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(36, 205 << 4, ContainerType.CONTAINER)); + inventoryTranslators.put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, 205 << 4, ContainerType.CONTAINER)); //inventoryTranslators.put(WindowType.BEACON, new BlockInventoryTranslator(1, 138 << 4, ContainerType.BEACON)); //TODO } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java index d2f94164..27dd19ca 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java @@ -26,6 +26,7 @@ package org.geysermc.connector.network.translators.bedrock; import com.github.steveice10.mc.protocol.data.game.window.*; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientConfirmTransactionPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCreativeInventoryActionPacket; import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; @@ -49,27 +50,36 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.inventory.Inventory; +import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.TranslatorsInit; import org.geysermc.connector.network.translators.inventory.InventoryTranslator; -import org.geysermc.connector.utils.InventoryUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; public class BedrockInventoryTransactionTranslator extends PacketTranslator { + private final ItemStack refreshItem = new ItemStack(1, 127, new CompoundTag("")); @Override public void translate(InventoryTransactionPacket packet, GeyserSession session) { switch (packet.getTransactionType()) { case NORMAL: + for (InventoryAction action : packet.getActions()) { + if (action.getSource().getContainerId() == ContainerId.CRAFTING_USE_INGREDIENT || + action.getSource().getContainerId() == ContainerId.CRAFTING_RESULT) { + return; + } + } + Inventory inventory = session.getInventoryCache().getOpenInventory(); if (inventory == null) inventory = session.getInventory(); InventoryTranslator translator = TranslatorsInit.getInventoryTranslators().get(inventory.getWindowType()); + int craftSlot = session.getCraftSlot(); + session.setCraftSlot(0); + if (session.getGameMode() == GameMode.CREATIVE && inventory.getId() == 0) { ItemStack javaItem; for (InventoryAction action : packet.getActions()) { @@ -87,17 +97,18 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator actions = packet.getActions(); if (inventory.getWindowType() == WindowType.ANVIL) { InventoryAction anvilResult = null; @@ -132,7 +142,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator(2); for (InventoryAction action : packet.getActions()) { //packet sent by client when grabbing anvil output needs useless actions stripped - if (!(action.getSource().getContainerId() == ContainerId.CONTAINER_INPUT || action.getSource().getContainerId() == ContainerId.ANVIL_MATERIAL)) { + if (!(action.getSource().getContainerId() == ContainerId.CONTAINER_INPUT || + action.getSource().getContainerId() == ContainerId.ANVIL_MATERIAL)) { actions.add(action); } } @@ -153,7 +164,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator 1 ? ClickItemParam.LEFT_CLICK : ClickItemParam.RIGHT_CLICK); session.getDownstream().getSession().send(dropPacket); + ItemStack cursor = session.getInventory().getCursor(); + if (cursor != null) { + session.getInventory().setCursor(new ItemStack(cursor.getId(), dropAmount > 1 ? 0 : cursor.getAmount() - 1, cursor.getNbt())); + } return; } } @@ -199,43 +234,81 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator cursorAction.getFromItem().getCount()) { //fill stack - int javaSlot = session.getLastClickedSlot(); - ClientWindowActionPacket fillStackPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(), - javaSlot, null, WindowAction.FILL_STACK, FillStackParam.FILL); - session.getDownstream().getSession().send(fillStackPacket); - translator.updateInventory(session, inventory); //bedrock fill stack can sometimes differ from java version, refresh and let server change slots - return; - } else { - //left/right click - int javaSlot = translator.bedrockSlotToJava(containerAction); - boolean rightClick; - if (cursorAction.getFromItem().getCount() == 0) { //picking up item - rightClick = containerAction.getToItem().getCount() != 0; - } else { //releasing item - rightClick = cursorAction.getToItem().getCount() != 0 && cursorAction.getFromItem().getCount() - cursorAction.getToItem().getCount() == 1; + //left/right click + List plan = new ArrayList<>(); + ItemStack translatedCursor = TranslatorsInit.getItemTranslator().translateToJava(cursorAction.getFromItem()); + boolean refresh = !Objects.equals(session.getInventory().getCursor(), translatedCursor.getId() == 0 ? null : translatedCursor); //refresh slot if there is a cursor mismatch + int javaSlot = translator.bedrockSlotToJava(containerAction); + if (cursorAction.getFromItem().equals(containerAction.getToItem()) && + containerAction.getFromItem().equals(cursorAction.getToItem()) && + !canStack(cursorAction.getFromItem(), containerAction.getFromItem())) { //simple swap + Click.LEFT.onSlot(javaSlot, plan); + } else if (cursorAction.getFromItem().getCount() > cursorAction.getToItem().getCount()) { //release + if (cursorAction.getToItem().getCount() == 0) { + Click.LEFT.onSlot(javaSlot, plan); + } else { + int difference = cursorAction.getFromItem().getCount() - cursorAction.getToItem().getCount(); + for (int i = 0; i < difference; i++) { + Click.RIGHT.onSlot(javaSlot, plan); + } + } + } else { //pickup + if (cursorAction.getFromItem().getCount() == 0) { + if (containerAction.getToItem().getCount() == 0) { //pickup all + Click.LEFT.onSlot(javaSlot, plan); + } else { //pickup some + if (translator.isOutputSlot(javaSlot) || + containerAction.getToItem().getCount() == containerAction.getFromItem().getCount() / 2) { //right click + Click.RIGHT.onSlot(javaSlot, plan); + } else { + Click.LEFT.onSlot(javaSlot, plan); + int difference = containerAction.getFromItem().getCount() - cursorAction.getToItem().getCount(); + for (int i = 0; i < difference; i++) { + Click.RIGHT.onSlot(javaSlot, plan); + } + } + } + } else { //pickup into non-empty cursor + if (translator.isOutputSlot(javaSlot)) { + if (containerAction.getToItem().getCount() == 0) { + Click.LEFT.onSlot(javaSlot, plan); + } else { + ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(), + inventory.getTransactionId().getAndIncrement(), + javaSlot, refreshItem, WindowAction.SHIFT_CLICK_ITEM, + ShiftClickItemParam.LEFT_CLICK); + session.getDownstream().getSession().send(shiftClickPacket); + translator.updateInventory(session, inventory); + return; + } + } else if ((inventory.getId() == 0 || inventory.getWindowType() == WindowType.CRAFTING) && javaSlot == 0) { //crafting output + Click.LEFT.onSlot(javaSlot, plan); + } else { + int cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Collections.singletonList(javaSlot)); + if (cursorSlot != -1) { + Click.LEFT.onSlot(cursorSlot, plan); + } else { + translator.updateInventory(session, inventory); + return; + } + Click.LEFT.onSlot(javaSlot, plan); + int difference = cursorAction.getToItem().getCount() - cursorAction.getFromItem().getCount(); + for (int i = 0; i < difference; i++) { + Click.RIGHT.onSlot(cursorSlot, plan); + } + Click.LEFT.onSlot(javaSlot, plan); + Click.LEFT.onSlot(cursorSlot, plan); + } } - ItemStack translatedCursor = TranslatorsInit.getItemTranslator().translateToJava(cursorAction.getFromItem()); - boolean refresh = !Objects.equals(session.getInventory().getCursor(), translatedCursor.getId() == 0 ? null : translatedCursor); //refresh slot if there is a cursor mismatch - ClientWindowActionPacket clickPacket = new ClientWindowActionPacket(inventory.getId(), - inventory.getTransactionId().getAndIncrement(), javaSlot, - refresh ? new ItemStack(1, 127, new CompoundTag("")) : InventoryUtils.fixStack(TranslatorsInit.getItemTranslator().translateToJava(containerAction.getFromItem())), //send invalid item stack to refresh slot - WindowAction.CLICK_ITEM, rightClick ? ClickItemParam.RIGHT_CLICK : ClickItemParam.LEFT_CLICK); - session.getDownstream().getSession().send(clickPacket); - inventory.getItems()[javaSlot] = TranslatorsInit.getItemTranslator().translateToJava(containerAction.getToItem()); - translator.updateSlot(session, inventory, javaSlot); - session.getInventory().setCursor(TranslatorsInit.getItemTranslator().translateToJava(cursorAction.getToItem())); - session.setLastClickedSlot(javaSlot); - return; } + executePlan(session, inventory, translator, plan, refresh); + return; } } else { - //either moving 1 item or swapping 2 slots (touchscreen or one slot shift click) + List plan = new ArrayList<>(); InventoryAction fromAction; InventoryAction toAction; - //find source slot - if (actions.get(0).getFromItem().getCount() > actions.get(0).getToItem().getCount()) { + if (actions.get(0).getFromItem().getCount() >= actions.get(0).getToItem().getCount()) { fromAction = actions.get(0); toAction = actions.get(1); } else { @@ -245,120 +318,93 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator 2) { - //shift click or fill stack? - ItemData firstItem; - if (actions.get(0).getFromItem().getId() != 0) { - firstItem = actions.get(0).getFromItem(); - } else { - firstItem = actions.get(0).getToItem(); - } - List sourceActions = new ArrayList<>(actions.size()); - List destActions = new ArrayList<>(actions.size()); - boolean sameItems = true; - for (InventoryAction action : actions) { - if (action.getFromItem().getCount() > action.getToItem().getCount()) { - if (!InventoryUtils.canCombine(action.getFromItem(), firstItem)) - sameItems = false; - sourceActions.add(action); - } else { - if (!InventoryUtils.canCombine(action.getToItem(), firstItem)) - sameItems = false; - destActions.add(action); - } - } - if (sameItems) { - if (sourceActions.size() == 1) { //shift click - InventoryAction sourceAction = sourceActions.get(0); - //in java edition, shift clicked item must move across hotbar and main inventory - if (sourceAction.getSource().getContainerId() == ContainerId.INVENTORY) { - for (InventoryAction action : actions) { - if (action != sourceAction && action.getSource().getContainerId() == ContainerId.INVENTORY) { - if ((sourceAction.getSlot() < 9 && action.getSlot() < 9) || (sourceAction.getSlot() >= 9 && action.getSlot() >= 9)) { - //shift click not compatible with java edition. refresh inventory and abort - translator.updateInventory(session, inventory); - return; - } + if ((inventory.getId() == 0 || inventory.getWindowType() == WindowType.CRAFTING) && fromSlot == 0) { + if ((craftSlot != 0 && craftSlot != -2) && (inventory.getItem(toSlot) == null || + canStack(session.getInventory().getCursor(), inventory.getItem(toSlot)))) { + boolean refresh = false; + if (fromAction.getToItem().getCount() == 0) { + refresh = true; + Click.LEFT.onSlot(toSlot, plan); + if (craftSlot != -1) { + Click.LEFT.onSlot(craftSlot, plan); } + } else { + int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount(); + for (int i = 0; i < difference; i++) { + Click.RIGHT.onSlot(toSlot, plan); + } + session.setCraftSlot(craftSlot); } - } - int javaSlot = translator.bedrockSlotToJava(sourceAction); - ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(), - javaSlot, InventoryUtils.fixStack(inventory.getItem(javaSlot)), WindowAction.SHIFT_CLICK_ITEM, ShiftClickItemParam.LEFT_CLICK); - session.getDownstream().getSession().send(shiftClickPacket); - return; - } else if (destActions.size() == 1) { //fill stack - InventoryAction destAction = destActions.get(0); - int javaSlot; - if (destAction != cursorAction) { //if touchscreen - javaSlot = translator.bedrockSlotToJava(destAction); - ClientWindowActionPacket leftClickPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(), - javaSlot, InventoryUtils.fixStack(inventory.getItem(javaSlot)), WindowAction.CLICK_ITEM, ClickItemParam.LEFT_CLICK); - session.getDownstream().getSession().send(leftClickPacket); + executePlan(session, inventory, translator, plan, refresh); + return; } else { - javaSlot = session.getLastClickedSlot(); + session.setCraftSlot(-2); } - ClientWindowActionPacket fillStackPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(), - javaSlot, null, WindowAction.FILL_STACK, FillStackParam.FILL); - session.getDownstream().getSession().send(fillStackPacket); - if (destAction != cursorAction) { //if touchscreen - ClientWindowActionPacket leftClickPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(), - javaSlot, null, WindowAction.CLICK_ITEM, ClickItemParam.LEFT_CLICK); - session.getDownstream().getSession().send(leftClickPacket); - inventory.getItems()[javaSlot] = TranslatorsInit.getItemTranslator().translateToJava(destAction.getToItem()); - } - translator.updateInventory(session, inventory); - return; } + + int cursorSlot = -1; + if (session.getInventory().getCursor() != null) { //move cursor contents to a temporary slot + cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Arrays.asList(fromSlot, toSlot)); + if (cursorSlot != -1) { + Click.LEFT.onSlot(cursorSlot, plan); + } else { + translator.updateInventory(session, inventory); + return; + } + } + if ((fromAction.getFromItem().equals(toAction.getToItem()) && !canStack(fromAction.getFromItem(), toAction.getFromItem())) || fromAction.getToItem().getId() == 0) { //slot swap + Click.LEFT.onSlot(fromSlot, plan); + Click.LEFT.onSlot(toSlot, plan); + if (fromAction.getToItem().getId() != 0) { + Click.LEFT.onSlot(fromSlot, plan); + } + } else if (canStack(fromAction.getFromItem(), toAction.getToItem())) { //partial item move + if (translator.isOutputSlot(fromSlot)) { + ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(), + inventory.getTransactionId().getAndIncrement(), + fromSlot, refreshItem, WindowAction.SHIFT_CLICK_ITEM, + ShiftClickItemParam.LEFT_CLICK); + session.getDownstream().getSession().send(shiftClickPacket); + translator.updateInventory(session, inventory); + return; + } else if ((inventory.getId() == 0 || inventory.getWindowType() == WindowType.CRAFTING) && fromSlot == 0) { + session.setCraftSlot(cursorSlot); + Click.LEFT.onSlot(fromSlot, plan); + int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount(); + for (int i = 0; i < difference; i++) { + Click.RIGHT.onSlot(toSlot, plan); + } + //client will send additional packets later to finish transferring crafting output + //translator will know how to handle this using the craftSlot variable + } else { + Click.LEFT.onSlot(fromSlot, plan); + int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount(); + for (int i = 0; i < difference; i++) { + Click.RIGHT.onSlot(toSlot, plan); + } + Click.LEFT.onSlot(fromSlot, plan); + } + } + if (cursorSlot != -1) { + Click.LEFT.onSlot(cursorSlot, plan); + } + executePlan(session, inventory, translator, plan, false); + return; } } - - //refresh inventory, transaction was not translated translator.updateInventory(session, inventory); break; case INVENTORY_MISMATCH: InventorySlotPacket cursorPacket = new InventorySlotPacket(); cursorPacket.setContainerId(ContainerId.CURSOR); cursorPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(session.getInventory().getCursor())); - session.getUpstream().sendPacket(cursorPacket); + //session.getUpstream().sendPacket(cursorPacket); Inventory inv = session.getInventoryCache().getOpenInventory(); if (inv == null) inv = session.getInventory(); TranslatorsInit.getInventoryTranslators().get(inv.getWindowType()).updateInventory(session, inv); + break; case ITEM_USE: if (packet.getActionType() == 1) { ClientPlayerUseItemPacket useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND); @@ -389,4 +435,134 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator slotBlacklist) { + /*try and find a slot that can temporarily store the given item + only look in the main inventory and hotbar + only slots that are empty or contain a different type of item are valid*/ + int offset = inventory.getId() == 0 ? 1 : 0; //offhand is not a viable slot (some servers disable it) + List itemBlacklist = new ArrayList<>(slotBlacklist.size() + 1); + itemBlacklist.add(item); + for (int slot : slotBlacklist) { + ItemStack blacklistItem = inventory.getItem(slot); + if (blacklistItem != null) + itemBlacklist.add(blacklistItem); + } + for (int i = inventory.getSize() - (36 + offset); i < inventory.getSize() - offset; i++) { + ItemStack testItem = inventory.getItem(i); + boolean acceptable = true; + if (testItem != null) { + for (ItemStack blacklistItem : itemBlacklist) { + if (canStack(testItem, blacklistItem)) { + acceptable = false; + break; + } + } + } + if (acceptable && !slotBlacklist.contains(i)) + return i; + } + //could not find a viable temp slot + return -1; + } + + //NPE if compound tag is null + private ItemStack fixStack(ItemStack stack) { + if (stack == null || stack.getId() == 0) + return null; + return new ItemStack(stack.getId(), stack.getAmount(), stack.getNbt() == null ? new CompoundTag("") : stack.getNbt()); + } + + private boolean canStack(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) + return false; + return item1.getId() == item2.getId() && item1.getNbt() == item2.getNbt(); + } + + private boolean canStack(ItemData item1, ItemData item2) { + if (item1 == null || item2 == null) + return false; + return item1.equals(item2, false, true, true); + } + + private void executePlan(GeyserSession session, Inventory inventory, InventoryTranslator translator, List plan, boolean refresh) { + PlayerInventory playerInventory = session.getInventory(); + ListIterator planIter = plan.listIterator(); + while (planIter.hasNext()) { + ClickAction action = planIter.next(); + ItemStack cursorItem = playerInventory.getCursor(); + ItemStack clickedItem = inventory.getItem(action.slot); + short actionId = (short) inventory.getTransactionId().getAndIncrement(); + boolean craftingOutput = (inventory.getId() == 0 || inventory.getWindowType() == WindowType.CRAFTING) && action.slot == 0; + if (craftingOutput) + refresh = true; + ClientWindowActionPacket clickPacket = new ClientWindowActionPacket(inventory.getId(), + actionId, action.slot, !planIter.hasNext() && refresh ? refreshItem : fixStack(clickedItem), + WindowAction.CLICK_ITEM, action.click.actionParam); + if (craftingOutput) { //crafting output + if (cursorItem == null && clickedItem != null) { + playerInventory.setCursor(clickedItem); + } else if (canStack(cursorItem, clickedItem)) { + playerInventory.setCursor(new ItemStack(cursorItem.getId(), + cursorItem.getAmount() + clickedItem.getAmount(), cursorItem.getNbt())); + } + } else { + switch (action.click) { + case LEFT: + if (!canStack(cursorItem, clickedItem)) { + playerInventory.setCursor(clickedItem); + inventory.setItem(action.slot, cursorItem); + } else { + playerInventory.setCursor(null); + inventory.setItem(action.slot, new ItemStack(clickedItem.getId(), + clickedItem.getAmount() + cursorItem.getAmount(), clickedItem.getNbt())); + } + break; + case RIGHT: + if (cursorItem == null && clickedItem != null) { + ItemStack halfItem = new ItemStack(clickedItem.getId(), + clickedItem.getAmount() / 2, clickedItem.getNbt()); + inventory.setItem(action.slot, halfItem); + playerInventory.setCursor(new ItemStack(clickedItem.getId(), + clickedItem.getAmount() - halfItem.getAmount(), clickedItem.getNbt())); + } else if (cursorItem != null && clickedItem == null) { + playerInventory.setCursor(new ItemStack(cursorItem.getId(), + cursorItem.getAmount() - 1, cursorItem.getNbt())); + inventory.setItem(action.slot, new ItemStack(cursorItem.getId(), + 1, cursorItem.getNbt())); + } else if (canStack(cursorItem, clickedItem)) { + playerInventory.setCursor(new ItemStack(cursorItem.getId(), + cursorItem.getAmount() - 1, cursorItem.getNbt())); + inventory.setItem(action.slot, new ItemStack(clickedItem.getId(), + clickedItem.getAmount() + 1, clickedItem.getNbt())); + } + break; + } + } + session.getDownstream().getSession().send(clickPacket); + session.getDownstream().getSession().send(new ClientConfirmTransactionPacket(inventory.getId(), actionId, true)); + } + } + + private enum Click { + LEFT(ClickItemParam.LEFT_CLICK), + RIGHT(ClickItemParam.RIGHT_CLICK); + + final WindowActionParam actionParam; + Click(WindowActionParam actionParam) { + this.actionParam = actionParam; + } + void onSlot(int slot, List plan) { + plan.add(new ClickAction(slot, this)); + } + } + + private static class ClickAction { + final int slot; + final Click click; + ClickAction(int slot, Click click) { + this.slot = slot; + this.click = click; + } + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java index 9c027dc2..53750130 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java @@ -54,7 +54,7 @@ public class AnvilInventoryTranslator extends BlockInventoryTranslator { } @Override - public boolean isOutputSlot(InventoryAction action) { - return action.getSource().getContainerId() == ContainerId.ANVIL_RESULT; + public boolean isOutputSlot(int slot) { + return slot == 2; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java index 5c3ae3c8..22d73b9b 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java @@ -43,7 +43,7 @@ public abstract class ContainerInventoryTranslator extends InventoryTranslator { public void updateInventory(GeyserSession session, Inventory inventory) { ItemData[] bedrockItems = new ItemData[this.size]; for (int i = 0; i < bedrockItems.length; i++) { - bedrockItems[javaSlotToBedrock(i)] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); + bedrockItems[javaSlotToBedrock(i)] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)); } InventoryContentPacket contentPacket = new InventoryContentPacket(); contentPacket.setContainerId(inventory.getId()); @@ -52,7 +52,7 @@ public abstract class ContainerInventoryTranslator extends InventoryTranslator { Inventory playerInventory = session.getInventory(); for (int i = 0; i < 36; i++) { - playerInventory.getItems()[i + 9] = inventory.getItems()[i + this.size]; + playerInventory.setItem(i + 9, inventory.getItem(i + this.size)); } TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateInventory(session, playerInventory); } @@ -61,13 +61,13 @@ public abstract class ContainerInventoryTranslator extends InventoryTranslator { public void updateSlot(GeyserSession session, Inventory inventory, int slot) { if (slot >= this.size) { Inventory playerInventory = session.getInventory(); - playerInventory.getItems()[(slot + 9) - this.size] = inventory.getItem(slot); + playerInventory.setItem((slot + 9) - this.size, inventory.getItem(slot)); TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateSlot(session, playerInventory, (slot + 9) - this.size); } else { InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(inventory.getId()); slotPacket.setInventorySlot(javaSlotToBedrock(slot)); - slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[slot])); + slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(slot))); session.getUpstream().sendPacket(slotPacket); } } @@ -97,7 +97,7 @@ public abstract class ContainerInventoryTranslator extends InventoryTranslator { } @Override - public boolean isOutputSlot(InventoryAction action) { + public boolean isOutputSlot(int slot) { return false; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java index e5f614f4..911fddc3 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java @@ -1,7 +1,9 @@ package org.geysermc.connector.network.translators.inventory; import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.protocol.bedrock.data.ContainerId; import com.nukkitx.protocol.bedrock.data.ContainerType; +import com.nukkitx.protocol.bedrock.data.InventoryAction; import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; @@ -30,4 +32,24 @@ public class CraftingTableInventoryTranslator extends ContainerInventoryTranslat public void closeInventory(GeyserSession session, Inventory inventory) { } + + @Override + public int bedrockSlotToJava(InventoryAction action) { + int slotnum = action.getSlot(); + if (action.getSource().getContainerId() == ContainerId.INVENTORY) { + //hotbar + if (slotnum >= 9) { + return slotnum + this.size - 9; + } else { + return slotnum + this.size + 27; + } + } else { + if (slotnum >= 32 && 42 >= slotnum) { + return slotnum - 31; + } else if (slotnum == 50) { + return 0; + } + return slotnum; + } + } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java index 4b90ba07..6c5057bb 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java @@ -118,7 +118,7 @@ public class DoubleChestInventoryTranslator extends BlockInventoryTranslator { ItemData[] bedrockItems = new ItemData[54]; for (int i = 0; i < bedrockItems.length; i++) { if (i <= this.size) { - bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); + bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)); } else { bedrockItems[i] = ItemData.AIR; } @@ -130,7 +130,7 @@ public class DoubleChestInventoryTranslator extends BlockInventoryTranslator { Inventory playerInventory = session.getInventory(); for (int i = 0; i < 36; i++) { - playerInventory.getItems()[i + 9] = inventory.getItems()[i + this.size]; + playerInventory.setItem(i + 9, inventory.getItem(i + this.size)); } TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateInventory(session, playerInventory); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java index c5296d37..9c274c87 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java @@ -59,7 +59,7 @@ public class FurnaceInventoryTranslator extends BlockInventoryTranslator { } @Override - public boolean isOutputSlot(InventoryAction action) { - return action.getSlot() == 2; + public boolean isOutputSlot(int slot) { + return slot == 2; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java index 7181a345..edc7f495 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java @@ -44,5 +44,5 @@ public abstract class InventoryTranslator { public abstract void updateSlot(GeyserSession session, Inventory inventory, int slot); public abstract int bedrockSlotToJava(InventoryAction action); public abstract int javaSlotToBedrock(int slot); - public abstract boolean isOutputSlot(InventoryAction action); + public abstract boolean isOutputSlot(int slot); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java index cfd92699..2608fd6c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java @@ -45,12 +45,12 @@ public class PlayerInventoryTranslator extends InventoryTranslator { ItemData[] contents = new ItemData[36]; // Inventory for (int i = 9; i < 36; i++) { - contents[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); + contents[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)); } // Hotbar for (int i = 36; i < 45; i++) { - contents[i - 36] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); + contents[i - 36] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)); } inventoryContentPacket.setContents(contents); @@ -61,7 +61,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator { armorContentPacket.setContainerId(ContainerId.ARMOR); contents = new ItemData[4]; for (int i = 5; i < 9; i++) { - contents[i - 5] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); + contents[i - 5] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)); } armorContentPacket.setContents(contents); session.getUpstream().sendPacket(armorContentPacket); @@ -124,6 +124,12 @@ public class PlayerInventoryTranslator extends InventoryTranslator { case ContainerId.CRAFTING_ADD_INGREDIENT: case ContainerId.CRAFTING_REMOVE_INGREDIENT: return slotnum + 1; + case ContainerId.CURSOR: + if (slotnum >= 28 && 31 >= slotnum) { + return slotnum - 27; + } else if (slotnum == 50) { + return 0; + } } return slotnum; } @@ -134,7 +140,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator { } @Override - public boolean isOutputSlot(InventoryAction action) { + public boolean isOutputSlot(int slot) { return false; } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java index 43ceb283..3b2c70d9 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java @@ -43,7 +43,7 @@ public class SingleChestInventoryTranslator extends BlockInventoryTranslator { ItemData[] bedrockItems = new ItemData[27]; for (int i = 0; i < bedrockItems.length; i++) { if (i <= this.size) { - bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItems()[i]); + bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)); } else { bedrockItems[i] = ItemData.AIR; } @@ -55,7 +55,7 @@ public class SingleChestInventoryTranslator extends BlockInventoryTranslator { Inventory playerInventory = session.getInventory(); for (int i = 0; i < 36; i++) { - playerInventory.getItems()[i + 9] = inventory.getItems()[i + this.size]; + playerInventory.setItem(i + 9, inventory.getItem(i + this.size)); } TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateInventory(session, playerInventory); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java new file mode 100644 index 00000000..afa4836c --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.connector.network.translators.java; + +import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient; +import com.github.steveice10.mc.protocol.data.game.recipe.Recipe; +import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData; +import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerDeclareRecipesPacket; +import com.nukkitx.nbt.tag.CompoundTag; +import com.nukkitx.protocol.bedrock.data.CraftingData; +import com.nukkitx.protocol.bedrock.data.ItemData; +import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.TranslatorsInit; + +import java.util.*; +import java.util.stream.Collectors; + +public class JavaDeclareRecipesTranslator extends PacketTranslator { + + @Override + public void translate(ServerDeclareRecipesPacket packet, GeyserSession session) { + CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); + craftingDataPacket.setCleanRecipes(true); + for (Recipe recipe : packet.getRecipes()) { + switch (recipe.getType()) { + case CRAFTING_SHAPELESS: { + ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData(); + ItemData output = TranslatorsInit.getItemTranslator().translateToBedrock(shapelessRecipeData.getResult()); + List inputList = combinations(shapelessRecipeData.getIngredients()); + for (ItemData[] inputs : inputList) { + UUID uuid = UUID.randomUUID(); + craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(), + inputs, new ItemData[]{output}, uuid, "crafting_table", 0)); + } + break; + } + case CRAFTING_SHAPED: { + ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData(); + ItemData output = TranslatorsInit.getItemTranslator().translateToBedrock(shapedRecipeData.getResult()); + List inputList = combinations(shapedRecipeData.getIngredients()); + for (ItemData[] inputs : inputList) { + UUID uuid = UUID.randomUUID(); + craftingDataPacket.getCraftingData().add(CraftingData.fromShaped(uuid.toString(), + shapedRecipeData.getWidth(), shapedRecipeData.getHeight(), inputs, + new ItemData[]{output}, uuid, "crafting_table", 0)); + } + break; + } + } + } + session.getUpstream().sendPacket(craftingDataPacket); + } + + private List combinations(Ingredient[] ingredients) { + ItemData[][] squashed = new ItemData[ingredients.length][]; + for (int i = 0; i < ingredients.length; i++) { + if (ingredients[i].getOptions().length == 0) { + squashed[i] = new ItemData[]{ItemData.AIR}; + continue; + } + Ingredient ingredient = ingredients[i]; + Map> groupedByIds = Arrays.stream(ingredient.getOptions()) + .map(item -> TranslatorsInit.getItemTranslator().translateToBedrock(item)) + .collect(Collectors.groupingBy(item -> new GroupedItem(item.getId(), item.getCount(), item.getTag()))); + squashed[i] = new ItemData[groupedByIds.size()]; + int index = 0; + for (Map.Entry> entry : groupedByIds.entrySet()) { + if (entry.getValue().size() > 1) { + GroupedItem groupedItem = entry.getKey(); + squashed[i][index++] = ItemData.of(groupedItem.id, (short) -1, groupedItem.count, groupedItem.tag); + } else { + ItemData item = entry.getValue().get(0); + squashed[i][index++] = item; + } + } + } + int[] sizeArray = new int[squashed.length]; + int[] counterArray = new int[squashed.length]; + int totalCombinationCount = 1; + for(int i = 0; i < squashed.length; i++) { + sizeArray[i] = squashed[i].length; + totalCombinationCount *= squashed[i].length; + } + if (totalCombinationCount > 10000) { + ItemData[] translatedItems = new ItemData[ingredients.length]; + for (int i = 0; i < ingredients.length; i++) { + if (ingredients[i].getOptions().length > 0) { + translatedItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(ingredients[i].getOptions()[0]); + } else { + translatedItems[i] = ItemData.AIR; + } + } + return Collections.singletonList(translatedItems); + } + List combinationList = new ArrayList<>(totalCombinationCount); + for (int countdown = totalCombinationCount; countdown > 0; --countdown) { + ItemData[] translatedItems = new ItemData[squashed.length]; + for(int i = 0; i < squashed.length; ++i) { + if (squashed[i].length > 0) + translatedItems[i] = squashed[i][counterArray[i]]; + } + combinationList.add(translatedItems); + for(int incIndex = squashed.length - 1; incIndex >= 0; --incIndex) { + if(counterArray[incIndex] + 1 < sizeArray[incIndex]) { + ++counterArray[incIndex]; + break; + } + counterArray[incIndex] = 0; + } + } + return combinationList; + } + + @EqualsAndHashCode + @AllArgsConstructor + private static class GroupedItem { + int id; + int count; + CompoundTag tag; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java index 562eb193..d658535f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java @@ -47,9 +47,12 @@ public class JavaSetSlotTranslator extends PacketTranslator if (packet.getWindowId() == 255 && packet.getSlot() == -1) { //cursor if (Objects.equals(session.getInventory().getCursor(), packet.getItem())) return; + if (session.getCraftSlot() != 0) + return; //bedrock client is bugged when changing the cursor. reopen inventory after changing it - if (packet.getItem() == null && session.getInventory().getCursor() != null) { + //TODO: fix this. too buggy rn + /*if (packet.getItem() == null && session.getInventory().getCursor() != null) { InventorySlotPacket cursorPacket = new InventorySlotPacket(); cursorPacket.setContainerId(ContainerId.CURSOR); cursorPacket.setSlot(ItemData.AIR); @@ -64,7 +67,7 @@ public class JavaSetSlotTranslator extends PacketTranslator ContainerClosePacket closePacket = new ContainerClosePacket(); closePacket.setWindowId((byte) inventory.getId()); Geyser.getGeneralThreadPool().schedule(() -> session.getUpstream().sendPacket(closePacket), 150, TimeUnit.MILLISECONDS); - } + }*/ session.getInventory().setCursor(packet.getItem()); return; @@ -76,7 +79,7 @@ public class JavaSetSlotTranslator extends PacketTranslator InventoryTranslator translator = TranslatorsInit.getInventoryTranslators().get(inventory.getWindowType()); if (translator != null) { - inventory.getItems()[packet.getSlot()] = packet.getItem(); + inventory.setItem(packet.getSlot(), packet.getItem()); translator.updateSlot(session, inventory, packet.getSlot()); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java index 86c38fe9..77c69eed 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java @@ -44,14 +44,13 @@ public class InventoryUtils { session.getInventoryCache().uncacheInventory(windowId); session.getInventoryCache().setOpenInventory(null); } + } else { + Inventory inventory = session.getInventory(); + InventoryTranslator translator = TranslatorsInit.getInventoryTranslators().get(inventory.getWindowType()); + translator.updateInventory(session, inventory); } - } - - //currently, ItemStack.equals() does not check the item id - public static boolean canCombine(ItemData stack1, ItemData stack2) { - if (stack1 == null || stack2 == null) - return false; - return stack1.getId() == stack2.getId() && stack1.equals(stack2, false, true, true); + session.setCraftSlot(0); + session.getInventory().setCursor(null); } //NPE if nbt tag is null