215 lines
12 KiB
Java
215 lines
12 KiB
Java
package games.dmg.spigotinventoryhelper;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import net.minecraft.core.registries.Registries;
|
|
import net.minecraft.resources.ResourceKey;
|
|
import net.minecraft.stats.ServerRecipeBook;
|
|
import net.minecraft.world.flag.FeatureFlags;
|
|
import net.minecraft.world.inventory.AbstractCraftingMenu;
|
|
import net.minecraft.world.inventory.ContainerLevelAccess;
|
|
import net.minecraft.world.inventory.CraftingMenu;
|
|
import net.minecraft.world.item.crafting.CraftingRecipe;
|
|
import net.minecraft.world.item.crafting.RecipeHolder;
|
|
import net.minecraft.world.item.crafting.RecipeManager;
|
|
import net.minecraft.world.item.crafting.RecipeMap;
|
|
import org.bukkit.Keyed;
|
|
import org.bukkit.NamespacedKey;
|
|
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
|
import org.bukkit.craftbukkit.inventory.CraftRecipe;
|
|
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
|
import org.bukkit.event.EventPriority;
|
|
import org.bukkit.event.Listener;
|
|
import org.bukkit.event.player.PlayerRecipeDiscoverEvent;
|
|
import org.bukkit.inventory.Recipe;
|
|
import org.bukkit.inventory.ShapedRecipe;
|
|
import org.bukkit.inventory.ShapelessRecipe;
|
|
|
|
/** Reuses strict actor/packet ports. Native recipe manager/book/menu/result/event code stays real.
|
|
* The Server.addRecipe port converts the exact recipe submitted by actual onEnable, following
|
|
* pinned CraftShaped/ShapelessRecipe conversion (including MaterialChoice's native matching).
|
|
* No worlds or clients run; packets are captured by the existing native synchronizer port.
|
|
*/
|
|
final class NativePowderSnowFixture implements AutoCloseable {
|
|
final NativeAnvilFixture actor;
|
|
final NativeAnvilPacketFixture packets;
|
|
final RecipeManager manager;
|
|
final ServerRecipeBook book;
|
|
final Map<NamespacedKey, Recipe> registered = new HashMap<>();
|
|
final Map<NamespacedKey, RecipeHolder<CraftingRecipe>> holders = new HashMap<>();
|
|
final List<PlayerRecipeDiscoverEvent> discoveries = new ArrayList<>();
|
|
final AbstractCraftingMenu menu;
|
|
NamespacedKey removalFailureKey;
|
|
RuntimeException removalFailure;
|
|
|
|
NativePowderSnowFixture(Path directory, int width) throws Exception {
|
|
this(directory, width, () -> 99);
|
|
}
|
|
|
|
NativePowderSnowFixture(Path directory, int width, java.util.function.IntSupplier risk) throws Exception {
|
|
this(directory, width, risk, () -> 1.0);
|
|
}
|
|
|
|
NativePowderSnowFixture(Path directory, int width, java.util.function.IntSupplier risk,
|
|
java.util.function.DoubleSupplier wear) throws Exception {
|
|
actor = new NativeAnvilFixture(directory, risk, wear);
|
|
packets = new NativeAnvilPacketFixture(actor);
|
|
actor.events.registerEvent(PlayerRecipeDiscoverEvent.class, new Listener() {}, EventPriority.MONITOR,
|
|
(ignored, event) -> discoveries.add((PlayerRecipeDiscoverEvent) event), actor.plugin, false);
|
|
actor.plugin.setEnabled(false);
|
|
manager = NativeRuntime.recipeManager();
|
|
when(packets.nativeServer.getRecipeManager()).thenReturn(manager);
|
|
when(actor.level.recipeAccess()).thenReturn(manager);
|
|
when(actor.level.getGameRules()).thenReturn(
|
|
new net.minecraft.world.level.gamerules.GameRules(FeatureFlags.VANILLA_SET));
|
|
book = new ServerRecipeBook(manager::listDisplaysForRecipe);
|
|
var bookField = net.minecraft.server.level.ServerPlayer.class.getDeclaredField("recipeBook");
|
|
assertEquals(ServerRecipeBook.class, bookField.getType());
|
|
bookField.setAccessible(true);
|
|
bookField.set(actor.nativePlayer, book);
|
|
when(actor.nativePlayer.getRecipeBook()).thenReturn(book);
|
|
when(actor.nativePlayer.getAdvancements()).thenReturn(mock(net.minecraft.server.PlayerAdvancements.class));
|
|
doCallRealMethod().when(actor.nativePlayer).awardRecipes(any());
|
|
doCallRealMethod().when(actor.player).hasDiscoveredRecipe(any());
|
|
doCallRealMethod().when(actor.player).discoverRecipes(any());
|
|
when(actor.server.getRecipe(any(NamespacedKey.class))).thenAnswer(inv -> registered.get(inv.getArgument(0)));
|
|
when(actor.server.addRecipe(any(Recipe.class))).thenAnswer(inv -> {
|
|
Recipe bukkit = inv.getArgument(0);
|
|
NamespacedKey key = ((Keyed) bukkit).getKey();
|
|
if (registered.putIfAbsent(key, bukkit) != null) return false;
|
|
holders.put(key, new RecipeHolder<>(nativeKey(key), convert(bukkit)));
|
|
refreshManager();
|
|
return true;
|
|
});
|
|
when(actor.server.removeRecipe(any(NamespacedKey.class))).thenAnswer(inv -> {
|
|
NamespacedKey key = inv.getArgument(0);
|
|
if (key.equals(removalFailureKey) && removalFailure != null) throw removalFailure;
|
|
holders.remove(key);
|
|
refreshManager();
|
|
return registered.remove(key) != null;
|
|
});
|
|
doReturn(List.of(actor.player)).when(actor.server).getOnlinePlayers();
|
|
actor.plugin.setEnabled(true);
|
|
assertTrue(book.contains(nativeKey(NativePowderSnowRecipeTest.KEY)));
|
|
assertEquals(6, registered.size());
|
|
assertEquals(6, discoveries.size());
|
|
assertEquals(0, count(actor.realInventory, org.bukkit.Material.POWDER_SNOW_BUCKET));
|
|
if (width == 2) {
|
|
menu = packets.inventoryMenu;
|
|
} else if (width == 3) {
|
|
when(actor.level.getBlockState(actor.position)).thenReturn(
|
|
net.minecraft.world.level.block.Blocks.CRAFTING_TABLE.defaultBlockState());
|
|
menu = new CraftingMenu(8, actor.realInventory, ContainerLevelAccess.create(actor.level, actor.position));
|
|
menu.setTitle(net.minecraft.network.chat.Component.literal("Crafting"));
|
|
} else {
|
|
throw new IllegalArgumentException("Unsupported grid width " + width);
|
|
}
|
|
actor.nativePlayer.containerMenu = menu;
|
|
menu.setSynchronizer(packets.synchronizer);
|
|
actor.assertNoEventErrors();
|
|
}
|
|
|
|
/** A later connection has a real native book, with only player/world/network ports doubled. */
|
|
BookPlayer joiningPlayer(ServerRecipeBook.Packed saved) throws Exception {
|
|
var nativePlayer = mock(net.minecraft.server.level.ServerPlayer.class);
|
|
var player = mock(org.bukkit.craftbukkit.entity.CraftPlayer.class);
|
|
var recipeBook = new ServerRecipeBook(manager::listDisplaysForRecipe);
|
|
if (saved != null) recipeBook.loadUntrusted(saved, key -> true);
|
|
var field = net.minecraft.server.level.ServerPlayer.class.getDeclaredField("recipeBook");
|
|
assertEquals(ServerRecipeBook.class, field.getType());
|
|
field.setAccessible(true);
|
|
field.set(nativePlayer, recipeBook);
|
|
when(nativePlayer.getRecipeBook()).thenReturn(recipeBook);
|
|
when(nativePlayer.getBukkitEntity()).thenReturn(player);
|
|
when(nativePlayer.level()).thenReturn(actor.level);
|
|
when(nativePlayer.getAdvancements()).thenReturn(mock(net.minecraft.server.PlayerAdvancements.class));
|
|
when(player.getHandle()).thenReturn(nativePlayer);
|
|
when(player.getName()).thenReturn("JoiningPowderPlayer");
|
|
doCallRealMethod().when(nativePlayer).awardRecipes(any());
|
|
doCallRealMethod().when(player).hasDiscoveredRecipe(any());
|
|
doCallRealMethod().when(player).discoverRecipes(any());
|
|
nativePlayer.connection = mock(net.minecraft.server.network.ServerGamePacketListenerImpl.class);
|
|
var sent = new ArrayList<net.minecraft.network.protocol.Packet<?>>();
|
|
doAnswer(inv -> { sent.add(inv.getArgument(0)); return null; })
|
|
.when(nativePlayer.connection).send(any(net.minecraft.network.protocol.Packet.class));
|
|
return new BookPlayer(player, nativePlayer, recipeBook, sent);
|
|
}
|
|
|
|
record BookPlayer(org.bukkit.craftbukkit.entity.CraftPlayer player,
|
|
net.minecraft.server.level.ServerPlayer nativePlayer, ServerRecipeBook book,
|
|
List<net.minecraft.network.protocol.Packet<?>> sent) {}
|
|
|
|
RecipeHolder<CraftingRecipe> powder() { return holders.get(NativePowderSnowRecipeTest.KEY); }
|
|
|
|
net.minecraft.world.inventory.RecipeBookMenu.PostPlaceAction autofill(boolean batch) {
|
|
var action = menu.handlePlacement(batch, false, powder(), actor.level, actor.realInventory);
|
|
actor.assertNoEventErrors();
|
|
return action;
|
|
}
|
|
|
|
List<org.bukkit.inventory.ItemStack> gridSnapshot() {
|
|
return menu.craftSlots.getItems().stream().map(CraftItemStack::asBukkitCopy).toList();
|
|
}
|
|
|
|
static long count(net.minecraft.world.Container container, org.bukkit.Material material) {
|
|
return java.util.stream.IntStream.range(0, container.getContainerSize()).mapToObj(container::getItem)
|
|
.map(CraftItemStack::asBukkitCopy).filter(item -> item.getType() == material)
|
|
.mapToInt(org.bukkit.inventory.ItemStack::getAmount).sum();
|
|
}
|
|
|
|
static ResourceKey<net.minecraft.world.item.crafting.Recipe<?>> nativeKey(NamespacedKey key) {
|
|
return CraftNamespacedKey.toResourceKey(Registries.RECIPE, key);
|
|
}
|
|
|
|
private void refreshManager() {
|
|
manager.recipes = RecipeMap.createEmpty();
|
|
holders.values().forEach(manager.recipes::addRecipe);
|
|
manager.finalizeRecipeLoading(FeatureFlags.VANILLA_SET);
|
|
// Pinned CraftServer.add/removeRecipe calls PlayerList.reloadRecipes after finalizing.
|
|
// Run that real native book synchronization at the actor/network boundary too: later
|
|
// registrations can renumber displays without changing discovered resource keys.
|
|
for (var online : actor.server.getOnlinePlayers()) {
|
|
if (!(online instanceof org.bukkit.craftbukkit.entity.CraftPlayer craft))
|
|
throw new AssertionError("Expected typed native recipe-book player port");
|
|
var nativePlayer = craft.getHandle();
|
|
nativePlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket(
|
|
manager.getSynchronizedItemProperties(), manager.getSynchronizedStonecutterRecipes()));
|
|
nativePlayer.getRecipeBook().sendInitialRecipeBook(nativePlayer);
|
|
}
|
|
}
|
|
|
|
private static CraftingRecipe convert(Recipe recipe) {
|
|
if (recipe instanceof ShapelessRecipe shapeless) {
|
|
return new net.minecraft.world.item.crafting.ShapelessRecipe(
|
|
new net.minecraft.world.item.crafting.Recipe.CommonInfo(true),
|
|
new CraftingRecipe.CraftingBookInfo(CraftRecipe.getCategory(shapeless.getCategory()), shapeless.getGroup()),
|
|
CraftItemStack.asTemplate(shapeless.getResult()), shapeless.getChoiceList().stream()
|
|
.map(choice -> CraftRecipe.toIngredient(choice, true)).toList());
|
|
}
|
|
if (recipe instanceof ShapedRecipe shaped) {
|
|
Map<Character, net.minecraft.world.item.crafting.Ingredient> ingredients = new HashMap<>();
|
|
shaped.getChoiceMap().forEach((symbol, choice) -> {
|
|
if (choice != null) ingredients.put(symbol, CraftRecipe.toIngredient(choice, true));
|
|
});
|
|
return new net.minecraft.world.item.crafting.ShapedRecipe(
|
|
new net.minecraft.world.item.crafting.Recipe.CommonInfo(true),
|
|
new CraftingRecipe.CraftingBookInfo(CraftRecipe.getCategory(shaped.getCategory()), shaped.getGroup()),
|
|
net.minecraft.world.item.crafting.ShapedRecipePattern.of(ingredients, shaped.getShape()),
|
|
CraftItemStack.asTemplate(shaped.getResult()));
|
|
}
|
|
throw new AssertionError("Unexpected registered recipe " + recipe.getClass());
|
|
}
|
|
|
|
@Override public void close() {
|
|
packets.close();
|
|
actor.close();
|
|
}
|
|
}
|