Merge branch 'master' of https://github.com/GeyserMC/Geyser into feature/1.18

This commit is contained in:
Camotoy 2021-11-14 20:31:31 -05:00
commit 951b616f98
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
17 changed files with 766 additions and 15 deletions

View file

@ -233,6 +233,12 @@
<version>${adventure.version}</version> <version>${adventure.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>${adventure.version}</version>
<scope>compile</scope>
</dependency>
<!-- Other --> <!-- Other -->
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>

View file

@ -26,12 +26,48 @@
package org.geysermc.connector.inventory; package org.geysermc.connector.inventory;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
import lombok.Getter;
import lombok.Setter;
/** /**
* Used to determine if rename packets should be sent. * Used to determine if rename packets should be sent and stores
* the expected level cost for AnvilInventoryUpdater
*/ */
@Getter @Setter
public class AnvilContainer extends Container { public class AnvilContainer extends Container {
/**
* Stores the level cost received as a window property from Java
*/
private int javaLevelCost = 0;
/**
* A flag to specify whether javaLevelCost can be used as it can
* be outdated or not sent at all.
*/
private boolean useJavaLevelCost = false;
/**
* The new name of the item as received from Bedrock
*/
private String newName = null;
private GeyserItemStack lastInput = GeyserItemStack.EMPTY;
private GeyserItemStack lastMaterial = GeyserItemStack.EMPTY;
private int lastTargetSlot = -1;
public AnvilContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) { public AnvilContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) {
super(title, id, size, containerType, playerInventory); super(title, id, size, containerType, playerInventory);
} }
public GeyserItemStack getInput() {
return getItem(0);
}
public GeyserItemStack getMaterial() {
return getItem(1);
}
public GeyserItemStack getResult() {
return getItem(2);
}
} }

View file

