diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java index 9d98e9fb3..8b7fa9522 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserRecipe.java @@ -25,6 +25,9 @@ package org.geysermc.geyser.inventory.recipe; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; + /** * A more compact version of {@link org.geysermc.mcprotocollib.protocol.data.game.recipe.Recipe}. */ @@ -33,4 +36,7 @@ public interface GeyserRecipe { * Whether the recipe is flexible or not in which items can be placed where. */ boolean isShaped(); + + @Nullable + ItemStack result(); } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java b/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java index cf0105622..4af2b4630 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/BannerItem.java @@ -60,9 +60,6 @@ public class BannerItem extends BlockItem { */ private static final List> OMINOUS_BANNER_PATTERN; - // TODO fix - we somehow need to be able to get the sessions banner pattern registry, which we don't have where we need this :/ - private static final int[] ominousBannerPattern = new int[] { 21, 29, 30, 1, 34, 15, 3, 1 }; - static { // Construct what an ominous banner is supposed to look like OMINOUS_BANNER_PATTERN = List.of( @@ -215,20 +212,22 @@ public class BannerItem extends BlockItem { } @Override - public void translateNbtToJava(@NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { - super.translateNbtToJava(bedrockTag, components, mapping); + public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { + super.translateNbtToJava(session, bedrockTag, components, mapping); if (bedrockTag.getInt("Type") == 1) { // Ominous banner pattern List patternLayers = new ArrayList<>(); - for (int i = 0; i < ominousBannerPattern.length; i++) { - patternLayers.add(new BannerPatternLayer(Holder.ofId(ominousBannerPattern[i]), OMINOUS_BANNER_PATTERN.get(i).right().ordinal())); + for (int i = 0; i < OMINOUS_BANNER_PATTERN.size(); i++) { + var pair = OMINOUS_BANNER_PATTERN.get(i); + patternLayers.add(new BannerPatternLayer(Holder.ofId(session.getRegistryCache().bannerPatterns().byValue(pair.left())), + pair.right().ordinal())); } components.put(DataComponentType.BANNER_PATTERNS, patternLayers); components.put(DataComponentType.HIDE_ADDITIONAL_TOOLTIP, Unit.INSTANCE); components.put(DataComponentType.ITEM_NAME, Component - .translatable("block.minecraft.ominous_banner") // thank god this works + .translatable("block.minecraft.ominous_banner") .style(Style.style(TextColor.color(16755200))) ); } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java index 540270555..8b0f3e22e 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/EnchantedBookItem.java @@ -32,6 +32,7 @@ import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtType; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.item.BedrockEnchantment; +import org.geysermc.geyser.item.enchantment.Enchantment; import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.item.BedrockItemBuilder; @@ -69,8 +70,8 @@ public class EnchantedBookItem extends Item { } @Override - public void translateNbtToJava(@NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { - super.translateNbtToJava(bedrockTag, components, mapping); + public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { + super.translateNbtToJava(session, bedrockTag, components, mapping); List enchantmentTag = bedrockTag.getList("ench", NbtType.COMPOUND); if (enchantmentTag != null) { @@ -80,9 +81,14 @@ public class EnchantedBookItem extends Item { BedrockEnchantment enchantment = BedrockEnchantment.getByBedrockId(bedrockId); if (enchantment != null) { - int level = bedrockEnchantment.getShort("lvl", (short) 1); - // TODO - //javaEnchantments.put(BedrockEnchantment.JavaEnchantment.valueOf(enchantment.name()).ordinal(), level); + List enchantments = session.getRegistryCache().enchantments().values(); + for (int i = 0; i < enchantments.size(); i++) { + if (enchantments.get(i).bedrockEnchantment() == enchantment) { + int level = bedrockEnchantment.getShort("lvl", (short) 1); + javaEnchantments.put(i, level); + break; + } + } } else { GeyserImpl.getInstance().getLogger().debug("Unknown bedrock enchantment: " + bedrockId); } diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java index c70467b4c..9c637afde 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FireworkRocketItem.java @@ -70,8 +70,8 @@ public class FireworkRocketItem extends Item { } @Override - public void translateNbtToJava(@NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { - super.translateNbtToJava(bedrockTag, components, mapping); + public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { + super.translateNbtToJava(session, bedrockTag, components, mapping); NbtMap fireworksTag = bedrockTag.getCompound("Fireworks"); if (!fireworksTag.isEmpty()) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java b/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java index 18234975d..2ba9b4258 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/FireworkStarItem.java @@ -78,8 +78,8 @@ public class FireworkStarItem extends Item { } @Override - public void translateNbtToJava(@NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { - super.translateNbtToJava(bedrockTag, components, mapping); + public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { + super.translateNbtToJava(session, bedrockTag, components, mapping); NbtMap explosion = bedrockTag.getCompound("FireworksItem"); if (!explosion.isEmpty()) { diff --git a/core/src/main/java/org/geysermc/geyser/item/type/Item.java b/core/src/main/java/org/geysermc/geyser/item/type/Item.java index 2caa65dac..1ebf85065 100644 --- a/core/src/main/java/org/geysermc/geyser/item/type/Item.java +++ b/core/src/main/java/org/geysermc/geyser/item/type/Item.java @@ -172,7 +172,7 @@ public class Item { * * Therefore, if translation cannot be achieved for a certain item, it is not necessarily bad. */ - public void translateNbtToJava(@NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { + public void translateNbtToJava(@NonNull GeyserSession session, @NonNull NbtMap bedrockTag, @NonNull DataComponents components, @NonNull ItemMapping mapping) { // TODO see if any items from the creative menu need this // CompoundTag displayTag = tag.get("display"); // if (displayTag != null) { @@ -190,41 +190,6 @@ public class Item { // } // displayTag.put(new ListTag("Lore", lore)); // } -// } - - // TODO no creative item should have enchantments *except* enchanted books -// List enchantmentTag = bedrockTag.getList("ench", NbtType.COMPOUND); -// if (enchantmentTag != null) { -// List enchantments = new ArrayList<>(); -// for (Tag value : enchantmentTag.getValue()) { -// if (!(value instanceof CompoundTag tagValue)) -// continue; -// -// ShortTag bedrockId = tagValue.get("id"); -// if (bedrockId == null) continue; -// -// BedrockEnchantment enchantment = BedrockEnchantment.getByBedrockId(bedrockId.getValue()); -// if (enchantment != null) { -// CompoundTag javaTag = new CompoundTag(""); -// Map javaValue = javaTag.getValue(); -// javaValue.put("id", new StringTag("id", enchantment.getJavaIdentifier())); -// ShortTag levelTag = tagValue.get("lvl"); -// javaValue.put("lvl", new IntTag("lvl", levelTag != null ? levelTag.getValue() : 1)); -// javaTag.setValue(javaValue); -// -// enchantments.add(javaTag); -// } else { -// GeyserImpl.getInstance().getLogger().debug("Unknown bedrock enchantment: " + bedrockId); -// } -// } -// if (!enchantments.isEmpty()) { -// if ((this instanceof EnchantedBookItem)) { -// bedrockTag.put(new ListTag("StoredEnchantments", enchantments)); -// components.put(DataComponentType.STORED_ENCHANTMENTS, enchantments); -// } else { -// components.put(DataComponentType.ENCHANTMENTS, enchantments); -// } -// } // } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index 7815768ef..deafbdf7e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -25,14 +25,12 @@ package org.geysermc.geyser.registry; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData; -import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.pack.ResourcePack; @@ -42,7 +40,7 @@ import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.loader.*; import org.geysermc.geyser.registry.populator.ItemRegistryPopulator; import org.geysermc.geyser.registry.populator.PacketRegistryPopulator; -import org.geysermc.geyser.registry.populator.RecipeRegistryPopulator; +import org.geysermc.geyser.registry.loader.RecipeRegistryLoader; import org.geysermc.geyser.registry.provider.ProviderSupplier; import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.ParticleMapping; @@ -95,11 +93,6 @@ public final class Registries { */ public static final SimpleMappedRegistry BLOCK_ENTITIES = SimpleMappedRegistry.create("org.geysermc.geyser.translator.level.block.entity.BlockEntity", BlockEntityRegistryLoader::new); - /** - * A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link RecipeData}. - */ - public static final VersionedRegistry>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); - /** * A map containing all entity types and their respective Geyser definitions */ @@ -147,7 +140,7 @@ public final class Registries { /** * A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value. */ - public static final VersionedRegistry> RECIPES = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); + public static final SimpleMappedRegistry> RECIPES = SimpleMappedRegistry.create("mappings/recipes.nbt", RecipeRegistryLoader::new); /** * A mapped registry holding {@link ResourcePack}'s with the pack uuid as keys. @@ -176,7 +169,7 @@ public final class Registries { static { PacketRegistryPopulator.populate(); ItemRegistryPopulator.populate(); - RecipeRegistryPopulator.populate(); + System.out.println(RECIPES.get()); // Create registries that require other registries to load first POTION_MIXES = VersionedRegistry.create(PotionMixRegistryLoader::new); diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/RecipeRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/RecipeRegistryLoader.java new file mode 100644 index 000000000..5d1236581 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/RecipeRegistryLoader.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.registry.loader; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.cloudburstmc.nbt.NBTInputStream; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; +import org.geysermc.geyser.text.GeyserLocale; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec; +import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.Ingredient; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.RecipeType; + +import java.io.DataInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * Populates the recipe registry with some recipes that Java does not send, to ensure they show up as intended + * in the recipe book. + */ +public final class RecipeRegistryLoader implements RegistryLoader>> { + + @Override + public Map> load(String input) { + Map> deserializedRecipes = new Object2ObjectOpenHashMap<>(); + + List recipes; + try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("mappings/recipes.nbt")) { + try (NBTInputStream nbtStream = new NBTInputStream(new DataInputStream(stream))) { + recipes = ((NbtMap) nbtStream.readTag()).getList("recipes", NbtType.COMPOUND); + } + } catch (Exception e) { + throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.toolbox.fail.runtime_java"), e); + } + + MinecraftCodecHelper helper = MinecraftCodec.CODEC.getHelperFactory().get(); + for (NbtMap recipeCollection : recipes) { + var pair = getRecipes(recipeCollection, helper); + deserializedRecipes.put(pair.key(), pair.value()); + } + return deserializedRecipes; + } + + private static Pair> getRecipes(NbtMap recipes, MinecraftCodecHelper helper) { + List typedRecipes = recipes.getList("recipes", NbtType.COMPOUND); + RecipeType recipeType = RecipeType.from(recipes.getInt("recipe_type", -1)); + if (recipeType == RecipeType.CRAFTING_SPECIAL_TIPPEDARROW) { + return Pair.of(recipeType, getShapedRecipes(typedRecipes, helper)); + } else { + return Pair.of(recipeType, getShapelessRecipes(typedRecipes, helper)); + } + } + + private static List getShapelessRecipes(List recipes, MinecraftCodecHelper helper) { + List deserializedRecipes = new ObjectArrayList<>(recipes.size()); + for (NbtMap recipe : recipes) { + ItemStack output = toItemStack(recipe.getCompound("output"), helper); + List rawInputs = recipe.getList("inputs", NbtType.COMPOUND); + Ingredient[] javaInputs = new Ingredient[rawInputs.size()]; + for (int i = 0; i < rawInputs.size(); i++) { + javaInputs[i] = new Ingredient(new ItemStack[] {toItemStack(rawInputs.get(i), helper)}); + } + deserializedRecipes.add(new GeyserShapelessRecipe(javaInputs, output)); + } + return deserializedRecipes; + } + + private static List getShapedRecipes(List recipes, MinecraftCodecHelper helper) { + List deserializedRecipes = new ObjectArrayList<>(recipes.size()); + for (NbtMap recipe : recipes) { + ItemStack output = toItemStack(recipe.getCompound("output"), helper); + List shape = recipe.getList("shape", NbtType.INT_ARRAY); + + // In the recipes mapping, each recipe is mapped by a number + List letterToRecipe = new ArrayList<>(); + for (NbtMap rawInput : recipe.getList("inputs", NbtType.COMPOUND)) { + letterToRecipe.add(toItemStack(rawInput, helper)); + } + + Ingredient[] inputs = new Ingredient[shape.size() * shape.get(0).length]; + int i = 0; + // Create a linear array of items from the "cube" of the shape + for (int j = 0; i < shape.size() * shape.get(0).length; j++) { + for (int index : shape.get(j)) { + ItemStack stack = letterToRecipe.get(index); + inputs[i++] = new Ingredient(new ItemStack[] {stack}); + } + } + deserializedRecipes.add(new GeyserShapedRecipe(shape.size(), shape.get(0).length, inputs, output)); + } + return deserializedRecipes; + } + + /** + * Converts our serialized NBT into an ItemStack. + * id is the Java item ID as an integer, components is an optional String of the data components serialized + * as bytes in Base64 (so MCProtocolLib can parse the data). + */ + private static ItemStack toItemStack(NbtMap nbt, MinecraftCodecHelper helper) { + int id = nbt.getInt("id"); + int count = nbt.getInt("count"); + String componentsRaw = nbt.getString("components", null); + if (componentsRaw != null) { + byte[] bytes = Base64.getDecoder().decode(componentsRaw); + ByteBuf buf = Unpooled.wrappedBuffer(bytes); + DataComponents components = helper.readDataComponentPatch(buf); + return new ItemStack(id, count, components); + } + return new ItemStack(id, count); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java deleted file mode 100644 index 4c6d53518..000000000 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/RecipeRegistryPopulator.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Geyser - */ - -package org.geysermc.geyser.registry.populator; - -import com.fasterxml.jackson.databind.JsonNode; -import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; -import org.geysermc.mcprotocollib.protocol.data.game.recipe.Ingredient; -import org.geysermc.mcprotocollib.protocol.data.game.recipe.RecipeType; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtUtils; -import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; -import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.RecipeUnlockingRequirement; -import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.MultiRecipeData; -import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData; -import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData; -import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData; -import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; -import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.inventory.recipe.GeyserRecipe; -import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; -import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; -import org.geysermc.geyser.registry.Registries; -import org.geysermc.geyser.registry.type.ItemMapping; -import org.geysermc.geyser.registry.type.ItemMappings; -import org.geysermc.geyser.text.GeyserLocale; -import org.geysermc.geyser.translator.item.ItemTranslator; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; - -import static org.geysermc.geyser.util.InventoryUtils.LAST_RECIPE_NET_ID; - -/** - * Populates the recipe registry. - */ -public class RecipeRegistryPopulator { - - public static void populate() { - JsonNode items; - try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResourceOrThrow("mappings/recipes.json")) { - items = GeyserImpl.JSON_MAPPER.readTree(stream); - } catch (Exception e) { - throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.toolbox.fail.runtime_java"), e); - } - - int currentRecipeId = LAST_RECIPE_NET_ID; - for (Int2ObjectMap.Entry version : Registries.ITEMS.get().int2ObjectEntrySet()) { - // Make a bit of an assumption here that the last recipe net ID will be equivalent between all versions - LAST_RECIPE_NET_ID = currentRecipeId; - Map> craftingData = new EnumMap<>(RecipeType.class); - Int2ObjectMap recipes = new Int2ObjectOpenHashMap<>(); - - craftingData.put(RecipeType.CRAFTING_SPECIAL_BOOKCLONING, - Collections.singletonList(MultiRecipeData.of(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), ++LAST_RECIPE_NET_ID))); - craftingData.put(RecipeType.CRAFTING_SPECIAL_REPAIRITEM, - Collections.singletonList(MultiRecipeData.of(UUID.fromString("00000000-0000-0000-0000-000000000001"), ++LAST_RECIPE_NET_ID))); - craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPEXTENDING, - Collections.singletonList(MultiRecipeData.of(UUID.fromString("d392b075-4ba1-40ae-8789-af868d56f6ce"), ++LAST_RECIPE_NET_ID))); - craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPCLONING, - Collections.singletonList(MultiRecipeData.of(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), ++LAST_RECIPE_NET_ID))); - - // https://github.com/pmmp/PocketMine-MP/blob/stable/src/pocketmine/inventory/MultiRecipe.php - - for (JsonNode entry : items.get("leather_armor")) { - // This won't be perfect, as we can't possibly send every leather input for every kind of color - // But it does display the correct output from a base leather armor, and besides visuals everything works fine - craftingData.computeIfAbsent(RecipeType.CRAFTING_SPECIAL_ARMORDYE, - c -> new ObjectArrayList<>()).add(getCraftingDataFromJsonNode(entry, recipes, version.getValue())); - } - for (JsonNode entry : items.get("firework_rockets")) { - craftingData.computeIfAbsent(RecipeType.CRAFTING_SPECIAL_FIREWORK_ROCKET, - c -> new ObjectArrayList<>()).add(getCraftingDataFromJsonNode(entry, recipes, version.getValue())); - } - for (JsonNode entry : items.get("firework_stars")) { - craftingData.computeIfAbsent(RecipeType.CRAFTING_SPECIAL_FIREWORK_STAR, - c -> new ObjectArrayList<>()).add(getCraftingDataFromJsonNode(entry, recipes, version.getValue())); - } - for (JsonNode entry : items.get("shulker_boxes")) { - craftingData.computeIfAbsent(RecipeType.CRAFTING_SPECIAL_SHULKERBOXCOLORING, - c -> new ObjectArrayList<>()).add(getCraftingDataFromJsonNode(entry, recipes, version.getValue())); - } - for (JsonNode entry : items.get("suspicious_stew")) { - craftingData.computeIfAbsent(RecipeType.CRAFTING_SPECIAL_SUSPICIOUSSTEW, - c -> new ObjectArrayList<>()).add(getCraftingDataFromJsonNode(entry, recipes, version.getValue())); - } - for (JsonNode entry : items.get("tipped_arrows")) { - craftingData.computeIfAbsent(RecipeType.CRAFTING_SPECIAL_TIPPEDARROW, - c -> new ObjectArrayList<>()).add(getCraftingDataFromJsonNode(entry, recipes, version.getValue())); - } - - Registries.CRAFTING_DATA.register(version.getIntKey(), craftingData); - Registries.RECIPES.register(version.getIntKey(), recipes); - } - } - - /** - * Computes a Bedrock crafting recipe from the given JSON data. - * @param node the JSON data to compute - * @param recipes a list of all the recipes - * @return the {@link RecipeData} to send to the Bedrock client. - */ - private static RecipeData getCraftingDataFromJsonNode(JsonNode node, Int2ObjectMap recipes, ItemMappings mappings) { - int netId = ++LAST_RECIPE_NET_ID; - int type = node.get("bedrockRecipeType").asInt(); - JsonNode outputNode = node.get("output"); - ItemMapping outputEntry = mappings.getMapping(outputNode.get("identifier").asText()); - ItemData output = getBedrockItemFromIdentifierJson(outputEntry, outputNode); - UUID uuid = UUID.randomUUID(); - if (type == 1) { - // Shaped recipe - List shape = new ArrayList<>(); - // Get the shape of the recipe - for (JsonNode chars : node.get("shape")) { - shape.add(chars.asText()); - } - - // In recipes.json each recipe is mapped by a letter - Map letterToRecipe = new HashMap<>(); - Iterator> iterator = node.get("inputs").fields(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - JsonNode inputNode = entry.getValue(); - ItemMapping inputEntry = mappings.getMapping(inputNode.get("identifier").asText()); - letterToRecipe.put(entry.getKey(), getBedrockItemFromIdentifierJson(inputEntry, inputNode)); - } - - List inputs = new ArrayList<>(shape.size() * shape.get(0).length()); - int i = 0; - // Create a linear array of items from the "cube" of the shape - for (int j = 0; i < shape.size() * shape.get(0).length(); j++) { - for (char c : shape.get(j).toCharArray()) { - ItemData data = letterToRecipe.getOrDefault(String.valueOf(c), ItemData.AIR); - inputs.add(data); - i++; - } - } - - /* Convert into a Java recipe class for autocrafting */ - List ingredients = new ArrayList<>(); - for (ItemData input : inputs) { - ingredients.add(new Ingredient(new ItemStack[]{ItemTranslator.translateToJava(input, mappings)})); - } - GeyserRecipe recipe = new GeyserShapedRecipe(shape.get(0).length(), shape.size(), - ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output, mappings)); - recipes.put(netId, recipe); - /* Convert end */ - - return ShapedRecipeData.shaped(uuid.toString(), shape.get(0).length(), shape.size(), - inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId, false, RecipeUnlockingRequirement.INVALID); - } - List inputs = new ObjectArrayList<>(); - for (JsonNode entry : node.get("inputs")) { - ItemMapping inputEntry = mappings.getMapping(entry.get("identifier").asText()); - inputs.add(getBedrockItemFromIdentifierJson(inputEntry, entry)); - } - - /* Convert into a Java Recipe class for autocrafting */ - List ingredients = new ArrayList<>(); - for (ItemData input : inputs) { - ingredients.add(new Ingredient(new ItemStack[]{ItemTranslator.translateToJava(input, mappings)})); - } - GeyserRecipe recipe = new GeyserShapelessRecipe(ingredients.toArray(new Ingredient[0]), ItemTranslator.translateToJava(output, mappings)); - recipes.put(netId, recipe); - /* Convert end */ - - if (type == 5) { - // Shulker box - return ShapelessRecipeData.shulkerBox(uuid.toString(), - inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId); - } - return ShapelessRecipeData.shapeless(uuid.toString(), - inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId, RecipeUnlockingRequirement.INVALID); - } - - private static ItemData getBedrockItemFromIdentifierJson(ItemMapping mapping, JsonNode itemNode) { - int count = 1; - short damage = 0; - NbtMap tag = null; - JsonNode damageNode = itemNode.get("bedrockDamage"); - if (damageNode != null) { - damage = damageNode.numberValue().shortValue(); - } - JsonNode countNode = itemNode.get("count"); - if (countNode != null) { - count = countNode.asInt(); - } - JsonNode nbtNode = itemNode.get("bedrockNbt"); - if (nbtNode != null) { - byte[] bytes = Base64.getDecoder().decode(nbtNode.asText()); - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - try { - tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return ItemData.builder() - .definition(mapping.getBedrockDefinition()) - .damage(damage) - .count(count) - .blockDefinition(mapping.getBedrockBlockDefinition()) - .tag(tag) - .build(); - } -} diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index ae5e1d338..836c77379 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -356,8 +356,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { * Stores all Java recipes by recipe identifier, and matches them to all possible Bedrock recipe identifiers. * They are not 1:1, since Bedrock can have multiple recipes for the same Java recipe. */ - @Setter - private Map> javaToBedrockRecipeIds; + private final Map> javaToBedrockRecipeIds; @Setter private Int2ObjectMap craftingRecipes; diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java index 18d6a22eb..bc6ff2adf 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java @@ -423,7 +423,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator { } // Reference the creative items list we send to the client to know what it's asking of us ItemData creativeItem = creativeItems[creativeId]; - javaCreativeItem = ItemTranslator.translateToJava(creativeItem, session.getItemMappings()); + javaCreativeItem = ItemTranslator.translateToJava(session, creativeItem); break; } case CRAFT_RESULTS_DEPRECATED: { diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index 251aacba8..8b61e435a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -47,7 +47,6 @@ import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.type.CustomSkull; import org.geysermc.geyser.registry.type.ItemMapping; -import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.MinecraftLocale; @@ -83,25 +82,21 @@ public final class ItemTranslator { private ItemTranslator() { } - /** - * @param mappings item mappings to use while translating. This can't just be a Geyser session as this method is used - * when loading recipes. - */ - public static ItemStack translateToJava(ItemData data, ItemMappings mappings) { + public static ItemStack translateToJava(GeyserSession session, ItemData data) { if (data == null) { return new ItemStack(Items.AIR_ID); } - ItemMapping bedrockItem = mappings.getMapping(data); + ItemMapping bedrockItem = session.getItemMappings().getMapping(data); Item javaItem = bedrockItem.getJavaItem(); - GeyserItemStack itemStack = javaItem.translateToJava(data, bedrockItem, mappings); + GeyserItemStack itemStack = javaItem.translateToJava(data, bedrockItem, session.getItemMappings()); NbtMap nbt = data.getTag(); if (nbt != null && !nbt.isEmpty()) { // translateToJava may have added components DataComponents components = itemStack.getComponents() == null ? new DataComponents(new HashMap<>()) : itemStack.getComponents(); - javaItem.translateNbtToJava(nbt, components, bedrockItem); + javaItem.translateNbtToJava(session, nbt, components, bedrockItem); if (!components.getDataComponents().isEmpty()) { itemStack.setComponents(components); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index 2a0c38221..886b31e09 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -103,66 +103,31 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator> recipeTypes = Registries.CRAFTING_DATA.forVersion(session.getUpstream().getProtocolVersion()); - // Get the last known network ID (first used for the pregenerated recipes) and increment from there. - int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1; boolean sendTrimRecipes = false; Map> recipeIDs = session.getJavaToBedrockRecipeIds(); - Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion())); + recipeIDs.clear(); + Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(); Int2ObjectMap> unsortedStonecutterData = new Int2ObjectOpenHashMap<>(); CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); craftingDataPacket.setCleanRecipes(true); + RecipeContext context = new RecipeContext(session, craftingDataPacket, recipeMap); + for (Recipe recipe : packet.getRecipes()) { switch (recipe.getType()) { case CRAFTING_SHAPELESS -> { ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData(); - ItemData output = ItemTranslator.translateToBedrock(session, shapelessRecipeData.getResult()); - if (!output.isValid()) { - // Likely modded item that Bedrock will complain about if it persists - continue; + List bedrockRecipeIDs = context.translateShapelessRecipe(new GeyserShapelessRecipe(shapelessRecipeData)); + if (bedrockRecipeIDs != null) { + context.addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs); } - // Strip NBT - tools won't appear in the recipe book otherwise - output = output.toBuilder().tag(null).build(); - ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapelessRecipeData.getIngredients()); - if (inputCombinations == null) { - continue; - } - - List bedrockRecipeIDs = new ArrayList<>(); - for (ItemDescriptorWithCount[] inputs : inputCombinations) { - UUID uuid = UUID.randomUUID(); - bedrockRecipeIDs.add(uuid.toString()); - craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(), - Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId, RecipeUnlockingRequirement.INVALID)); - recipeMap.put(netId++, new GeyserShapelessRecipe(shapelessRecipeData)); - } - addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs); } case CRAFTING_SHAPED -> { ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData(); - ItemData output = ItemTranslator.translateToBedrock(session, shapedRecipeData.getResult()); - if (!output.isValid()) { - // Likely modded item that Bedrock will complain about if it persists - continue; + List bedrockRecipeIDs = context.translateShapedRecipe(new GeyserShapedRecipe(shapedRecipeData)); + if (bedrockRecipeIDs != null) { + context.addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs); } - // See above - output = output.toBuilder().tag(null).build(); - ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapedRecipeData.getIngredients()); - if (inputCombinations == null) { - continue; - } - - List bedrockRecipeIDs = new ArrayList<>(); - for (ItemDescriptorWithCount[] inputs : inputCombinations) { - UUID uuid = UUID.randomUUID(); - bedrockRecipeIDs.add(uuid.toString()); - craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData.shaped(uuid.toString(), - shapedRecipeData.getWidth(), shapedRecipeData.getHeight(), Arrays.asList(inputs), - Collections.singletonList(output), uuid, "crafting_table", 0, netId, false, RecipeUnlockingRequirement.INVALID)); - recipeMap.put(netId++, new GeyserShapedRecipe(shapedRecipeData)); - } - addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs); } case STONECUTTING -> { StoneCuttingRecipeData stoneCuttingData = (StoneCuttingRecipeData) recipe.getData(); @@ -198,7 +163,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator(Collections.singletonList(id))); } @@ -212,13 +177,48 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { // Paper 1.20 seems to send only one recipe, which seems to be hardcoded to include all recipes. // We can send the equivalent Bedrock MultiRecipe! :) - craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("685a742a-c42e-4a4e-88ea-5eb83fc98e5b"), netId++)); + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("685a742a-c42e-4a4e-88ea-5eb83fc98e5b"), context.getAndIncrementNetId())); + } + case CRAFTING_SPECIAL_BOOKCLONING -> { + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), context.getAndIncrementNetId())); + } + case CRAFTING_SPECIAL_REPAIRITEM -> { + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("00000000-0000-0000-0000-000000000001"), context.getAndIncrementNetId())); + } + case CRAFTING_SPECIAL_MAPEXTENDING -> { + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("d392b075-4ba1-40ae-8789-af868d56f6ce"), context.getAndIncrementNetId())); + } + case CRAFTING_SPECIAL_MAPCLONING -> { + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), context.getAndIncrementNetId())); } default -> { - List craftingData = recipeTypes.get(recipe.getType()); - if (craftingData != null) { - addSpecialRecipesIdentifiers(session, recipe, craftingData); - craftingDataPacket.getCraftingData().addAll(craftingData); + List recipes = Registries.RECIPES.get(recipe.getType()); + if (recipes != null) { + List bedrockRecipeIds = new ArrayList<>(); + if (recipe.getType() == RecipeType.CRAFTING_SPECIAL_TIPPEDARROW) { + // Only shaped recipe at this moment + for (GeyserRecipe builtInRecipe : recipes) { + var recipeIds = context.translateShapedRecipe((GeyserShapedRecipe) builtInRecipe); + if (recipeIds != null) { + bedrockRecipeIds.addAll(recipeIds); + } + } + } else if (recipe.getType() == RecipeType.CRAFTING_SPECIAL_SHULKERBOXCOLORING) { + for (GeyserRecipe builtInRecipe : recipes) { + var recipeIds = context.translateShulkerBoxRecipe((GeyserShapelessRecipe) builtInRecipe); + if (recipeIds != null) { + bedrockRecipeIds.addAll(recipeIds); + } + } + } else { + for (GeyserRecipe builtInRecipe : recipes) { + var recipeIds = context.translateShapelessRecipe((GeyserShapelessRecipe) builtInRecipe); + if (recipeIds != null) { + bedrockRecipeIds.addAll(recipeIds); + } + } + } + context.addSpecialRecipesIdentifiers(recipe, bedrockRecipeIds); } } } @@ -250,17 +250,17 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator craftingData) { - String javaRecipeID = recipe.getIdentifier(); - - switch (recipe.getType()) { - case CRAFTING_SPECIAL_BOOKCLONING, CRAFTING_SPECIAL_REPAIRITEM, CRAFTING_SPECIAL_MAPEXTENDING, CRAFTING_SPECIAL_MAPCLONING: - // We do not want to (un)lock these, since BDS does not do it for MultiRecipes - return; - case CRAFTING_SPECIAL_SHULKERBOXCOLORING: - // BDS (un)locks the dyeing with the shulker box recipe, Java never - we want BDS behavior for ease of use - javaRecipeID = "minecraft:shulker_box"; - break; - case CRAFTING_SPECIAL_TIPPEDARROW: - // similar as above - javaRecipeID = "minecraft:arrow"; - break; - } - List bedrockRecipeIDs = new ArrayList<>(); - - // defined in the recipes.json mappings file: Only tipped arrows use shaped recipes, we need the cast for the identifier - if (recipe.getType() == RecipeType.CRAFTING_SPECIAL_TIPPEDARROW) { - for (RecipeData data : craftingData) { - bedrockRecipeIDs.add(((org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData) data).getId()); - } - } else { - for (RecipeData data : craftingData) { - bedrockRecipeIDs.add(((org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData) data).getId()); - } - } - addRecipeIdentifier(session, javaRecipeID, bedrockRecipeIDs); + System.out.println(craftingDataPacket); } //TODO: rewrite @@ -323,7 +292,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator, IntSet> squashedOptions = new HashMap<>(); for (int i = 0; i < ingredients.length; i++) { @@ -407,17 +376,6 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator bedrockIdentifiers) { - session.getJavaToBedrockRecipeIds().computeIfAbsent(javaIdentifier, k -> new ArrayList<>()).addAll(bedrockIdentifiers); - } - - @EqualsAndHashCode - @AllArgsConstructor - private static class GroupedItem { - ItemDefinition id; - int count; - } - private List getSmithingTransformRecipes(GeyserSession session) { List recipes = new ArrayList<>(); ItemMapping template = session.getItemMappings().getStoredItems().upgradeTemplate(); @@ -442,4 +400,120 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator recipeMap; + // Get the last known network ID (first used for some pregenerated recipes) and increment from there. + private int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1; + + private RecipeContext(GeyserSession session, CraftingDataPacket packet, Int2ObjectMap recipeMap) { + this.session = session; + this.packet = packet; + this.recipeMap = recipeMap; + } + + List translateShulkerBoxRecipe(GeyserShapelessRecipe recipe) { + ItemData output = ItemTranslator.translateToBedrock(session, recipe.result()); + if (!output.isValid()) { + // Likely modded item that Bedrock will complain about if it persists + return null; + } + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + ItemDescriptorWithCount[][] inputCombinations = combinations(session, recipe.ingredients()); + if (inputCombinations == null) { + return null; + } + + List bedrockRecipeIDs = new ArrayList<>(); + for (ItemDescriptorWithCount[] inputs : inputCombinations) { + UUID uuid = UUID.randomUUID(); + bedrockRecipeIDs.add(uuid.toString()); + packet.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shulkerBox(uuid.toString(), + Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId)); + recipeMap.put(netId++, recipe); + } + return bedrockRecipeIDs; + } + + List translateShapelessRecipe(GeyserShapelessRecipe recipe) { + ItemData output = ItemTranslator.translateToBedrock(session, recipe.result()); + if (!output.isValid()) { + // Likely modded item that Bedrock will complain about if it persists + return null; + } + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + ItemDescriptorWithCount[][] inputCombinations = combinations(session, recipe.ingredients()); + if (inputCombinations == null) { + return null; + } + + List bedrockRecipeIDs = new ArrayList<>(); + for (ItemDescriptorWithCount[] inputs : inputCombinations) { + UUID uuid = UUID.randomUUID(); + bedrockRecipeIDs.add(uuid.toString()); + packet.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(), + Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId, RecipeUnlockingRequirement.INVALID)); + recipeMap.put(netId++, recipe); + } + return bedrockRecipeIDs; + } + + List translateShapedRecipe(GeyserShapedRecipe recipe) { + ItemData output = ItemTranslator.translateToBedrock(session, recipe.result()); + if (!output.isValid()) { + // Likely modded item that Bedrock will complain about if it persists + return null; + } + // See above + output = output.toBuilder().tag(null).build(); + ItemDescriptorWithCount[][] inputCombinations = combinations(session, recipe.ingredients()); + if (inputCombinations == null) { + return null; + } + + List bedrockRecipeIDs = new ArrayList<>(); + for (ItemDescriptorWithCount[] inputs : inputCombinations) { + UUID uuid = UUID.randomUUID(); + bedrockRecipeIDs.add(uuid.toString()); + packet.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData.shaped(uuid.toString(), + recipe.width(), recipe.height(), Arrays.asList(inputs), + Collections.singletonList(output), uuid, "crafting_table", 0, netId, false, RecipeUnlockingRequirement.INVALID)); + recipeMap.put(netId++, recipe); + } + return bedrockRecipeIDs; + } + + void addSpecialRecipesIdentifiers(Recipe recipe, List identifiers) { + String javaRecipeID = switch (recipe.getType()) { + case CRAFTING_SPECIAL_SHULKERBOXCOLORING -> + // BDS (un)locks the dyeing with the shulker box recipe, Java never - we want BDS behavior for ease of use + "minecraft:shulker_box"; + case CRAFTING_SPECIAL_TIPPEDARROW -> + // similar as above + "minecraft:arrow"; + default -> recipe.getIdentifier(); + }; + + addRecipeIdentifier(session, javaRecipeID, identifiers); + } + + void addRecipeIdentifier(GeyserSession session, String javaIdentifier, List bedrockIdentifiers) { + session.getJavaToBedrockRecipeIds().computeIfAbsent(javaIdentifier, k -> new ArrayList<>()).addAll(bedrockIdentifiers); + } + + int getAndIncrementNetId() { + return this.netId++; + } + } } diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 54705bcd2..8795baeb1 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 54705bcd2bcba830267efbb1fbfd4e52972c40f7 +Subproject commit 8795baeb170f7c9832da2def8625f0c5702abd91