Untitled
public
Dec 09, 2024
Never
18
1 /* 2 * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 * 22 * @author GeyserMC 23 * @link https://github.com/GeyserMC/Geyser 24 */ 25 26 package org.geysermc.geyser.translator.protocol.java; 27 28 import com.google.common.collect.Lists; 29 import it.unimi.dsi.fastutil.Pair; 30 import it.unimi.dsi.fastutil.ints.Int2ObjectMap; 31 import it.unimi.dsi.fastutil.ints.IntComparators; 32 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 33 import net.kyori.adventure.key.Key; 34 import org.checkerframework.checker.nullness.qual.Nullable; 35 import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; 36 import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; 37 import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.RecipeUnlockingRequirement; 38 import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData; 39 import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData; 40 import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData; 41 import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor; 42 import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; 43 import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor; 44 import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; 45 import org.cloudburstmc.protocol.bedrock.packet.UnlockedRecipesPacket; 46 import org.geysermc.geyser.inventory.recipe.GeyserRecipe; 47 import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; 48 import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; 49 import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe; 50 import org.geysermc.geyser.item.Items; 51 import org.geysermc.geyser.item.type.BedrockRequiresTagItem; 52 import org.geysermc.geyser.item.type.Item; 53 import org.geysermc.geyser.registry.Registries; 54 import org.geysermc.geyser.registry.type.ItemMapping; 55 import org.geysermc.geyser.session.GeyserSession; 56 import org.geysermc.geyser.session.cache.registry.JavaRegistries; 57 import org.geysermc.geyser.session.cache.tags.Tag; 58 import org.geysermc.geyser.translator.item.ItemTranslator; 59 import org.geysermc.geyser.translator.protocol.PacketTranslator; 60 import org.geysermc.geyser.translator.protocol.Translator; 61 import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; 62 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDisplay; 63 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.RecipeDisplayEntry; 64 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapedCraftingRecipeDisplay; 65 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.ShapelessCraftingRecipeDisplay; 66 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.SmithingRecipeDisplay; 67 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.CompositeSlotDisplay; 68 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay; 69 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemSlotDisplay; 70 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay; 71 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; 72 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SmithingTrimDemoSlotDisplay; 73 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.TagSlotDisplay; 74 import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.WithRemainderSlotDisplay; 75 import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundRecipeBookAddPacket; 76 77 import java.util.ArrayList; 78 import java.util.Arrays; 79 import java.util.Collections; 80 import java.util.Comparator; 81 import java.util.HashSet; 82 import java.util.List; 83 import java.util.Map; 84 import java.util.Objects; 85 import java.util.Set; 86 import java.util.UUID; 87 import java.util.stream.Collectors; 88 89 @Translator(packet = ClientboundRecipeBookAddPacket.class) 90 public class JavaRecipeBookAddTranslator extends PacketTranslator<ClientboundRecipeBookAddPacket> { 91 92 @Override 93 public void translate(GeyserSession session, ClientboundRecipeBookAddPacket packet) { 94 int netId = session.getLastRecipeNetId().get(); 95 Int2ObjectMap<List<String>> javaToBedrockRecipeIds = session.getJavaToBedrockRecipeIds(); 96 Int2ObjectMap<GeyserRecipe> geyserRecipes = session.getCraftingRecipes(); 97 CraftingDataPacket craftingDataPacket = new CraftingDataPacket(); 98 99 UnlockedRecipesPacket recipesPacket = new UnlockedRecipesPacket(); 100 recipesPacket.setAction(packet.isReplace() ? UnlockedRecipesPacket.ActionType.INITIALLY_UNLOCKED : UnlockedRecipesPacket.ActionType.NEWLY_UNLOCKED); 101 102 for (ClientboundRecipeBookAddPacket.Entry entry : packet.getEntries()) { 103 RecipeDisplayEntry contents = entry.contents(); 104 RecipeDisplay display = contents.display(); 105 106 switch (display.getType()) { 107 case CRAFTING_SHAPED -> { 108 ShapedCraftingRecipeDisplay shapedRecipe = (ShapedCraftingRecipeDisplay) display; 109 var bedrockRecipes = combinations(session, display, shapedRecipe.ingredients()); 110 if (bedrockRecipes == null) { 111 continue; 112 } 113 List<String> bedrockRecipeIds = new ArrayList<>(); 114 ItemData output = bedrockRecipes.right(); 115 List<List<ItemDescriptorWithCount>> left = bedrockRecipes.left(); 116 GeyserRecipe geyserRecipe = new GeyserShapedRecipe(shapedRecipe); 117 for (int i = 0; i < left.size(); i++) { 118 List<ItemDescriptorWithCount> inputs = left.get(i); 119 String recipeId = contents.id() + "_" + i; 120 int recipeNetworkId = netId++; 121 craftingDataPacket.getCraftingData().add(ShapedRecipeData.shaped(recipeId, 122 shapedRecipe.width(), shapedRecipe.height(), inputs, 123 Collections.singletonList(output), UUID.randomUUID(), "crafting_table", 0, recipeNetworkId, false, RecipeUnlockingRequirement.INVALID)); 124 recipesPacket.getUnlockedRecipes().add(recipeId); 125 bedrockRecipeIds.add(recipeId); 126 geyserRecipes.put(recipeNetworkId, geyserRecipe); 127 } 128 javaToBedrockRecipeIds.put(contents.id(), List.copyOf(bedrockRecipeIds)); 129 } 130 case CRAFTING_SHAPELESS -> { 131 ShapelessCraftingRecipeDisplay shapelessRecipe = (ShapelessCraftingRecipeDisplay) display; 132 var bedrockRecipes = combinations(session, display, shapelessRecipe.ingredients()); 133 if (bedrockRecipes == null) { 134 continue; 135 } 136 List<String> bedrockRecipeIds = new ArrayList<>(); 137 ItemData output = bedrockRecipes.right(); 138 List<List<ItemDescriptorWithCount>> left = bedrockRecipes.left(); 139 GeyserRecipe geyserRecipe = new GeyserShapelessRecipe(shapelessRecipe); 140 for (int i = 0; i < left.size(); i++) { 141 List<ItemDescriptorWithCount> inputs = left.get(i); 142 String recipeId = contents.id() + "_" + i; 143 int recipeNetworkId = netId++; 144 craftingDataPacket.getCraftingData().add(ShapelessRecipeData.shapeless(recipeId, 145 inputs, Collections.singletonList(output), UUID.randomUUID(), "crafting_table", 0, recipeNetworkId, RecipeUnlockingRequirement.INVALID)); 146 recipesPacket.getUnlockedRecipes().add(recipeId); 147 bedrockRecipeIds.add(recipeId); 148 geyserRecipes.put(recipeNetworkId, geyserRecipe); 149 } 150 javaToBedrockRecipeIds.put(contents.id(), List.copyOf(bedrockRecipeIds)); 151 } 152 case SMITHING -> { 153 if (display.result() instanceof SmithingTrimDemoSlotDisplay) { 154 // Skip these - Bedrock already knows about them from the TrimDataPacket 155 continue; 156 } 157 SmithingRecipeDisplay smithingRecipe = (SmithingRecipeDisplay) display; 158 Pair<Item, ItemData> output = translateToOutput(session, smithingRecipe.result()); 159 if (output == null) { 160 continue; 161 } 162 163 List<ItemDescriptorWithCount> bases = translateToInput(session, smithingRecipe.base()); 164 List<ItemDescriptorWithCount> templates = translateToInput(session, smithingRecipe.template()); 165 List<ItemDescriptorWithCount> additions = translateToInput(session, smithingRecipe.addition()); 166 167 if (bases == null || templates == null || additions == null) { 168 continue; 169 } 170 171 int i = 0; 172 List<String> bedrockRecipeIds = new ArrayList<>(); 173 for (ItemDescriptorWithCount template : templates) { 174 for (ItemDescriptorWithCount base : bases) { 175 for (ItemDescriptorWithCount addition : additions) { 176 String id = contents.id() + "_" + i++; 177 // Note: vanilla inputs use aux value of Short.MAX_VALUE 178 craftingDataPacket.getCraftingData().add(SmithingTransformRecipeData.of(id, 179 template, base, addition, output.right(), "smithing_table", netId++)); 180 181 recipesPacket.getUnlockedRecipes().add(id); 182 bedrockRecipeIds.add(id); 183 } 184 } 185 } 186 javaToBedrockRecipeIds.put(contents.id(), bedrockRecipeIds); 187 session.getSmithingRecipes().add(new GeyserSmithingRecipe(smithingRecipe)); 188 } 189 } 190 } 191 192 if (!recipesPacket.getUnlockedRecipes().isEmpty()) { 193 // Sending an empty list here will crash the client as of 1.20.60 194 // This was definitely in the codebase the entire time and did not 195 // accidentally get refactored out during Java 1.21.3. :) 196 session.sendUpstreamPacket(craftingDataPacket); 197 session.sendUpstreamPacket(recipesPacket); 198 } 199 session.getLastRecipeNetId().set(netId); 200 201 // Multi-version can mean different Bedrock item IDs 202 TAG_TO_ITEM_DESCRIPTOR_CACHE.remove(); 203 } 204 205 // Arrays are usually an issue in maps, but because it's referencing the tag array that is unchanged, it actually works out for us. 206 private static final ThreadLocal<Map<int[], List<ItemDescriptorWithCount>>> TAG_TO_ITEM_DESCRIPTOR_CACHE = ThreadLocal.withInitial(Object2ObjectOpenHashMap::new); 207 208 private List<ItemDescriptorWithCount> translateToInput(GeyserSession session, SlotDisplay slotDisplay) { 209 if (slotDisplay instanceof EmptySlotDisplay) { 210 return Collections.singletonList(ItemDescriptorWithCount.EMPTY); 211 } 212 if (slotDisplay instanceof CompositeSlotDisplay composite) { 213 if (composite.contents().size() == 1) { 214 return translateToInput(session, composite.contents().get(0)); 215 } 216 217 // Try and see if the contents match a tag. 218 // ViaVersion maps pre-1.21.2 ingredient lists to CompositeSlotDisplays. 219 int[] items = new int[composite.contents().size()]; 220 List<SlotDisplay> contents = composite.contents(); 221 for (int i = 0; i < contents.size(); i++) { 222 SlotDisplay subDisplay = contents.get(i); 223 int id; 224 if (subDisplay instanceof ItemSlotDisplay item) { 225 id = item.item(); 226 } else if (!(subDisplay instanceof ItemStackSlotDisplay itemStackSlotDisplay)) { 227 id = -1; 228 } else if (itemStackSlotDisplay.itemStack().getAmount() == 1 229 && itemStackSlotDisplay.itemStack().getDataComponents() == null) { 230 id = itemStackSlotDisplay.itemStack().getId(); 231 } else { 232 id = -1; 233 } 234 if (id == -1) { 235 // We couldn't guarantee a "normal" item from this stack. 236 return fallbackCompositeMapping(session, composite); 237 } 238 items[i] = id; 239 } 240 // For searching in the tag map. 241 Arrays.sort(items); 242 243 List<ItemDescriptorWithCount> tagDescriptor = lookupBedrockTag(session, items); 244 if (tagDescriptor != null) { 245 return tagDescriptor; 246 } 247 248 return fallbackCompositeMapping(session, composite); 249 } 250 if (slotDisplay instanceof WithRemainderSlotDisplay remainder) { 251 // Don't need to worry about what will stay in the crafting table after crafting for the purposes of sending recipes to Bedrock 252 return translateToInput(session, remainder.input()); 253 } 254 if (slotDisplay instanceof ItemSlotDisplay itemSlot) { 255 return Collections.singletonList(fromItem(session, itemSlot.item())); 256 } 257 if (slotDisplay instanceof ItemStackSlotDisplay itemStackSlot) { 258 ItemData item = ItemTranslator.translateToBedrock(session, itemStackSlot.itemStack()); 259 return Collections.singletonList(ItemDescriptorWithCount.fromItem(item)); 260 } 261 if (slotDisplay instanceof TagSlotDisplay tagSlot) { 262 Key tag = tagSlot.tag(); 263 int[] items = session.getTagCache().getRaw(new Tag<>(JavaRegistries.ITEM, tag)); // I don't like this... 264 if (items == null || items.length == 0) { 265 return Collections.singletonList(ItemDescriptorWithCount.EMPTY); 266 } else if (items.length == 1) { 267 return Collections.singletonList(fromItem(session, items[0])); 268 } else { 269 // Cache is implemented as, presumably, an item tag will be used multiple times in succession 270 // (E.G. a chest with planks tags) 271 return TAG_TO_ITEM_DESCRIPTOR_CACHE.get().computeIfAbsent(items, key -> { 272 List<ItemDescriptorWithCount> tagDescriptor = lookupBedrockTag(session, key); 273 if (tagDescriptor != null) { 274 return tagDescriptor; 275 } 276 277 // In the future, we can probably search through and use subsets of tags as well. 278 // I.E. if a Bedrock tag contains [stone stone_brick] and the Java tag uses [stone stone_brick bricks] 279 // we can still use that Bedrock tag alongside plain item descriptors for "bricks". 280 281 Set<ItemDescriptorWithCount> itemDescriptors = new HashSet<>(); 282 for (int item : key) { 283 itemDescriptors.add(fromItem(session, item)); 284 } 285 return List.copyOf(itemDescriptors); // This, or a list from the start with contains -> add? 286 }); 287 } 288 } 289 session.getGeyser().getLogger().warning("Unimplemented slot display type for input: " + slotDisplay); 290 return null; 291 } 292 293 private Pair<Item, ItemData> translateToOutput(GeyserSession session, SlotDisplay slotDisplay) { 294 if (slotDisplay instanceof EmptySlotDisplay) { 295 return null; 296 } 297 if (slotDisplay instanceof ItemSlotDisplay itemSlot) { 298 int item = itemSlot.item(); 299 return Pair.of(Registries.JAVA_ITEMS.get(item), ItemTranslator.translateToBedrock(session, new ItemStack(item))); 300 } 301 if (slotDisplay instanceof ItemStackSlotDisplay itemStackSlot) { 302 ItemStack stack = itemStackSlot.itemStack(); 303 return Pair.of(Registries.JAVA_ITEMS.get(stack.getId()), ItemTranslator.translateToBedrock(session, stack)); 304 } 305 session.getGeyser().getLogger().warning("Unimplemented slot display type for output: " + slotDisplay); 306 return null; 307 } 308 309 private ItemDescriptorWithCount fromItem(GeyserSession session, int item) { 310 if (item == Items.AIR_ID) { 311 return ItemDescriptorWithCount.EMPTY; 312 } 313 ItemMapping mapping = session.getItemMappings().getMapping(item); 314 return new ItemDescriptorWithCount(new DefaultDescriptor(mapping.getBedrockDefinition(), mapping.getBedrockData()), 1); // Need to check count 315 } 316 317 /** 318 * Checks to see if this list of items matches with one of this Bedrock version's tags. 319 */ 320 @Nullable 321 private List<ItemDescriptorWithCount> lookupBedrockTag(GeyserSession session, int[] items) { 322 var bedrockTags = Registries.TAGS.forVersion(session.getUpstream().getProtocolVersion()); 323 String bedrockTag = bedrockTags.get(items); 324 if (bedrockTag != null) { 325 return Collections.singletonList( 326 new ItemDescriptorWithCount(new ItemTagDescriptor(bedrockTag), 1) 327 ); 328 } else { 329 return null; 330 } 331 } 332 333 /** 334 * Converts CompositeSlotDisplay contents to a list of basic ItemDescriptorWithCounts. 335 */ 336 private List<ItemDescriptorWithCount> fallbackCompositeMapping(GeyserSession session, CompositeSlotDisplay composite) { 337 return composite.contents().stream() 338 .map(subDisplay -> translateToInput(session, subDisplay)) 339 .filter(Objects::nonNull) 340 .flatMap(List::stream) 341 .toList(); 342 } 343 344 private Pair<List<List<ItemDescriptorWithCount>>, ItemData> combinations(GeyserSession session, RecipeDisplay display, List<SlotDisplay> ingredients) { 345 Pair<Item, ItemData> pair = translateToOutput(session, display.result()); 346 if (pair == null || !pair.right().isValid()) { 347 // Likely modded item Bedrock will complain about 348 // Implementation note: ItemData#isValid() may return true for air because count might be > 0 and the air definition may not be ItemDefinition.AIR 349 return null; 350 } 351 352 ItemData output = pair.right(); 353 if (!(pair.left() instanceof BedrockRequiresTagItem)) { 354 // Strip NBT - tools won't appear in the recipe book otherwise 355 output = output.toBuilder().tag(null).build(); 356 } 357 358 boolean empty = true; 359 boolean complexInputs = false; 360 List<List<ItemDescriptorWithCount>> inputs = new ArrayList<>(ingredients.size()); 361 for (SlotDisplay input : ingredients) { 362 List<ItemDescriptorWithCount> translated = translateToInput(session, input); 363 if (translated == null) { 364 continue; 365 } 366 inputs.add(translated); 367 if (translated.size() != 1 || translated.get(0) != ItemDescriptorWithCount.EMPTY) { 368 empty = false; 369 } 370 complexInputs |= translated.size() > 1; 371 } 372 if (empty) { 373 // Crashes Bedrock 1.19.70 otherwise 374 // Fixes https://github.com/GeyserMC/Geyser/issues/3549 375 return null; 376 } 377 378 if (complexInputs) { 379 long size = 1; 380 // See how big a cartesian product will get without creating one (Guava throws an error; not really ideal) 381 for (List<ItemDescriptorWithCount> list : inputs) { 382 size *= list.size(); 383 if (size > 500) { 384 // Too much. No. 385 complexInputs = false; 386 break; 387 } 388 } 389 if (complexInputs) { 390 return Pair.of(Lists.cartesianProduct(inputs), output); 391 } 392 } 393 394 int totalSimpleRecipes = inputs.stream().mapToInt(List::size).max().orElse(1); 395 396 // Sort inputs to create "uniform" recipes, if possible 397 inputs = inputs.stream() 398 .map(descriptors -> descriptors.stream() 399 .sorted(ItemDescriptorWithCountComparator.INSTANCE) 400 .collect(Collectors.toList())) 401 .collect(Collectors.toList()); 402 403 List<List<ItemDescriptorWithCount>> finalRecipes = new ArrayList<>(totalSimpleRecipes); 404 for (int i = 0; i < totalSimpleRecipes; i++) { 405 int current = i; 406 finalRecipes.add(inputs.stream().map(descriptors -> { 407 var preferred = descriptors.get(current); 408 if (preferred == null) { 409 return descriptors.get(0); 410 } 411 return preferred; 412 }).toList()); 413 } 414 415 // Attempt to create "basic" recipes, without mixing items 416 return Pair.of(finalRecipes, output); 417 } 418 419 static class ItemDescriptorWithCountComparator implements Comparator<ItemDescriptorWithCount> { 420 421 static ItemDescriptorWithCountComparator INSTANCE = new ItemDescriptorWithCountComparator(); 422 423 @Override 424 public int compare(ItemDescriptorWithCount o1, ItemDescriptorWithCount o2) { 425 String tag1 = null, tag2 = null; 426 427 // Collect item tags first 428 if (o1.getDescriptor() instanceof ItemTagDescriptor itemTagDescriptor) { 429 tag1 = itemTagDescriptor.getItemTag(); 430 } 431 432 if (o2.getDescriptor() instanceof ItemTagDescriptor itemTagDescriptor) { 433 tag2 = itemTagDescriptor.getItemTag(); 434 } 435 436 if (tag1 != null || tag2 != null) { 437 if (tag1 != null && tag2 != null) { 438 return tag1.compareTo(tag2); // Just sort based on their string id 439 } 440 441 if (tag1 != null) { 442 return -1; 443 } 444 445 return 1; // the second is an item tag; which should be r 446 } 447 448 if (o1.getDescriptor() instanceof DefaultDescriptor defaultDescriptor1 && o2.getDescriptor() instanceof DefaultDescriptor defaultDescriptor2) { 449 return IntComparators.NATURAL_COMPARATOR.compare(defaultDescriptor1.getItemId().getRuntimeId(), defaultDescriptor2.getItemId().getRuntimeId()); 450 } 451 452 throw new IllegalStateException("Unable to compare unknown item descriptors: " + o1 + " and " + o2); 453 } 454 } 455 }