@ -32,6 +32,8 @@ import org.geysermc.connector.inventory.CartographyContainer;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.utils.ItemUtils;
/** /**
* Used to send strings to the server and filter out unwanted words. * Used to send strings to the server and filter out unwanted words.
@ -47,12 +49,31 @@ public class BedrockFilterTextTranslator extends PacketTranslator<FilterTextPack
return; return;
} }
packet.setFromServer(true); packet.setFromServer(true);
session.sendUpstreamPacket(packet); if (session.getOpenInventory() instanceof AnvilContainer anvilContainer) {
anvilContainer.setNewName(packet.getText());
if (session.getOpenInventory() instanceof AnvilContainer) { String originalName = ItemUtils.getCustomName(anvilContainer.getInput().getNbt());
// Java Edition sends a packet every time an item is renamed even slightly in GUI. Fortunately, this works out for us now
ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(packet.getText()); String plainOriginalName = MessageTranslator.convertToPlainText(originalName, session.getLocale());
session.sendDownstreamPacket(renameItemPacket); String plainNewName = MessageTranslator.convertToPlainText(packet.getText(), session.getLocale());
if (!plainOriginalName.equals(plainNewName)) {
// Strip out formatting since Java Edition does not allow it
packet.setText(plainNewName);
// Java Edition sends a packet every time an item is renamed even slightly in GUI. Fortunately, this works out for us now
ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(plainNewName);
session.sendDownstreamPacket(renameItemPacket);
} else {
// Restore formatting for item since we're not renaming
packet.setText(MessageTranslator.convertMessageLenient(originalName));
// Java Edition sends the original custom name when not renaming,
// if there isn't a custom name an empty string is sent
ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(plainOriginalName);
session.sendDownstreamPacket(renameItemPacket);
}
anvilContainer.setUseJavaLevelCost(false);
session.getInventoryTranslator().updateSlot(session, anvilContainer, 1);
} }
session.sendUpstreamPacket(packet);
} }
} }

View file

@ -31,6 +31,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.renderer.TranslatableComponentRenderer; import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
@ -179,6 +180,33 @@ public class MessageTranslator {
return GSON_SERIALIZER.serialize(component); return GSON_SERIALIZER.serialize(component);
} }
/**
* Convert JSON and legacy format message to plain text
*
* @param message Message to convert
* @param locale Locale to use for translation strings
* @return The plain text of the message
*/
public static String convertToPlainText(String message, String locale) {
if (message == null) {
return "";
}
Component messageComponent = null;
if (message.startsWith("{") && message.endsWith("}")) {
// Message is a JSON object
try {
messageComponent = GSON_SERIALIZER.deserialize(message);
// Translate any components that require it
messageComponent = RENDERER.render(messageComponent, locale);
} catch (Exception ignored) {
}
}
if (messageComponent == null) {
messageComponent = LegacyComponentSerializer.legacySection().deserialize(message);
}
return PlainTextComponentSerializer.plainText().serialize(messageComponent);
}
/** /**
* Convert a team color to a chat color * Convert a team color to a chat color
* *

View file

@ -31,12 +31,13 @@ import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
import org.geysermc.connector.inventory.AnvilContainer; import org.geysermc.connector.inventory.AnvilContainer;
import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.inventory.PlayerInventory; import org.geysermc.connector.inventory.PlayerInventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot; import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater; import org.geysermc.connector.network.translators.inventory.updater.AnvilInventoryUpdater;
public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator { public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator {
public AnvilInventoryTranslator() { public AnvilInventoryTranslator() {
super(3, "minecraft:anvil[facing=north]", com.nukkitx.protocol.bedrock.data.inventory.ContainerType.ANVIL, UIInventoryUpdater.INSTANCE, super(3, "minecraft:anvil[facing=north]", com.nukkitx.protocol.bedrock.data.inventory.ContainerType.ANVIL, AnvilInventoryUpdater.INSTANCE,
"minecraft:chipped_anvil", "minecraft:damaged_anvil"); "minecraft:chipped_anvil", "minecraft:damaged_anvil");
} }
@ -74,4 +75,14 @@ public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator {
public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) { public Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory) {
return new AnvilContainer(name, windowId, this.size, containerType, playerInventory); return new AnvilContainer(name, windowId, this.size, containerType, playerInventory);
} }
@Override
public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) {
// The only property sent by Java is key 0 which is the level cost
if (key != 0) return;
AnvilContainer anvilContainer = (AnvilContainer) inventory;
anvilContainer.setJavaLevelCost(value);
anvilContainer.setUseJavaLevelCost(true);
updateSlot(session, anvilContainer, 1);
}
} }

View file

@ -0,0 +1,460 @@
/*
* Copyright (c) 2019-2021 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.connector.network.translators.inventory.updater;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundRenameItemPacket;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtMapBuilder;
import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.inventory.AnvilContainer;
import org.geysermc.connector.inventory.GeyserItemStack;
import org.geysermc.connector.inventory.Inventory;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
import org.geysermc.connector.registry.Registries;
import org.geysermc.connector.registry.type.EnchantmentData;
import org.geysermc.connector.utils.ItemUtils;
import java.util.Objects;
import java.util.Set;
public class AnvilInventoryUpdater extends InventoryUpdater {
public static final AnvilInventoryUpdater INSTANCE = new AnvilInventoryUpdater();
private static final int MAX_LEVEL_COST = 40;
@Override
public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
super.updateInventory(translator, session, inventory);
AnvilContainer anvilContainer = (AnvilContainer) inventory;
updateInventoryState(session, anvilContainer);
int targetSlot = getTargetSlot(session, anvilContainer);
for (int i = 0; i < translator.size; i++) {
final int bedrockSlot = translator.javaSlotToBedrock(i);
if (bedrockSlot == 50)
continue;
if (i == targetSlot) {
updateTargetSlot(translator, session, anvilContainer, targetSlot);
} else {
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(bedrockSlot);
slotPacket.setItem(inventory.getItem(i).getItemData(session));
session.sendUpstreamPacket(slotPacket);
}
}
}
@Override
public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) {
if (super.updateSlot(translator, session, inventory, javaSlot))
return true;
AnvilContainer anvilContainer = (AnvilContainer) inventory;
updateInventoryState(session, anvilContainer);
int lastTargetSlot = anvilContainer.getLastTargetSlot();
int targetSlot = getTargetSlot(session, anvilContainer);
if (targetSlot != javaSlot) {
// Update the requested slot
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
session.sendUpstreamPacket(slotPacket);
} else if (lastTargetSlot != javaSlot) {
// Update the previous target slot to remove repair cost changes
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(lastTargetSlot));
slotPacket.setItem(inventory.getItem(lastTargetSlot).getItemData(session));
session.sendUpstreamPacket(slotPacket);
}
updateTargetSlot(translator, session, anvilContainer, targetSlot);
return true;
}
private void updateInventoryState(GeyserSession session, AnvilContainer anvilContainer) {
GeyserItemStack input = anvilContainer.getInput();
if (!input.equals(anvilContainer.getLastInput())) {
anvilContainer.setLastInput(input.copy());
anvilContainer.setUseJavaLevelCost(false);
// Changing the item in the input slot resets the name field on Bedrock, but
// does not result in a FilterTextPacket
String originalName = MessageTranslator.convertToPlainText(ItemUtils.getCustomName(input.getNbt()), session.getLocale());
ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(originalName);
session.sendDownstreamPacket(renameItemPacket);
anvilContainer.setNewName(null);
}
GeyserItemStack material = anvilContainer.getMaterial();
if (!material.equals(anvilContainer.getLastMaterial())) {
anvilContainer.setLastMaterial(material.copy());
anvilContainer.setUseJavaLevelCost(false);
}
}
/**
* @param anvilContainer the anvil inventory
* @return the slot to change the repair cost
*/
private int getTargetSlot(GeyserSession session, AnvilContainer anvilContainer) {
GeyserItemStack input = anvilContainer.getInput();
GeyserItemStack material = anvilContainer.getMaterial();
if (!material.isEmpty()) {
if (!input.isEmpty() && isRepairing(session, input, material)) {
// Changing the repair cost on the material item makes it non-stackable
return 0;
}
// Prefer changing the material item because it does not reset the name field
return 1;
}
return 0;
}
private void updateTargetSlot(InventoryTranslator translator, GeyserSession session, AnvilContainer anvilContainer, int slot) {
ItemData itemData = anvilContainer.getItem(slot).getItemData(session);
itemData = hijackRepairCost(session, anvilContainer, itemData);
if (slot == 0 && isRenaming(session, anvilContainer, true)) {
// Can't change the RepairCost because it resets the name field on Bedrock
return;
}
anvilContainer.setLastTargetSlot(slot);
InventorySlotPacket slotPacket = new InventorySlotPacket();
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(slot));
slotPacket.setItem(itemData);
session.sendUpstreamPacket(slotPacket);
}
private ItemData hijackRepairCost(GeyserSession session, AnvilContainer anvilContainer, ItemData itemData) {
if (itemData.isNull()) {
return itemData;
}
// Fix level count by adjusting repair cost
int newRepairCost;
if (anvilContainer.isUseJavaLevelCost()) {
newRepairCost = anvilContainer.getJavaLevelCost();
} else {
// Did not receive a ServerWindowPropertyPacket with the level cost
newRepairCost = calcLevelCost(session, anvilContainer, false);
}
int bedrockLevelCost = calcLevelCost(session, anvilContainer, true);
if (bedrockLevelCost == -1) {
// Bedrock is unable to combine/repair the items
return itemData;
}
newRepairCost -= bedrockLevelCost;
if (newRepairCost == 0) {
// No change to the repair cost needed
return itemData;
}
NbtMapBuilder tagBuilder = NbtMap.builder();
if (itemData.getTag() != null) {
newRepairCost += itemData.getTag().getInt("RepairCost", 0);
tagBuilder.putAll(itemData.getTag());
}
tagBuilder.put("RepairCost", newRepairCost);
return itemData.toBuilder().tag(tagBuilder.build()).build();
}
/**
* Calculate the number of levels needed to combine/rename an item
*
* @param session the geyser session
* @param anvilContainer the anvil container
* @param bedrock True to count enchantments like Bedrock
* @return the number of levels needed
*/
public int calcLevelCost(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) {
GeyserItemStack input = anvilContainer.getInput();
GeyserItemStack material = anvilContainer.getMaterial();
if (input.isEmpty()) {
return 0;
}
int totalRepairCost = getRepairCost(input);
int cost = 0;
if (!material.isEmpty()) {
totalRepairCost += getRepairCost(material);
if (isCombining(session, input, material)) {
if (hasDurability(session, input) && input.getJavaId() == material.getJavaId()) {
cost += calcMergeRepairCost(session, input, material);
}
int enchantmentLevelCost = calcMergeEnchantmentCost(session, input, material, bedrock);
if (enchantmentLevelCost != -1) {
cost += enchantmentLevelCost;
} else if (cost == 0) {
// Can't repair or merge enchantments
return -1;
}
} else if (hasDurability(session, input) && isRepairing(session, input, material)) {
cost = calcRepairLevelCost(session, input, material);
if (cost == -1) {
// No damage to repair
return -1;
}
} else {
return -1;
}
}
int totalCost = totalRepairCost + cost;
if (isRenaming(session, anvilContainer, bedrock)) {
totalCost++;
if (cost == 0 && totalCost >= MAX_LEVEL_COST) {
// Items can still be renamed when the level cost for renaming exceeds 40
totalCost = MAX_LEVEL_COST - 1;
}
}
return totalCost;
}
/**
* Calculate the levels needed to repair an item with its repair material
* E.g. iron_sword + iron_ingot
*
* @param session Geyser session
* @param input an item with durability
* @param material the item's respective repair material
* @return the number of levels needed or 0 if it is not possible to repair any further
*/
private int calcRepairLevelCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
int newDamage = getDamage(input);
int unitRepair = Math.min(newDamage, input.getMapping(session).getMaxDamage() / 4);
if (unitRepair <= 0) {
// No damage to repair
return -1;
}
for (int i = 0; i < material.getAmount(); i++) {
newDamage -= unitRepair;
unitRepair = Math.min(newDamage, input.getMapping(session).getMaxDamage() / 4);
if (unitRepair <= 0) {
return i + 1;
}
}
return material.getAmount();
}
/**
* Calculate the levels cost for repairing items by combining two of the same item
*
* @param session Geyser session
* @param input an item with durability
* @param material a matching item
* @return the number of levels needed or 0 if it is not possible to repair any further
*/
private int calcMergeRepairCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
// If the material item is damaged 112% or more, then the input item will not be repaired
if (getDamage(input) > 0 && getDamage(material) < (material.getMapping(session).getMaxDamage() * 112 / 100)) {
return 2;
}
return 0;
}
/**
* Calculate the levels needed for combining the enchantments of two items
*
* @param session Geyser session
* @param input an item with durability
* @param material a matching item
* @param bedrock True to count enchantments like Bedrock, False to count like Java
* @return the number of levels needed or -1 if no enchantments can be applied
*/
private int calcMergeEnchantmentCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material, boolean bedrock) {
boolean hasCompatible = false;
Object2IntMap<JavaEnchantment> combinedEnchantments = getEnchantments(session, input, bedrock);
int cost = 0;
for (Object2IntMap.Entry<JavaEnchantment> entry : getEnchantments(session, material, bedrock).object2IntEntrySet()) {
JavaEnchantment enchantment = entry.getKey();
EnchantmentData data = Registries.ENCHANTMENTS.get(enchantment);
if (data == null) {
GeyserConnector.getInstance().getLogger().debug("Java enchantment not in registry: " + enchantment);
continue;
}
boolean canApply = isEnchantedBook(session, input) || data.validItems().contains(input.getJavaId());
for (JavaEnchantment incompatible : data.incompatibleEnchantments()) {
if (combinedEnchantments.containsKey(incompatible)) {
canApply = false;
if (!bedrock) {
cost++;
}
}
}
if (canApply || (!bedrock && session.getGameMode() == GameMode.CREATIVE)) {
int currentLevel = combinedEnchantments.getOrDefault(enchantment, 0);
int newLevel = entry.getIntValue();
if (newLevel == currentLevel) {
newLevel++;
}
newLevel = Math.max(currentLevel, newLevel);
if (newLevel > data.maxLevel()) {
newLevel = data.maxLevel();
}
combinedEnchantments.put(enchantment, newLevel);
int rarityMultiplier = data.rarityMultiplier();
if (isEnchantedBook(session, material) && rarityMultiplier > 1) {
rarityMultiplier /= 2;
}
if (bedrock) {
if (newLevel > currentLevel) {
hasCompatible = true;
}
if (enchantment == JavaEnchantment.IMPALING) {
// Multiplier is halved on Bedrock for some reason
rarityMultiplier /= 2;
} else if (enchantment == JavaEnchantment.SWEEPING) {
// Doesn't exist on Bedrock
rarityMultiplier = 0;
}
cost += rarityMultiplier * (newLevel - currentLevel);
} else {
hasCompatible = true;
cost += rarityMultiplier * newLevel;
}
}
}
if (!hasCompatible) {
return -1;
}
return cost;
}
private Object2IntMap<JavaEnchantment> getEnchantments(GeyserSession session, GeyserItemStack itemStack, boolean bedrock) {
if (itemStack.getNbt() == null) {
return Object2IntMaps.emptyMap();
}
Object2IntMap<JavaEnchantment> enchantments = new Object2IntOpenHashMap<>();
Tag enchantmentTag;
if (isEnchantedBook(session, itemStack)) {
enchantmentTag = itemStack.getNbt().get("StoredEnchantments");
} else {
enchantmentTag = itemStack.getNbt().get("Enchantments");
}
if (enchantmentTag instanceof ListTag listTag) {
for (Tag tag : listTag.getValue()) {
if (tag instanceof CompoundTag enchantTag) {
if (enchantTag.get("id") instanceof StringTag javaEnchId) {
JavaEnchantment enchantment = JavaEnchantment.getByJavaIdentifier(javaEnchId.getValue());
if (enchantment == null) {
GeyserConnector.getInstance().getLogger().debug("Unknown java enchantment: " + javaEnchId.getValue());
continue;
}
Tag javaEnchLvl = enchantTag.get("lvl");
if (!(javaEnchLvl instanceof ShortTag || javaEnchLvl instanceof IntTag))
continue;
// Handle duplicate enchantments
if (bedrock) {
enchantments.putIfAbsent(enchantment, ((Number) javaEnchLvl.getValue()).intValue());
} else {
enchantments.mergeInt(enchantment, ((Number) javaEnchLvl.getValue()).intValue(), Math::max);
}
}
}
}
}
return enchantments;
}
private boolean isEnchantedBook(GeyserSession session, GeyserItemStack itemStack) {
return itemStack.getJavaId() == session.getItemMappings().getStoredItems().enchantedBook().getJavaId();
}
private boolean isCombining(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
return isEnchantedBook(session, material) || (input.getJavaId() == material.getJavaId() && hasDurability(session, input));
}
private boolean isRepairing(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
Set<String> repairMaterials = input.getMapping(session).getRepairMaterials();
return repairMaterials != null && repairMaterials.contains(material.getMapping(session).getJavaIdentifier());
}
private boolean isRenaming(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) {
if (anvilContainer.getResult().isEmpty()) {
return false;
}
// This should really check the name field in all cases, but that requires the localized name
// of the item which can change depending on NBT and Minecraft Edition
String originalName = ItemUtils.getCustomName(anvilContainer.getInput().getNbt());
if (bedrock && originalName != null && anvilContainer.getNewName() != null) {
// Check text and formatting
String legacyOriginalName = MessageTranslator.convertMessageLenient(originalName, session.getLocale());
return !legacyOriginalName.equals(anvilContainer.getNewName());
}
return !Objects.equals(originalName, ItemUtils.getCustomName(anvilContainer.getResult().getNbt()));
}
private int getTagIntValueOr(GeyserItemStack itemStack, String tagName, int defaultValue) {
if (itemStack.getNbt() != null) {
Tag tag = itemStack.getNbt().get(tagName);
if (tag != null && tag.getValue() instanceof Number value) {
return value.intValue();
}
}
return defaultValue;
}
private int getRepairCost(GeyserItemStack itemStack) {
return getTagIntValueOr(itemStack, "RepairCost", 0);
}
private boolean hasDurability(GeyserSession session, GeyserItemStack itemStack) {
if (itemStack.getMapping(session).getMaxDamage() > 0) {
return getTagIntValueOr(itemStack, "Unbreakable", 0) == 0;
}
return false;
}
private int getDamage(GeyserItemStack itemStack) {
return getTagIntValueOr(itemStack, "Damage", 0);
}
}

