Files
purpur-inventory-helper/src/nativeTest/java/games/dmg/spigotinventoryhelper/NativePowderSnowFixture.java
T
dmg 95a7ac41e1
CI / build (push) Successful in 54s
CI / build (pull_request) Successful in 1m43s
feat(platform): support Purpur 26.3
2026-10-16 23:46:50 -04:00

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();
}
}