Refactor static recipe loading

The only recipes added should be the ones that are sent on Bedrock, so it appears in the recipe book. Every other recipe will be handled through our fallback system.
This commit is contained in:
Camotoy 2024-06-12 20:34:42 -04:00
parent a42c979abb
commit ecffb564ed
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
14 changed files with 360 additions and 407 deletions

View file

@ -25,6 +25,9 @@
package org.geysermc.geyser.inventory.recipe; 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}. * 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. * Whether the recipe is flexible or not in which items can be placed where.
*/ */
boolean isShaped(); boolean isShaped();
@Nullable
ItemStack result();
} }

View file

@ -60,9 +60,6 @@ public class BannerItem extends BlockItem {
*/ */
private static final List<Pair<BannerPattern, DyeColor>> OMINOUS_BANNER_PATTERN; private static final List<Pair<BannerPattern, DyeColor>> 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 { static {
// Construct what an ominous banner is supposed to look like // Construct what an ominous banner is supposed to look like
OMINOUS_BANNER_PATTERN = List.of( OMINOUS_BANNER_PATTERN = List.of(
@ -215,20 +212,22 @@ public class BannerItem extends BlockItem {
} }
@Override @Override
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) {
super.translateNbtToJava(bedrockTag, components, mapping); super.translateNbtToJava(session, bedrockTag, components, mapping);
if (bedrockTag.getInt("Type") == 1) { if (bedrockTag.getInt("Type") == 1) {
// Ominous banner pattern // Ominous banner pattern
List<BannerPatternLayer> patternLayers = new ArrayList<>(); List<BannerPatternLayer> patternLayers = new ArrayList<>();
for (int i = 0; i < ominousBannerPattern.length; i++) { for (int i = 0; i < OMINOUS_BANNER_PATTERN.size(); i++) {
patternLayers.add(new BannerPatternLayer(Holder.ofId(ominousBannerPattern[i]), OMINOUS_BANNER_PATTERN.get(i).right().ordinal())); 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.BANNER_PATTERNS, patternLayers);
components.put(DataComponentType.HIDE_ADDITIONAL_TOOLTIP, Unit.INSTANCE); components.put(DataComponentType.HIDE_ADDITIONAL_TOOLTIP, Unit.INSTANCE);
components.put(DataComponentType.ITEM_NAME, Component 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))) .style(Style.style(TextColor.color(16755200)))
); );
} }

View file

@ -32,6 +32,7 @@ import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType; import org.cloudburstmc.nbt.NbtType;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.item.BedrockEnchantment; 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.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.item.BedrockItemBuilder; import org.geysermc.geyser.translator.item.BedrockItemBuilder;
@ -69,8 +70,8 @@ public class EnchantedBookItem extends Item {
} }
@Override @Override
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) {
super.translateNbtToJava(bedrockTag, components, mapping); super.translateNbtToJava(session, bedrockTag, components, mapping);
List<NbtMap> enchantmentTag = bedrockTag.getList("ench", NbtType.COMPOUND); List<NbtMap> enchantmentTag = bedrockTag.getList("ench", NbtType.COMPOUND);
if (enchantmentTag != null) { if (enchantmentTag != null) {
@ -80,9 +81,14 @@ public class EnchantedBookItem extends Item {
BedrockEnchantment enchantment = BedrockEnchantment.getByBedrockId(bedrockId); BedrockEnchantment enchantment = BedrockEnchantment.getByBedrockId(bedrockId);
if (enchantment != null) { if (enchantment != null) {
List<Enchantment> 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); int level = bedrockEnchantment.getShort("lvl", (short) 1);
// TODO javaEnchantments.put(i, level);
//javaEnchantments.put(BedrockEnchantment.JavaEnchantment.valueOf(enchantment.name()).ordinal(), level); break;
}
}
} else { } else {
GeyserImpl.getInstance().getLogger().debug("Unknown bedrock enchantment: " + bedrockId); GeyserImpl.getInstance().getLogger().debug("Unknown bedrock enchantment: " + bedrockId);
} }

View file

@ -70,8 +70,8 @@ public class FireworkRocketItem extends Item {
} }
@Override @Override
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) {
super.translateNbtToJava(bedrockTag, components, mapping); super.translateNbtToJava(session, bedrockTag, components, mapping);
NbtMap fireworksTag = bedrockTag.getCompound("Fireworks"); NbtMap fireworksTag = bedrockTag.getCompound("Fireworks");
if (!fireworksTag.isEmpty()) { if (!fireworksTag.isEmpty()) {

View file

@ -78,8 +78,8 @@ public class FireworkStarItem extends Item {
} }
@Override @Override
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) {
super.translateNbtToJava(bedrockTag, components, mapping); super.translateNbtToJava(session, bedrockTag, components, mapping);
NbtMap explosion = bedrockTag.getCompound("FireworksItem"); NbtMap explosion = bedrockTag.getCompound("FireworksItem");
if (!explosion.isEmpty()) { if (!explosion.isEmpty()) {

View file

@ -172,7 +172,7 @@ public class Item {
* </ul> * </ul>
* Therefore, if translation cannot be achieved for a certain item, it is not necessarily bad. * 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 // TODO see if any items from the creative menu need this
// CompoundTag displayTag = tag.get("display"); // CompoundTag displayTag = tag.get("display");
// if (displayTag != null) { // if (displayTag != null) {
@ -190,41 +190,6 @@ public class Item {
// } // }
// displayTag.put(new ListTag("Lore", lore)); // displayTag.put(new ListTag("Lore", lore));
// } // }
// }
// TODO no creative item should have enchantments *except* enchanted books
// List<NbtMap> enchantmentTag = bedrockTag.getList("ench", NbtType.COMPOUND);
// if (enchantmentTag != null) {
// List<Tag> 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<String, Tag> 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);
// }
// }
// } // }
} }

View file

@ -25,14 +25,12 @@
package org.geysermc.geyser.registry; package org.geysermc.geyser.registry;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData; 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.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.pack.ResourcePack; 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.loader.*;
import org.geysermc.geyser.registry.populator.ItemRegistryPopulator; import org.geysermc.geyser.registry.populator.ItemRegistryPopulator;
import org.geysermc.geyser.registry.populator.PacketRegistryPopulator; 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.provider.ProviderSupplier;
import org.geysermc.geyser.registry.type.ItemMappings; import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.registry.type.ParticleMapping; import org.geysermc.geyser.registry.type.ParticleMapping;
@ -95,11 +93,6 @@ public final class Registries {
*/ */
public static final SimpleMappedRegistry<BlockEntityType, BlockEntityTranslator> BLOCK_ENTITIES = SimpleMappedRegistry.create("org.geysermc.geyser.translator.level.block.entity.BlockEntity", BlockEntityRegistryLoader::new); public static final SimpleMappedRegistry<BlockEntityType, BlockEntityTranslator> 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<Map<RecipeType, List<RecipeData>>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
/** /**
* A map containing all entity types and their respective Geyser definitions * 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. * A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value.
*/ */
public static final VersionedRegistry<Int2ObjectMap<GeyserRecipe>> RECIPES = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); public static final SimpleMappedRegistry<RecipeType, List<GeyserRecipe>> RECIPES = SimpleMappedRegistry.create("mappings/recipes.nbt", RecipeRegistryLoader::new);
/** /**
* A mapped registry holding {@link ResourcePack}'s with the pack uuid as keys. * A mapped registry holding {@link ResourcePack}'s with the pack uuid as keys.
@ -176,7 +169,7 @@ public final class Registries {
static { static {
PacketRegistryPopulator.populate(); PacketRegistryPopulator.populate();
ItemRegistryPopulator.populate(); ItemRegistryPopulator.populate();
RecipeRegistryPopulator.populate(); System.out.println(RECIPES.get());
// Create registries that require other registries to load first // Create registries that require other registries to load first
POTION_MIXES = VersionedRegistry.create(PotionMixRegistryLoader::new); POTION_MIXES = VersionedRegistry.create(PotionMixRegistryLoader::new);

View file

@ -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<String, Map<RecipeType, List<GeyserRecipe>>> {
@Override
public Map<RecipeType, List<GeyserRecipe>> load(String input) {
Map<RecipeType, List<GeyserRecipe>> deserializedRecipes = new Object2ObjectOpenHashMap<>();
List<NbtMap> 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<RecipeType, List<GeyserRecipe>> getRecipes(NbtMap recipes, MinecraftCodecHelper helper) {
List<NbtMap> 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<GeyserRecipe> getShapelessRecipes(List<NbtMap> recipes, MinecraftCodecHelper helper) {
List<GeyserRecipe> deserializedRecipes = new ObjectArrayList<>(recipes.size());
for (NbtMap recipe : recipes) {
ItemStack output = toItemStack(recipe.getCompound("output"), helper);
List<NbtMap> 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<GeyserRecipe> getShapedRecipes(List<NbtMap> recipes, MinecraftCodecHelper helper) {
List<GeyserRecipe> deserializedRecipes = new ObjectArrayList<>(recipes.size());
for (NbtMap recipe : recipes) {
ItemStack output = toItemStack(recipe.getCompound("output"), helper);
List<int[]> shape = recipe.getList("shape", NbtType.INT_ARRAY);
// In the recipes mapping, each recipe is mapped by a number
List<ItemStack> 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);
}
}

View file

@ -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<ItemMappings> 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<RecipeType, List<RecipeData>> craftingData = new EnumMap<>(RecipeType.class);
Int2ObjectMap<GeyserRecipe> 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<GeyserRecipe> 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<String> 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<String, ItemData> letterToRecipe = new HashMap<>();
Iterator<Map.Entry<String, JsonNode>> iterator = node.get("inputs").fields();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
JsonNode inputNode = entry.getValue();
ItemMapping inputEntry = mappings.getMapping(inputNode.get("identifier").asText());
letterToRecipe.put(entry.getKey(), getBedrockItemFromIdentifierJson(inputEntry, inputNode));
}
List<ItemData> 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<Ingredient> 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<ItemData> 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<Ingredient> 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();
}
}

View file

@ -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. * 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. * They are not 1:1, since Bedrock can have multiple recipes for the same Java recipe.
*/ */
@Setter private final Map<String, List<String>> javaToBedrockRecipeIds;
private Map<String, List<String>> javaToBedrockRecipeIds;
@Setter @Setter
private Int2ObjectMap<GeyserRecipe> craftingRecipes; private Int2ObjectMap<GeyserRecipe> craftingRecipes;

View file

@ -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 // Reference the creative items list we send to the client to know what it's asking of us
ItemData creativeItem = creativeItems[creativeId]; ItemData creativeItem = creativeItems[creativeId];
javaCreativeItem = ItemTranslator.translateToJava(creativeItem, session.getItemMappings()); javaCreativeItem = ItemTranslator.translateToJava(session, creativeItem);
break; break;
} }
case CRAFT_RESULTS_DEPRECATED: { case CRAFT_RESULTS_DEPRECATED: {

View file

@ -47,7 +47,6 @@ import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.CustomSkull; import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale; import org.geysermc.geyser.text.MinecraftLocale;
@ -83,25 +82,21 @@ public final class ItemTranslator {
private ItemTranslator() { private ItemTranslator() {
} }
/** public static ItemStack translateToJava(GeyserSession session, ItemData data) {
* @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) {
if (data == null) { if (data == null) {
return new ItemStack(Items.AIR_ID); return new ItemStack(Items.AIR_ID);
} }
ItemMapping bedrockItem = mappings.getMapping(data); ItemMapping bedrockItem = session.getItemMappings().getMapping(data);
Item javaItem = bedrockItem.getJavaItem(); Item javaItem = bedrockItem.getJavaItem();
GeyserItemStack itemStack = javaItem.translateToJava(data, bedrockItem, mappings); GeyserItemStack itemStack = javaItem.translateToJava(data, bedrockItem, session.getItemMappings());
NbtMap nbt = data.getTag(); NbtMap nbt = data.getTag();
if (nbt != null && !nbt.isEmpty()) { if (nbt != null && !nbt.isEmpty()) {
// translateToJava may have added components // translateToJava may have added components
DataComponents components = itemStack.getComponents() == null ? new DataComponents(new HashMap<>()) : itemStack.getComponents(); 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()) { if (!components.getDataComponents().isEmpty()) {
itemStack.setComponents(components); itemStack.setComponents(components);
} }

View file

@ -103,66 +103,31 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
@Override @Override
public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) { public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) {
Map<RecipeType, List<RecipeData>> 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; boolean sendTrimRecipes = false;
Map<String, List<String>> recipeIDs = session.getJavaToBedrockRecipeIds(); Map<String, List<String>> recipeIDs = session.getJavaToBedrockRecipeIds();
Int2ObjectMap<GeyserRecipe> recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion())); recipeIDs.clear();
Int2ObjectMap<GeyserRecipe> recipeMap = new Int2ObjectOpenHashMap<>();
Int2ObjectMap<List<StoneCuttingRecipeData>> unsortedStonecutterData = new Int2ObjectOpenHashMap<>(); Int2ObjectMap<List<StoneCuttingRecipeData>> unsortedStonecutterData = new Int2ObjectOpenHashMap<>();
CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
craftingDataPacket.setCleanRecipes(true); craftingDataPacket.setCleanRecipes(true);
RecipeContext context = new RecipeContext(session, craftingDataPacket, recipeMap);
for (Recipe recipe : packet.getRecipes()) { for (Recipe recipe : packet.getRecipes()) {
switch (recipe.getType()) { switch (recipe.getType()) {
case CRAFTING_SHAPELESS -> { case CRAFTING_SHAPELESS -> {
ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData(); ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData();
ItemData output = ItemTranslator.translateToBedrock(session, shapelessRecipeData.getResult()); List<String> bedrockRecipeIDs = context.translateShapelessRecipe(new GeyserShapelessRecipe(shapelessRecipeData));
if (!output.isValid()) { if (bedrockRecipeIDs != null) {
// Likely modded item that Bedrock will complain about if it persists context.addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs);
continue;
} }
// 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<String> 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 -> { case CRAFTING_SHAPED -> {
ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData(); ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData();
ItemData output = ItemTranslator.translateToBedrock(session, shapedRecipeData.getResult()); List<String> bedrockRecipeIDs = context.translateShapedRecipe(new GeyserShapedRecipe(shapedRecipeData));
if (!output.isValid()) { if (bedrockRecipeIDs != null) {
// Likely modded item that Bedrock will complain about if it persists context.addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs);
continue;
} }
// See above
output = output.toBuilder().tag(null).build();
ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapedRecipeData.getIngredients());
if (inputCombinations == null) {
continue;
}
List<String> 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 -> { case STONECUTTING -> {
StoneCuttingRecipeData stoneCuttingData = (StoneCuttingRecipeData) recipe.getData(); StoneCuttingRecipeData stoneCuttingData = (StoneCuttingRecipeData) recipe.getData();
@ -198,7 +163,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
String id = recipe.getIdentifier(); String id = recipe.getIdentifier();
// Note: vanilla inputs use aux value of Short.MAX_VALUE // Note: vanilla inputs use aux value of Short.MAX_VALUE
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData.of(id, craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData.of(id,
bedrockTemplate, bedrockBase, bedrockAddition, output, "smithing_table", netId++)); bedrockTemplate, bedrockBase, bedrockAddition, output, "smithing_table", context.getAndIncrementNetId()));
recipeIDs.put(id, new ArrayList<>(Collections.singletonList(id))); recipeIDs.put(id, new ArrayList<>(Collections.singletonList(id)));
} }
@ -212,13 +177,48 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
case CRAFTING_DECORATED_POT -> { case CRAFTING_DECORATED_POT -> {
// Paper 1.20 seems to send only one recipe, which seems to be hardcoded to include all recipes. // 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! :) // 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 -> { default -> {
List<RecipeData> craftingData = recipeTypes.get(recipe.getType()); List<GeyserRecipe> recipes = Registries.RECIPES.get(recipe.getType());
if (craftingData != null) { if (recipes != null) {
addSpecialRecipesIdentifiers(session, recipe, craftingData); List<String> bedrockRecipeIds = new ArrayList<>();
craftingDataPacket.getCraftingData().addAll(craftingData); 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<ClientboundUpd
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
// We need to register stonecutting recipes, so they show up on Bedrock // We need to register stonecutting recipes, so they show up on Bedrock
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(), craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(),
Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, netId, RecipeUnlockingRequirement.INVALID)); Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, context.netId, RecipeUnlockingRequirement.INVALID));
// Save the recipe list for reference when crafting // Save the recipe list for reference when crafting
// Add the net ID as the key and the button required + output for the value // Add the net ID as the key and the button required + output for the value
stonecutterRecipeMap.put(netId++, new GeyserStonecutterData(buttonId++, javaOutput)); stonecutterRecipeMap.put(context.getAndIncrementNetId(), new GeyserStonecutterData(buttonId++, javaOutput));
// Currently, stone cutter recipes are not locked/unlocked on Bedrock; so no need to cache their identifiers. // Currently, stone cutter recipes are not locked/unlocked on Bedrock; so no need to cache their identifiers.
} }
} }
session.getLastRecipeNetId().set(netId); session.getLastRecipeNetId().set(context.netId); // No increment
// Only send smithing trim recipes if Java/ViaVersion sends them. // Only send smithing trim recipes if Java/ViaVersion sends them.
if (sendTrimRecipes) { if (sendTrimRecipes) {
@ -282,38 +282,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
session.sendUpstreamPacket(craftingDataPacket); session.sendUpstreamPacket(craftingDataPacket);
session.setCraftingRecipes(recipeMap); session.setCraftingRecipes(recipeMap);
session.setStonecutterRecipes(stonecutterRecipeMap); session.setStonecutterRecipes(stonecutterRecipeMap);
session.setJavaToBedrockRecipeIds(recipeIDs); System.out.println(craftingDataPacket);
}
private void addSpecialRecipesIdentifiers(GeyserSession session, Recipe recipe, List<RecipeData> 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<String> 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);
} }
//TODO: rewrite //TODO: rewrite
@ -323,7 +292,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
* *
* @return the Java ingredient list as an array that Bedrock can understand * @return the Java ingredient list as an array that Bedrock can understand
*/ */
private ItemDescriptorWithCount[][] combinations(GeyserSession session, Ingredient[] ingredients) { private static ItemDescriptorWithCount[][] combinations(GeyserSession session, Ingredient[] ingredients) {
boolean empty = true; boolean empty = true;
Map<Set<ItemDescriptorWithCount>, IntSet> squashedOptions = new HashMap<>(); Map<Set<ItemDescriptorWithCount>, IntSet> squashedOptions = new HashMap<>();
for (int i = 0; i < ingredients.length; i++) { for (int i = 0; i < ingredients.length; i++) {
@ -407,17 +376,6 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
return combinations; return combinations;
} }
private void addRecipeIdentifier(GeyserSession session, String javaIdentifier, List<String> bedrockIdentifiers) {
session.getJavaToBedrockRecipeIds().computeIfAbsent(javaIdentifier, k -> new ArrayList<>()).addAll(bedrockIdentifiers);
}
@EqualsAndHashCode
@AllArgsConstructor
private static class GroupedItem {
ItemDefinition id;
int count;
}
private List<RecipeData> getSmithingTransformRecipes(GeyserSession session) { private List<RecipeData> getSmithingTransformRecipes(GeyserSession session) {
List<RecipeData> recipes = new ArrayList<>(); List<RecipeData> recipes = new ArrayList<>();
ItemMapping template = session.getItemMappings().getStoredItems().upgradeTemplate(); ItemMapping template = session.getItemMappings().getStoredItems().upgradeTemplate();
@ -442,4 +400,120 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
GeyserImpl.getInstance().getLogger().debug("Unable to find item with identifier " + bedrockId); GeyserImpl.getInstance().getLogger().debug("Unable to find item with identifier " + bedrockId);
return ItemDescriptorWithCount.EMPTY; return ItemDescriptorWithCount.EMPTY;
} }
@EqualsAndHashCode
@AllArgsConstructor
private static class GroupedItem {
ItemDefinition id;
int count;
}
private static final class RecipeContext {
private final GeyserSession session;
private final CraftingDataPacket packet;
private final Int2ObjectMap<GeyserRecipe> 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<GeyserRecipe> recipeMap) {
this.session = session;
this.packet = packet;
this.recipeMap = recipeMap;
}
List<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> bedrockIdentifiers) {
session.getJavaToBedrockRecipeIds().computeIfAbsent(javaIdentifier, k -> new ArrayList<>()).addAll(bedrockIdentifiers);
}
int getAndIncrementNetId() {
return this.netId++;
}
}
} }

@ -1 +1 @@
Subproject commit 54705bcd2bcba830267efbb1fbfd4e52972c40f7 Subproject commit 8795baeb170f7c9832da2def8625f0c5702abd91