View file

@ -147,6 +147,18 @@ public enum Enchantment {
*/ */
public static final String[] ALL_JAVA_IDENTIFIERS; public static final String[] ALL_JAVA_IDENTIFIERS;
public static JavaEnchantment getByJavaIdentifier(String javaIdentifier) {
if (!javaIdentifier.startsWith("minecraft:")) {
javaIdentifier = "minecraft:" + javaIdentifier;
}
for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {
if (ALL_JAVA_IDENTIFIERS[i].equalsIgnoreCase(javaIdentifier)) {
return VALUES[i];
}
}
return null;
}
static { static {
ALL_JAVA_IDENTIFIERS = new String[VALUES.length]; ALL_JAVA_IDENTIFIERS = new String[VALUES.length];
for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) { for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {

View file

@ -43,6 +43,7 @@ public class StoredItemMappings {
private final ItemMapping barrier; private final ItemMapping barrier;
private final ItemMapping compass; private final ItemMapping compass;
private final ItemMapping crossbow; private final ItemMapping crossbow;
private final ItemMapping enchantedBook;
private final ItemMapping fishingRod; private final ItemMapping fishingRod;
private final ItemMapping lodestoneCompass; private final ItemMapping lodestoneCompass;
private final ItemMapping milkBucket; private final ItemMapping milkBucket;
@ -58,6 +59,7 @@ public class StoredItemMappings {
this.barrier = load(itemMappings, "barrier"); this.barrier = load(itemMappings, "barrier");
this.compass = load(itemMappings, "compass"); this.compass = load(itemMappings, "compass");
this.crossbow = load(itemMappings, "crossbow"); this.crossbow = load(itemMappings, "crossbow");
this.enchantedBook = load(itemMappings, "enchanted_book");
this.fishingRod = load(itemMappings, "fishing_rod"); this.fishingRod = load(itemMappings, "fishing_rod");
this.lodestoneCompass = load(itemMappings, "lodestone_compass"); this.lodestoneCompass = load(itemMappings, "lodestone_compass");
this.milkBucket = load(itemMappings, "milk_bucket"); this.milkBucket = load(itemMappings, "milk_bucket");

View file

@ -39,12 +39,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.geysermc.connector.network.translators.collision.translators.BlockCollision; import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
import org.geysermc.connector.network.translators.world.event.LevelEventTransformer; import org.geysermc.connector.network.translators.world.event.LevelEventTransformer;
import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
import org.geysermc.connector.network.translators.sound.SoundHandler; import org.geysermc.connector.network.translators.sound.SoundHandler;
import org.geysermc.connector.network.translators.sound.SoundInteractionHandler; import org.geysermc.connector.network.translators.sound.SoundInteractionHandler;
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
import org.geysermc.connector.registry.loader.*; import org.geysermc.connector.registry.loader.*;
import org.geysermc.connector.registry.populator.ItemRegistryPopulator; import org.geysermc.connector.registry.populator.ItemRegistryPopulator;
import org.geysermc.connector.registry.populator.RecipeRegistryPopulator; import org.geysermc.connector.registry.populator.RecipeRegistryPopulator;
import org.geysermc.connector.registry.type.EnchantmentData;
import org.geysermc.connector.registry.type.ItemMappings; import org.geysermc.connector.registry.type.ItemMappings;
import org.geysermc.connector.registry.type.ParticleMapping; import org.geysermc.connector.registry.type.ParticleMapping;
import org.geysermc.connector.registry.type.SoundMapping; import org.geysermc.connector.registry.type.SoundMapping;
@ -82,6 +84,11 @@ public class Registries {
*/ */
public static final VersionedRegistry<Map<RecipeType, List<CraftingData>>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); public static final VersionedRegistry<Map<RecipeType, List<CraftingData>>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
/**
* A registry holding data of all the known enchantments.
*/
public static final SimpleMappedRegistry<JavaEnchantment, EnchantmentData> ENCHANTMENTS;
/** /**
* A registry holding a CompoundTag of the known entity identifiers. * A registry holding a CompoundTag of the known entity identifiers.
*/ */
@ -140,5 +147,6 @@ public class Registries {
// Create registries that require other registries to load first // Create registries that require other registries to load first
POTION_MIXES = SimpleRegistry.create(PotionMixRegistryLoader::new); POTION_MIXES = SimpleRegistry.create(PotionMixRegistryLoader::new);
ENCHANTMENTS = SimpleMappedRegistry.create("mappings/enchantments.json", EnchantmentRegistryLoader::new);
} }
} }

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2019-2021 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.connector.registry.loader;
import com.fasterxml.jackson.databind.JsonNode;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.MinecraftProtocol;
import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
import org.geysermc.connector.registry.Registries;
import org.geysermc.connector.registry.type.EnchantmentData;
import org.geysermc.connector.registry.type.ItemMapping;
import org.geysermc.connector.utils.FileUtils;
import java.io.InputStream;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Map;
public class EnchantmentRegistryLoader implements RegistryLoader<String, Map<JavaEnchantment, EnchantmentData>> {
@Override
public Map<JavaEnchantment, EnchantmentData> load(String input) {
InputStream enchantmentsStream = FileUtils.getResource(input);
JsonNode enchantmentsNode;
try {
enchantmentsNode = GeyserConnector.JSON_MAPPER.readTree(enchantmentsStream);
} catch (Exception e) {
throw new AssertionError("Unable to load enchantment data", e);
}
Map<JavaEnchantment, EnchantmentData> enchantments = new EnumMap<>(JavaEnchantment.class);
Iterator<Map.Entry<String, JsonNode>> it = enchantmentsNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
JavaEnchantment key = JavaEnchantment.getByJavaIdentifier(entry.getKey());
JsonNode node = entry.getValue();
int rarityMultiplier = switch (node.get("rarity").textValue()) {
case "common" -> 1;
case "uncommon" -> 2;
case "rare" -> 4;
case "very_rare" -> 8;
default -> throw new IllegalStateException("Unexpected value: " + node.get("rarity").textValue());
};
int maxLevel = node.get("max_level").asInt();
EnumSet<JavaEnchantment> incompatibleEnchantments = EnumSet.noneOf(JavaEnchantment.class);
JsonNode incompatibleEnchantmentsNode = node.get("incompatible_enchantments");
if (incompatibleEnchantmentsNode != null) {
for (JsonNode incompatibleNode : incompatibleEnchantmentsNode) {
incompatibleEnchantments.add(JavaEnchantment.getByJavaIdentifier(incompatibleNode.textValue()));
}
}
IntSet validItems = new IntOpenHashSet();
for (JsonNode itemNode : node.get("valid_items")) {
String javaIdentifier = itemNode.textValue();
ItemMapping itemMapping = Registries.ITEMS.forVersion(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getMapping(javaIdentifier);
if (itemMapping != null) {
validItems.add(itemMapping.getJavaId());
} else {
throw new NullPointerException("No item entry exists for java identifier: " + javaIdentifier);
}
}
EnchantmentData enchantmentData = new EnchantmentData(rarityMultiplier, maxLevel, incompatibleEnchantments, validItems);
enchantments.put(key, enchantmentData);
}
return enchantments;
}
}

View file

@ -37,10 +37,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import com.nukkitx.protocol.bedrock.packet.StartGamePacket; import com.nukkitx.protocol.bedrock.packet.StartGamePacket;
import com.nukkitx.protocol.bedrock.v465.Bedrock_v465; import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
import com.nukkitx.protocol.bedrock.v471.Bedrock_v471; import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.*; import it.unimi.dsi.fastutil.objects.*;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.translators.item.StoredItemMappings; import org.geysermc.connector.network.translators.item.StoredItemMappings;
@ -365,7 +362,12 @@ public class ItemRegistryPopulator {
.bedrockId(bedrockId) .bedrockId(bedrockId)
.bedrockData(mappingItem.getBedrockData()) .bedrockData(mappingItem.getBedrockData())
.bedrockBlockId(bedrockBlockId) .bedrockBlockId(bedrockBlockId)
.stackSize(stackSize); .stackSize(stackSize)
.maxDamage(mappingItem.getMaxDamage());
if (mappingItem.getRepairMaterials() != null) {
mappingBuilder = mappingBuilder.repairMaterials(new ObjectOpenHashSet<>(mappingItem.getRepairMaterials()));
}
if (mappingItem.getToolType() != null) { if (mappingItem.getToolType() != null) {
if (mappingItem.getToolTier() != null) { if (mappingItem.getToolTier() != null) {

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2019-2021 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.connector.registry.type;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
import java.util.Set;
public record EnchantmentData(int rarityMultiplier, int maxLevel, Set<JavaEnchantment> incompatibleEnchantments,
IntSet validItems) {
}

View file

@ -28,6 +28,8 @@ package org.geysermc.connector.registry.type;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* Represents Geyser's own serialized item information before being processed per-version * Represents Geyser's own serialized item information before being processed per-version
*/ */
@ -40,4 +42,6 @@ public class GeyserMappingItem {
@JsonProperty("stack_size") int stackSize = 64; @JsonProperty("stack_size") int stackSize = 64;
@JsonProperty("tool_type") String toolType; @JsonProperty("tool_type") String toolType;
@JsonProperty("tool_tier") String toolTier; @JsonProperty("tool_tier") String toolTier;
@JsonProperty("max_damage") int maxDamage = 0;
@JsonProperty("repair_materials") List<String> repairMaterials;
} }

View file

@ -31,13 +31,15 @@ import lombok.Value;
import org.geysermc.connector.network.MinecraftProtocol; import org.geysermc.connector.network.MinecraftProtocol;
import org.geysermc.connector.registry.BlockRegistries; import org.geysermc.connector.registry.BlockRegistries;
import java.util.Set;
@Value @Value
@Builder @Builder
@EqualsAndHashCode @EqualsAndHashCode
public class ItemMapping { public class ItemMapping {
public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0, public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0,
BlockRegistries.BLOCKS.forVersion(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(), BlockRegistries.BLOCKS.forVersion(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(),
64, null, null, null); 64, null, null, null, 0, null);
String javaIdentifier; String javaIdentifier;
String bedrockIdentifier; String bedrockIdentifier;
@ -57,6 +59,10 @@ public class ItemMapping {
String translationString; String translationString;
int maxDamage;
Set<String> repairMaterials;
/** /**
* Gets if this item is a block. * Gets if this item is a block.
* *

View file

@ -58,4 +58,19 @@ public class ItemUtils {
} }
return original; return original;
} }
/**
* @param itemTag the NBT tag of the item
* @return the custom name of the item
*/
public static String getCustomName(CompoundTag itemTag) {
if (itemTag != null) {
if (itemTag.get("display") instanceof CompoundTag displayTag) {
if (displayTag.get("Name") instanceof StringTag nameTag) {
return nameTag.getValue();
}
}
}
return null;
}
} }

@ -1 +1 @@
Subproject commit 7ff1b6567b56c7b0b8e28786b9bbc30abfaededf Subproject commit 5b6239f0a43ec9a38d65ed53b8d1bfaf564c1c3b

View file

@ -81,6 +81,16 @@ public class MessageTranslatorTest {
Assert.assertEquals("Unimplemented formatting chars not stripped", "Bold Underline", MessageTranslator.convertMessageLenient("§m§nBold Underline")); Assert.assertEquals("Unimplemented formatting chars not stripped", "Bold Underline", MessageTranslator.convertMessageLenient("§m§nBold Underline"));
} }
@Test
public void convertToPlainText() {
Assert.assertEquals("JSON message is not handled properly", "Many colors here", MessageTranslator.convertToPlainText("{\"extra\":[{\"color\":\"red\",\"text\":\"M\"},{\"color\":\"gold\",\"text\":\"a\"},{\"color\":\"yellow\",\"text\":\"n\"},{\"color\":\"green\",\"text\":\"y \"},{\"color\":\"aqua\",\"text\":\"c\"},{\"color\":\"dark_purple\",\"text\":\"o\"},{\"color\":\"red\",\"text\":\"l\"},{\"color\":\"gold\",\"text\":\"o\"},{\"color\":\"yellow\",\"text\":\"r\"},{\"color\":\"green\",\"text\":\"s \"},{\"color\":\"aqua\",\"text\":\"h\"},{\"color\":\"dark_purple\",\"text\":\"e\"},{\"color\":\"red\",\"text\":\"r\"},{\"color\":\"gold\",\"text\":\"e\"}],\"text\":\"\"}", "en_US"));
Assert.assertEquals("Legacy formatted message is not handled properly (Colors)", "Many colors here", MessageTranslator.convertToPlainText("§cM§6a§en§ay §bc§5o§cl§6o§er§as §bh§5e§cr§6e", "en_US"));
Assert.assertEquals("Legacy formatted message is not handled properly (Style)", "Obf Bold Strikethrough Underline Italic Reset", MessageTranslator.convertToPlainText("§kObf §lBold §mStrikethrough §nUnderline §oItalic §rReset", "en_US"));
Assert.assertEquals("Valid lenient JSON is not handled properly", "Strange", MessageTranslator.convertToPlainText("§rStrange", "en_US"));
Assert.assertEquals("Empty message is not handled properly", "", MessageTranslator.convertToPlainText("", "en_US"));
Assert.assertEquals("Whitespace is not preserved", " ", MessageTranslator.convertToPlainText(" ", "en_US"));
}
@Test @Test
public void testNullTextPacket() { public void testNullTextPacket() {
DefaultComponentSerializer.get().deserialize("null"); DefaultComponentSerializer.get().deserialize("null");