Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@

/**
* @param buttonId the button that needs to be pressed for Java Edition to accept this item.
* @param input the input that this recipe accepts.
* @param output the expected output of this item when cut.
*/
public record GeyserStonecutterData(int buttonId, @Nullable ItemStack output) {
public record GeyserStonecutterData(int buttonId, int input, @Nullable ItemStack output) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is input the java item id? Would be lovely to clarify in Javadocs

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@

package org.geysermc.geyser.inventory.recipe;

import it.unimi.dsi.fastutil.objects.ObjectIntPair;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.TrimMaterial;
import org.cloudburstmc.protocol.bedrock.data.TrimPattern;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTrimRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor;
import org.geysermc.geyser.GeyserImpl;
Expand All @@ -43,19 +45,37 @@
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ProvidesTrimMaterial;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.geysermc.geyser.util.InventoryUtils.LAST_RECIPE_NET_ID;

/**
* Stores information on trim materials and patterns, including smithing armor hacks for pre-1.20.
*/
public final class TrimRecipe {
private static final Map<ProvidesTrimMaterial, Item> trimMaterialProviders = new HashMap<>();

// For CraftingDataPacket
public static final String ID = "minecraft:smithing_armor_trim";
public static final ItemDescriptorWithCount BASE = tagDescriptor("minecraft:trimmable_armors");
public static final ItemDescriptorWithCount ADDITION = tagDescriptor("minecraft:trim_materials");
public static final ItemDescriptorWithCount TEMPLATE = tagDescriptor("minecraft:trim_templates");
public static final List<ObjectIntPair<String>> NETHERITE_UPGRADES = List.of(
ObjectIntPair.of("minecraft:netherite_sword", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_shovel", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_pickaxe", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_axe", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_hoe", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_helmet", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_chestplate", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_leggings", ++LAST_RECIPE_NET_ID),
ObjectIntPair.of("minecraft:netherite_boots", ++LAST_RECIPE_NET_ID)
);

public static final SmithingTrimRecipeData RECIPE = SmithingTrimRecipeData.of(
"minecraft:smithing_armor_trim",
tagDescriptor("minecraft:trimmable_armors"),
tagDescriptor("minecraft:trim_materials"),
tagDescriptor("minecraft:trim_templates"),
"smithing_table",
++LAST_RECIPE_NET_ID
);

public static TrimMaterial readTrimMaterial(RegistryEntryContext context) {
String key = context.id().asMinimalString();
Expand Down
73 changes: 68 additions & 5 deletions core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@
import com.google.gson.JsonObject;
import io.netty.channel.Channel;
import io.netty.channel.EventLoop;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
Expand Down Expand Up @@ -76,14 +77,20 @@
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType;
import org.cloudburstmc.protocol.bedrock.data.definitions.DimensionDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.CraftingRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.RecipeUnlockingRequirement;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.cloudburstmc.protocol.bedrock.packet.BiomeDefinitionListPacket;
import org.cloudburstmc.protocol.bedrock.packet.CameraPresetsPacket;
import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.CreativeContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.DimensionDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket;
Expand Down Expand Up @@ -150,6 +157,8 @@
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe;
import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
import org.geysermc.geyser.inventory.recipe.RecipeUtil;
import org.geysermc.geyser.inventory.recipe.TrimRecipe;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.BlockItem;
import org.geysermc.geyser.level.BedrockDimension;
Expand All @@ -158,6 +167,7 @@
import org.geysermc.geyser.network.netty.LocalSession;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.auth.AuthData;
import org.geysermc.geyser.session.auth.BedrockClientData;
Expand Down Expand Up @@ -190,6 +200,7 @@
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.CooldownUtils;
Expand All @@ -214,6 +225,7 @@
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.HandPreference;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.setting.ChatVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.setting.ParticleStatus;
import org.geysermc.mcprotocollib.protocol.data.game.setting.SkinPart;
Expand Down Expand Up @@ -521,16 +533,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final Int2ObjectMap<List<String>> javaToBedrockRecipeIds;

private final Int2ObjectMap<GeyserRecipe> craftingRecipes;
@Setter
private Pair<CraftingRecipeData, GeyserRecipe> lastCreatedRecipe = null; // TODO try to prevent sending duplicate recipes
private final AtomicInteger lastRecipeNetId;

/**
* Saves a list of all stonecutter recipes, for use in a stonecutter inventory.
* The key is the Bedrock recipe net ID; the values are their respective output and button ID.
*/
@Setter
private Int2ObjectMap<GeyserStonecutterData> stonecutterRecipes;
private Int2ObjectMap<GeyserStonecutterData> stonecutterRecipes = Int2ObjectMaps.emptyMap();
private final List<GeyserSmithingRecipe> smithingRecipes = new ArrayList<>();

/**
Expand Down Expand Up @@ -2525,4 +2535,57 @@ public void sendNetworkLatencyStackPacket(long timestamp, boolean ensureEventLoo
public String getDebugInfo() {
return "Username: %s, DeviceOs: %s, Version: %s".formatted(bedrockUsername(), platform(), version());
}

public CraftingDataPacket getCraftingDataPacket() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to avoid confusion with getters for fields? e.g. a create prefix would be, imo, clearer

CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
craftingDataPacket.setCleanRecipes(true);
craftingDataPacket.getCraftingData().addAll(RecipeUtil.CARTOGRAPHY_RECIPES);
// Potion mixes are registered by default, as they are needed to be able to put ingredients into the brewing stand.
craftingDataPacket.getPotionMixData().addAll(Registries.POTION_MIXES.forVersion(getUpstream().getProtocolVersion()));
for (GeyserRecipe recipe : craftingRecipes.values()) {
craftingDataPacket.getCraftingData().addAll(recipe.asRecipeData(this));
}
Comment on lines +2545 to +2547
for (GeyserSmithingRecipe recipe : smithingRecipes) {
craftingDataPacket.getCraftingData().addAll(recipe.asRecipeData(this));
}
if (oldSmithingTable) {
ItemMapping template = itemMappings.getStoredItems().upgradeTemplate();

for (ObjectIntPair<String> identifierAndNetId : TrimRecipe.NETHERITE_UPGRADES) {
craftingDataPacket.getCraftingData().add(SmithingTransformRecipeData.of(identifierAndNetId.left() + "_smithing",
getDescriptorFromId(this, template.getBedrockIdentifier()),
getDescriptorFromId(this, identifierAndNetId.left().replace("netherite", "diamond")),
getDescriptorFromId(this, "minecraft:netherite_ingot"),
ItemData.builder().definition(Objects.requireNonNull(itemMappings.getDefinition(identifierAndNetId.left()))).count(1).build(),
"smithing_table",
identifierAndNetId.rightInt()));
}
} else {
craftingDataPacket.getCraftingData().add(TrimRecipe.RECIPE);
}
for (Int2ObjectMap.Entry<GeyserStonecutterData> recipe : stonecutterRecipes.int2ObjectEntrySet()) {
int buttonId = recipe.getValue().buttonId();
int javaInput = recipe.getValue().input();
ItemMapping mapping = itemMappings.getMapping(javaInput);
ItemDescriptorWithCount descriptor = new ItemDescriptorWithCount(new DefaultDescriptor(mapping.getBedrockDefinition(), mapping.getBedrockData()), 1);
ItemStack javaOutput = recipe.getValue().output();
ItemData output = ItemTranslator.translateToBedrock(this, javaOutput);
int recipeNetId = recipe.getIntKey();
UUID uuid = UUID.randomUUID();
// We need to register stonecutting recipes, so they show up on Bedrock
// (Implementation note: recipe ID creates the order which stonecutting recipes are shown in stonecutter)
craftingDataPacket.getCraftingData().add(ShapelessRecipeData.shapeless("stonecutter_" + javaInput + "_" + buttonId,
Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, recipeNetId, RecipeUnlockingRequirement.INVALID));
}
return craftingDataPacket;
}

private ItemDescriptorWithCount getDescriptorFromId(GeyserSession session, String bedrockId) {
ItemDefinition bedrockDefinition = session.getItemMappings().getDefinition(bedrockId);
if (bedrockDefinition != null) {
return ItemDescriptorWithCount.fromItem(ItemData.builder().definition(bedrockDefinition).count(1).build());
}
GeyserImpl.getInstance().getLogger().debug("Unable to find item with identifier " + bedrockId);
return ItemDescriptorWithCount.EMPTY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@

package org.geysermc.geyser.translator.protocol.java;

import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
Expand All @@ -38,8 +36,6 @@
import java.util.ArrayList;
import java.util.List;

import static org.geysermc.geyser.inventory.recipe.RecipeUtil.CARTOGRAPHY_RECIPES;

@Translator(packet = ClientboundFinishConfigurationPacket.class)
public class JavaFinishConfigurationTranslator extends PacketTranslator<ClientboundFinishConfigurationPacket> {

Expand All @@ -55,14 +51,7 @@ public void translate(GeyserSession session, ClientboundFinishConfigurationPacke
}
session.getEntityCache().removeAllPlayerEntities();

// Potion mixes are registered by default, as they are needed to be able to put ingredients into the brewing stand.
// (Also add it here so recipes get cleared on configuration - 1.21.3)
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
craftingDataPacket.setCleanRecipes(true);
craftingDataPacket.getCraftingData().addAll(CARTOGRAPHY_RECIPES);
craftingDataPacket.getPotionMixData().addAll(Registries.POTION_MIXES.forVersion(session.getUpstream().getProtocolVersion()));
if (session.isSentSpawnPacket()) {
session.getUpstream().sendPacket(craftingDataPacket);
// TODO proper fix to check if we've been online - in online mode (with auth screen),
// recipes are not yet known
if (session.getStonecutterRecipes() != null) {
Expand All @@ -72,8 +61,9 @@ public void translate(GeyserSession session, ClientboundFinishConfigurationPacke
session.getSmithingRecipes().clear();
session.getStonecutterRecipes().clear();
}
session.getUpstream().sendPacket(session.getCraftingDataPacket());
} else {
session.getUpstream().queuePostStartGamePacket(craftingDataPacket);
session.getUpstream().queuePostStartGamePacket(session.getCraftingDataPacket());
}

// while ClientboundLoginPacket holds the level, it doesn't hold the scoreboard.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ public void translate(GeyserSession session, ClientboundRecipeBookAddPacket pack
int netId = session.getLastRecipeNetId().get();
Int2ObjectMap<List<String>> javaToBedrockRecipeIds = session.getJavaToBedrockRecipeIds();
Int2ObjectMap<GeyserRecipe> geyserRecipes = session.getCraftingRecipes();
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();

UnlockedRecipesPacket recipesPacket = new UnlockedRecipesPacket();
recipesPacket.setAction(packet.isReplace() ? UnlockedRecipesPacket.ActionType.INITIALLY_UNLOCKED : UnlockedRecipesPacket.ActionType.NEWLY_UNLOCKED);
Expand All @@ -70,11 +69,10 @@ public void translate(GeyserSession session, ClientboundRecipeBookAddPacket pack
if (display instanceof ShapedCraftingRecipeDisplay shapedRecipe) {
GeyserRecipe geyserRecipe = new GeyserShapedRecipe(contents.id(), netId, shapedRecipe);

List<RecipeData> recipeData = geyserRecipe.asRecipeData(session);
craftingDataPacket.getCraftingData().addAll(recipeData);
int recipeCount = geyserRecipe.asRecipeData(session).size();

List<String> bedrockRecipeIds = new ArrayList<>();
for (int i = 0; i < recipeData.size(); i++) {
for (int i = 0; i < recipeCount; i++) {
String recipeId = contents.id() + "_" + i;
recipesPacket.getUnlockedRecipes().add(recipeId);
bedrockRecipeIds.add(recipeId);
Expand All @@ -84,11 +82,10 @@ public void translate(GeyserSession session, ClientboundRecipeBookAddPacket pack
} else if (display instanceof ShapelessCraftingRecipeDisplay shapelessRecipe) {
GeyserRecipe geyserRecipe = new GeyserShapelessRecipe(contents.id(), netId, shapelessRecipe);

List<RecipeData> recipeData = geyserRecipe.asRecipeData(session);
craftingDataPacket.getCraftingData().addAll(recipeData);
int recipeCount = geyserRecipe.asRecipeData(session).size();

List<String> bedrockRecipeIds = new ArrayList<>();
for (int i = 0; i < recipeData.size(); i++) {
for (int i = 0; i < recipeCount; i++) {
String recipeId = contents.id() + "_" + i;
recipesPacket.getUnlockedRecipes().add(recipeId);
bedrockRecipeIds.add(recipeId);
Expand All @@ -104,18 +101,15 @@ public void translate(GeyserSession session, ClientboundRecipeBookAddPacket pack
GeyserSmithingRecipe geyserRecipe = new GeyserSmithingRecipe(contents.id(), netId, smithingRecipe);
session.getSmithingRecipes().add(geyserRecipe);

List<RecipeData> recipeData = geyserRecipe.asRecipeData(session);
craftingDataPacket.getCraftingData().addAll(recipeData);

netId += recipeData.size();
netId += geyserRecipe.asRecipeData(session).size();
}
}

if (!recipesPacket.getUnlockedRecipes().isEmpty()) {
// Sending an empty list here will crash the client as of 1.20.60
// This was definitely in the codebase the entire time and did not
// accidentally get refactored out during Java 1.21.3. :)
session.sendUpstreamPacket(craftingDataPacket);
session.sendUpstreamPacket(session.getCraftingDataPacket());
session.sendUpstreamPacket(recipesPacket);
}
session.getLastRecipeNetId().set(netId);
Expand Down
Loading
Loading