From eb3d87b8aa0279784f8cf700924f124be2cd6a66 Mon Sep 17 00:00:00 2001 From: MC_XiaoHei Date: Sat, 26 Jul 2025 10:15:40 +0800 Subject: [PATCH] feat: finish MOVE_ITEM_NEW_PACKET, fix rei protocol packet transformer (#615) --- .../protocol/rei/PacketTransformer.java | 1 + .../protocol/rei/REIServerProtocol.java | 100 ++++- .../rei/transfer/InputSlotCrafter.java | 176 ++++++++ .../rei/transfer/ItemRecipeFinder.java | 126 ++++++ .../rei/transfer/NewInputSlotCrafter.java | 67 +++ .../protocol/rei/transfer/RecipeFinder.java | 409 ++++++++++++++++++ .../slot/PlayerInventorySlotAccessor.java | 56 +++ .../rei/transfer/slot/SlotAccessor.java | 43 ++ .../transfer/slot/VanillaSlotAccessor.java | 65 +++ 9 files changed, 1041 insertions(+), 2 deletions(-) create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java index fabd1f9e..7060e23b 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java @@ -37,6 +37,7 @@ public class PacketTransformer { public void inbound(ResourceLocation id, RegistryFriendlyByteBuf buf, ServerPlayer player, BiConsumer consumer) { UUID key = player.getUUID(); PartData data; + buf.readVarInt(); switch (buf.readByte()) { case START -> { int partsNum = buf.readInt(); diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java index 8c32ecf3..edc91a16 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java @@ -5,6 +5,11 @@ import com.google.common.collect.ImmutableMap; import io.netty.buffer.Unpooled; import net.minecraft.ChatFormatting; import net.minecraft.Util; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; @@ -46,8 +51,15 @@ import org.leavesmc.leaves.protocol.rei.display.SmeltingDisplay; import org.leavesmc.leaves.protocol.rei.display.SmokingDisplay; import org.leavesmc.leaves.protocol.rei.display.StoneCuttingDisplay; import org.leavesmc.leaves.protocol.rei.payload.DisplaySyncPayload; +import org.leavesmc.leaves.protocol.rei.transfer.InputSlotCrafter; +import org.leavesmc.leaves.protocol.rei.transfer.NewInputSlotCrafter; +import org.leavesmc.leaves.protocol.rei.transfer.slot.PlayerInventorySlotAccessor; +import org.leavesmc.leaves.protocol.rei.transfer.slot.SlotAccessor; +import org.leavesmc.leaves.protocol.rei.transfer.slot.VanillaSlotAccessor; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; @@ -63,9 +75,11 @@ public class REIServerProtocol implements LeavesProtocol { public static final String CHEAT_PERMISSION = "leaves.protocol.rei.cheat"; public static final ResourceLocation DELETE_ITEMS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "delete_item"); public static final ResourceLocation CREATE_ITEMS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item"); - public static final ResourceLocation CREATE_ITEMS_GRAB_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item_grab"); public static final ResourceLocation CREATE_ITEMS_HOTBAR_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item_hotbar"); + public static final ResourceLocation CREATE_ITEMS_GRAB_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item_grab"); public static final ResourceLocation CREATE_ITEMS_MESSAGE_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "ci_msg"); + public static final ResourceLocation MOVE_ITEMS_NEW_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "move_items_new"); + public static final ResourceLocation NOT_ENOUGH_ITEMS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "og_not_enough"); // this pack is under to-do at rei-client, so we don't handle it public static final ResourceLocation SYNC_DISPLAYS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "sync_displays"); public static final Map TRANSFORMERS = Util.make(() -> { @@ -75,6 +89,7 @@ public class REIServerProtocol implements LeavesProtocol { builder.put(CREATE_ITEMS_PACKET, new PacketTransformer()); builder.put(CREATE_ITEMS_GRAB_PACKET, new PacketTransformer()); builder.put(CREATE_ITEMS_HOTBAR_PACKET, new PacketTransformer()); + builder.put(MOVE_ITEMS_NEW_PACKET, new PacketTransformer()); return builder.build(); }); private static final Set enabledPlayers = new HashSet<>(); @@ -294,7 +309,48 @@ public class REIServerProtocol implements LeavesProtocol { @ProtocolHandler.BytebufReceiver(key = "move_items_new") public static void handleMoveItem(ServerPlayer player, RegistryFriendlyByteBuf buf) { - // TODO handle to disable REI client warning + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + AbstractContainerMenu container = player.containerMenu; + tmpBuf.readResourceLocation(); + try { + boolean shift = tmpBuf.readBoolean(); + try { + CompoundTag nbt = tmpBuf.readNbt(); + if (nbt == null) { + throw new IllegalStateException("NBT data is null"); + } + int version = nbt.getInt("Version").orElse(-1); + if (version != 1) { + throw new IllegalStateException("Server and client REI protocol version mismatch! Server: 1, Client: " + version); + } + + List> recipes = readInputs(player.registryAccess(), nbt.getListOrEmpty("Inputs")); + List input = readSlots(container, player, nbt.getListOrEmpty("InputSlots")); + List inventory = readSlots(container, player, nbt.getListOrEmpty("InventorySlots")); + NewInputSlotCrafter crafter = new NewInputSlotCrafter<>(container, input, inventory, recipes); + Bukkit.getScheduler().runTask(MinecraftInternalPlugin.INSTANCE, () -> { + try { + crafter.fillInputSlots(player, shift); + } catch (InputSlotCrafter.NotEnoughMaterialsException ignored1) { + } catch (IllegalStateException e) { + player.sendSystemMessage(Component.translatable(e.getMessage()).withStyle(ChatFormatting.RED)); + } catch (Exception e) { + player.sendSystemMessage(Component.translatable("error.rei.internal.error", e.getMessage()).withStyle(ChatFormatting.RED)); + e.printStackTrace(); + } + }); + } catch (IllegalStateException e) { + player.sendSystemMessage(Component.translatable(e.getMessage()).withStyle(ChatFormatting.RED)); + } catch (Exception e) { + player.sendSystemMessage(Component.translatable("error.rei.internal.error", e.getMessage()).withStyle(ChatFormatting.RED)); + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } + }; + inboundTransform(player, MOVE_ITEMS_NEW_PACKET, buf, consumer); } private static void inboundTransform(ServerPlayer player, ResourceLocation id, RegistryFriendlyByteBuf buf, BiConsumer consumer) { @@ -332,4 +388,44 @@ public class REIServerProtocol implements LeavesProtocol { public int tickerInterval(String tickerID) { return 200; } + + private static List> readInputs(RegistryAccess registryAccess, ListTag tag) { + List> items = new ArrayList<>(); + for (Tag t : tag) { + CompoundTag compoundTag = (CompoundTag) t; + compoundTag.getInt("Index").orElseThrow(); + ListTag ingredientList = compoundTag.getListOrEmpty("Ingredient"); + List slotItems = new ArrayList<>(); + for (Tag ingredient : ingredientList) { + CompoundTag ingredientTag = (CompoundTag) ingredient; + ItemStack stack = ItemStack.OPTIONAL_CODEC.parse( + registryAccess.createSerializationContext(NbtOps.INSTANCE), + ingredientTag.get("value") + ).getOrThrow(); + slotItems.add(stack); + } + items.add(slotItems); + } + return items; + } + + private static List readSlots(AbstractContainerMenu menu, ServerPlayer player, ListTag tag) { + List slots = new ArrayList<>(); + for (Tag t : tag) { + CompoundTag compoundTag = (CompoundTag) t; + String id = compoundTag.getString("id").orElseThrow(); + if (!id.startsWith(PROTOCOL_ID + ":")) { + throw new IllegalStateException("Invalid slot id: " + id + ", expected to start with '" + PROTOCOL_ID + ":'"); + } + id = id.substring((PROTOCOL_ID + ":").length()); + int slot = compoundTag.getInt("Slot").orElseThrow(); + SlotAccessor accessor = switch (id) { + case "vanilla" -> new VanillaSlotAccessor(menu.slots.get(slot)); + case "player" -> new PlayerInventorySlotAccessor(player, slot); + default -> throw new IllegalStateException("Unknown container id: " + id); + }; + slots.add(accessor); + } + return slots; + } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java new file mode 100644 index 00000000..b2fab13a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java @@ -0,0 +1,176 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * 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. + */ + +package org.leavesmc.leaves.protocol.rei.transfer; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.rei.transfer.slot.SlotAccessor; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public abstract class InputSlotCrafter { + protected T container; + private Iterable inputStacks; + private Iterable inventoryStacks; + protected ServerPlayer player; + + protected InputSlotCrafter(T container) { + this.container = container; + } + + public void fillInputSlots(ServerPlayer player, boolean hasShift) { + this.player = player; + this.inventoryStacks = this.getInventorySlots(); + this.inputStacks = this.getInputSlots(); + + // Return the already placed items on the grid + this.cleanInputs(); + + ItemRecipeFinder recipeFinder = new ItemRecipeFinder(); + this.populateRecipeFinder(recipeFinder); + List> ingredients = new ArrayList<>(this.getInputs()); + + if (recipeFinder.findRecipe(ingredients, 1, null)) { + this.fillInputSlots(recipeFinder, ingredients, hasShift); + } else { + this.cleanInputs(); + this.markDirty(); + throw new NotEnoughMaterialsException(); + } + + this.markDirty(); + } + + protected abstract Iterable getInputSlots(); + + protected abstract Iterable getInventorySlots(); + + protected abstract List> getInputs(); + + protected abstract void populateRecipeFinder(ItemRecipeFinder recipeFinder); + + protected abstract void markDirty(); + + public void alignRecipeToGrid(Iterable inputStacks, Iterator recipeItems, int craftsAmount) { + for (SlotAccessor inputStack : inputStacks) { + if (!recipeItems.hasNext()) { + return; + } + + this.acceptAlignedInput(recipeItems.next(), inputStack, craftsAmount); + } + } + + public void acceptAlignedInput(ItemStack toBeTakenStack, SlotAccessor inputStack, int craftsAmount) { + if (!toBeTakenStack.isEmpty()) { + for (int i = 0; i < craftsAmount; ++i) { + this.fillInputSlot(inputStack, toBeTakenStack); + } + } + } + + protected void fillInputSlot(SlotAccessor slot, ItemStack toBeTakenStack) { + SlotAccessor takenSlot = this.takeInventoryStack(toBeTakenStack); + if (takenSlot != null) { + ItemStack takenStack = takenSlot.getItemStack().copy(); + if (!takenStack.isEmpty()) { + if (takenStack.getCount() > 1) { + takenSlot.takeStack(1); + } else { + takenSlot.setItemStack(ItemStack.EMPTY); + } + + takenStack.setCount(1); + if (!slot.canPlace(takenStack)) { + return; + } + + if (slot.getItemStack().isEmpty()) { + slot.setItemStack(takenStack); + } else { + slot.getItemStack().grow(1); + } + } + } + } + + protected void fillInputSlots(ItemRecipeFinder recipeFinder, List> ingredients, boolean hasShift) { + int recipeCrafts = recipeFinder.countRecipeCrafts(ingredients, Integer.MAX_VALUE, null); + int amountToFill = hasShift ? recipeCrafts : 1; + List recipeItems = new ArrayList<>(); + if (recipeFinder.findRecipe(ingredients, amountToFill, recipeItems::add)) { + int finalCraftsAmount = amountToFill; + + for (ItemStack itemId : recipeItems) { + // Fix issue with empty item id (grid slot) [shift-click issue] + if (itemId.isEmpty()) { + continue; + } + finalCraftsAmount = Math.min(finalCraftsAmount, itemId.getMaxStackSize()); + } + + recipeItems.clear(); + + if (recipeFinder.findRecipe(ingredients, finalCraftsAmount, recipeItems::add)) { + this.cleanInputs(); + this.alignRecipeToGrid(inputStacks, recipeItems.iterator(), finalCraftsAmount); + } + } + } + + protected abstract void cleanInputs(); + + @Nullable + public SlotAccessor takeInventoryStack(ItemStack itemStack) { + boolean rejectedModification = false; + for (SlotAccessor inventoryStack : inventoryStacks) { + ItemStack itemStack1 = inventoryStack.getItemStack(); + if (!itemStack1.isEmpty() && areItemsEqual(itemStack, itemStack1) && !itemStack1.isDamaged() && !itemStack1.isEnchanted() && !itemStack1.has(DataComponents.CUSTOM_NAME)) { + if (!inventoryStack.allowModification(player)) { + rejectedModification = true; + } else { + return inventoryStack; + } + } + } + + if (rejectedModification) { + throw new IllegalStateException("Unable to take item from inventory due to slot not allowing modification! Item requested: " + itemStack); + } + + return null; + } + + private static boolean areItemsEqual(ItemStack stack1, ItemStack stack2) { + return ItemStack.isSameItemSameComponents(stack1, stack2); + } + + public static class NotEnoughMaterialsException extends RuntimeException { + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java new file mode 100644 index 00000000..d6f5a4cc --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java @@ -0,0 +1,126 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * 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. + */ + +package org.leavesmc.leaves.protocol.rei.transfer; + +import com.google.common.collect.Interner; +import com.google.common.collect.Interners; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class ItemRecipeFinder { + private final Interner keys = Interners.newWeakInterner(); + private final RecipeFinder finder = new RecipeFinder<>(); + + public boolean contains(ItemStack item) { + return finder.contains(ofKey(item)); + } + + public void take(ItemStack item, int amount) { + finder.take(ofKey(item), amount); + } + + public void put(ItemStack item, int amount) { + finder.put(ofKey(item), amount); + } + + public void addNormalItem(ItemStack itemStack) { + if (Inventory.isUsableForCrafting(itemStack)) { + this.addItem(itemStack); + } + } + + public void addItem(ItemStack itemStack) { + this.addItem(itemStack, itemStack.getMaxStackSize()); + } + + public void addItem(ItemStack itemStack, int i) { + if (!itemStack.isEmpty()) { + int j = Math.min(i, itemStack.getCount()); + this.finder.put(ofKey(itemStack), j); + } + } + + public boolean findRecipe(List> list, int maxCrafts, @Nullable Consumer output) { + return finder.findRecipe(toIngredients(list), maxCrafts, flatten(itemStack -> { + if (output != null) { + output.accept(itemStack); + } + })); + } + + public int countRecipeCrafts(List> list, int maxCrafts, @Nullable Consumer output) { + return finder.countRecipeCrafts(toIngredients(list), maxCrafts, flatten(itemStack -> { + if (output != null) { + output.accept(itemStack); + } + })); + } + + private ItemKey ofKey(ItemStack itemStack) { + return keys.intern(new ItemKey(itemStack.getItemHolder(), itemStack.getComponentsPatch())); + } + + private Ingredient ofKeys(int index, List itemStack) { + return new Ingredient(index, itemStack.stream().map(this::ofKey).toList()); + } + + private List toIngredients(List> list) { + List ingredients = new ArrayList<>(); + + for (int i = 0; i < list.size(); i++) { + List stacks = list.get(i); + if (!stacks.isEmpty()) { + ingredients.add(ofKeys(i, stacks)); + } + } + + return ingredients; + } + + private static BiConsumer flatten(Consumer consumer) { + int[] lastIndex = {-1}; + return (itemKey, ingredient) -> { + for (int i = lastIndex[0] + 1; i < ingredient.index(); i++) { + consumer.accept(ItemStack.EMPTY); + } + consumer.accept(new ItemStack(itemKey.item(), 1, itemKey.patch())); + lastIndex[0] = ingredient.index(); + }; + } + + private record Ingredient(int index, List elements) implements RecipeFinder.Ingredient { + } + + private record ItemKey(Holder item, DataComponentPatch patch) { + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java new file mode 100644 index 00000000..f981c047 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java @@ -0,0 +1,67 @@ +package org.leavesmc.leaves.protocol.rei.transfer; + +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.protocol.rei.transfer.slot.SlotAccessor; + +import java.util.HashMap; +import java.util.List; + +public class NewInputSlotCrafter extends InputSlotCrafter { + protected final List inputSlots; + protected final List inventorySlots; + protected final List> inputs; + + public NewInputSlotCrafter(T container, List inputSlots, List inventorySlots, List> inputs) { + super(container); + this.inputSlots = inputSlots; + this.inventorySlots = inventorySlots; + this.inputs = inputs; + } + + @Override + protected Iterable getInputSlots() { + return this.inputSlots; + } + + @Override + protected Iterable getInventorySlots() { + return this.inventorySlots; + } + + @Override + protected List> getInputs() { + return this.inputs; + } + + @Override + protected void populateRecipeFinder(ItemRecipeFinder recipeFinder) { + for (SlotAccessor slot : getInventorySlots()) { + recipeFinder.addNormalItem(slot.getItemStack()); + } + } + + @Override + protected void markDirty() { + player.getInventory().setChanged(); + container.sendAllDataToRemote(); + } + + @Override + protected void cleanInputs() { + for (SlotAccessor slot : getInputSlots()) { + org.bukkit.inventory.ItemStack bukkitStack = slot.getItemStack().getBukkitStack(); + if (bukkitStack.getType().isAir()) { + return; + } + HashMap notAdded = player.getBukkitEntity().getInventory().addItem(bukkitStack); + if (notAdded.isEmpty()) { + slot.setItemStack(ItemStack.EMPTY); + } else { + org.bukkit.inventory.ItemStack remain = notAdded.values().iterator().next(); + slot.setItemStack(ItemStack.fromBukkitCopy(remain)); + throw new IllegalStateException("rei.rei.no.slot.in.inv"); + } + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java new file mode 100644 index 00000000..d0ea2d8d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java @@ -0,0 +1,409 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * 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. + */ + +package org.leavesmc.leaves.protocol.rei.transfer; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.jetbrains.annotations.Nullable; + +import java.util.BitSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; + +public class RecipeFinder> { + public final Reference2IntOpenHashMap amounts = new Reference2IntOpenHashMap<>(); + + public boolean contains(T item) { + return this.amounts.getInt(item) > 0; + } + + boolean containsAtLeast(T object, int i) { + return this.amounts.getInt(object) >= i; + } + + public void take(T item, int amount) { + int taken = this.amounts.addTo(item, -amount); + if (taken < amount) { + throw new IllegalStateException("Took " + amount + " items, but only had " + taken); + } + } + + public void put(T item, int amount) { + this.amounts.addTo(item, amount); + } + + public boolean findRecipe(List list, int maxCrafts, @Nullable BiConsumer output) { + return new Filter(list).tryPick(maxCrafts, output); + } + + public int countRecipeCrafts(List list, int maxCrafts, @Nullable BiConsumer output) { + return new Filter(list).tryPickAll(maxCrafts, output); + } + + public void clear() { + this.amounts.clear(); + } + + class Filter { + private final List ingredients; + private final int ingredientCount; + private final List items; + private final int itemCount; + private final BitSet data; + private final IntList path = new IntArrayList(); + + public Filter(final List list) { + this.ingredients = list; + this.ingredientCount = this.ingredients.size(); + this.items = this.getUniqueAvailableIngredientItems(); + this.itemCount = this.items.size(); + this.data = new BitSet(this.visitedIngredientCount() + this.visitedItemCount() + this.satisfiedCount() + this.connectionCount() + this.residualCount()); + this.setInitialConnections(); + } + + private void setInitialConnections() { + for (int i = 0; i < this.ingredientCount; i++) { + List list = this.ingredients.get(i).elements(); + + for (int j = 0; j < this.itemCount; j++) { + if (list.contains(this.items.get(j))) { + this.setConnection(j, i); + } + } + } + } + + public boolean tryPick(int maxCrafts, @Nullable BiConsumer output) { + if (maxCrafts <= 0) { + return true; + } else { + int j = 0; + + while (true) { + IntList intList = this.tryAssigningNewItem(maxCrafts); + if (intList == null) { + boolean bl = j == this.ingredientCount; + boolean bl2 = bl && output != null; + this.clearAllVisited(); + this.clearSatisfied(); + + for (int l = 0; l < this.ingredientCount; l++) { + for (int m = 0; m < this.itemCount; m++) { + if (this.isAssigned(m, l)) { + this.unassign(m, l); + put(this.items.get(m), maxCrafts); + if (bl2) { + output.accept(this.items.get(m), this.ingredients.get(l)); + } + break; + } + } + } + + assert this.data.get(this.residualOffset(), this.residualOffset() + this.residualCount()).isEmpty(); + + return bl; + } + + int k = intList.getInt(0); + take(this.items.get(k), maxCrafts); + int l = intList.size() - 1; + this.setSatisfied(intList.getInt(l)); + j++; + + for (int mx = 0; mx < intList.size() - 1; mx++) { + if (isPathIndexItem(mx)) { + int n = intList.getInt(mx); + int o = intList.getInt(mx + 1); + this.assign(n, o); + } else { + int n = intList.getInt(mx + 1); + int o = intList.getInt(mx); + this.unassign(n, o); + } + } + } + } + } + + private static boolean isPathIndexItem(int i) { + return (i & 1) == 0; + } + + private List getUniqueAvailableIngredientItems() { + Set set = new ReferenceOpenHashSet<>(); + + for (Ingredient ingredient : this.ingredients) { + set.addAll(ingredient.elements()); + } + + set.removeIf(object -> !contains(object)); + return List.copyOf(set); + } + + @Nullable + private IntList tryAssigningNewItem(int i) { + this.clearAllVisited(); + + for (int j = 0; j < this.itemCount; j++) { + if (containsAtLeast(this.items.get(j), i)) { + IntList intList = this.findNewItemAssignmentPath(j); + if (intList != null) { + return intList; + } + } + } + + return null; + } + + @Nullable + private IntList findNewItemAssignmentPath(int i) { + this.path.clear(); + this.visitItem(i); + this.path.add(i); + + while (!this.path.isEmpty()) { + int j = this.path.size(); + int k = this.path.getInt(j - 1); + if (isPathIndexItem(j - 1)) { + for (int l = 0; l < this.ingredientCount; l++) { + if (!this.hasVisitedIngredient(l) && this.hasConnection(k, l) && !this.isAssigned(k, l)) { + this.visitIngredient(l); + this.path.add(l); + break; + } + } + } else { + if (!this.isSatisfied(k)) { + return this.path; + } + + for (int lx = 0; lx < this.itemCount; lx++) { + if (!this.hasVisitedItem(lx) && this.isAssigned(lx, k)) { + assert this.hasConnection(lx, k); + + this.visitItem(lx); + this.path.add(lx); + break; + } + } + } + + int l = this.path.size(); + if (l == j) { + this.path.removeInt(l - 1); + } + } + + return null; + } + + private int visitedIngredientOffset() { + return 0; + } + + private int visitedIngredientCount() { + return this.ingredientCount; + } + + private int visitedItemOffset() { + return this.visitedIngredientOffset() + this.visitedIngredientCount(); + } + + private int visitedItemCount() { + return this.itemCount; + } + + private int satisfiedOffset() { + return this.visitedItemOffset() + this.visitedItemCount(); + } + + private int satisfiedCount() { + return this.ingredientCount; + } + + private int connectionOffset() { + return this.satisfiedOffset() + this.satisfiedCount(); + } + + private int connectionCount() { + return this.ingredientCount * this.itemCount; + } + + private int residualOffset() { + return this.connectionOffset() + this.connectionCount(); + } + + private int residualCount() { + return this.ingredientCount * this.itemCount; + } + + private boolean isSatisfied(int i) { + return this.data.get(this.getSatisfiedIndex(i)); + } + + private void setSatisfied(int i) { + this.data.set(this.getSatisfiedIndex(i)); + } + + private int getSatisfiedIndex(int i) { + assert i >= 0 && i < this.ingredientCount; + + return this.satisfiedOffset() + i; + } + + private void clearSatisfied() { + this.clearRange(this.satisfiedOffset(), this.satisfiedCount()); + } + + private void setConnection(int i, int j) { + this.data.set(this.getConnectionIndex(i, j)); + } + + private boolean hasConnection(int i, int j) { + return this.data.get(this.getConnectionIndex(i, j)); + } + + private int getConnectionIndex(int i, int j) { + assert i >= 0 && i < this.itemCount; + + assert j >= 0 && j < this.ingredientCount; + + return this.connectionOffset() + i * this.ingredientCount + j; + } + + private boolean isAssigned(int i, int j) { + return this.data.get(this.getResidualIndex(i, j)); + } + + private void assign(int i, int j) { + int k = this.getResidualIndex(i, j); + + assert !this.data.get(k); + + this.data.set(k); + } + + private void unassign(int i, int j) { + int k = this.getResidualIndex(i, j); + + assert this.data.get(k); + + this.data.clear(k); + } + + private int getResidualIndex(int i, int j) { + assert i >= 0 && i < this.itemCount; + + assert j >= 0 && j < this.ingredientCount; + + return this.residualOffset() + i * this.ingredientCount + j; + } + + private void visitIngredient(int i) { + this.data.set(this.getVisitedIngredientIndex(i)); + } + + private boolean hasVisitedIngredient(int i) { + return this.data.get(this.getVisitedIngredientIndex(i)); + } + + private int getVisitedIngredientIndex(int i) { + assert i >= 0 && i < this.ingredientCount; + + return this.visitedIngredientOffset() + i; + } + + private void visitItem(int i) { + this.data.set(this.getVisitiedItemIndex(i)); + } + + private boolean hasVisitedItem(int i) { + return this.data.get(this.getVisitiedItemIndex(i)); + } + + private int getVisitiedItemIndex(int i) { + assert i >= 0 && i < this.itemCount; + + return this.visitedItemOffset() + i; + } + + private void clearAllVisited() { + this.clearRange(this.visitedIngredientOffset(), this.visitedIngredientCount()); + this.clearRange(this.visitedItemOffset(), this.visitedItemCount()); + } + + private void clearRange(int i, int j) { + this.data.clear(i, i + j); + } + + public int tryPickAll(int i, @Nullable BiConsumer output) { + int j = 0; + int k = Math.min(i, this.getMinIngredientCount()) + 1; + + while (true) { + int l = (j + k) / 2; + if (this.tryPick(l, null)) { + if (k - j <= 1) { + if (l > 0) { + this.tryPick(l, output); + } + + return l; + } + + j = l; + } else { + k = l; + } + } + } + + private int getMinIngredientCount() { + int i = Integer.MAX_VALUE; + + for (Ingredient ingredient : this.ingredients) { + int j = 0; + + for (T object : ingredient.elements()) { + j = Math.max(j, amounts.getInt(object)); + } + + if (i > 0) { + i = Math.min(i, j); + } + } + + return i; + } + } + + public interface Ingredient { + List elements(); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java new file mode 100644 index 00000000..490ee89a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java @@ -0,0 +1,56 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * 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. + */ + +package org.leavesmc.leaves.protocol.rei.transfer.slot; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public class PlayerInventorySlotAccessor implements SlotAccessor { + protected Player player; + protected int index; + + public PlayerInventorySlotAccessor(Player player, int index) { + this.player = player; + this.index = index; + } + + @Override + public ItemStack getItemStack() { + return player.getInventory().getItem(index); + } + + @Override + public void setItemStack(ItemStack stack) { + this.player.getInventory().setItem(index, stack); + } + + @Override + public void takeStack(int amount) { + this.player.getInventory().removeItem(index, amount); + } + + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java new file mode 100644 index 00000000..02e4e64e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java @@ -0,0 +1,43 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * 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. + */ + +package org.leavesmc.leaves.protocol.rei.transfer.slot; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public interface SlotAccessor { + ItemStack getItemStack(); + + void setItemStack(ItemStack stack); + + void takeStack(int amount); + + default boolean allowModification(Player player) { + return true; + } + + default boolean canPlace(ItemStack stack) { + return true; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java new file mode 100644 index 00000000..fd51bf3f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java @@ -0,0 +1,65 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * 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. + */ + +package org.leavesmc.leaves.protocol.rei.transfer.slot; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public class VanillaSlotAccessor implements SlotAccessor { + protected Slot slot; + + public VanillaSlotAccessor(Slot slot) { + this.slot = slot; + } + + @Override + public ItemStack getItemStack() { + return slot.getItem(); + } + + @Override + public void setItemStack(ItemStack stack) { + this.slot.set(stack); + } + + @Override + public void takeStack(int amount) { + slot.remove(amount); + } + + public Slot getSlot() { + return slot; + } + + @Override + public boolean allowModification(Player player) { + return slot.allowModification(player); + } + + @Override + public boolean canPlace(ItemStack stack) { + return slot.mayPlace(stack); + } +} \ No newline at end of file