diff --git a/leaves-server/minecraft-patches/features/0117-Recipe-send-all.patch b/leaves-server/minecraft-patches/features/0117-Recipe-send-all.patch deleted file mode 100644 index 28dcaa3d..00000000 --- a/leaves-server/minecraft-patches/features/0117-Recipe-send-all.patch +++ /dev/null @@ -1,38 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 27 Mar 2025 13:04:35 +0800 -Subject: [PATCH] Recipe send all - - -diff --git a/net/minecraft/stats/ServerRecipeBook.java b/net/minecraft/stats/ServerRecipeBook.java -index e3985b70cee7f7d56f179aeef8c2a6a6b312d83a..a476f6b6554b4c7ba1625ab4b9da3bcf3d40955b 100644 ---- a/net/minecraft/stats/ServerRecipeBook.java -+++ b/net/minecraft/stats/ServerRecipeBook.java -@@ -150,12 +150,23 @@ public class ServerRecipeBook extends RecipeBook { - - public void sendInitialRecipeBook(ServerPlayer player) { - player.connection.send(new ClientboundRecipeBookSettingsPacket(this.getBookSettings())); -- List list = new ArrayList<>(this.known.size()); -+ // Leaves start - recipe-send-all -+ List list; - -- for (ResourceKey> resourceKey : this.known) { -- this.displayResolver -- .displaysForRecipe(resourceKey, entry -> list.add(new ClientboundRecipeBookAddPacket.Entry(entry, false, this.highlight.contains(resourceKey)))); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.recipeSendAll) { -+ list = new ArrayList<>(player.server.getRecipeManager().getRecipes().size()); -+ player.server.getRecipeManager().getRecipes().stream().map(RecipeHolder::id).forEach( key -> { -+ this.displayResolver -+ .displaysForRecipe(key, entry -> list.add(new ClientboundRecipeBookAddPacket.Entry(entry, false, this.highlight.contains(key)))); -+ }); -+ } else { -+ list = new ArrayList<>(this.known.size()); -+ for (ResourceKey> resourceKey : this.known) { -+ this.displayResolver -+ .displaysForRecipe(resourceKey, entry -> list.add(new ClientboundRecipeBookAddPacket.Entry(entry, false, this.highlight.contains(resourceKey)))); -+ } - } -+ // Leaves end - recipe-send-all - - player.connection.send(new ClientboundRecipeBookAddPacket(list, true)); - } diff --git a/leaves-server/minecraft-patches/features/0117-Support-REI-protocol.patch b/leaves-server/minecraft-patches/features/0117-Support-REI-protocol.patch new file mode 100644 index 00000000..4caaf573 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0117-Support-REI-protocol.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 27 Mar 2025 13:04:35 +0800 +Subject: [PATCH] Support REI protocol + + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index d89a1aa9355205883412eaaf535dad30f945a4dc..33e05636164144b3d2bdbd091c72583728cc294e 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1624,6 +1624,7 @@ public abstract class PlayerList { + serverPlayer.getRecipeBook().sendInitialRecipeBook(serverPlayer); + } + org.leavesmc.leaves.protocol.BBORProtocol.onDataPackReload(); // Leaves - bbor ++ org.leavesmc.leaves.protocol.rei.REIServerProtocol.onRecipeReload(); // Leaves - rei + } + + public boolean isAllowCommandsForAllPlayers() { +diff --git a/net/minecraft/world/item/crafting/SmithingTransformRecipe.java b/net/minecraft/world/item/crafting/SmithingTransformRecipe.java +index 143601053a6aeea4396f8e0ee0746ff7d5bbb323..0af83ec1fe3aa85c6bfc814a1339a54e9d3725d6 100644 +--- a/net/minecraft/world/item/crafting/SmithingTransformRecipe.java ++++ b/net/minecraft/world/item/crafting/SmithingTransformRecipe.java +@@ -87,6 +87,12 @@ public class SmithingTransformRecipe implements SmithingRecipe { + ); + } + ++ // Leaves start ++ public ItemStack getResult() { ++ return this.result.copy(); ++ } ++ // Leaves end ++ + // CraftBukkit start + @Override + public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) { diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java index 9e364f1d..3567f7c2 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -841,13 +841,22 @@ public final class LeavesConfig { @GlobalConfig("lms-paster-protocol") public boolean lmsPasterProtocol = false; - @GlobalConfig("rei-server-protocol") + @GlobalConfig(value = "rei-server-protocol", validator = ReiValidator.class) public boolean reiServerProtocol = false; + public static class ReiValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (old != value && value != null) { + org.leavesmc.leaves.protocol.rei.REIServerProtocol.onConfigModify(value); + } + } + } + @GlobalConfig("chat-image-protocol") public boolean chatImageProtocol = false; - @GlobalConfig("recipe-send-all") + @RemovedConfig(name = "recipe-send-all", category = {"protocol"}) public boolean recipeSendAll = false; } 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 new file mode 100644 index 00000000..242a84cd --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java @@ -0,0 +1,138 @@ +package org.leavesmc.leaves.protocol.rei; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.LeavesLogger; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiConsumer; + +public class PacketTransformer { + + private static final byte START = 0x0; + private static final byte PART = 0x1; + private static final byte END = 0x2; + private static final byte ONLY = 0x3; + + private final Map cache = Collections.synchronizedMap(new HashMap<>()); + + private static class PartData { + private final ResourceLocation id; + private final int partsNum; + private final List parts; + + public PartData(ResourceLocation id, int partsNum) { + this.id = id; + this.partsNum = partsNum; + this.parts = new ArrayList<>(); + } + } + + public void inbound(ResourceLocation id, RegistryFriendlyByteBuf buf, ServerPlayer player, BiConsumer consumer) { + UUID key = player.getUUID(); + PartData data; + switch (buf.readByte()) { + case START -> { + int partsNum = buf.readInt(); + data = new PartData(id, partsNum); + if (cache.put(key, data) != null) { + LeavesLogger.LOGGER.warning("Received invalid START packet for SplitPacketTransformer with packet id " + id); + } + buf.retain(); + data.parts.add(buf); + } + case PART -> { + if ((data = cache.get(key)) == null) { + LeavesLogger.LOGGER.warning("Received invalid PART packet for SplitPacketTransformer with packet id " + id); + buf.release(); + } else if (!data.id.equals(id)) { + LeavesLogger.LOGGER.warning("Received invalid PART packet for SplitPacketTransformer with packet id " + id + ", id in cache is {}" + data.id); + buf.release(); + for (RegistryFriendlyByteBuf part : data.parts) { + if (part != buf) { + part.release(); + } + } + cache.remove(key); + } else { + buf.retain(); + data.parts.add(buf); + } + } + case END -> { + if ((data = cache.get(key)) == null) { + LeavesLogger.LOGGER.warning("Received invalid END packet for SplitPacketTransformer with packet id {}" + id); + buf.release(); + } else if (!data.id.equals(id)) { + LeavesLogger.LOGGER.warning("Received invalid END packet for SplitPacketTransformer with packet id " + id + ", id in cache is {}" + data.id); + buf.release(); + for (RegistryFriendlyByteBuf part : data.parts) { + if (part != buf) { + part.release(); + } + } + cache.remove(key); + } else { + buf.retain(); + data.parts.add(buf); + } + if (data == null) { + return; + } + if (data.parts.size() != data.partsNum) { + LeavesLogger.LOGGER.warning("Received invalid END packet for SplitPacketTransformer with packet id " + id + " with size " + data.parts + ", parts expected is {}" + data.partsNum); + for (RegistryFriendlyByteBuf part : data.parts) { + if (part != buf) { + part.release(); + } + } + } else { + RegistryFriendlyByteBuf byteBuf = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(data.parts.toArray(new ByteBuf[0])), buf.registryAccess()); + consumer.accept(data.id, byteBuf); + byteBuf.release(); + } + cache.remove(key); + } + case ONLY -> consumer.accept(id, buf); + default -> throw new IllegalStateException("Illegal split packet header!"); + } + } + + public void outbound(ResourceLocation id, RegistryFriendlyByteBuf buf, BiConsumer consumer) { + int maxSize = 1048576 - 1 - 20 - id.toString().getBytes(StandardCharsets.UTF_8).length; + if (buf.readableBytes() <= maxSize) { + ByteBuf stateBuf = Unpooled.buffer(1); + stateBuf.writeByte(ONLY); + RegistryFriendlyByteBuf packetBuffer = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(stateBuf, buf), buf.registryAccess()); + consumer.accept(id, packetBuffer); + } else { + int partSize = maxSize - 4; + int parts = (int) Math.ceil(buf.readableBytes() / (float) partSize); + for (int i = 0; i < parts; i++) { + RegistryFriendlyByteBuf packetBuffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), buf.registryAccess()); + if (i == 0) { + packetBuffer.writeByte(START); + packetBuffer.writeInt(parts); + } else if (i == parts - 1) { + packetBuffer.writeByte(END); + } else { + packetBuffer.writeByte(PART); + } + int next = Math.min(buf.readableBytes(), partSize); + packetBuffer.writeBytes(buf.retainedSlice(buf.readerIndex(), next)); + buf.skipBytes(next); + consumer.accept(id, packetBuffer); + } + buf.release(); + } + } +} 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 fdf5d035..1333d638 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 @@ -1,107 +1,341 @@ package org.leavesmc.leaves.protocol.rei; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Mth; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SmithingTemplateItem; +import net.minecraft.world.item.crafting.FireworkRocketRecipe; +import net.minecraft.world.item.crafting.MapCloningRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeMap; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.item.crafting.SmithingTrimRecipe; +import net.minecraft.world.item.crafting.TippedArrowRecipe; +import net.minecraft.world.item.crafting.TransmuteRecipe; +import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import org.leavesmc.leaves.LeavesConfig; import org.leavesmc.leaves.protocol.core.LeavesProtocol; -import org.leavesmc.leaves.protocol.core.LeavesProtocolManager.EmptyPayload; import org.leavesmc.leaves.protocol.core.ProtocolHandler; import org.leavesmc.leaves.protocol.core.ProtocolUtils; -import org.leavesmc.leaves.protocol.rei.payload.CreateItemGrabPayload; -import org.leavesmc.leaves.protocol.rei.payload.CreateItemHotbarPayload; -import org.leavesmc.leaves.protocol.rei.payload.CreateItemMessagePayload; -import org.leavesmc.leaves.protocol.rei.payload.CreateItemPayload; +import org.leavesmc.leaves.protocol.rei.display.BlastingDisplay; +import org.leavesmc.leaves.protocol.rei.display.CampfireDisplay; +import org.leavesmc.leaves.protocol.rei.display.Display; +import org.leavesmc.leaves.protocol.rei.display.ShapedDisplay; +import org.leavesmc.leaves.protocol.rei.display.ShapelessDisplay; +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.BufCustomPacketPayload; +import org.leavesmc.leaves.protocol.rei.payload.DisplaySyncPayload; -// @LeavesProtocol(namespace = "roughlyenoughitems") TODO will fix +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +@LeavesProtocol(namespace = REIServerProtocol.PROTOCOL_ID) public class REIServerProtocol { public static final String PROTOCOL_ID = "roughlyenoughitems"; + 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_MESSAGE_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "ci_msg"); + public static final ResourceLocation SYNC_DISPLAYS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "sync_displays"); + + public static final Map TRANSFORMERS = Util.make(() -> { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(SYNC_DISPLAYS_PACKET, new PacketTransformer()); + builder.put(DELETE_ITEMS_PACKET, new PacketTransformer()); + builder.put(CREATE_ITEMS_PACKET, new PacketTransformer()); + builder.put(CREATE_ITEMS_GRAB_PACKET, new PacketTransformer()); + builder.put(CREATE_ITEMS_HOTBAR_PACKET, new PacketTransformer()); + return builder.build(); + }); + private static final Set enabledPlayers = new HashSet<>(); + private static int minecraftRecipeVer = 0; + private static int nextReiRecipeVer = -1; + private static ImmutableList cachedPayloads; + private static final Executor executor = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1), + new ThreadPoolExecutor.DiscardOldestPolicy() + ); + + public static void onRecipeReload() { + minecraftRecipeVer = MinecraftServer.getServer().getTickCount(); + } @Contract("_ -> new") public static ResourceLocation id(String path) { return ResourceLocation.tryBuild(PROTOCOL_ID, path); } - @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "delete_item") - public static void handleDeleteItem(ServerPlayer player, EmptyPayload payload) { - if (!check(player, true)) { - return; - } - - AbstractContainerMenu menu = player.containerMenu; - if (!menu.getCarried().isEmpty()) { - menu.setCarried(ItemStack.EMPTY); - menu.broadcastChanges(); - } - } - - @ProtocolHandler.PayloadReceiver(payload = CreateItemPayload.class, payloadId = "create_item") - public static void handleCreateItem(ServerPlayer player, CreateItemPayload payload) { - if (!check(player, true)) { - return; - } - - ItemStack stack = payload.item(); - if (player.getInventory().add(stack.copy())) { - ProtocolUtils.sendPayloadPacket(player, new CreateItemMessagePayload(stack.copy(), player.getScoreboardName())); + public static void onConfigModify(boolean enabled) { + PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + if (enabled) { + if (pluginManager.getPermission(CHEAT_PERMISSION) == null) { + pluginManager.addPermission(new Permission(CHEAT_PERMISSION, PermissionDefault.OP)); + } } else { - player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + pluginManager.removePermission(CHEAT_PERMISSION); + enabledPlayers.clear(); } } - @ProtocolHandler.PayloadReceiver(payload = CreateItemGrabPayload.class, payloadId = "create_item_grab") - public static void handleCreateItemGrab(ServerPlayer player, CreateItemGrabPayload payload) { - if (!check(player, true)) { - return; - } - - AbstractContainerMenu menu = player.containerMenu; - ItemStack itemStack = payload.item(); - ItemStack stack = itemStack.copy(); - if (!menu.getCarried().isEmpty() && ItemStack.isSameItemSameComponents(menu.getCarried(), stack)) { - stack.setCount(Mth.clamp(stack.getCount() + menu.getCarried().getCount(), 1, stack.getMaxStackSize())); - } else if (!menu.getCarried().isEmpty()) { - return; - } - menu.setCarried(stack.copy()); - menu.broadcastChanges(); - ProtocolUtils.sendPayloadPacket(player, new CreateItemMessagePayload(stack, player.getScoreboardName())); - } - - @ProtocolHandler.PayloadReceiver(payload = CreateItemHotbarPayload.class, payloadId = "create_item_hotbar") - public static void handleCreateItemHotbar(ServerPlayer player, CreateItemHotbarPayload payload) { - if (!check(player, true)) { - return; - } - - ItemStack stack = payload.item(); - int hotbarSlotId = payload.hotbarSlot(); - if (hotbarSlotId >= 0 && hotbarSlotId < 9) { - AbstractContainerMenu menu = player.containerMenu; - player.getInventory().items.set(hotbarSlotId, stack.copy()); - menu.broadcastChanges(); - ProtocolUtils.sendPayloadPacket(player, new CreateItemMessagePayload(stack, player.getScoreboardName())); - } else { - player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.reiServerProtocol) { + enabledPlayers.remove(player); } } - private static boolean check(ServerPlayer player, boolean needOP) { + @ProtocolHandler.Ticker + public static void tick() { if (!LeavesConfig.protocol.reiServerProtocol) { - return false; + return; } + if (MinecraftServer.getServer().getTickCount() % 200 == 1 && minecraftRecipeVer != nextReiRecipeVer) { + nextReiRecipeVer = minecraftRecipeVer; + executor.execute(() -> reloadRecipe(nextReiRecipeVer)); + } + } - if (needOP && MinecraftServer.getServer().getPlayerList().isOp(player.gameProfile)) { // TODO check permission node - player.displayClientMessage(Component.translatable("text.rei.no_permission_cheat").withStyle(ChatFormatting.RED), false); - return false; + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void reloadRecipe(int reiRecipeVer) { + ImmutableList.Builder builder = ImmutableList.builder(); + MinecraftServer server = MinecraftServer.getServer(); + RecipeMap recipeMap = server.getRecipeManager().recipes; + recipeMap.byType(RecipeType.CRAFTING).forEach(holder -> { + switch (holder.value()) { + case ShapedRecipe ignored -> builder.add(new ShapedDisplay((RecipeHolder) holder)); + case ShapelessRecipe ignored -> builder.add(new ShapelessDisplay((RecipeHolder) holder)); + case TransmuteRecipe ignored -> builder.addAll(Display.ofTransmuteRecipe((RecipeHolder) holder)); + case TippedArrowRecipe ignored -> builder.addAll(Display.ofTippedArrowRecipe((RecipeHolder) holder)); + case FireworkRocketRecipe ignored -> builder.addAll(Display.ofFireworkRocketRecipe((RecipeHolder) holder)); + case MapCloningRecipe ignored -> builder.addAll(Display.ofMapCloningRecipe((RecipeHolder) holder)); + // ignore ArmorDyeRecipe, BannerDuplicateRecipe, BookCloningRecipe, ShieldDecorationRecipe + default -> { + } + } + }); + recipeMap.byType(RecipeType.STONECUTTING).forEach(holder -> builder.add(new StoneCuttingDisplay(holder))); + recipeMap.byType(RecipeType.SMELTING).forEach(holder -> builder.add(new SmeltingDisplay(holder))); + recipeMap.byType(RecipeType.BLASTING).forEach(holder -> builder.add(new BlastingDisplay(holder))); + recipeMap.byType(RecipeType.SMOKING).forEach(holder -> builder.add(new SmokingDisplay(holder))); + recipeMap.byType(RecipeType.CAMPFIRE_COOKING).forEach(holder -> builder.add(new CampfireDisplay(holder))); + recipeMap.byType(RecipeType.SMITHING).forEach(holder -> { + switch (holder.value()) { + case SmithingTrimRecipe ignored -> builder.addAll(Display.ofSmithingTrimRecipe((RecipeHolder) holder)); + case SmithingTemplateItem ignored -> builder.add(Display.ofTransforming((RecipeHolder) holder)); + default -> { + } + } + }); + + DisplaySyncPayload displaySyncPayload = new DisplaySyncPayload( + DisplaySyncPayload.SyncType.SET, + builder.build(), + reiRecipeVer + ); + + RegistryFriendlyByteBuf s2cBuf = ProtocolUtils.decorate(Unpooled.buffer()); + DisplaySyncPayload.STREAM_CODEC.encode(s2cBuf, displaySyncPayload); + ImmutableList.Builder listBuilder = ImmutableList.builder(); + outboundTransform(SYNC_DISPLAYS_PACKET, s2cBuf, (id, splitBuf) -> + listBuilder.add(new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(splitBuf))) + ); + + cachedPayloads = listBuilder.build(); + MinecraftServer.getServer().execute(() -> { + for (ServerPlayer player : enabledPlayers) { + for (CustomPacketPayload payload : cachedPayloads) { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + }); + } + + @ProtocolHandler.MinecraftRegister(ignoreId = true) + public static void onPlayerSubscribed(@NotNull ServerPlayer player, String channel) { + if (!LeavesConfig.protocol.reiServerProtocol) { + return; } - return true; + enabledPlayers.add(player); + if (channel.equals("sync_displays")) { + if (cachedPayloads != null) { + cachedPayloads.forEach(payload -> ProtocolUtils.sendPayloadPacket(player, payload)); + } + } else if (channel.equals("ci_msg")) { + // cheat rei-client into using "delete_item" packet + if (player.getServer().getProfilePermissions(player.getGameProfile()) < 1) { + player.getBukkitEntity().sendOpLevel((byte) 1); + } + } + } + + @ProtocolHandler.PayloadReceiver(payload = BufCustomPacketPayload.class, payloadId = "delete_item") + public static void handleDeleteItem(ServerPlayer player, BufCustomPacketPayload payload) { + if (!LeavesConfig.protocol.reiServerProtocol || !hasCheatPermission(player)) { + return; + } + RegistryFriendlyByteBuf c2sBuf = ProtocolUtils.decorate(Unpooled.buffer()); + c2sBuf.writeBytes(payload.payload()); + + inboundTransform(player, payload.id(), c2sBuf, (id, wholeBuf) -> { + AbstractContainerMenu menu = player.containerMenu; + if (!menu.getCarried().isEmpty()) { + menu.setCarried(ItemStack.EMPTY); + menu.broadcastChanges(); + } + }); + } + + @ProtocolHandler.PayloadReceiver(payload = BufCustomPacketPayload.class, payloadId = "create_item") + public static void handleCreateItem(ServerPlayer player, BufCustomPacketPayload payload) { + if (!LeavesConfig.protocol.reiServerProtocol || !hasCheatPermission(player)) { + return; + } + RegistryFriendlyByteBuf c2sBuf = ProtocolUtils.decorate(Unpooled.buffer()); + c2sBuf.writeBytes(payload.payload()); + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + ItemStack itemStack = tmpBuf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC); + if (player.getInventory().add(itemStack.copy())) { + RegistryFriendlyByteBuf s2cWholeBuf = ProtocolUtils.decorate(Unpooled.buffer()); + s2cWholeBuf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, itemStack.copy()); + s2cWholeBuf.writeUtf(player.getScoreboardName(), 32767); + // Due to the bug in REI, no packets are actually sent here. + /* + outboundTransform(CREATE_ITEMS_MESSAGE_PACKET, s2cWholeBuf, (id, s2cSplitBuf) -> { + ProtocolUtils.sendPayloadPacket(player, new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(s2cSplitBuf))); + }); + */ + } else { + player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + } + }; + inboundTransform(player, payload.id(), c2sBuf, consumer); + } + + @ProtocolHandler.PayloadReceiver(payload = BufCustomPacketPayload.class, payloadId = "create_item_grab") + public static void handleCreateItemGrab(ServerPlayer player, BufCustomPacketPayload payload) { + if (!LeavesConfig.protocol.reiServerProtocol || !hasCheatPermission(player)) { + return; + } + RegistryFriendlyByteBuf c2sBuf = ProtocolUtils.decorate(Unpooled.buffer()); + c2sBuf.writeBytes(payload.payload()); + + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + ItemStack itemStack = tmpBuf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC); + ItemStack stack = itemStack.copy(); + AbstractContainerMenu menu = player.containerMenu; + if (!menu.getCarried().isEmpty() && ItemStack.isSameItemSameComponents(menu.getCarried(), stack)) { + stack.setCount(Mth.clamp(stack.getCount() + menu.getCarried().getCount(), 1, stack.getMaxStackSize())); + } else if (!menu.getCarried().isEmpty()) { + return; + } + menu.setCarried(stack.copy()); + menu.broadcastChanges(); + RegistryFriendlyByteBuf s2cWholeBuf = ProtocolUtils.decorate(Unpooled.buffer()); + s2cWholeBuf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, stack.copy()); + s2cWholeBuf.writeUtf(player.getScoreboardName(), 32767); + // Due to the bug in REI, no packets are actually sent here. + /* + outboundTransform(CREATE_ITEMS_MESSAGE_PACKET, s2cWholeBuf, (id, s2cSplitBuf) -> { + ProtocolUtils.sendPayloadPacket(player, new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(s2cSplitBuf))); + }); + */ + }; + inboundTransform(player, payload.id(), c2sBuf, consumer); + } + + @ProtocolHandler.PayloadReceiver(payload = BufCustomPacketPayload.class, payloadId = "create_item_hotbar") + public static void handleCreateItemHotbar(ServerPlayer player, BufCustomPacketPayload payload) { + if (!LeavesConfig.protocol.reiServerProtocol || !hasCheatPermission(player)) { + return; + } + RegistryFriendlyByteBuf c2sBuf = ProtocolUtils.decorate(Unpooled.buffer()); + c2sBuf.writeBytes(payload.payload()); + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + ItemStack stack = tmpBuf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC); + int hotbarSlotId = tmpBuf.readVarInt(); + if (hotbarSlotId >= 0 && hotbarSlotId < 9) { + AbstractContainerMenu menu = player.containerMenu; + player.getInventory().items.set(hotbarSlotId, stack.copy()); + menu.broadcastChanges(); + RegistryFriendlyByteBuf s2cWholeBuf = ProtocolUtils.decorate(Unpooled.buffer()); + s2cWholeBuf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, stack.copy()); + s2cWholeBuf.writeUtf(player.getScoreboardName(), 32767); + // Due to the bug in REI, no packets are actually sent here. + /* + outboundTransform(CREATE_ITEMS_MESSAGE_PACKET, s2cWholeBuf, (id, s2cSplitBuf) -> { + ProtocolUtils.sendPayloadPacket(player, new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(s2cSplitBuf))); + }); + */ + } else { + player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + } + }; + inboundTransform(player, payload.id(), c2sBuf, consumer); + } + + private static void inboundTransform(ServerPlayer player, + ResourceLocation id, + RegistryFriendlyByteBuf buf, + BiConsumer consumer) { + PacketTransformer transformer = TRANSFORMERS.get(id); + if (transformer != null) { + transformer.inbound(id, buf, player, consumer); + } else { + consumer.accept(id, buf); + } + } + + private static void outboundTransform(ResourceLocation id, + RegistryFriendlyByteBuf buf, + BiConsumer consumer) { + PacketTransformer transformer = TRANSFORMERS.get(id); + if (transformer != null) { + transformer.outbound(id, buf, consumer); + } else { + consumer.accept(id, buf); + } + } + + private static boolean hasCheatPermission(ServerPlayer player) { + if (player.getBukkitEntity().hasPermission(CHEAT_PERMISSION)) { + return true; + } + player.displayClientMessage(Component.translatable("text.rei.no_permission_cheat").withStyle(ChatFormatting.RED), false); + return false; } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.java new file mode 100644 index 00000000..47f6e133 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; + +public class BlastingDisplay extends CookingDisplay { + public BlastingDisplay(RecipeHolder recipe) { + super(recipe); + } + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/blasting"); + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.java new file mode 100644 index 00000000..b9234cc9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.CampfireCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; + +public class CampfireDisplay extends CookingDisplay { + public CampfireDisplay(RecipeHolder recipe) { + super(recipe); + } + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/campfire"); + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java new file mode 100644 index 00000000..e530e980 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java @@ -0,0 +1,68 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SingleRecipeInput; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +public abstract class CookingDisplay extends Display { + protected float xp; + protected double cookTime; + + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CookingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CookingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CookingDisplay::getOptionalLocation, + ByteBufCodecs.FLOAT, + CookingDisplay::getXp, + ByteBufCodecs.DOUBLE, + CookingDisplay::getCookTime, + CookingDisplay::of + ); + + private CookingDisplay(@NotNull List inputs, @NotNull List outputs, @NotNull ResourceLocation id, float xp, double cookTime) { + super(inputs, outputs, id); + this.xp = xp; + this.cookTime = cookTime; + } + + public CookingDisplay(RecipeHolder recipe) { + this( + List.of(EntryIngredient.ofIngredient(recipe.value().input())), + List.of(EntryIngredient.of(recipe.value().assemble(new SingleRecipeInput(ItemStack.EMPTY), CraftRegistry.getMinecraftRegistry()))), + recipe.id().location(), + recipe.value().experience(), + recipe.value().cookingTime() + ); + } + + public float getXp() { + return xp; + } + + public double getCookTime() { + return cookTime; + } + + public StreamCodec streamCodec() { + return CODEC; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CookingDisplay of(@NotNull List inputs, @NotNull List outputs, @NotNull Optional id, float xp, double cookTime) { + throw new UnsupportedOperationException(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java new file mode 100644 index 00000000..069276fd --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java @@ -0,0 +1,21 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; + +public abstract class CraftingDisplay extends Display { + + public CraftingDisplay(@NotNull List inputs, + @NotNull List outputs, + @NotNull ResourceLocation location) { + super(inputs, outputs, location); + } + + public abstract int getWidth(); + + public abstract int getHeight(); + +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.java new file mode 100644 index 00000000..3c2d892f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.java @@ -0,0 +1,72 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.BitSet; +import java.util.List; +import java.util.Optional; + +public class CustomDisplay extends CraftingDisplay { + private final int width; + private final int height; + + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CustomDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CustomDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CustomDisplay::getOptionalLocation, + CustomDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/crafting/custom"); + + /** + * see me.shedaniel.rei.plugin.common.displays.crafting.DefaultCustomDisplay#DefaultCustomDisplay + */ + public CustomDisplay(@NotNull List inputs, @NotNull List outputs, @NotNull ResourceLocation location) { + super(inputs, outputs, location); + BitSet row = new BitSet(3); + BitSet column = new BitSet(3); + for (int i = 0; i < 9; i++) + if (i < inputs.size()) { + EntryIngredient stacks = inputs.get(i); + if (stacks.stream().anyMatch(stack -> !stack.isEmpty())) { + row.set((i - (i % 3)) / 3); + column.set(i % 3); + } + } + this.width = column.cardinality(); + this.height = row.cardinality(); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + public StreamCodec streamCodec() { + return CODEC; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CustomDisplay of(@NotNull List inputs, @NotNull List outputs, @NotNull Optional id) { + throw new UnsupportedOperationException(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java new file mode 100644 index 00000000..81f64a63 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java @@ -0,0 +1,327 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import com.google.common.collect.ImmutableList; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tags.TagKey; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.component.Fireworks; +import net.minecraft.world.item.crafting.FireworkRocketRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.MapCloningRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SmithingTransformRecipe; +import net.minecraft.world.item.crafting.SmithingTrimRecipe; +import net.minecraft.world.item.crafting.TippedArrowRecipe; +import net.minecraft.world.item.crafting.TransmuteRecipe; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import net.minecraft.world.item.equipment.trim.ArmorTrim; +import net.minecraft.world.item.equipment.trim.TrimMaterial; +import net.minecraft.world.item.equipment.trim.TrimMaterials; +import net.minecraft.world.item.equipment.trim.TrimPattern; +import net.minecraft.world.item.equipment.trim.TrimPatterns; +import net.minecraft.world.level.ItemLike; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * A display to be used alongside Roughly Enough Items. + *

+ * see me.shedaniel.rei.api.common.display.Display + */ +public abstract class Display { + + protected ResourceLocation id; + + protected List inputs; + + protected List outputs; + + public Display(@NotNull List inputs, + @NotNull List outputs, + @NotNull ResourceLocation id) { + this.inputs = inputs; + this.outputs = outputs; + this.id = id; + } + + public List getInputEntries() { + return inputs; + } + + public List getOutputEntries() { + return outputs; + } + + public ResourceLocation getDisplayLocation() { + return id; + } + + public Optional getOptionalLocation() { + return Optional.ofNullable(id); + } + + @SuppressWarnings("unchecked") + public static StreamCodec dispatchCodec() { + return new StreamCodec<>() { + @NotNull + @Override + public Display decode(@NotNull RegistryFriendlyByteBuf buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(@NotNull RegistryFriendlyByteBuf buffer, @NotNull Display display) { + new FriendlyByteBuf(buffer).writeResourceLocation(display.getSerializerId()); + ((StreamCodec) display.streamCodec()).encode(buffer, display); + } + }; + } + + public abstract ResourceLocation getSerializerId(); + + public abstract StreamCodec streamCodec(); + + public static Collection ofTransmuteRecipe(@NotNull RecipeHolder recipeHolder) { + TransmuteRecipe recipe = recipeHolder.value(); + List displays = recipe.display(); + List displayList = new ArrayList<>(); + if (!displays.isEmpty()) { + RecipeDisplay recipeDisplay = displays.getFirst(); + if (recipeDisplay instanceof ShapelessCraftingRecipeDisplay shapelessRecipeDisplay) { + displayList.add(new ShapelessDisplay(shapelessRecipeDisplay, recipeHolder.id().location())); + } else if (recipeDisplay instanceof ShapedCraftingRecipeDisplay shapelessRecipe) { + displayList.add(new ShapedDisplay(shapelessRecipe, recipeHolder.id().location())); + } + } + return displayList; + } + + /** + * see me.shedaniel.rei.plugin.client.categories.crafting.filler.TippedArrowRecipeFiller#apply + */ + @NotNull + public static Collection ofTippedArrowRecipe(@NotNull RecipeHolder recipeHolder) { + EntryIngredient arrowIngredient = EntryIngredient.of(Items.ARROW); + Set registeredPotions = new HashSet<>(); + List displays = new ArrayList<>(); + MinecraftServer.getServer().registryAccess().lookup(Registries.POTION).stream() + .flatMap(Registry::listElements) + .map(reference -> PotionContents.createItemStack(Items.LINGERING_POTION, reference)) + .forEach(itemStack -> { + PotionContents potion = itemStack.get(DataComponents.POTION_CONTENTS); + if (potion == null || potion.potion().isEmpty()) { + return; + } + if (potion.potion().get().unwrapKey().isPresent() && registeredPotions.add(potion.potion().get().unwrapKey().get().location())) { + List input = new ArrayList<>(); + for (int i = 0; i < 4; i++) + input.add(arrowIngredient); + input.add(EntryIngredient.of(itemStack)); + for (int i = 0; i < 4; i++) + input.add(arrowIngredient); + ItemStack outputStack = new ItemStack(Items.TIPPED_ARROW, 8); + outputStack.set(DataComponents.POTION_CONTENTS, potion); + displays.add(new CustomDisplay(input, List.of(EntryIngredient.of(outputStack)), recipeHolder.id().location())); + } + }); + return displays; + } + + /** + * see me.shedaniel.rei.plugin.client.categories.crafting.filler.TippedArrowRecipeFiller#apply + */ + @NotNull + public static Collection ofFireworkRocketRecipe(@NotNull RecipeHolder recipeHolder) { + EntryIngredient[] inputs = new EntryIngredient[4]; + inputs[0] = EntryIngredient.of(Items.GUNPOWDER); + inputs[1] = EntryIngredient.of(Items.PAPER); + inputs[2] = EntryIngredient.of(new ItemStack(Items.AIR), new ItemStack(Items.GUNPOWDER), new ItemStack(Items.GUNPOWDER)); + inputs[3] = EntryIngredient.of(new ItemStack(Items.AIR), new ItemStack(Items.AIR), new ItemStack(Items.GUNPOWDER)); + ItemStack[] outputs = new ItemStack[3]; + for (int i = 0; i < 3; i++) { + outputs[i] = new ItemStack(Items.FIREWORK_ROCKET, 3); + outputs[i].set(DataComponents.FIREWORKS, new Fireworks(i + 1, List.of())); + } + return Collections.singleton(new ShapelessDisplay(List.of(inputs), List.of(EntryIngredient.of(outputs)), recipeHolder.id().location())); + } + + /** + * see me.shedaniel.rei.plugin.client.categories.crafting.filler.MapCloningRecipeFiller#apply + */ + @NotNull + public static Collection ofMapCloningRecipe(@NotNull RecipeHolder recipeHolder) { + return Collections.singleton( + new ShapelessDisplay( + List.of(EntryIngredient.of(Items.FILLED_MAP), EntryIngredient.of(Items.MAP)), + List.of(EntryIngredient.of(new ItemStack(Items.FILLED_MAP, 2))), + recipeHolder.id().location()) + ); + } + + /** + * see me.shedaniel.rei.plugin.common.displays.DefaultSmithingDisplay#ofTransforming + */ + @NotNull + public static SmithingDisplay ofTransforming(RecipeHolder recipeHolder) { + + + return new SmithingDisplay( + List.of( + recipeHolder.value().templateIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()), + recipeHolder.value().baseIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()), + recipeHolder.value().additionIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()) + ), + List.of(EntryIngredient.of(recipeHolder.value().getResult())), + SmithingDisplay.SmithingRecipeType.TRANSFORM, + recipeHolder.id().location() + ); + } + + /** + * see me.shedaniel.rei.plugin.common.displays.DefaultSmithingDisplay#fromTrimming + */ + @NotNull + public static Collection ofSmithingTrimRecipe(@NotNull RecipeHolder recipeHolder) { + RegistryAccess registryAccess = MinecraftServer.getServer().registryAccess(); + SmithingTrimRecipe recipe = recipeHolder.value(); + List displays = new ArrayList<>(); + for (Holder templateItem : (Iterable>) recipe.templateIngredient().map(Ingredient::items).orElse(Stream.of())::iterator) { + Holder.Reference trimPattern = getPatternFromTemplate(registryAccess, templateItem) + .orElse(null); + if (trimPattern == null) continue; + + for (Holder additionStack : (Iterable>) recipe.additionIngredient().map(Ingredient::items).orElse(Stream.of())::iterator) { + Holder.Reference trimMaterial = getMaterialFromIngredient(registryAccess, additionStack) + .orElse(null); + if (trimMaterial == null) continue; + + EntryIngredient baseIngredient = recipe.baseIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()); + EntryIngredient templateOutput = baseIngredient.isEmpty() ? EntryIngredient.empty() + : getTrimmingOutput(registryAccess, templateItem.value().getDefaultInstance(), baseIngredient.get(0), additionStack.value().getDefaultInstance()); + + displays.add(new SmithingDisplay(List.of( + EntryIngredient.ofItemHolder(templateItem), + baseIngredient, + EntryIngredient.ofItemHolder(additionStack) + ), List.of(templateOutput), SmithingDisplay.SmithingRecipeType.TRIM, recipeHolder.id().location())); + } + } + return displays; + } + + public static EntryIngredient getTrimmingOutput(RegistryAccess registryAccess, ItemStack templateItem, ItemStack baseItem, ItemStack additionItem) { + Holder.Reference trimPattern = TrimPatterns.getFromTemplate(registryAccess, templateItem) + .orElse(null); + if (trimPattern == null) return EntryIngredient.empty(); + Holder.Reference trimMaterial = TrimMaterials.getFromIngredient(registryAccess, additionItem) + .orElse(null); + if (trimMaterial == null) return EntryIngredient.empty(); + ArmorTrim armorTrim = new ArmorTrim(trimMaterial, trimPattern); + ArmorTrim trim = baseItem.get(DataComponents.TRIM); + if (trim != null && trim.hasPatternAndMaterial(trimPattern, trimMaterial)) return EntryIngredient.empty(); + ItemStack newItem = baseItem.copyWithCount(1); + newItem.set(DataComponents.TRIM, armorTrim); + return EntryIngredient.of(newItem); + } + + private static Optional> getPatternFromTemplate(HolderLookup.Provider provider, Holder item) { + return provider.lookupOrThrow(Registries.TRIM_PATTERN) + .listElements() + .filter(reference -> item == reference.value().templateItem()) + .findFirst(); + } + + private static Optional> getMaterialFromIngredient(HolderLookup.Provider provider, Holder item) { + return provider.lookupOrThrow(Registries.TRIM_MATERIAL) + .listElements() + .filter(reference -> item == reference.value().ingredient()) + .findFirst(); + } + + public static EntryIngredient ofSlotDisplay(SlotDisplay slot) { + return switch (slot) { + case SlotDisplay.Empty ignored -> EntryIngredient.empty(); + case SlotDisplay.ItemSlotDisplay s -> EntryIngredient.of(s.item().value()); + case SlotDisplay.ItemStackSlotDisplay s -> EntryIngredient.of(s.stack()); + case SlotDisplay.TagSlotDisplay s -> ofItemTag(s.tag()); + case SlotDisplay.Composite s -> { + ArrayList list = new ArrayList<>(); + for (SlotDisplay slotDisplay : s.contents()) { + ofSlotDisplay(slotDisplay).stream().forEach(list::add); + } + yield EntryIngredient.of(list.toArray(new ItemStack[0])); + } + // REI Bad idea + case SlotDisplay.AnyFuel ignored -> EntryIngredient.empty(); + default -> { + RegistryAccess access = MinecraftServer.getServer().registryAccess(); + try { + List stacks = slot.resolveForStacks(new ContextMap.Builder() + .withParameter(SlotDisplayContext.REGISTRIES, access) + .create(SlotDisplayContext.CONTEXT)); + yield EntryIngredient.of(stacks.toArray(new ItemStack[0])); + } catch (Exception e) { + MinecraftServer.LOGGER.warn("Failed to resolve slot display: {}", slot, e); + yield EntryIngredient.empty(); + } + } + }; + } + + public static List ofSlotDisplays(Collection slots) { + if (slots instanceof Collection collection && collection.isEmpty()) return Collections.emptyList(); + ImmutableList.Builder ingredients = ImmutableList.builder(); + for (SlotDisplay slot : slots) { + ingredients.add(ofSlotDisplay(slot)); + } + return ingredients.build(); + } + + public static EntryIngredient ofItemTag(TagKey tagKey) { + HolderGetter getter = MinecraftServer.getServer().registryAccess().lookupOrThrow(tagKey.registry()); + HolderSet.Named holders = getter.get(tagKey).orElse(null); + if (holders == null) return EntryIngredient.empty(); + + int size = holders.size(); + if (size == 0) return EntryIngredient.empty(); + if (size == 1) return EntryIngredient.of(new ItemStack(holders.get(0).value())); + + List stackList = new ArrayList<>(); + for (Holder t : holders) { + ItemStack stack = new ItemStack(t.value()); + if (!stack.isEmpty()) { + stackList.add(stack); + } + } + return EntryIngredient.of(stackList.toArray(new ItemStack[0])); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java new file mode 100644 index 00000000..cf09b72c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java @@ -0,0 +1,98 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +/** + * see me.shedaniel.rei.plugin.common.displays.crafting.DefaultShapedDisplay#DefaultShapedDisplay(RecipeHolder) + */ +public class ShapedDisplay extends CraftingDisplay { + private final int width; + + private final int height; + + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CraftingDisplay::getOptionalLocation, + ByteBufCodecs.INT, + CraftingDisplay::getWidth, + ByteBufCodecs.INT, + CraftingDisplay::getHeight, + ShapedDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/crafting/shaped"); + + public ShapedDisplay(@NotNull RecipeHolder recipeHolder) { + super( + ofIngredient(recipeHolder.value()), + List.of(EntryIngredient.of(recipeHolder.value().assemble(CraftingInput.EMPTY, CraftRegistry.getMinecraftRegistry()))), + recipeHolder.id().location() + ); + this.width = recipeHolder.value().getWidth(); + this.height = recipeHolder.value().getHeight(); + } + + public ShapedDisplay(@NotNull ShapedCraftingRecipeDisplay recipeDisplay, ResourceLocation id) { + super( + Display.ofSlotDisplays(recipeDisplay.ingredients()), + List.of(ofSlotDisplay(recipeDisplay.result())), + id + ); + this.width = recipeDisplay.width(); + this.height = recipeDisplay.height(); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + public StreamCodec streamCodec() { + return CODEC; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CraftingDisplay of(List inputs, List outputs, Optional location, int width, int height) { + throw new UnsupportedOperationException(); + } + + private static List ofIngredient(ShapedRecipe recipe) { + return recipe.getIngredients().stream().map(ingredient -> { + if (ingredient.isEmpty()) { + return EntryIngredient.empty(); + } + ItemStack[] itemStacks = ingredient.get().items() + .map(itemHolder -> new ItemStack(itemHolder, 1)) + .toArray(ItemStack[]::new); + return EntryIngredient.of(itemStacks); + }).toList(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java new file mode 100644 index 00000000..b8e42d1e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java @@ -0,0 +1,77 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import org.apache.commons.lang.NotImplementedException; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +public class ShapelessDisplay extends CraftingDisplay { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CraftingDisplay::getOptionalLocation, + ShapelessDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/crafting/shapeless"); + + public ShapelessDisplay(@NotNull List inputs, + @NotNull List outputs, + @NotNull ResourceLocation location) { + super(inputs, outputs, location); + } + + public ShapelessDisplay(@NotNull RecipeHolder recipeHolder) { + this( + recipeHolder.value().placementInfo().ingredients().stream().map(EntryIngredient::ofIngredient).toList(), + List.of(EntryIngredient.of(recipeHolder.value().assemble(CraftingInput.EMPTY, CraftRegistry.getMinecraftRegistry()))), + recipeHolder.id().location() + ); + } + + public ShapelessDisplay(@NotNull ShapelessCraftingRecipeDisplay recipeDisplay, ResourceLocation id) { + this( + ofSlotDisplays(recipeDisplay.ingredients()), + List.of(ofSlotDisplay(recipeDisplay.result())), + id + ); + } + + @Override + public int getWidth() { + return getInputEntries().size() > 4 ? 3 : 2; + } + + @Override + public int getHeight() { + return getInputEntries().size() > 4 ? 3 : 2; + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + public StreamCodec streamCodec() { + return CODEC; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CraftingDisplay of(List inputs, List outputs, Optional location) { + throw new NotImplementedException(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.java new file mode 100644 index 00000000..7d9923ee --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SmeltingRecipe; + +public class SmeltingDisplay extends CookingDisplay { + public SmeltingDisplay(RecipeHolder recipe) { + super(recipe); + } + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smelting"); + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java new file mode 100644 index 00000000..cda51825 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java @@ -0,0 +1,72 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import com.mojang.serialization.Codec; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ByIdMap; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; +import java.util.function.IntFunction; + +public class SmithingDisplay extends Display { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + SmithingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + SmithingDisplay::getOutputEntries, + ByteBufCodecs.optional(SmithingRecipeType.STREAM_CODEC), + SmithingDisplay::getOptionalType, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + SmithingDisplay::getOptionalLocation, + SmithingDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smithing"); + + private final SmithingRecipeType type; + + public Optional getOptionalType() { + return Optional.of(type); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + @Override + public StreamCodec streamCodec() { + return CODEC; + } + + public SmithingDisplay( + @NotNull List inputs, + @NotNull List outputs, + @NotNull SmithingRecipeType type, + @NotNull ResourceLocation location + ) { + super(inputs, outputs, location); + this.type = type; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static SmithingDisplay of(List inputs, List outputs, Optional type, Optional location) { + throw new UnsupportedOperationException(); + } + + public enum SmithingRecipeType { + TRIM, + TRANSFORM, + ; + + public static final Codec CODEC = Codec.STRING.xmap(SmithingRecipeType::valueOf, SmithingRecipeType::name); + public static final IntFunction BY_ID = ByIdMap.continuous(Enum::ordinal, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Enum::ordinal); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.java new file mode 100644 index 00000000..f858a65e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; + +public class SmokingDisplay extends CookingDisplay { + public SmokingDisplay(RecipeHolder recipe) { + super(recipe); + } + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smoking"); + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java new file mode 100644 index 00000000..dcea6a34 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java @@ -0,0 +1,60 @@ +package org.leavesmc.leaves.protocol.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SingleRecipeInput; +import net.minecraft.world.item.crafting.StonecutterRecipe; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +/** + * see me.shedaniel.rei.plugin.common.displays.DefaultStoneCuttingDisplay + */ +public class StoneCuttingDisplay extends Display { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + StoneCuttingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + StoneCuttingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + StoneCuttingDisplay::getOptionalLocation, + StoneCuttingDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/stone_cutting"); + + public StoneCuttingDisplay(@NotNull List inputs, @NotNull List outputs, @NotNull ResourceLocation id) { + super(inputs, outputs, id); + } + + public StoneCuttingDisplay(RecipeHolder recipeHolder) { + this( + List.of(EntryIngredient.ofIngredient(recipeHolder.value().input())), + List.of(EntryIngredient.of(recipeHolder.value().assemble(new SingleRecipeInput(ItemStack.EMPTY), CraftRegistry.getMinecraftRegistry()))), + recipeHolder.id().location() + ); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + @Override + public StreamCodec streamCodec() { + return CODEC; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static StoneCuttingDisplay of(@NotNull List inputs, @NotNull List outputs, @NotNull Optional id) { + throw new UnsupportedOperationException(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java new file mode 100644 index 00000000..6be33031 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java @@ -0,0 +1,101 @@ +package org.leavesmc.leaves.protocol.rei.ingredient; + +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; + +public class EntryIngredient { + private static final StreamCodec> ITEM_STREAM_CODEC = ByteBufCodecs.holderRegistry(Registries.ITEM); + public static final StreamCodec CODEC = new StreamCodec<>() { + @NotNull + @Override + public EntryIngredient decode(@NotNull RegistryFriendlyByteBuf buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(@NotNull RegistryFriendlyByteBuf buffer, @NotNull EntryIngredient value) { + ByteBufCodecs.writeCount(buffer, value.size(), Integer.MAX_VALUE); + value.stream().forEach(itemStack -> { + buffer.writeResourceLocation(ITEM_ID); + if (itemStack.isEmpty()) { + buffer.writeVarInt(0); + } else { + buffer.writeVarInt(itemStack.getCount()); + ITEM_STREAM_CODEC.encode(buffer, itemStack.getItemHolder()); + DataComponentPatch.STREAM_CODEC.encode(buffer, itemStack.components.asPatch()); + } + }); + } + }; + + private static final ResourceLocation ITEM_ID = ResourceLocation.withDefaultNamespace("item"); + + @NotNull + private final ItemStack[] array; + + private static final EntryIngredient EMPTY = new EntryIngredient(new ItemStack[0]); + + private EntryIngredient(@NotNull ItemStack[] array) { + this.array = Objects.requireNonNull(array); + } + + public Stream stream() { + return Arrays.stream(array); + } + + public static EntryIngredient empty() { + return EMPTY; + } + + public boolean isEmpty() { + return array.length == 0; + } + + public ItemStack get(int index) { + return array[index].copy(); + } + + public int size() { + return array.length; + } + + public static EntryIngredient ofItemHolder(@NotNull Holder item) { + return EntryIngredient.of(item.value()); + } + + public static EntryIngredient of(@NotNull ItemLike item) { + return EntryIngredient.of(new ItemStack(item)); + } + + public static EntryIngredient of(@NotNull ItemStack itemStack) { + return new EntryIngredient(new ItemStack[]{itemStack}); + } + + public static EntryIngredient of(@NotNull ItemStack... itemStacks) { + return new EntryIngredient(Arrays.copyOf(itemStacks, itemStacks.length)); + } + + public static EntryIngredient ofIngredient(Ingredient ingredient) { + if (ingredient.isEmpty()) { + return EntryIngredient.empty(); + } + ItemStack[] itemStacks = ingredient.items() + .map(itemHolder -> new ItemStack(itemHolder, 1)) + .toArray(ItemStack[]::new); + return EntryIngredient.of(itemStacks); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/BufCustomPacketPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/BufCustomPacketPayload.java new file mode 100644 index 00000000..08092a37 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/BufCustomPacketPayload.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; + +public record BufCustomPacketPayload( + Type type, + byte[] payload +) implements LeavesCustomPayload { + @New + public static BufCustomPacketPayload create(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + return new BufCustomPacketPayload(new Type<>(location), buf.readByteArray()); + } + + @Override + public void write(FriendlyByteBuf buf) { + FriendlyByteBuf.writeByteArray(buf, this.payload); + } + + @Override + public ResourceLocation id() { + return type.id(); + } + + @NotNull + @Override + public Type type() { + return type; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java deleted file mode 100644 index 8be991f8..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.leavesmc.leaves.protocol.rei.payload; - -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -import org.leavesmc.leaves.protocol.rei.REIServerProtocol; - -public record CreateItemGrabPayload(ItemStack item) implements LeavesCustomPayload { - - private static final ResourceLocation ID = REIServerProtocol.id("create_item_grab"); - - @New - public CreateItemGrabPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { - this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC)); - } - - @Override - public void write(@NotNull FriendlyByteBuf buf) { - buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); - } - - @Override - public ResourceLocation id() { - return ID; - } -} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java deleted file mode 100644 index 8728f3e6..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.leavesmc.leaves.protocol.rei.payload; - -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -import org.leavesmc.leaves.protocol.rei.REIServerProtocol; - -public record CreateItemHotbarPayload(ItemStack item, int hotbarSlot) implements LeavesCustomPayload { - - private static final ResourceLocation ID = REIServerProtocol.id("create_item_hotbar"); - - @New - public CreateItemHotbarPayload(ResourceLocation location, FriendlyByteBuf buf) { - this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC), buf.readVarInt()); - } - - @Override - public void write(FriendlyByteBuf buf) { - buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); - buf.writeVarInt(hotbarSlot); - } - - @Override - public ResourceLocation id() { - return ID; - } -} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java deleted file mode 100644 index ef6c70f8..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.leavesmc.leaves.protocol.rei.payload; - -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -import org.leavesmc.leaves.protocol.rei.REIServerProtocol; - -public record CreateItemMessagePayload(ItemStack item, String playerName) implements LeavesCustomPayload { - - private static final ResourceLocation ID = REIServerProtocol.id("ci_msg"); - - @New - public CreateItemMessagePayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { - this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC), buf.readUtf()); - } - - @Override - public void write(@NotNull FriendlyByteBuf buf) { - buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); - buf.writeUtf(playerName); - } - - @Override - public ResourceLocation id() { - return ID; - } -} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java deleted file mode 100644 index 29ea16b2..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.leavesmc.leaves.protocol.rei.payload; - -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -import org.leavesmc.leaves.protocol.rei.REIServerProtocol; - -public record CreateItemPayload(ItemStack item) implements LeavesCustomPayload { - - private static final ResourceLocation ID = REIServerProtocol.id("create_item"); - - @New - public CreateItemPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { - this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC)); - } - - @Override - public void write(@NotNull FriendlyByteBuf buf) { - buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); - } - - @Override - public ResourceLocation id() { - return ID; - } -} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java new file mode 100644 index 00000000..f779c3c2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java @@ -0,0 +1,78 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.util.ByIdMap; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.rei.REIServerProtocol; +import org.leavesmc.leaves.protocol.rei.display.Display; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.function.IntFunction; +import java.util.function.UnaryOperator; + +public record DisplaySyncPayload( + SyncType syncType, + Collection displays, + long version +) implements CustomPacketPayload { + public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(REIServerProtocol.SYNC_DISPLAYS_PACKET); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + SyncType.STREAM_CODEC, + DisplaySyncPayload::syncType, + Display.dispatchCodec().apply(codec -> new StreamCodec() { + @Override + public void encode(@NotNull RegistryFriendlyByteBuf buf, @NotNull Display display) { + RegistryFriendlyByteBuf tmpBuf = new RegistryFriendlyByteBuf(Unpooled.buffer(), buf.registryAccess()); + try { + codec.encode(tmpBuf, display); + } catch (Exception e) { + tmpBuf.release(); + buf.writeBoolean(false); + LeavesLogger.LOGGER.warning("Failed to encode display: " + display, e); + return; + } + buf.writeBoolean(true); + RegistryFriendlyByteBuf.writeByteArray(buf, ByteBufUtil.getBytes(tmpBuf)); + tmpBuf.release(); + } + + @NotNull + @Override + public Display decode(@NotNull RegistryFriendlyByteBuf buf) { + // The DisplayDecoder will not be called on the server side + throw new UnsupportedOperationException(); + } + } + ).apply(ByteBufCodecs.>collection(ArrayList::new)).map( + collection -> collection.stream().filter(Objects::nonNull).toList(), + UnaryOperator.identity() + ), + DisplaySyncPayload::displays, + ByteBufCodecs.LONG, + DisplaySyncPayload::version, + DisplaySyncPayload::new + ); + + @Override + @NotNull + public Type type() { + return TYPE; + } + + public enum SyncType { + APPEND, + SET; + + public static final IntFunction BY_ID = ByIdMap.continuous(Enum::ordinal, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Enum::ordinal); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java deleted file mode 100644 index 760aae1d..00000000 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.leavesmc.leaves.protocol.rei.payload; - -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.NotNull; -import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -import org.leavesmc.leaves.protocol.rei.REIServerProtocol; - -public record MoveItemPayload(ResourceLocation category, boolean isShift, CompoundTag nbt) implements LeavesCustomPayload { - - private static final ResourceLocation ID = REIServerProtocol.id("move_items_new"); - - @New - public MoveItemPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { - this(buf.readResourceLocation(), buf.readBoolean(), buf.readNbt()); - } - - @Override - public void write(@NotNull FriendlyByteBuf buf) { - buf.writeResourceLocation(category); - buf.writeBoolean(isShift); - buf.writeNbt(nbt); - } - - @Override - public ResourceLocation id() { - return ID; - } -}