G

Untitled

public
Guest Dec 09, 2024 Never 18
Clone
Plaintext paste1.txt 455 lines (412 loc) | 23.68 KB
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
}