Translate client-computed recipes (#1181)

* Translate client-computed recipes

A handful of recipes are complex enough on Java Edition that the client simply calculates them after getting an assurance that they are valid recipes. This PR stores those recipes in a Bedrock-compatible format in mappings, then generates the CraftingData information on startup to send to the Bedrock client when called. This fixes firework rocket and star crafting, and fixes leather armor and shulker box dyeing.

The recipe information for everything except leather armor was taken right from the Bedrock server. The leather armor had to be created separately (see https://github.com/DoctorMacc/LeatherDyeingCreation). There will be a slight visual difference in the crafting result preview if the armor is not perfectly dyed to one of the sixteen colors, but this is a visual issue that will persist unless we calculate every single possbile combination.

* Revert other changes

* Register shulker box recipes properly

* Add break

* Update mappings
This commit is contained in:
Camotoy 2020-08-24 22:14:44 -04:00 committed by GitHub
parent 6e8106eeec
commit c1a70c7754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 170 additions and 19 deletions

View File

@ -47,6 +47,7 @@ import org.geysermc.connector.network.translators.effect.EffectRegistry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
import org.geysermc.connector.network.translators.item.PotionMixRegistry;
import org.geysermc.connector.network.translators.item.RecipeRegistry;
import org.geysermc.connector.network.translators.sound.SoundHandlerRegistry;
import org.geysermc.connector.network.translators.sound.SoundRegistry;
import org.geysermc.connector.network.translators.world.WorldManager;
@ -131,6 +132,7 @@ public class GeyserConnector {
ItemTranslator.init();
LocaleUtils.init();
PotionMixRegistry.init();
RecipeRegistry.init();
SoundRegistry.init();
SoundHandlerRegistry.init();

View File

@ -173,21 +173,8 @@ public class ItemRegistry {
int netId = 1;
List<ItemData> creativeItems = new ArrayList<>();
for (JsonNode itemNode : creativeItemEntries) {
try {
short damage = 0;
NbtMap tag = null;
if (itemNode.has("damage")) {
damage = itemNode.get("damage").numberValue().shortValue();
}
if (itemNode.has("nbt_b64")) {
byte[] bytes = Base64.getDecoder().decode(itemNode.get("nbt_b64").asText());
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag();
}
creativeItems.add(ItemData.fromNet(netId++, itemNode.get("id").asInt(), damage, 1, tag));
} catch (IOException e) {
e.printStackTrace();
}
ItemData item = getBedrockItemFromJson(itemNode);
creativeItems.add(ItemData.fromNet(netId++, item.getId(), item.getDamage(), item.getCount(), item.getTag()));
}
CREATIVE_ITEMS = creativeItems.toArray(new ItemData[0]);
}
@ -233,4 +220,31 @@ public class ItemRegistry {
return JAVA_IDENTIFIER_MAP.computeIfAbsent(javaIdentifier, key -> ITEM_ENTRIES.values()
.stream().filter(itemEntry -> itemEntry.getJavaIdentifier().equals(key)).findFirst().orElse(null));
}
/**
* Gets a Bedrock {@link ItemData} from a {@link JsonNode}
* @param itemNode the JSON node that contains ProxyPass-compatible Bedrock item data
* @return
*/
public static ItemData getBedrockItemFromJson(JsonNode itemNode) {
int count = 1;
short damage = 0;
NbtMap tag = null;
if (itemNode.has("damage")) {
damage = itemNode.get("damage").numberValue().shortValue();
}
if (itemNode.has("count")) {
count = itemNode.get("count").asInt();
}
if (itemNode.has("nbt_b64")) {
byte[] bytes = Base64.getDecoder().decode(itemNode.get("nbt_b64").asText());
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
try {
tag = (NbtMap) NbtUtils.createReaderLE(bais).readTag();
} catch (IOException e) {
e.printStackTrace();
}
}
return ItemData.of(itemNode.get("id").asInt(), damage, count, tag);
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2019-2020 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.item;
import com.fasterxml.jackson.databind.JsonNode;
import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.utils.FileUtils;
import org.geysermc.connector.utils.LanguageUtils;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
/**
* Manages any recipe-related storing
*/
public class RecipeRegistry {
/**
* A list of all possible leather armor dyeing recipes.
* Created manually.
*/
public static List<CraftingData> LEATHER_DYEING_RECIPES = new ObjectArrayList<>();
/**
* A list of all possible firework rocket recipes, including the base rocket.
* Obtained from a ProxyPass dump of protocol v407
*/
public static List<CraftingData> FIREWORK_ROCKET_RECIPES = new ObjectArrayList<>(21);
/**
* A list of all possible firework star recipes.
* Obtained from a ProxyPass dump of protocol v407
*/
public static List<CraftingData> FIREWORK_STAR_RECIPES = new ObjectArrayList<>(40);
/**
* A list of all possible shulker box dyeing options.
* Obtained from a ProxyPass dump of protocol v407
*/
public static List<CraftingData> SHULKER_BOX_DYEING_RECIPES = new ObjectArrayList<>();
static {
// Get all recipes that are not directly sent from a Java server
InputStream stream = FileUtils.getResource("mappings/recipes.json");
JsonNode items;
try {
items = GeyserConnector.JSON_MAPPER.readTree(stream);
} catch (Exception e) {
throw new AssertionError(LanguageUtils.getLocaleStringLog("geyser.toolbox.fail.runtime_java"), e);
}
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
LEATHER_DYEING_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
for (JsonNode entry : items.get("firework_rockets")) {
FIREWORK_ROCKET_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
for (JsonNode entry : items.get("firework_stars")) {
FIREWORK_STAR_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
for (JsonNode entry : items.get("shulker_boxes")) {
SHULKER_BOX_DYEING_RECIPES.add(getCraftingDataFromJsonNode(entry));
}
}
/**
* Computes a Bedrock crafting recipe from the given JSON data.
* @param node the JSON data to compute
* @return the {@link CraftingData} to send to the Bedrock client.
*/
private static CraftingData getCraftingDataFromJsonNode(JsonNode node) {
ItemData output = ItemRegistry.getBedrockItemFromJson(node.get("output").get(0));
List<ItemData> inputs = new ObjectArrayList<>();
for (JsonNode entry : node.get("input")) {
inputs.add(ItemRegistry.getBedrockItemFromJson(entry));
}
UUID uuid = UUID.randomUUID();
if (node.get("type").asInt() == 5) {
// Shulker box
return CraftingData.fromShulkerBox(uuid.toString(),
inputs.toArray(new ItemData[0]), new ItemData[]{output}, uuid, "crafting_table", 0);
}
return CraftingData.fromShapeless(uuid.toString(),
inputs.toArray(new ItemData[0]), new ItemData[]{output}, uuid, "crafting_table", 0);
}
public static void init() {
// no-op
}
}

View File

@ -107,6 +107,9 @@ public class FireworkTranslator extends NbtItemStackTranslator {
fireworks.put(new ByteTag("Flight", MathUtils.convertByte(fireworks.get("Flight").getValue())));
}
if (!itemTag.contains("Explosions")) {
return;
}
ListTag explosions = fireworks.get("Explosions");
for (Tag effect : explosions.getValue()) {
CompoundTag effectData = (CompoundTag) effect;

View File

@ -41,10 +41,7 @@ import lombok.EqualsAndHashCode;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.item.ItemEntry;
import org.geysermc.connector.network.translators.item.ItemRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
import org.geysermc.connector.network.translators.item.PotionMixRegistry;
import org.geysermc.connector.network.translators.item.*;
import java.util.*;
import java.util.stream.Collectors;
@ -83,6 +80,24 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclare
}
break;
}
case CRAFTING_SPECIAL_FIREWORK_ROCKET: {
// Java doesn't actually tell us the recipes so we need to calculate this ahead of time.
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.FIREWORK_ROCKET_RECIPES);
break;
}
case CRAFTING_SPECIAL_FIREWORK_STAR: {
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.FIREWORK_STAR_RECIPES);
break;
}
case CRAFTING_SPECIAL_SHULKERBOXCOLORING: {
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.SHULKER_BOX_DYEING_RECIPES);
break;
}
case CRAFTING_SPECIAL_ARMORDYE: {
// This one's even worse since it's not actually on Bedrock, but it still works!
craftingDataPacket.getCraftingData().addAll(RecipeRegistry.LEATHER_DYEING_RECIPES);
break;
}
}
}
craftingDataPacket.getPotionMixData().addAll(PotionMixRegistry.POTION_MIXES);