Skip to content

Commit

Permalink
Fix smithing tables on pre-1.20 servers (#4056)
Browse files Browse the repository at this point in the history
  • Loading branch information
onebeastchris committed Aug 17, 2023
1 parent a99d2bc commit 706d1b9
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ public class StoredItemMappings {
private final ItemMapping barrier;
private final ItemMapping compass;
private final ItemMapping crossbow;
private final ItemMapping egg;
private final ItemMapping glassBottle;
private final ItemMapping milkBucket;
private final ItemMapping powderSnowBucket;
private final ItemMapping egg;
private final ItemMapping shield;
private final ItemMapping upgradeTemplate;
private final ItemMapping wheat;
private final ItemMapping writableBook;

Expand All @@ -59,11 +60,12 @@ public StoredItemMappings(Map<Item, ItemMapping> itemMappings) {
this.barrier = load(itemMappings, Items.BARRIER);
this.compass = load(itemMappings, Items.COMPASS);
this.crossbow = load(itemMappings, Items.CROSSBOW);
this.egg = load(itemMappings, Items.EGG);
this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE);
this.milkBucket = load(itemMappings, Items.MILK_BUCKET);
this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET);
this.egg = load(itemMappings, Items.EGG);
this.shield = load(itemMappings, Items.SHIELD);
this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE);
this.wheat = load(itemMappings, Items.WHEAT);
this.writableBook = load(itemMappings, Items.WRITABLE_BOOK);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import lombok.Builder;
import lombok.Value;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
Expand All @@ -39,7 +41,6 @@
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.item.type.PotionItem;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -77,7 +78,7 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
* @param itemStack the itemstack
* @return an item entry from the given java edition identifier
*/
@Nonnull
@NonNull
public ItemMapping getMapping(ItemStack itemStack) {
return this.getMapping(itemStack.getId());
}
Expand All @@ -89,11 +90,12 @@ public ItemMapping getMapping(ItemStack itemStack) {
* @param javaId the id
* @return an item entry from the given java edition identifier
*/
@Nonnull
@NonNull
public ItemMapping getMapping(int javaId) {
return javaId >= 0 && javaId < this.items.length ? this.items[javaId] : ItemMapping.AIR;
}

@Nullable
public ItemMapping getMapping(Item javaItem) {
return getMapping(javaItem.javaIdentifier());
}
Expand All @@ -105,6 +107,7 @@ public ItemMapping getMapping(Item javaItem) {
* @param javaIdentifier the block state identifier
* @return an item entry from the given java edition identifier
*/
@Nullable
public ItemMapping getMapping(String javaIdentifier) {
return this.cachedJavaMappings.computeIfAbsent(javaIdentifier, key -> {
for (ItemMapping mapping : this.items) {
Expand All @@ -122,6 +125,7 @@ public ItemMapping getMapping(String javaIdentifier) {
* @param data the item data
* @return an item entry from the given item data
*/
@NonNull
public ItemMapping getMapping(ItemData data) {
ItemDefinition definition = data.getDefinition();
if (ItemDefinition.AIR.equals(definition)) {
Expand Down Expand Up @@ -158,11 +162,22 @@ public ItemMapping getMapping(ItemData data) {
return ItemMapping.AIR;
}

@Nullable
@Override
public ItemDefinition getDefinition(int bedrockId) {
return this.itemDefinitions.get(bedrockId);
}

@Nullable
public ItemDefinition getDefinition(String bedrockIdentifier) {
for (ItemDefinition itemDefinition : this.itemDefinitions.values()) {
if (itemDefinition.getIdentifier().equals(bedrockIdentifier)) {
return itemDefinition;
}
}
return null;
}

@Override
public boolean isRegistered(ItemDefinition definition) {
return getDefinition(definition.getRuntimeId()) == definition;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private boolean emulatePost1_18Logic = true;

/**
* Whether to emulate pre-1.20 smithing table behavior.
* Adapts ViaVersion's furnace UI to one Bedrock can use.
* See {@link org.geysermc.geyser.translator.inventory.OldSmithingTableTranslator}.
*/
@Setter
private boolean oldSmithingTable = false;

/**
* The current attack speed of the player. Used for sending proper cooldown timings.
* Setting a default fixes cooldowns not showing up on a fresh world.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2019-2023 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.inventory;

import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.DropAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.PlaceAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.SwapAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.TakeAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.inventory.BedrockContainerSlot;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.InventoryUtils;

import java.util.function.IntFunction;

/**
* Translator for smithing tables for pre-1.20 servers.
* This adapts ViaVersion's furnace ui to the 1.20+ smithing table; with the addition of a fake smithing template so Bedrock clients can use it.
*/
public class OldSmithingTableTranslator extends AbstractBlockInventoryTranslator {

public static final OldSmithingTableTranslator INSTANCE = new OldSmithingTableTranslator();

private static final IntFunction<ItemData> UPGRADE_TEMPLATE = InventoryUtils.getUpgradeTemplate();

private OldSmithingTableTranslator() {
super(3, "minecraft:smithing_table", ContainerType.SMITHING_TABLE, UIInventoryUpdater.INSTANCE);
}

@Override
public int bedrockSlotToJava(ItemStackRequestSlotData slotInfoData) {
return switch (slotInfoData.getContainer()) {
case SMITHING_TABLE_INPUT -> 0;
case SMITHING_TABLE_MATERIAL -> 1;
case SMITHING_TABLE_RESULT, CREATED_OUTPUT -> 2;
default -> super.bedrockSlotToJava(slotInfoData);
};
}

@Override
public BedrockContainerSlot javaSlotToBedrockContainer(int slot) {
return switch (slot) {
case 0 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_INPUT, 51);
case 1 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_MATERIAL, 52);
case 2 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_RESULT, 50);
default -> super.javaSlotToBedrockContainer(slot);
};
}

@Override
public int javaSlotToBedrock(int slot) {
return switch (slot) {
case 0 -> 51;
case 1 -> 52;
case 2 -> 50;
default -> super.javaSlotToBedrock(slot);
};
}

@Override
public boolean shouldHandleRequestFirst(ItemStackRequestAction action, Inventory inventory) {
return true;
}

@Override
protected ItemStackResponse translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
for (var action: request.getActions()) {
switch (action.getType()) {
case DROP -> {
if (isInvalidAction(((DropAction) action).getSource())) {
return rejectRequest(request, false);
}
}
case TAKE -> {
if (isInvalidAction(((TakeAction) action).getSource()) ||
isInvalidAction(((TakeAction) action).getDestination())) {
return rejectRequest(request, false);
}
}
case SWAP -> {
if (isInvalidAction(((SwapAction) action).getSource()) ||
isInvalidAction(((SwapAction) action).getDestination())) {
return rejectRequest(request, false);
}
}
case PLACE -> {
if (isInvalidAction(((PlaceAction) action).getSource()) ||
isInvalidAction(((PlaceAction) action).getDestination())) {
return rejectRequest(request, false);
}
}
}
}
// Allow everything else that doesn't involve the fake template
return super.translateRequest(session, inventory, request);
}

private boolean isInvalidAction(ItemStackRequestSlotData slotData) {
return slotData.getContainer().equals(ContainerSlotType.SMITHING_TABLE_TEMPLATE);
}

@Override
public void openInventory(GeyserSession session, Inventory inventory) {
super.openInventory(session, inventory);

// pre-1.20 server has no concept of templates, but we are working with a 1.20 client
// put a fake netherite upgrade template in the template slot otherwise the client doesn't recognize a valid recipe
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(53);
slotPacket.setItem(UPGRADE_TEMPLATE.apply(session.getUpstream().getProtocolVersion()));
session.sendUpstreamPacket(slotPacket);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.TrimDataPacket;
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;
Expand Down Expand Up @@ -81,11 +82,24 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
MultiRecipeData.of(UUID.fromString("602234e4-cac1-4353-8bb7-b1ebff70024b"), ++LAST_RECIPE_NET_ID) // Map locking
);

private static final List<String> NETHERITE_UPGRADES = List.of(
"minecraft:netherite_sword",
"minecraft:netherite_shovel",
"minecraft:netherite_pickaxe",
"minecraft:netherite_axe",
"minecraft:netherite_hoe",
"minecraft:netherite_helmet",
"minecraft:netherite_chestplate",
"minecraft:netherite_leggings",
"minecraft:netherite_boots"
);

@Override
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;

Int2ObjectMap<GeyserRecipe> recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion()));
Int2ObjectMap<List<StoneCuttingRecipeData>> unsortedStonecutterData = new Int2ObjectOpenHashMap<>();
Expand Down Expand Up @@ -168,6 +182,7 @@ public void translate(GeyserSession session, ClientboundUpdateRecipesPacket pack

}
case SMITHING_TRIM -> {
sendTrimRecipes = true;
// ignored currently - see below
}
default -> {
Expand Down Expand Up @@ -214,23 +229,28 @@ public void translate(GeyserSession session, ClientboundUpdateRecipesPacket pack
}
}

// FIXME: if the server/viaversion doesn't send trim recipes then we shouldn't either.

// BDS sends armor trim templates and materials before the CraftingDataPacket
TrimDataPacket trimDataPacket = new TrimDataPacket();
trimDataPacket.getPatterns().addAll(TrimRecipe.PATTERNS);
trimDataPacket.getMaterials().addAll(TrimRecipe.MATERIALS);
session.sendUpstreamPacket(trimDataPacket);
session.getLastRecipeNetId().set(netId);

// Identical smithing_trim recipe sent by BDS that uses tag-descriptors, as the client seems to ignore the
// approach of using many default-descriptors (which we do for smithing_transform)
craftingDataPacket.getCraftingData().add(SmithingTrimRecipeData.of(TrimRecipe.ID,
TrimRecipe.BASE, TrimRecipe.ADDITION, TrimRecipe.TEMPLATE, "smithing_table", netId++));
// Only send smithing trim recipes if Java/ViaVersion sends them.
if (sendTrimRecipes) {
// BDS sends armor trim templates and materials before the CraftingDataPacket
TrimDataPacket trimDataPacket = new TrimDataPacket();
trimDataPacket.getPatterns().addAll(TrimRecipe.PATTERNS);
trimDataPacket.getMaterials().addAll(TrimRecipe.MATERIALS);
session.sendUpstreamPacket(trimDataPacket);

// Identical smithing_trim recipe sent by BDS that uses tag-descriptors, as the client seems to ignore the
// approach of using many default-descriptors (which we do for smithing_transform)
craftingDataPacket.getCraftingData().add(SmithingTrimRecipeData.of(TrimRecipe.ID,
TrimRecipe.BASE, TrimRecipe.ADDITION, TrimRecipe.TEMPLATE, "smithing_table", session.getLastRecipeNetId().getAndIncrement()));
} else {
// manually add recipes for the upgrade template (workaround), since Java pre-1.20 doesn't
craftingDataPacket.getCraftingData().addAll(getSmithingTransformRecipes(session));
}
session.setOldSmithingTable(!sendTrimRecipes);
session.sendUpstreamPacket(craftingDataPacket);
session.setCraftingRecipes(recipeMap);
session.setStonecutterRecipes(stonecutterRecipeMap);
session.getLastRecipeNetId().set(netId);
}

//TODO: rewrite
Expand Down Expand Up @@ -323,4 +343,29 @@ private static class GroupedItem {
ItemDefinition id;
int count;
}

private List<RecipeData> getSmithingTransformRecipes(GeyserSession session) {
List<RecipeData> recipes = new ArrayList<>();
ItemMapping template = session.getItemMappings().getStoredItems().upgradeTemplate();

for (String identifier : NETHERITE_UPGRADES) {
recipes.add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData.of(identifier + "_smithing",
getDescriptorFromId(session, template.getBedrockIdentifier()),
getDescriptorFromId(session, identifier.replace("netherite", "diamond")),
getDescriptorFromId(session, "minecraft:netherite_ingot"),
ItemData.builder().definition(Objects.requireNonNull(session.getItemMappings().getDefinition(identifier))).count(1).build(),
"smithing_table",
session.getLastRecipeNetId().getAndIncrement()));
}
return recipes;
}

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;
}
}

0 comments on commit 706d1b9

Please sign in to comment.