diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/Protocol1_21To1_21_2.java b/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/Protocol1_21To1_21_2.java index 64548c82d..db68f496f 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/Protocol1_21To1_21_2.java +++ b/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/Protocol1_21To1_21_2.java @@ -18,8 +18,6 @@ package com.viaversion.viaversion.protocols.v1_21to1_21_2; import com.viaversion.viaversion.api.connection.UserConnection; -import com.viaversion.viaversion.api.data.MappingData; -import com.viaversion.viaversion.api.data.MappingDataBase; import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; import com.viaversion.viaversion.api.minecraft.entities.EntityTypes1_21_2; import com.viaversion.viaversion.api.protocol.AbstractProtocol; @@ -39,6 +37,7 @@ import com.viaversion.viaversion.protocols.v1_20_3to1_20_5.packet.ServerboundPac import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundConfigurationPackets1_21; import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundPacket1_21; import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundPackets1_21; +import com.viaversion.viaversion.protocols.v1_21to1_21_2.data.MappingData1_21_2; import com.viaversion.viaversion.protocols.v1_21to1_21_2.packet.ClientboundPacket1_21_2; import com.viaversion.viaversion.protocols.v1_21to1_21_2.packet.ClientboundPackets1_21_2; import com.viaversion.viaversion.protocols.v1_21to1_21_2.packet.ServerboundPacket1_21_2; @@ -55,7 +54,7 @@ import static com.viaversion.viaversion.util.ProtocolUtil.packetTypeMap; public final class Protocol1_21To1_21_2 extends AbstractProtocol { - public static final MappingData MAPPINGS = new MappingDataBase("1.21", "1.21.2"); + public static final MappingData1_21_2 MAPPINGS = new MappingData1_21_2(); private final EntityPacketRewriter1_21_2 entityRewriter = new EntityPacketRewriter1_21_2(this); private final BlockItemPacketRewriter1_21_2 itemRewriter = new BlockItemPacketRewriter1_21_2(this); private final TagRewriter tagRewriter = new TagRewriter<>(this); @@ -181,7 +180,7 @@ public final class Protocol1_21To1_21_2 extends AbstractProtocol. + */ +package com.viaversion.viaversion.protocols.v1_21to1_21_2.data; + +import com.viaversion.nbt.tag.CompoundTag; +import com.viaversion.viaversion.api.data.MappingDataBase; +import com.viaversion.viaversion.api.data.MappingDataLoader; +import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; +import com.viaversion.viaversion.api.type.Types; +import java.util.ArrayList; +import java.util.List; + +public final class MappingData1_21_2 extends MappingDataBase { + + private final List recipeInputs = new ArrayList<>(); + + public MappingData1_21_2() { + super("1.21", "1.21.2"); + } + + @Override + protected void loadExtras(final CompoundTag data) { + final CompoundTag extraMappings = MappingDataLoader.INSTANCE.loadNBT("recipe-inputs-1.21.2.nbt"); + addRecipeInputs(extraMappings, "smithing_addition"); + addRecipeInputs(extraMappings, "smithing_template"); + addRecipeInputs(extraMappings, "smithing_base"); + addRecipeInputs(extraMappings, "furnace_input"); + addRecipeInputs(extraMappings, "smoker_input", "smelting_input"); // Pretty sure Mojang typo'd this + addRecipeInputs(extraMappings, "blast_furnace_input"); + addRecipeInputs(extraMappings, "campfire_input"); + } + + private void addRecipeInputs(final CompoundTag tag, final String key, final String outputKey) { + final int[] ids = tag.getIntArrayTag(key).getValue(); + recipeInputs.add(new RecipeInputs(outputKey, ids)); + } + + private void addRecipeInputs(final CompoundTag tag, final String key) { + addRecipeInputs(tag, key, key); + } + + public void writeInputs(final PacketWrapper wrapper) { + wrapper.write(Types.VAR_INT, recipeInputs.size()); + for (final RecipeInputs inputs : recipeInputs) { + wrapper.write(Types.STRING, inputs.key); + wrapper.write(Types.VAR_INT_ARRAY_PRIMITIVE, inputs.ids.clone()); + } + } + + public record RecipeInputs(String key, int[] ids) { + } +} diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/BlockItemPacketRewriter1_21_2.java b/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/BlockItemPacketRewriter1_21_2.java index 6d58feb2d..df0d9301d 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/BlockItemPacketRewriter1_21_2.java +++ b/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/BlockItemPacketRewriter1_21_2.java @@ -23,6 +23,7 @@ import com.viaversion.viaversion.api.connection.UserConnection; import com.viaversion.viaversion.api.data.FullMappings; import com.viaversion.viaversion.api.data.MappingData; import com.viaversion.viaversion.api.minecraft.Holder; +import com.viaversion.viaversion.api.minecraft.HolderSet; import com.viaversion.viaversion.api.minecraft.Particle; import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer; import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; @@ -50,6 +51,8 @@ import com.viaversion.viaversion.rewriter.StructuredItemRewriter; import com.viaversion.viaversion.util.Key; import com.viaversion.viaversion.util.TagUtil; import com.viaversion.viaversion.util.Unit; +import java.util.Arrays; +import java.util.List; import java.util.Set; public final class BlockItemPacketRewriter1_21_2 extends StructuredItemRewriter { @@ -122,23 +125,20 @@ public final class BlockItemPacketRewriter1_21_2 extends StructuredItemRewriter< protocol.registerClientbound(ClientboundPackets1_21.PLACE_GHOST_RECIPE, wrapper -> { this.updateContainerId(wrapper); + // TODO + if (true) { + wrapper.cancel(); + return; + } + final String recipe = wrapper.read(Types.STRING); - wrapper.write(Types.VAR_INT, recipeDisplay(recipe)); // TODO + wrapper.write(Types.VAR_INT, recipeDisplay(recipe)); }); protocol.registerServerbound(ServerboundPackets1_21_2.PLACE_RECIPE, wrapper -> { this.updateContainerIdServerbound(wrapper); - - final String recipe = wrapper.read(Types.STRING); - wrapper.write(Types.VAR_INT, 0); // TODO Display id, from recipe packet - wrapper.cancel(); - }); - protocol.registerServerbound(ServerboundPackets1_21_2.RECIPE_BOOK_SEEN_RECIPE, wrapper -> { - this.updateContainerIdServerbound(wrapper); - - final String recipe = wrapper.read(Types.STRING); - wrapper.write(Types.VAR_INT, 0); // TODO Display id, from recipe packet - wrapper.cancel(); + convertServerboundRecipeDisplayId(wrapper); }); + protocol.registerServerbound(ServerboundPackets1_21_2.RECIPE_BOOK_SEEN_RECIPE, this::convertServerboundRecipeDisplayId); protocol.registerServerbound(ServerboundPackets1_21_2.USE_ITEM_ON, wrapper -> { wrapper.passthrough(Types.VAR_INT); // Hand @@ -193,26 +193,25 @@ public final class BlockItemPacketRewriter1_21_2 extends StructuredItemRewriter< }); protocol.registerClientbound(ClientboundPackets1_21.UPDATE_RECIPES, wrapper -> { + // TODO Handle resending. Updating them in newer versions isn't as easy anymore + final FullMappings recipeSerializerMappings = protocol.getMappingData().getRecipeSerializerMappings(); final RecipeRewriter1_21_2 rewriter = new RecipeRewriter1_21_2(protocol); // Holds state, create a new one for each packet - wrapper.cancel(); // TODO + wrapper.user().put(rewriter); - final int size = wrapper.passthrough(Types.VAR_INT); - int newSize = size; + final int size = wrapper.read(Types.VAR_INT); for (int i = 0; i < size; i++) { final String recipeIdentifier = wrapper.read(Types.STRING); - - final FullMappings recipeSerializerMappings = protocol.getMappingData().getRecipeSerializerMappings(); - final int typeId = wrapper.read(Types.VAR_INT); - final int mappedId = recipeSerializerMappings.getNewId(typeId); - if (mappedId != -1) { - wrapper.write(Types.STRING, recipeIdentifier); - wrapper.write(Types.VAR_INT, mappedId); - } else { - wrapper.set(Types.VAR_INT, 0, --newSize); - } - - rewriter.handleRecipeType(wrapper, Key.stripMinecraftNamespace(recipeSerializerMappings.identifier(typeId))); // Use the original + final int serializerTypeId = wrapper.read(Types.VAR_INT); + final String serializerTypeIdentifier = recipeSerializerMappings.identifier(serializerTypeId); + rewriter.setCurrentRecipeIdentifier(recipeIdentifier); + rewriter.handleRecipeType(wrapper, serializerTypeIdentifier); } + + // These are used for client predictions, such as what items can be used as fuel in a furnace + protocol.getMappingData().writeInputs(wrapper); + + // TODO Stonecutter ingredients + wrapper.write(Types.VAR_INT, 0); }); protocol.registerClientbound(ClientboundPackets1_21.RECIPE, ClientboundPackets1_21_2.RECIPE_BOOK_ADD, wrapper -> { @@ -225,33 +224,99 @@ public final class BlockItemPacketRewriter1_21_2 extends StructuredItemRewriter< } settingsPacket.send(Protocol1_21To1_21_2.class); - // TODO - wrapper.cancel(); - - final String[] recipes = wrapper.passthrough(Types.STRING_ARRAY); + // Read recipe keys from the packet + final String[] recipes = wrapper.read(Types.STRING_ARRAY); Set toHighlight = Set.of(); if (state == 0) { // Init (1=Add, 2=Remove) - final String[] highlightRecipes = wrapper.passthrough(Types.STRING_ARRAY); + final String[] highlightRecipes = wrapper.read(Types.STRING_ARRAY); toHighlight = Set.of(highlightRecipes); } - wrapper.write(Types.VAR_INT, recipes.length); - for (final String recipe : recipes) { - // Display id - // Display type - // Optional group - // Category type - // Item contents + final RecipeRewriter1_21_2 recipeRewriter = wrapper.user().get(RecipeRewriter1_21_2.class); + if (recipeRewriter == null) { + protocol.getLogger().severe("Recipes not yet sent for recipe add packet"); + wrapper.cancel(); + return; + } + + wrapper.clearPacket(); + + if (state == 2) { + // Remove recipes + wrapper.setPacketType(ClientboundPackets1_21_2.RECIPE_BOOK_REMOVE); + + final int[] ids = new int[recipes.length]; + for (final String recipeKey : recipes) { + final RecipeRewriter1_21_2.Recipe recipe = recipeRewriter.recipe(recipeKey); + if (recipe == null) { + protocol.getLogger().severe("Recipe not found for key " + recipeKey); + wrapper.cancel(); + return; + } + + ids[recipe.index()] = recipe.index(); + } + + wrapper.write(Types.VAR_INT_ARRAY_PRIMITIVE, ids); + return; + } + + // Add or init recipes + int size = recipes.length; + wrapper.write(Types.VAR_INT, size); + for (final String recipeKey : recipes) { + final RecipeRewriter1_21_2.Recipe recipe = recipeRewriter.recipe(recipeKey); + if (recipe == null) { + // Special recipes, or bad data + size--; + continue; + } + + wrapper.write(Types.VAR_INT, recipe.index()); // Display ID, just an arbitrary index as determined by the server + + wrapper.write(Types.VAR_INT, recipe.recipeDisplayId()); + recipe.writeRecipeDisplay(wrapper); + + wrapper.write(Types.OPTIONAL_VAR_INT, recipe.group() != -1 ? recipe.group() : null); + wrapper.write(Types.VAR_INT, recipe.category()); + + final Item[][] ingredients = recipe.ingredients(); + if (ingredients != null) { + wrapper.write(Types.BOOLEAN, true); + + // Why are some of these empty? Who knows, but they can't be + final List filteredIngredients = Arrays.stream(ingredients).filter(ingredient -> ingredient.length > 0).map(recipeRewriter::toHolderSet).toList(); + wrapper.write(Types.VAR_INT, filteredIngredients.size()); + for (final HolderSet ingredient : filteredIngredients) { + wrapper.write(Types.HOLDER_SET, ingredient); + } + } else { + wrapper.write(Types.BOOLEAN, false); + } byte flags = 0; - if (toHighlight.contains(recipe)) { + if (toHighlight.contains(recipeKey)) { flags |= (1 << 1); // Highlight } wrapper.write(Types.BYTE, flags); } + + // Update final size + wrapper.set(Types.VAR_INT, 0, size); }); } + private void convertServerboundRecipeDisplayId(final PacketWrapper wrapper) { + final int recipeDisplayId = wrapper.read(Types.VAR_INT); + final RecipeRewriter1_21_2.Recipe recipe = wrapper.user().get(RecipeRewriter1_21_2.class).recipe(recipeDisplayId); + if (recipe == null) { + wrapper.cancel(); + return; + } + + wrapper.write(Types.STRING, recipe.identifier()); + } + @Override public Item handleItemToClient(final UserConnection connection, final Item item) { super.handleItemToClient(connection, item); diff --git a/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/RecipeRewriter1_21_2.java b/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/RecipeRewriter1_21_2.java index 364a7db3a..24cf5376b 100644 --- a/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/RecipeRewriter1_21_2.java +++ b/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/rewriter/RecipeRewriter1_21_2.java @@ -17,72 +17,173 @@ */ package com.viaversion.viaversion.protocols.v1_21to1_21_2.rewriter; +import com.viaversion.viaversion.api.connection.StorableObject; import com.viaversion.viaversion.api.data.MappingData; import com.viaversion.viaversion.api.minecraft.HolderSet; import com.viaversion.viaversion.api.minecraft.item.Item; +import com.viaversion.viaversion.api.minecraft.item.StructuredItem; import com.viaversion.viaversion.api.protocol.Protocol; import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.api.type.types.version.Types1_21_2; import com.viaversion.viaversion.protocols.v1_20_2to1_20_3.rewriter.RecipeRewriter1_20_3; import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundPacket1_21; +import com.viaversion.viaversion.util.Key; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntCollection; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; -final class RecipeRewriter1_21_2 extends RecipeRewriter1_20_3 { +// Use directly as a connection storage. Slightly weird, but easiest and closed off from other packages +final class RecipeRewriter1_21_2 extends RecipeRewriter1_20_3 implements StorableObject { + private static final int FALLBACK_CATEGORY = 3; // TODO ? private final List shapelessRecipes = new ArrayList<>(); private final List shapedRecipes = new ArrayList<>(); private final List smithingRecipes = new ArrayList<>(); private final List stoneCutterRecipes = new ArrayList<>(); - private final List simpleRecipes = new ArrayList<>(); + private final List furnaceRecipes = new ArrayList<>(); private final Object2IntMap recipeGroups = new Object2IntOpenHashMap<>(); - // TODO - private final IntList smithingAddition = new IntArrayList(); - private final IntList smithingTemplate = new IntArrayList(); - private final IntList smithingBase = new IntArrayList(); - private final IntList furnaceInput = new IntArrayList(); - private final IntList smokerInput = new IntArrayList(); - private final IntList blastFurnaceInput = new IntArrayList(); - private final IntList campfireInput = new IntArrayList(); + private final Map recipesByKey = new HashMap<>(); + private final List recipes = new ArrayList<>(); + private String currentRecipeIdentifier; - record SimpleRecipe(int category) { - } - - record ShapelessRecipe(int group, int category, HolderSet[] ingredients, Item result) { - } - - record ShapedRecipe(int group, int category, int width, int height, HolderSet[] ingredients, Item result) { - } - - record StoneCutterRecipe(int group, HolderSet ingredient, Item result) { - } - - record SmithingRecipe(HolderSet ingredient, Item result) { + public void setCurrentRecipeIdentifier(final String recipeIdentifier) { + this.currentRecipeIdentifier = Key.stripMinecraftNamespace(recipeIdentifier); } RecipeRewriter1_21_2(final Protocol protocol) { super(protocol); - // TODO ? - recipeHandlers.put("smelting", wrapper -> handleSmelting(wrapper, furnaceInput)); - recipeHandlers.put("blasting", wrapper -> handleSmelting(wrapper, blastFurnaceInput)); - recipeHandlers.put("smoking", wrapper -> handleSmelting(wrapper, smokerInput)); - recipeHandlers.put("campfire_cooking", wrapper -> handleSmelting(wrapper, campfireInput)); recipeGroups.defaultReturnValue(-1); } - private HolderSet readIngredient(final PacketWrapper wrapper) { - // Ingredients are no longer full items, already store them as just holder sets + @Override + public void handleSimpleRecipe(final PacketWrapper wrapper) { + final int category = wrapper.read(Types.VAR_INT); + // Special recipes aren't sent to the client + } + + @Override + public void handleStonecutting(final PacketWrapper wrapper) { + final int group = recipeGroupId(wrapper.read(Types.STRING)); + final Item[] ingredient = readIngredient(wrapper); + final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); + + final StoneCutterRecipe recipe = new StoneCutterRecipe(recipesByKey.size(), currentRecipeIdentifier, group, ingredient, result); + addRecipe(stoneCutterRecipes, recipe); + } + + private void addRecipe(final List list, final T recipe) { + list.add(recipe); + recipes.add(recipe); + recipesByKey.put(currentRecipeIdentifier, recipe); + } + + private int recipeGroupId(final String recipeGroup) { + final int size = recipeGroups.size(); + final int value = recipeGroups.putIfAbsent(Key.stripMinecraftNamespace(recipeGroup), size); + return value != -1 ? value : size; + } + + @Override + public void handleSmithingTransform(final PacketWrapper wrapper) { + final IntList template = new IntArrayList(); + readIngredientToList(wrapper, template); + //smithingTemplate.addAll(template); + readIngredient(wrapper); + readIngredient(wrapper); + //readIngredientToList(wrapper, smithingBase); + //readIngredientToList(wrapper, smithingAddition); + final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); + + // TODO Correct ingredients? + final Item[] ingredients = new Item[template.size()]; + for (int i = 0; i < template.size(); i++) { + ingredients[i] = new StructuredItem(template.getInt(i), 1); + } + + final SmithingRecipe recipe = new SmithingRecipe(recipesByKey.size(), currentRecipeIdentifier, ingredients, result); + addRecipe(smithingRecipes, recipe); + } + + @Override + public void handleSmithingTrim(final PacketWrapper wrapper) { + readIngredient(wrapper); + readIngredient(wrapper); + readIngredient(wrapper); + //readIngredientToList(wrapper, smithingTemplate); + //readIngredientToList(wrapper, smithingBase); + //readIngredientToList(wrapper, smithingAddition); + } + + @Override + public void handleCraftingShaped(final PacketWrapper wrapper) { + final int group = recipeGroupId(wrapper.read(Types.STRING)); + final int category = wrapper.read(Types.VAR_INT); + final int width = wrapper.read(Types.VAR_INT); + final int height = wrapper.read(Types.VAR_INT); + final int ingredientsSize = width * height; + final Item[][] ingredients = new Item[ingredientsSize][]; + for (int i = 0; i < ingredientsSize; i++) { + ingredients[i] = readIngredient(wrapper); + } + final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); + + final ShapedRecipe recipe = new ShapedRecipe(recipesByKey.size(), currentRecipeIdentifier, group, category, width, height, ingredients, result); + addRecipe(shapedRecipes, recipe); + + wrapper.read(Types.BOOLEAN); // Show notification + } + + @Override + public void handleCraftingShapeless(final PacketWrapper wrapper) { + final int group = recipeGroupId(wrapper.read(Types.STRING)); + final int category = wrapper.read(Types.VAR_INT); + final int ingredientsSize = wrapper.read(Types.VAR_INT); + final Item[][] ingredients = new Item[ingredientsSize][]; + for (int i = 0; i < ingredientsSize; i++) { + ingredients[i] = readIngredient(wrapper); + } + final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); + + final ShapelessRecipe recipe = new ShapelessRecipe(recipesByKey.size(), currentRecipeIdentifier, group, category, ingredients, result); + addRecipe(shapelessRecipes, recipe); + } + + @Override + public void handleSmelting(final PacketWrapper wrapper) { + final int group = recipeGroupId(wrapper.read(Types.STRING)); + final int category = wrapper.read(Types.VAR_INT); + final Item[] ingredient = readIngredient(wrapper); + final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); + + final FurnaceRecipe recipe = new FurnaceRecipe(recipesByKey.size(), currentRecipeIdentifier, group, category, ingredient, result); + addRecipe(furnaceRecipes, recipe); + + wrapper.read(Types.FLOAT); // EXP + wrapper.read(Types.VAR_INT); // Cooking time + } + + private Item[] readIngredient(final PacketWrapper wrapper) { final Item[] items = wrapper.read(itemArrayType()); - final int[] ids = new int[items.length]; - final MappingData mappings = protocol.getMappingData(); for (int i = 0; i < items.length; i++) { final Item item = items[i]; - ids[i] = mappings.getNewItemId(item.identifier()); + items[i] = rewrite(wrapper.user(), item); + } + return items; + } + + public HolderSet toHolderSet(final Item[] ingredient) { + // Ingredients are no longer full items, already store them as just holder sets + final int[] ids = new int[ingredient.length]; + for (int i = 0; i < ingredient.length; i++) { + ids[i] = ingredient[i].identifier(); } return HolderSet.of(ids); } @@ -96,82 +197,149 @@ final class RecipeRewriter1_21_2 extends RecipeRewriter1_20_3= 0 && displayId < recipes.size() ? recipes.get(displayId) : null; } - private int recipeGroupId(final String recipeGroup) { - final int size = recipeGroups.size(); - final int value = recipeGroups.putIfAbsent(recipeGroup, size); - return value != -1 ? value : size; - } + interface Recipe { + int SLOT_DISPLAY_EMPTY = 0; + int SLOT_DISPLAY_ANY_FUEL = 1; + int SLOT_DISPLAY_ITEM = 3; + int SLOT_DISPLAY_COMPOSITE = 6; - @Override - public void handleSmithingTransform(final PacketWrapper wrapper) { - final IntList template = new IntArrayList(); - readIngredientToList(wrapper, template); - smithingTemplate.addAll(template); - readIngredientToList(wrapper, smithingBase); - readIngredientToList(wrapper, smithingAddition); - final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); - smithingRecipes.add(new SmithingRecipe(HolderSet.of(template.toIntArray()), result)); // TODO ? - } + int index(); - @Override - public void handleSmithingTrim(final PacketWrapper wrapper) { - readIngredientToList(wrapper, smithingTemplate); - readIngredientToList(wrapper, smithingBase); - readIngredientToList(wrapper, smithingAddition); - } + String identifier(); - @Override - public void handleCraftingShaped(final PacketWrapper wrapper) { - final int group = recipeGroupId(wrapper.read(Types.STRING)); - final int category = wrapper.read(Types.VAR_INT); - final int width = wrapper.read(Types.VAR_INT); - final int height = wrapper.read(Types.VAR_INT); - final int ingredientsSize = width * height; - final HolderSet[] ingredients = new HolderSet[ingredientsSize]; - for (int i = 0; i < ingredientsSize; i++) { - ingredients[i] = readIngredient(wrapper); - } - final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); - shapedRecipes.add(new ShapedRecipe(group, category, width, height, ingredients, result)); + int recipeDisplayId(); - wrapper.read(Types.BOOLEAN); // Show notification - } - - @Override - public void handleCraftingShapeless(final PacketWrapper wrapper) { - final int group = recipeGroupId(wrapper.read(Types.STRING)); - final int category = wrapper.read(Types.VAR_INT); - final int ingredientsSize = wrapper.read(Types.VAR_INT); - final HolderSet[] ingredients = new HolderSet[ingredientsSize]; - for (int i = 0; i < ingredientsSize; i++) { - ingredients[i] = readIngredient(wrapper); + default Item @Nullable [][] ingredients() { + final Item[] ingredient = ingredient(); + return ingredient == null ? null : new Item[][]{ingredient}; } - final Item result = rewrite(wrapper.user(), wrapper.read(itemType())); - shapelessRecipes.add(new ShapelessRecipe(group, category, ingredients, result)); + default Item @Nullable [] ingredient() { + return null; + } + + default int group() { + return -1; + } + + default int category() { + return FALLBACK_CATEGORY; + } + + void writeRecipeDisplay(PacketWrapper wrapper); } - private void handleSmelting(final PacketWrapper wrapper, final IntCollection list) { - final int group = recipeGroupId(wrapper.read(Types.STRING)); - final int category = wrapper.read(Types.VAR_INT); - readIngredientToList(wrapper, list); - wrapper.read(itemType()); // Result + record ShapelessRecipe(int index, String identifier, int group, int category, Item[][] ingredients, + Item result) implements Recipe { + @Override + public int recipeDisplayId() { + return 0; + } - wrapper.read(Types.FLOAT); // EXP - wrapper.read(Types.VAR_INT); // Cooking time + @Override + public void writeRecipeDisplay(final PacketWrapper wrapper) { + writeIngredientsDisplay(wrapper, ingredients); + writeItemDisplay(wrapper, result); + writeCraftingStationDisplay(wrapper); + } + } + + record ShapedRecipe(int index, String identifier, int group, int category, int width, int height, + Item[][] ingredients, Item result) implements Recipe { + @Override + public int recipeDisplayId() { + return 1; + } + + @Override + public void writeRecipeDisplay(final PacketWrapper wrapper) { + wrapper.write(Types.VAR_INT, width); + wrapper.write(Types.VAR_INT, height); + writeIngredientsDisplay(wrapper, ingredients); + writeItemDisplay(wrapper, result); + writeCraftingStationDisplay(wrapper); + } + } + + record FurnaceRecipe(int index, String identifier, int group, int category, Item[] ingredient, + Item result) implements Recipe { + @Override + public int recipeDisplayId() { + return 2; + } + + @Override + public void writeRecipeDisplay(final PacketWrapper wrapper) { + writeIngredientDisplay(wrapper, ingredient); + wrapper.write(Types.VAR_INT, SLOT_DISPLAY_ANY_FUEL); // Fuel + writeItemDisplay(wrapper, result); + writeCraftingStationDisplay(wrapper); + } + } + + record StoneCutterRecipe(int index, String identifier, int group, Item[] ingredient, + Item result) implements Recipe { + @Override + public int recipeDisplayId() { + return 3; + } + + @Override + public void writeRecipeDisplay(final PacketWrapper wrapper) { + writeItemDisplay(wrapper, result); + writeCraftingStationDisplay(wrapper); + } + } + + record SmithingRecipe(int index, String identifier, Item[] ingredient, Item result) implements Recipe { + @Override + public int recipeDisplayId() { + return 4; + } + + @Override + public void writeRecipeDisplay(final PacketWrapper wrapper) { + writeItemDisplay(wrapper, result); + writeCraftingStationDisplay(wrapper); + } + } + + private static void writeIngredientsDisplay(final PacketWrapper wrapper, final Item[][] ingredients) { + // TODO Check what the 24w40a server sends for shapeless/shaped ingredient slot displays + wrapper.write(Types.VAR_INT, ingredients.length); + for (final Item[] ingredient : ingredients) { + writeIngredientDisplay(wrapper, ingredient); + } + } + + private static void writeIngredientDisplay(final PacketWrapper wrapper, final Item[] ingredient) { + if (ingredient.length == 0) { + wrapper.write(Types.VAR_INT, Recipe.SLOT_DISPLAY_EMPTY); + return; + } + + // TODO Check what's sent by the server + wrapper.write(Types.VAR_INT, Recipe.SLOT_DISPLAY_COMPOSITE); + wrapper.write(Types.VAR_INT, ingredient.length); + for (final Item item : ingredient) { + writeItemDisplay(wrapper, item); + } + } + + private static void writeItemDisplay(final PacketWrapper wrapper, final Item item) { + wrapper.write(Types.VAR_INT, Recipe.SLOT_DISPLAY_ITEM); + wrapper.write(Types1_21_2.ITEM, item); + } + + private static void writeCraftingStationDisplay(final PacketWrapper wrapper) { + wrapper.write(Types.VAR_INT, Recipe.SLOT_DISPLAY_EMPTY); } } diff --git a/common/src/main/resources/assets/viaversion/data/recipe-inputs-1.21.2.nbt b/common/src/main/resources/assets/viaversion/data/recipe-inputs-1.21.2.nbt new file mode 100644 index 000000000..90ae695c2 Binary files /dev/null and b/common/src/main/resources/assets/viaversion/data/recipe-inputs-1.21.2.nbt differ