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 8b7fa9522..3037b725e 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 @@ -29,7 +29,7 @@ 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}. + * A more compact version of {link org.geysermc.mcprotocollib.protocol.data.game.recipe.Recipe}. */ public interface GeyserRecipe { /** diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java index ac9fa3ab4..413041ba7 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapedRecipe.java @@ -28,13 +28,12 @@ package org.geysermc.geyser.inventory.recipe; import org.checkerframework.checker.nullness.qual.Nullable; 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.data.ShapedRecipeData; public record GeyserShapedRecipe(int width, int height, Ingredient[] ingredients, @Nullable ItemStack result) implements GeyserRecipe { - public GeyserShapedRecipe(ShapedRecipeData data) { - this(data.getWidth(), data.getHeight(), data.getIngredients(), data.getResult()); - } +// public GeyserShapedRecipe(ShapedRecipeData data) { +// this(data.getWidth(), data.getHeight(), data.getIngredients(), data.getResult()); +// } @Override public boolean isShaped() { diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java index 388831d4c..6b9e36956 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserShapelessRecipe.java @@ -25,16 +25,15 @@ package org.geysermc.geyser.inventory.recipe; +import org.checkerframework.checker.nullness.qual.Nullable; 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.data.ShapelessRecipeData; -import org.checkerframework.checker.nullness.qual.Nullable; public record GeyserShapelessRecipe(Ingredient[] ingredients, @Nullable ItemStack result) implements GeyserRecipe { - public GeyserShapelessRecipe(ShapelessRecipeData data) { - this(data.getIngredients(), data.getResult()); - } +// public GeyserShapelessRecipe(ShapelessRecipeData data) { +// this(data.getIngredients(), data.getResult()); +// } @Override public boolean isShaped() { 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 30d3c0763..3f9ad6815 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -37,10 +37,17 @@ import org.geysermc.geyser.api.pack.ResourcePack; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.item.type.Item; -import org.geysermc.geyser.registry.loader.*; +import org.geysermc.geyser.registry.loader.BiomeIdentifierRegistryLoader; +import org.geysermc.geyser.registry.loader.BlockEntityRegistryLoader; +import org.geysermc.geyser.registry.loader.ParticleTypesRegistryLoader; +import org.geysermc.geyser.registry.loader.PotionMixRegistryLoader; +import org.geysermc.geyser.registry.loader.ProviderRegistryLoader; +import org.geysermc.geyser.registry.loader.RegistryLoaders; +import org.geysermc.geyser.registry.loader.SoundEventsRegistryLoader; +import org.geysermc.geyser.registry.loader.SoundRegistryLoader; +import org.geysermc.geyser.registry.loader.SoundTranslatorRegistryLoader; import org.geysermc.geyser.registry.populator.ItemRegistryPopulator; import org.geysermc.geyser.registry.populator.PacketRegistryPopulator; -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; @@ -54,9 +61,13 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType; import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEvent; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; -import org.geysermc.mcprotocollib.protocol.data.game.recipe.RecipeType; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; /** * Holds all the common registries in Geyser. @@ -140,7 +151,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 SimpleMappedRegistry> RECIPES = SimpleMappedRegistry.create("mappings/recipes.nbt", RecipeRegistryLoader::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. 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 index 1af6a8661..f061ed070 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/RecipeRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/RecipeRegistryLoader.java @@ -27,29 +27,19 @@ 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.Collections; import java.util.List; import java.util.Map; @@ -57,41 +47,41 @@ 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>> { +public abstract class RecipeRegistryLoader implements RegistryLoader>> { - @Override - public Map> load(String input) { - if (true) { - return Collections.emptyMap(); - } - Map> deserializedRecipes = new Object2ObjectOpenHashMap<>(); +// @Override +// public Map> load(String input) { +// if (true) { +// return Collections.emptyMap(); +// } +// 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; +// } - 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 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()); 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 fb64cfc41..211486963 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -441,10 +441,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private Entity mouseoverEntity; /** - * 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. + * Stores all Java recipes by ID, and matches them to all possible Bedrock recipe identifiers. */ - private final Map> javaToBedrockRecipeIds; + private final Int2ObjectMap> javaToBedrockRecipeIds; @Setter private Int2ObjectMap craftingRecipes; @@ -694,7 +693,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { this.playerInventory = new PlayerInventory(); this.openInventory = null; this.craftingRecipes = new Int2ObjectOpenHashMap<>(); - this.javaToBedrockRecipeIds = new Object2ObjectOpenHashMap<>(); + this.javaToBedrockRecipeIds = new Int2ObjectOpenHashMap<>(); this.lastRecipeNetId = new AtomicInteger(1); this.spawned = false; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java index 74fb11814..49c62989c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java @@ -25,57 +25,258 @@ package org.geysermc.geyser.translator.protocol.java; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.kyori.adventure.key.Key; +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.ShapedRecipeData; +import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor; +import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; +import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; import org.cloudburstmc.protocol.bedrock.packet.UnlockedRecipesPacket; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.BedrockRequiresTagItem; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.registry.Registries; +import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.JavaRegistries; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDisplayEntry; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapedCraftingRecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapelessCraftingRecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.CompositeSlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.TagSlotDisplay; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundRecipeBookAddPacket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + @Translator(packet = ClientboundRecipeBookAddPacket.class) public class JavaRecipeBookAddTranslator extends PacketTranslator { @Override public void translate(GeyserSession session, ClientboundRecipeBookAddPacket packet) { + System.out.println(packet); + int netId = session.getLastRecipeNetId().get(); + Int2ObjectMap> javaToBedrockRecipeIds = session.getJavaToBedrockRecipeIds(); + CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); + // Check if we should set cleanRecipes here or not. + + UnlockedRecipesPacket recipesPacket = new UnlockedRecipesPacket(); recipesPacket.setAction(packet.isReplace() ? UnlockedRecipesPacket.ActionType.INITIALLY_UNLOCKED : UnlockedRecipesPacket.ActionType.NEWLY_UNLOCKED); -// List recipes = getBedrockRecipes(session, packet.getEntries()); -// if (recipes.isEmpty() && !packet.isReplace()) { -// // Sending an empty list here packet will crash the client as of 1.20.60 -// return; -// } -// switch (packet.getAction()) { -// case INIT -> { -// recipesPacket.setAction(UnlockedRecipesPacket.ActionType.INITIALLY_UNLOCKED); -// recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getAlreadyKnownRecipes())); -// } -// case ADD -> { -// -// recipesPacket.setAction(UnlockedRecipesPacket.ActionType.NEWLY_UNLOCKED); -// recipesPacket.getUnlockedRecipes().addAll(recipes); -// } -// case REMOVE -> { -// List recipes = getBedrockRecipes(session, packet.getRecipes()); -// if (recipes.isEmpty()) { -// // Sending an empty list here will crash the client as of 1.20.60 -// return; -// } -// recipesPacket.setAction(UnlockedRecipesPacket.ActionType.REMOVE_UNLOCKED); -// recipesPacket.getUnlockedRecipes().addAll(recipes); -// } -// } -// session.sendUpstreamPacket(recipesPacket); + + for (ClientboundRecipeBookAddPacket.Entry entry : packet.getEntries()) { + RecipeDisplayEntry contents = entry.contents(); + RecipeDisplay display = contents.display(); + + switch (display.getType()) { + case CRAFTING_SHAPED -> { + ShapedCraftingRecipeDisplay shapedRecipe = (ShapedCraftingRecipeDisplay) display; + Pair pair = translateToOutput(session, shapedRecipe.result()); + if (pair == null || !pair.right().isValid()) { + // Likely modded item Bedrock will complain about + continue; + } + + ItemData output = pair.right(); + if (!(pair.left() instanceof BedrockRequiresTagItem)) { + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + } + + boolean empty = true; + boolean complexInputs = false; + List inputs = new ArrayList<>(shapedRecipe.ingredients().size()); + for (SlotDisplay input : shapedRecipe.ingredients()) { + ItemDescriptorWithCount[] translated = translateToInput(session, input); + if (translated == null) { + continue; + } + inputs.add(translated); + if (translated.length != 1 || translated[0] != ItemDescriptorWithCount.EMPTY) { + empty = false; + } + complexInputs |= translated.length > 1; + } + if (empty) { + // Crashes Bedrock 1.19.70 otherwise + // Fixes https://github.com/GeyserMC/Geyser/issues/3549 + continue; + } + + if (complexInputs) { + + } else { + String recipeId = Integer.toString(contents.id()); + craftingDataPacket.getCraftingData().add(ShapedRecipeData.shaped(recipeId, + shapedRecipe.width(), shapedRecipe.height(), inputs.stream().map(descriptors -> descriptors[0]).toList(), + Collections.singletonList(output), UUID.randomUUID(), "crafting_table", 0, netId++, false, RecipeUnlockingRequirement.INVALID)); + recipesPacket.getUnlockedRecipes().add(recipeId); + javaToBedrockRecipeIds.put(contents.id(), Collections.singletonList(recipeId)); + } + } + case CRAFTING_SHAPELESS -> { + ShapelessCraftingRecipeDisplay shapelessRecipe = (ShapelessCraftingRecipeDisplay) display; + Pair pair = translateToOutput(session, shapelessRecipe.result()); + if (pair == null || !pair.right().isValid()) { + // Likely modded item Bedrock will complain about + continue; + } + + ItemData output = pair.right(); + if (!(pair.left() instanceof BedrockRequiresTagItem)) { + // Strip NBT - tools won't appear in the recipe book otherwise + output = output.toBuilder().tag(null).build(); + } + } + } + } + + System.out.println(craftingDataPacket); + session.sendUpstreamPacket(craftingDataPacket); + session.sendUpstreamPacket(recipesPacket); + session.getLastRecipeNetId().set(netId); + + // Multi-version can mean different Bedrock item IDs + TAG_TO_ITEM_DESCRIPTOR_CACHE.remove(); } -// private List getBedrockRecipes(GeyserSession session, List entry) { -// List recipes = new ArrayList<>(); -// for (String javaIdentifier : javaRecipeIdentifiers) { -// List bedrockRecipes = session.getJavaToBedrockRecipeIds().get(javaIdentifier); -// // Some recipes are not (un)lockable on Bedrock edition, like furnace or stonecutter recipes. -// // So we don't store/send these. -// if (bedrockRecipes != null) { -// recipes.addAll(bedrockRecipes); -// } + private static final ThreadLocal> TAG_TO_ITEM_DESCRIPTOR_CACHE = ThreadLocal.withInitial(Object2ObjectOpenHashMap::new); + + private ItemDescriptorWithCount[] translateToInput(GeyserSession session, SlotDisplay slotDisplay) { + if (slotDisplay instanceof EmptySlotDisplay) { + return new ItemDescriptorWithCount[] {ItemDescriptorWithCount.EMPTY}; + } + if (slotDisplay instanceof CompositeSlotDisplay composite) { + if (composite.contents().size() == 1) { + return translateToInput(session, composite.contents().get(0)); + } + return composite.contents().stream() + .map(subDisplay -> translateToInput(session, subDisplay)) + .filter(Objects::nonNull) + .flatMap(Arrays::stream) + .toArray(ItemDescriptorWithCount[]::new); + } + if (slotDisplay instanceof ItemSlotDisplay itemSlot) { + return new ItemDescriptorWithCount[] {fromItem(session, itemSlot.item())}; + } + if (slotDisplay instanceof ItemStackSlotDisplay itemStackSlot) { + ItemData item = ItemTranslator.translateToBedrock(session, itemStackSlot.itemStack()); + return new ItemDescriptorWithCount[] {ItemDescriptorWithCount.fromItem(item)}; + } + if (slotDisplay instanceof TagSlotDisplay tagSlot) { + Key tag = tagSlot.tag(); + int[] items = session.getTagCache().getRaw(new Tag<>(JavaRegistries.ITEM, tag)); // I don't like this... + if (items == null || items.length == 0) { + return new ItemDescriptorWithCount[] {ItemDescriptorWithCount.EMPTY}; + } else if (items.length == 1) { + return new ItemDescriptorWithCount[] {fromItem(session, items[0])}; + } else { + // Cache is implemented as, presumably, an item tag will be used multiple times in succession + // (E.G. a chest with planks tags) + return TAG_TO_ITEM_DESCRIPTOR_CACHE.get().computeIfAbsent(items, key -> { +// String molang = "q.is_item_name_any('', " +// + Arrays.stream(items).mapToObj(item -> { +// ItemMapping mapping = session.getItemMappings().getMapping(item); +// return "'" + mapping.getBedrockIdentifier() + "'"; +// }).collect(Collectors.joining(", ")) +// + ")"; +// String molang = Arrays.stream(items).mapToObj(item -> { +// ItemMapping mapping = session.getItemMappings().getMapping(item); +// return "q.identifier == '" + mapping.getBedrockIdentifier() + "'"; +// }).collect(Collectors.joining(" || ")); +// if ("minecraft:planks".equals(tag.toString())) { +// String molang = "q.any_tag('minecraft:planks')"; +// return new ItemDescriptorWithCount[] {new ItemDescriptorWithCount(new MolangDescriptor(molang, 10), 1)}; +// } + return null; +// Set itemDescriptors = new HashSet<>(); +// for (int item : key) { +// itemDescriptors.add(fromItem(session, item)); +// } +// return itemDescriptors.toArray(ItemDescriptorWithCount[]::new); + }); + } + } + session.getGeyser().getLogger().warning("Unimplemented slot display type for input: " + slotDisplay); + return null; + } + + private Pair translateToOutput(GeyserSession session, SlotDisplay slotDisplay) { + if (slotDisplay instanceof EmptySlotDisplay) { + return null; + } + if (slotDisplay instanceof ItemSlotDisplay itemSlot) { + int item = itemSlot.item(); + return Pair.of(Registries.JAVA_ITEMS.get(item), ItemTranslator.translateToBedrock(session, new ItemStack(item))); + } + if (slotDisplay instanceof ItemStackSlotDisplay itemStackSlot) { + ItemStack stack = itemStackSlot.itemStack(); + return Pair.of(Registries.JAVA_ITEMS.get(stack.getId()), ItemTranslator.translateToBedrock(session, stack)); + } + session.getGeyser().getLogger().warning("Unimplemented slot display type for output: " + slotDisplay); + return null; + } + + private ItemDescriptorWithCount fromItem(GeyserSession session, int item) { + if (item == Items.AIR_ID) { + return ItemDescriptorWithCount.EMPTY; + } + ItemMapping mapping = session.getItemMappings().getMapping(item); + return new ItemDescriptorWithCount(new DefaultDescriptor(mapping.getBedrockDefinition(), mapping.getBedrockData()), 1); // Need to check count + } + +// private static ItemDescriptorWithCount[][] combinations(ItemDescriptorWithCount[] itemDescriptors) { +// int totalCombinations = 1; +// for (Set optionSet : squashedOptions.keySet()) { +// totalCombinations *= optionSet.size(); +// } +// if (totalCombinations > 500) { +// ItemDescriptorWithCount[] translatedItems = new ItemDescriptorWithCount[ingredients.length]; +// for (int i = 0; i < ingredients.length; i++) { +// if (ingredients[i].getOptions().length > 0) { +// translatedItems[i] = ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, ingredients[i].getOptions()[0])); +// } else { +// translatedItems[i] = ItemDescriptorWithCount.EMPTY; +// } +// } +// return new ItemDescriptorWithCount[][]{translatedItems}; +// } +// List> sortedSets = new ArrayList<>(squashedOptions.keySet()); +// sortedSets.sort(Comparator.comparing(Set::size, Comparator.reverseOrder())); +// ItemDescriptorWithCount[][] combinations = new ItemDescriptorWithCount[totalCombinations][ingredients.length]; +// int x = 1; +// for (Set set : sortedSets) { +// IntSet slotSet = squashedOptions.get(set); +// int i = 0; +// for (ItemDescriptorWithCount item : set) { +// for (int j = 0; j < totalCombinations / set.size(); j++) { +// final int comboIndex = (i * x) + (j % x) + ((j / x) * set.size() * x); +// for (IntIterator it = slotSet.iterator(); it.hasNext(); ) { +// combinations[comboIndex][it.nextInt()] = item; +// } +// } +// i++; +// } +// x *= set.size(); // } -// return recipes; // } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookRemoveTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookRemoveTranslator.java new file mode 100644 index 000000000..27faecef7 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookRemoveTranslator.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 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.translator.protocol.java; + +import org.cloudburstmc.protocol.bedrock.packet.UnlockedRecipesPacket; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.translator.protocol.PacketTranslator; +import org.geysermc.geyser.translator.protocol.Translator; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundRecipeBookRemovePacket; + +import java.util.ArrayList; +import java.util.List; + +@Translator(packet = ClientboundRecipeBookRemovePacket.class) +public class JavaRecipeBookRemoveTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundRecipeBookRemovePacket packet) { + List recipes = getBedrockRecipes(session, packet.getRecipes()); + if (recipes.isEmpty()) { + // Sending an empty list here will crash the client as of 1.20.60 + return; + } + UnlockedRecipesPacket recipesPacket = new UnlockedRecipesPacket(); + recipesPacket.setAction(UnlockedRecipesPacket.ActionType.REMOVE_UNLOCKED); + recipesPacket.getUnlockedRecipes().addAll(recipes); + session.sendUpstreamPacket(recipesPacket); + } + + private List getBedrockRecipes(GeyserSession session, int[] javaRecipeIds) { + List recipes = new ArrayList<>(); + for (int javaIdentifier : javaRecipeIds) { + List bedrockRecipes = session.getJavaToBedrockRecipeIds().get(javaIdentifier); + // Some recipes are not (un)lockable on Bedrock edition, like furnace or stonecutter recipes. + // So we don't store/send these. + if (bedrockRecipes != null) { + recipes.addAll(bedrockRecipes); + } + } + return recipes; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java index f5dab4cd0..2ea116de5 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java @@ -109,6 +109,9 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator