title(final @NotNull Component title);
+
+ /**
+ * Determines whether or not the server should check if the player can reach
+ * the location.
+ *
+ * Not providing a location but setting checkReachable to true will
+ * automatically close the view when opened.
+ *
+ * If checkReachable is set to false and a location is set on the builder if
+ * the target block exists and this builder is the correct menu for that
+ * block, e.g. MenuType.GENERIC_9X3 builder and target block set to chest,
+ * if that block is destroyed the view would persist.
+ *
+ * @param checkReachable whether or not to check if the view is "reachable"
+ * @return this builder
+ */
+ LocationInventoryViewBuilder checkReachable(final boolean checkReachable);
+
+ /**
+ * Binds a location to this builder.
+ *
+ * By binding a location in an unloaded chunk to this builder it is likely
+ * that the given chunk the location is will load. That means that when,
+ * building this view it may come with the costs associated with chunk
+ * loading.
+ *
+ * Providing a location of a tile entity with a non matching menu comes with
+ * extra costs associated with ensuring that the correct view is created.
+ *
+ * @param location the location to bind to this view
+ * @return this builder
+ */
+ LocationInventoryViewBuilder location(final Location location);
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/view/builder/MerchantInventoryViewBuilder.java b/paper-api/src/main/java/org/bukkit/inventory/view/builder/MerchantInventoryViewBuilder.java
new file mode 100644
index 0000000000..76aecb54a9
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/view/builder/MerchantInventoryViewBuilder.java
@@ -0,0 +1,44 @@
+package org.bukkit.inventory.view.builder;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Server;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.Merchant;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An InventoryViewBuilder for creating merchant views
+ *
+ * @param the type of InventoryView created by this builder
+ */
+@ApiStatus.Experimental
+public interface MerchantInventoryViewBuilder extends InventoryViewBuilder {
+
+ @Override
+ MerchantInventoryViewBuilder copy();
+
+ @Override
+ MerchantInventoryViewBuilder title(final @NotNull Component title);
+
+ /**
+ * Adds a merchant to this builder
+ *
+ * @param merchant the merchant
+ * @return this builder
+ */
+ MerchantInventoryViewBuilder merchant(final Merchant merchant);
+
+ /**
+ * Determines whether or not the server should check if the player can reach
+ * the location.
+ *
+ * Given checkReachable is provided and a virtual merchant is provided to
+ * the builder from {@link Server#createMerchant(net.kyori.adventure.text.Component)} this method will
+ * have no effect on the actual menu status.
+ *
+ * @param checkReachable whether or not to check if the view is "reachable"
+ * @return this builder
+ */
+ MerchantInventoryViewBuilder checkReachable(final boolean checkReachable);
+}
diff --git a/paper-api/src/main/java/org/bukkit/inventory/view/builder/package-info.java b/paper-api/src/main/java/org/bukkit/inventory/view/builder/package-info.java
new file mode 100644
index 0000000000..b1e4203dae
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/inventory/view/builder/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * A Package that contains builders for building InventoryViews.
+ */
+@NullMarked
+@ApiStatus.Experimental
+package org.bukkit.inventory.view.builder;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
index e33c611feb..6b7e7d70cb 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
@@ -9,7 +9,7 @@
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.Container;
-@@ -63,6 +_,31 @@
+@@ -63,6 +_,32 @@
@Nullable
private ContainerSynchronizer synchronizer;
private boolean suppressRemoteUpdates;
@@ -37,6 +37,7 @@
+ com.google.common.base.Preconditions.checkState(this.title == null, "Title already set");
+ this.title = title;
+ }
++ public void startOpen() {}
+ // CraftBukkit end
protected AbstractContainerMenu(@Nullable MenuType> menuType, int containerId) {
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
index c86ead27cc..1be2d1bc41 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ChestMenu.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/world/inventory/ChestMenu.java
+++ b/net/minecraft/world/inventory/ChestMenu.java
-@@ -9,6 +_,29 @@
+@@ -9,6 +_,34 @@
public class ChestMenu extends AbstractContainerMenu {
private final Container container;
private final int containerRows;
@@ -26,14 +26,21 @@
+ this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), inventory, this);
+ return this.bukkitEntity;
+ }
++
++ @Override
++ public void startOpen() {
++ this.container.startOpen(this.player.player);
++ }
+ // CraftBukkit end
private ChestMenu(MenuType> type, int containerId, Inventory playerInventory, int rows) {
this(type, containerId, playerInventory, new SimpleContainer(9 * rows), rows);
-@@ -52,6 +_,9 @@
+@@ -51,7 +_,10 @@
+ checkContainerSize(container, rows * 9);
this.container = container;
this.containerRows = rows;
- container.startOpen(playerInventory.player);
+- container.startOpen(playerInventory.player);
++ // container.startOpen(playerInventory.player); // Paper - don't startOpen until menu actually opens
+ // CraftBukkit start - Save player
+ this.player = playerInventory;
+ // CraftBukkit end
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
index d3c402326c..f1c3c78a5b 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/MerchantMenu.java.patch
@@ -27,6 +27,14 @@
this.addStandardInventorySlots(playerInventory, 108, 84);
}
+@@ -61,6 +_,7 @@
+
+ @Override
+ public boolean stillValid(Player player) {
++ if (!checkReachable) return true; // Paper - checkReachable
+ return this.trader.stillValid(player);
+ }
+
@@ -105,12 +_,12 @@
ItemStack item = slot.getItem();
itemStack = item.copy();
diff --git a/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch b/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
index a65b4c24dc..590e2788dd 100644
--- a/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/world/inventory/ShulkerBoxMenu.java
+++ b/net/minecraft/world/inventory/ShulkerBoxMenu.java
-@@ -9,6 +_,20 @@
+@@ -9,6 +_,25 @@
public class ShulkerBoxMenu extends AbstractContainerMenu {
private static final int CONTAINER_SIZE = 27;
private final Container container;
@@ -17,18 +17,25 @@
+ this.bukkitEntity = new org.bukkit.craftbukkit.inventory.CraftInventoryView(this.player.player.getBukkitEntity(), new org.bukkit.craftbukkit.inventory.CraftInventory(this.container), this);
+ return this.bukkitEntity;
+ }
++
++ @Override
++ public void startOpen() {
++ container.startOpen(player.player);
++ }
+ // CraftBukkit end
public ShulkerBoxMenu(int containerId, Inventory playerInventory) {
this(containerId, playerInventory, new SimpleContainer(27));
-@@ -18,6 +_,7 @@
+@@ -18,7 +_,8 @@
super(MenuType.SHULKER_BOX, containerId);
checkContainerSize(container, 27);
this.container = container;
+- container.startOpen(playerInventory.player);
+ this.player = playerInventory; // CraftBukkit - save player
- container.startOpen(playerInventory.player);
++ // container.startOpen(playerInventory.player); // Paper - don't startOpen until menu actually opens
int i = 3;
int i1 = 9;
+
@@ -33,6 +_,7 @@
@Override
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index fda9daa636..d2de789967 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -258,6 +258,7 @@ import org.bukkit.scoreboard.Criteria;
import org.bukkit.structure.StructureManager;
import org.bukkit.util.StringUtil;
import org.bukkit.util.permissions.DefaultPermissions;
+import org.jetbrains.annotations.NotNull;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
@@ -2485,6 +2486,11 @@ public final class CraftServer implements Server {
return new CraftMerchantCustom(title == null ? InventoryType.MERCHANT.getDefaultTitle() : title);
}
+ @Override
+ public @NotNull Merchant createMerchant() {
+ return new CraftMerchantCustom(net.kyori.adventure.text.Component.empty());
+ }
+
@Override
public int getMaxChainedNeighborUpdates() {
return this.getServer().getMaxChainedNeighborUpdates();
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
index a1f42f860f..cafd8c5349 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java
@@ -25,6 +25,7 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.MerchantMenu;
import net.minecraft.world.item.ItemCooldowns;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.RecipeManager;
@@ -50,6 +51,8 @@ import org.bukkit.craftbukkit.inventory.CraftInventoryView;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.inventory.CraftMerchantCustom;
import org.bukkit.craftbukkit.inventory.CraftRecipe;
+import org.bukkit.craftbukkit.inventory.util.CraftMenus;
+import org.bukkit.craftbukkit.util.CraftChatMessage;
import org.bukkit.craftbukkit.util.CraftLocation;
import org.bukkit.entity.Firework;
import org.bukkit.entity.HumanEntity;
@@ -467,6 +470,11 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity {
// Now open the window
MenuType> windowType = CraftContainer.getNotchInventoryType(inventory.getTopInventory());
+ // we can open these now, delegate for now
+ if (windowType == MenuType.MERCHANT) {
+ CraftMenus.openMerchantMenu(player, (MerchantMenu) container);
+ return;
+ }
//String title = inventory.getTitle(); // Paper - comment
net.kyori.adventure.text.Component adventure$title = inventory.title(); // Paper
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index e37aaf77f9..d7a52220e9 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -1430,6 +1430,7 @@ public class CraftEventFactory {
}
public static com.mojang.datafixers.util.Pair callInventoryOpenEventWithTitle(ServerPlayer player, AbstractContainerMenu container, boolean cancelled) {
// Paper end - Add titleOverride to InventoryOpenEvent
+ container.startOpen(); // delegate start open logic to before InventoryOpenEvent is fired
if (player.containerMenu != player.inventoryMenu) { // fire INVENTORY_CLOSE if one already open
player.connection.handleContainerClose(new ServerboundContainerClosePacket(player.containerMenu.containerId), InventoryCloseEvent.Reason.OPEN_NEW); // Paper - Inventory close reason
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
index 6d3f9d5dab..1ce328bed5 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java
@@ -132,7 +132,7 @@ public class CraftContainer extends AbstractContainerMenu {
if (menu == null) {
return net.minecraft.world.inventory.MenuType.GENERIC_9x3;
} else {
- return ((CraftMenuType>) menu).getHandle();
+ return ((CraftMenuType, ?>) menu).getHandle();
}
}
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
index e4d81ef26a..4c6cf43cee 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftMenuType.java
@@ -15,12 +15,14 @@ import org.bukkit.craftbukkit.util.Handleable;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.MenuType;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.jetbrains.annotations.NotNull;
-public class CraftMenuType implements MenuType.Typed, Handleable>, io.papermc.paper.world.flag.PaperFeatureDependent { // Paper - make FeatureDependant
+public class CraftMenuType> implements MenuType.Typed, Handleable>, io.papermc.paper.world.flag.PaperFeatureDependent { // Paper - make FeatureDependant
private final NamespacedKey key;
private final net.minecraft.world.inventory.MenuType> handle;
- private final Supplier> typeData;
+ private final Supplier> typeData;
public CraftMenuType(NamespacedKey key, net.minecraft.world.inventory.MenuType> handle) {
this.key = key;
@@ -36,33 +38,28 @@ public class CraftMenuType implements MenuType.Typed
@Override
public V create(final HumanEntity player, final String title) {
// Paper start - adventure
- return create(player, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title));
+ return builder().title(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(title)).build(player);
}
@Override
public V create(final HumanEntity player, final net.kyori.adventure.text.Component title) {
// Paper end - adventure
- Preconditions.checkArgument(player != null, "The given player must not be null");
- Preconditions.checkArgument(title != null, "The given title must not be null");
- Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
- final CraftHumanEntity craftHuman = (CraftHumanEntity) player;
- Preconditions.checkArgument(craftHuman.getHandle() instanceof ServerPlayer, "The given player must be an EntityPlayer");
- final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
-
- final AbstractContainerMenu container = this.typeData.get().menuBuilder().build(serverPlayer, this.handle);
- container.setTitle(io.papermc.paper.adventure.PaperAdventure.asVanilla(title)); // Paper - adventure
- container.checkReachable = false;
- return (V) container.getBukkitView();
+ return builder().title(title).build(player);
}
@Override
- public Typed typed() {
+ public B builder() {
+ return typeData.get().viewBuilder().get();
+ }
+
+ @Override
+ public Typed> typed() {
return this.typed(InventoryView.class);
}
@Override
- public Typed typed(Class clazz) {
+ public > Typed typed(Class clazz) {
if (clazz.isAssignableFrom(this.typeData.get().viewClass())) {
- return (Typed) this;
+ return (Typed) this;
}
throw new IllegalArgumentException("Cannot type InventoryView " + this.key.toString() + " to InventoryView type " + clazz.getSimpleName());
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java
index 66e93f8444..84c35792c4 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/util/CraftMenus.java
@@ -1,16 +1,18 @@
package org.bukkit.craftbukkit.inventory.util;
-import static org.bukkit.craftbukkit.inventory.util.CraftMenuBuilder.*;
-
-import net.minecraft.network.chat.Component;
-import net.minecraft.world.SimpleMenuProvider;
+import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.npc.Villager;
import net.minecraft.world.inventory.AnvilMenu;
import net.minecraft.world.inventory.CartographyTableMenu;
import net.minecraft.world.inventory.CraftingMenu;
import net.minecraft.world.inventory.EnchantmentMenu;
import net.minecraft.world.inventory.GrindstoneMenu;
+import net.minecraft.world.inventory.MerchantMenu;
import net.minecraft.world.inventory.SmithingMenu;
import net.minecraft.world.inventory.StonecutterMenu;
+import net.minecraft.world.item.trading.Merchant;
+import net.minecraft.world.item.trading.MerchantOffers;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BeaconBlockEntity;
import net.minecraft.world.level.block.entity.BlastFurnaceBlockEntity;
@@ -20,8 +22,15 @@ import net.minecraft.world.level.block.entity.DispenserBlockEntity;
import net.minecraft.world.level.block.entity.FurnaceBlockEntity;
import net.minecraft.world.level.block.entity.HopperBlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
+import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity;
import net.minecraft.world.level.block.entity.SmokerBlockEntity;
import org.bukkit.craftbukkit.inventory.CraftMenuType;
+import org.bukkit.craftbukkit.inventory.CraftMerchant;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftAccessLocationInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftBlockEntityInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftDoubleChestInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftMerchantInventoryViewBuilder;
+import org.bukkit.craftbukkit.inventory.view.builder.CraftStandardInventoryViewBuilder;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.MenuType;
import org.bukkit.inventory.view.AnvilView;
@@ -34,83 +43,120 @@ import org.bukkit.inventory.view.LecternView;
import org.bukkit.inventory.view.LoomView;
import org.bukkit.inventory.view.MerchantView;
import org.bukkit.inventory.view.StonecutterView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.jspecify.annotations.NullMarked;
+import java.util.function.Supplier;
+
+@NullMarked
public final class CraftMenus {
- public record MenuTypeData(Class viewClass, CraftMenuBuilder menuBuilder) {
+ public record MenuTypeData>(Class viewClass, Supplier viewBuilder) {
}
- private static final CraftMenuBuilder STANDARD = (player, menuType) -> menuType.create(player.nextContainerCounter(), player.getInventory());
+ // This is a temporary measure that will likely be removed with the rewrite of HumanEntity#open[] methods
+ public static void openMerchantMenu(final ServerPlayer player, final MerchantMenu merchant) {
+ final Merchant minecraftMerchant = ((CraftMerchant) merchant.getBukkitView().getMerchant()).getMerchant();
+ int level = 1;
+ if (minecraftMerchant instanceof final Villager villager) {
+ level = villager.getVillagerData().getLevel();
+ }
- public static MenuTypeData getMenuTypeData(CraftMenuType> menuType) {
+ if (minecraftMerchant.getTradingPlayer() != null) { // merchant's can only have one trader
+ minecraftMerchant.getTradingPlayer().closeContainer();
+ }
+
+ minecraftMerchant.setTradingPlayer(player);
+
+ player.connection.send(new ClientboundOpenScreenPacket(merchant.containerId, net.minecraft.world.inventory.MenuType.MERCHANT, merchant.getTitle()));
+ player.containerMenu = merchant;
+ player.initMenu(merchant);
+ // Copy IMerchant#openTradingScreen
+ MerchantOffers merchantrecipelist = minecraftMerchant.getOffers();
+
+ if (!merchantrecipelist.isEmpty()) {
+ player.sendMerchantOffers(merchant.containerId, merchantrecipelist, level, minecraftMerchant.getVillagerXp(), minecraftMerchant.showProgressBar(), minecraftMerchant.canRestock());
+ }
+ // End Copy IMerchant#openTradingScreen
+ }
+
+ public static > MenuTypeData getMenuTypeData(final CraftMenuType, ?> menuType) {
+ final net.minecraft.world.inventory.MenuType> handle = menuType.getHandle();
+ // this sucks horribly but it should work for now
+ if (menuType == MenuType.GENERIC_9X6) {
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftDoubleChestInventoryViewBuilder<>(handle)));
+ }
+ if (menuType == MenuType.GENERIC_9X3) {
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.CHEST, null)));
+ }
// this isn't ideal as both dispenser and dropper are 3x3, InventoryType can't currently handle generic 3x3s with size 9
// this needs to be removed when inventory creation is overhauled
if (menuType == MenuType.GENERIC_3X3) {
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, tileEntity(DispenserBlockEntity::new, Blocks.DISPENSER)));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.DISPENSER, DispenserBlockEntity::new)));
}
if (menuType == MenuType.CRAFTER_3X3) {
- return CraftMenus.asType(new MenuTypeData<>(CrafterView.class, tileEntity(CrafterBlockEntity::new, Blocks.CRAFTER)));
+ return asType(new MenuTypeData<>(CrafterView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.CRAFTER, CrafterBlockEntity::new)));
}
if (menuType == MenuType.ANVIL) {
- return CraftMenus.asType(new MenuTypeData<>(AnvilView.class, worldAccess(AnvilMenu::new)));
+ return asType(new MenuTypeData<>(AnvilView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, AnvilMenu::new)));
}
if (menuType == MenuType.BEACON) {
- return CraftMenus.asType(new MenuTypeData<>(BeaconView.class, tileEntity(BeaconBlockEntity::new, Blocks.BEACON)));
+ return asType(new MenuTypeData<>(BeaconView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.BEACON, BeaconBlockEntity::new)));
}
if (menuType == MenuType.BLAST_FURNACE) {
- return CraftMenus.asType(new MenuTypeData<>(FurnaceView.class, tileEntity(BlastFurnaceBlockEntity::new, Blocks.BLAST_FURNACE)));
+ return asType(new MenuTypeData<>(FurnaceView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.BLAST_FURNACE, BlastFurnaceBlockEntity::new)));
}
if (menuType == MenuType.BREWING_STAND) {
- return CraftMenus.asType(new MenuTypeData<>(BrewingStandView.class, tileEntity(BrewingStandBlockEntity::new, Blocks.BREWING_STAND)));
+ return asType(new MenuTypeData<>(BrewingStandView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.BREWING_STAND, BrewingStandBlockEntity::new)));
}
if (menuType == MenuType.CRAFTING) {
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(CraftingMenu::new)));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, CraftingMenu::new)));
}
if (menuType == MenuType.ENCHANTMENT) {
- return CraftMenus.asType(new MenuTypeData<>(EnchantmentView.class, (player, type) -> {
- return new SimpleMenuProvider((syncId, inventory, human) -> {
- return worldAccess(EnchantmentMenu::new).build(player, type);
- }, Component.empty()).createMenu(player.nextContainerCounter(), player.getInventory(), player);
- }));
+ return asType(new MenuTypeData<>(EnchantmentView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, EnchantmentMenu::new)));
}
if (menuType == MenuType.FURNACE) {
- return CraftMenus.asType(new MenuTypeData<>(FurnaceView.class, tileEntity(FurnaceBlockEntity::new, Blocks.FURNACE)));
+ return asType(new MenuTypeData<>(FurnaceView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.FURNACE, FurnaceBlockEntity::new)));
}
if (menuType == MenuType.GRINDSTONE) {
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(GrindstoneMenu::new)));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, GrindstoneMenu::new)));
}
// We really don't need to be creating a tile entity for hopper but currently InventoryType doesn't have capacity
// to understand otherwise
if (menuType == MenuType.HOPPER) {
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, tileEntity(HopperBlockEntity::new, Blocks.HOPPER)));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.HOPPER, HopperBlockEntity::new)));
}
// We also don't need to create a tile entity for lectern, but again InventoryType isn't smart enough to know any better
if (menuType == MenuType.LECTERN) {
- return CraftMenus.asType(new MenuTypeData<>(LecternView.class, tileEntity(LecternBlockEntity::new, Blocks.LECTERN)));
+ return asType(new MenuTypeData<>(LecternView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.LECTERN, LecternBlockEntity::new)));
}
if (menuType == MenuType.LOOM) {
- return CraftMenus.asType(new MenuTypeData<>(LoomView.class, CraftMenus.STANDARD));
+ return asType(new MenuTypeData<>(LoomView.class, () -> new CraftStandardInventoryViewBuilder<>(handle)));
}
if (menuType == MenuType.MERCHANT) {
- return CraftMenus.asType(new MenuTypeData<>(MerchantView.class, CraftMenus.STANDARD));
+ return asType(new MenuTypeData<>(MerchantView.class, () -> new CraftMerchantInventoryViewBuilder<>(handle)));
+ }
+ if (menuType == MenuType.SHULKER_BOX) {
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.SHULKER_BOX, ShulkerBoxBlockEntity::new)));
}
if (menuType == MenuType.SMITHING) {
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(SmithingMenu::new)));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, SmithingMenu::new)));
}
if (menuType == MenuType.SMOKER) {
- return CraftMenus.asType(new MenuTypeData<>(FurnaceView.class, tileEntity(SmokerBlockEntity::new, Blocks.SMOKER)));
+ return asType(new MenuTypeData<>(FurnaceView.class, () -> new CraftBlockEntityInventoryViewBuilder<>(handle, Blocks.SMOKER, SmokerBlockEntity::new)));
}
if (menuType == MenuType.CARTOGRAPHY_TABLE) {
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, worldAccess(CartographyTableMenu::new)));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, CartographyTableMenu::new)));
}
if (menuType == MenuType.STONECUTTER) {
- return CraftMenus.asType(new MenuTypeData<>(StonecutterView.class, worldAccess(StonecutterMenu::new)));
+ return asType(new MenuTypeData<>(StonecutterView.class, () -> new CraftAccessLocationInventoryViewBuilder<>(handle, StonecutterMenu::new)));
}
- return CraftMenus.asType(new MenuTypeData<>(InventoryView.class, CraftMenus.STANDARD));
+ return asType(new MenuTypeData<>(InventoryView.class, () -> new CraftStandardInventoryViewBuilder<>(handle)));
}
- private static MenuTypeData asType(MenuTypeData> data) {
- return (MenuTypeData) data;
+ @SuppressWarnings("unchecked")
+ private static > MenuTypeData asType(final MenuTypeData, ?> data) {
+ return (MenuTypeData) data;
}
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractInventoryViewBuilder.java
new file mode 100644
index 0000000000..185ad0fc16
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractInventoryViewBuilder.java
@@ -0,0 +1,48 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.text.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+public abstract class CraftAbstractInventoryViewBuilder implements InventoryViewBuilder {
+
+ protected final MenuType> handle;
+
+ protected boolean checkReachable = false;
+ protected @MonotonicNonNull Component title = null;
+
+ public CraftAbstractInventoryViewBuilder(final MenuType> handle) {
+ this.handle = handle;
+ }
+
+ @Override
+ public InventoryViewBuilder title(final Component title) {
+ this.title = title;
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public V build(final HumanEntity player) {
+ Preconditions.checkArgument(player != null, "The given player must not be null");
+ Preconditions.checkArgument(this.title != null, "The given title must not be null");
+ Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
+ final CraftHumanEntity craftHuman = (CraftHumanEntity) player;
+ Preconditions.checkArgument(craftHuman.getHandle() instanceof ServerPlayer, "The given player must be an EntityPlayer");
+ final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
+ final AbstractContainerMenu container = buildContainer(serverPlayer);
+ container.checkReachable = this.checkReachable;
+ container.setTitle(PaperAdventure.asVanilla(this.title));
+ return (V) container.getBukkitView();
+ }
+
+ protected abstract AbstractContainerMenu buildContainer(ServerPlayer player);
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractLocationInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractLocationInventoryViewBuilder.java
new file mode 100644
index 0000000000..7a894ca078
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAbstractLocationInventoryViewBuilder.java
@@ -0,0 +1,48 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import com.google.common.base.Preconditions;
+import net.kyori.adventure.text.Component;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.level.Level;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.util.CraftLocation;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+import org.jspecify.annotations.Nullable;
+
+public abstract class CraftAbstractLocationInventoryViewBuilder extends CraftAbstractInventoryViewBuilder implements LocationInventoryViewBuilder {
+
+ protected @Nullable Level world;
+ protected @Nullable BlockPos position;
+
+ public CraftAbstractLocationInventoryViewBuilder(final MenuType> handle) {
+ super(handle);
+ }
+
+ @Override
+ public LocationInventoryViewBuilder title(final Component title) {
+ return (LocationInventoryViewBuilder) super.title(title);
+ }
+
+ @Override
+ public LocationInventoryViewBuilder copy() {
+ throw new UnsupportedOperationException("copy is not implemented on CraftAbstractLocationInventoryViewBuilder");
+ }
+
+ @Override
+ public LocationInventoryViewBuilder checkReachable(final boolean checkReachable) {
+ super.checkReachable = checkReachable;
+ return this;
+ }
+
+ @Override
+ public LocationInventoryViewBuilder location(final Location location) {
+ Preconditions.checkArgument(location != null, "The provided location must not be null");
+ Preconditions.checkArgument(location.getWorld() != null, "The provided location must be associated with a world");
+ this.world = ((CraftWorld) location.getWorld()).getHandle();
+ this.position = CraftLocation.toBlockPosition(location);
+ return this;
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAccessLocationInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAccessLocationInventoryViewBuilder.java
new file mode 100644
index 0000000000..096f3ebf81
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftAccessLocationInventoryViewBuilder.java
@@ -0,0 +1,45 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.Inventory;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.ContainerLevelAccess;
+import net.minecraft.world.inventory.MenuType;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+
+public class CraftAccessLocationInventoryViewBuilder extends CraftAbstractLocationInventoryViewBuilder {
+
+ private final CraftAccessContainerObjectBuilder containerBuilder;
+
+ public CraftAccessLocationInventoryViewBuilder(final MenuType> handle, final CraftAccessContainerObjectBuilder containerBuilder) {
+ super(handle);
+ this.containerBuilder = containerBuilder;
+ }
+
+ @Override
+ protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+ final ContainerLevelAccess access;
+ if (super.position == null) {
+ access = ContainerLevelAccess.create(player.level(), player.blockPosition());
+ } else {
+ access = ContainerLevelAccess.create(super.world, super.position);
+ }
+
+ return this.containerBuilder.build(player.nextContainerCounter(), player.getInventory(), access);
+ }
+
+ @Override
+ public LocationInventoryViewBuilder copy() {
+ final CraftAccessLocationInventoryViewBuilder copy = new CraftAccessLocationInventoryViewBuilder<>(this.handle, this.containerBuilder);
+ copy.world = super.world;
+ copy.position = super.position;
+ copy.checkReachable = super.checkReachable;
+ copy.title = title;
+ return copy;
+ }
+
+ public interface CraftAccessContainerObjectBuilder {
+ AbstractContainerMenu build(final int syncId, final Inventory inventory, ContainerLevelAccess access);
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftBlockEntityInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftBlockEntityInventoryViewBuilder.java
new file mode 100644
index 0000000000..2625814440
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftBlockEntityInventoryViewBuilder.java
@@ -0,0 +1,74 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.MenuProvider;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuConstructor;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+import org.jspecify.annotations.Nullable;
+
+public class CraftBlockEntityInventoryViewBuilder extends CraftAbstractLocationInventoryViewBuilder {
+
+ private final Block block;
+ private final @Nullable CraftTileInventoryBuilder builder;
+
+ public CraftBlockEntityInventoryViewBuilder(final MenuType> handle, final Block block, final @Nullable CraftTileInventoryBuilder builder) {
+ super(handle);
+ this.block = block;
+ this.builder = builder;
+ }
+
+ @Override
+ protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+ if (this.world == null) {
+ this.world = player.level();
+ }
+
+ if (this.position == null) {
+ this.position = player.blockPosition();
+ }
+
+ final BlockEntity entity = this.world.getBlockEntity(position);
+ if (!(entity instanceof final MenuConstructor container)) {
+ return buildFakeTile(player);
+ }
+
+ final AbstractContainerMenu atBlock = container.createMenu(player.nextContainerCounter(), player.getInventory(), player);
+ if (atBlock.getType() != super.handle) {
+ return buildFakeTile(player);
+ }
+
+ return atBlock;
+ }
+
+ private AbstractContainerMenu buildFakeTile(final ServerPlayer player) {
+ if (this.builder == null) {
+ return handle.create(player.nextContainerCounter(), player.getInventory());
+ }
+ final MenuProvider inventory = this.builder.build(this.position, this.block.defaultBlockState());
+ if (inventory instanceof final BlockEntity tile) {
+ tile.setLevel(this.world);
+ }
+ return inventory.createMenu(player.nextContainerCounter(), player.getInventory(), player);
+ }
+
+ @Override
+ public LocationInventoryViewBuilder copy() {
+ final CraftBlockEntityInventoryViewBuilder copy = new CraftBlockEntityInventoryViewBuilder<>(super.handle, this.block, this.builder);
+ copy.world = this.world;
+ copy.position = this.position;
+ copy.checkReachable = super.checkReachable;
+ copy.title = title;
+ return copy;
+ }
+
+ public interface CraftTileInventoryBuilder {
+ MenuProvider build(BlockPos blockPosition, BlockState blockData);
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftDoubleChestInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftDoubleChestInventoryViewBuilder.java
new file mode 100644
index 0000000000..331e3797a5
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftDoubleChestInventoryViewBuilder.java
@@ -0,0 +1,48 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.MenuProvider;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.ChestBlock;
+import net.minecraft.world.level.block.DoubleBlockCombiner;
+import net.minecraft.world.level.block.entity.ChestBlockEntity;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.LocationInventoryViewBuilder;
+
+public class CraftDoubleChestInventoryViewBuilder extends CraftAbstractLocationInventoryViewBuilder {
+
+ public CraftDoubleChestInventoryViewBuilder(final MenuType> handle) {
+ super(handle);
+ }
+
+ @Override
+ protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+ if (super.world == null) {
+ return handle.create(player.nextContainerCounter(), player.getInventory());
+ }
+
+ final ChestBlock chest = (ChestBlock) Blocks.CHEST;
+ final DoubleBlockCombiner.NeighborCombineResult extends ChestBlockEntity> result = chest.combine(super.world.getBlockState(super.position), super.world, super.position, false);
+ if (result instanceof DoubleBlockCombiner.NeighborCombineResult.Single extends ChestBlockEntity>) {
+ return handle.create(player.nextContainerCounter(), player.getInventory());
+ }
+
+ final MenuProvider combined = result.apply(ChestBlock.MENU_PROVIDER_COMBINER).orElse(null);
+ if (combined == null) {
+ return handle.create(player.nextContainerCounter(), player.getInventory());
+ }
+ return combined.createMenu(player.nextContainerCounter(), player.getInventory(), player);
+ }
+
+ @Override
+ public LocationInventoryViewBuilder copy() {
+ final CraftDoubleChestInventoryViewBuilder copy = new CraftDoubleChestInventoryViewBuilder<>(super.handle);
+ copy.world = this.world;
+ copy.position = this.position;
+ copy.checkReachable = super.checkReachable;
+ copy.title = title;
+ return copy;
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftMerchantInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftMerchantInventoryViewBuilder.java
new file mode 100644
index 0000000000..7f7518aa73
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftMerchantInventoryViewBuilder.java
@@ -0,0 +1,78 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.text.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.MerchantMenu;
+import org.bukkit.craftbukkit.entity.CraftHumanEntity;
+import org.bukkit.craftbukkit.inventory.CraftMerchant;
+import org.bukkit.craftbukkit.inventory.CraftMerchantCustom;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.Merchant;
+import org.bukkit.inventory.view.builder.MerchantInventoryViewBuilder;
+import org.jspecify.annotations.Nullable;
+
+public class CraftMerchantInventoryViewBuilder extends CraftAbstractInventoryViewBuilder implements MerchantInventoryViewBuilder {
+
+ private net.minecraft.world.item.trading.@Nullable Merchant merchant;
+
+ public CraftMerchantInventoryViewBuilder(final MenuType> handle) {
+ super(handle);
+ }
+
+ @Override
+ public MerchantInventoryViewBuilder title(final Component title) {
+ return (MerchantInventoryViewBuilder) super.title(title);
+ }
+
+ @Override
+ public MerchantInventoryViewBuilder merchant(final Merchant merchant) {
+ this.merchant = ((CraftMerchant) merchant).getMerchant();
+ return this;
+ }
+
+ @Override
+ public MerchantInventoryViewBuilder checkReachable(final boolean checkReachable) {
+ super.checkReachable = checkReachable;
+ return this;
+ }
+
+ @Override
+ public V build(final HumanEntity player) {
+ Preconditions.checkArgument(player != null, "The given player must not be null");
+ Preconditions.checkArgument(this.title != null, "The given title must not be null");
+ Preconditions.checkArgument(player instanceof CraftHumanEntity, "The given player must be a CraftHumanEntity");
+ final CraftHumanEntity craftHuman = (CraftHumanEntity) player;
+ Preconditions.checkArgument(craftHuman.getHandle() instanceof ServerPlayer, "The given player must be an EntityPlayer");
+ final ServerPlayer serverPlayer = (ServerPlayer) craftHuman.getHandle();
+
+ final MerchantMenu container;
+ if (this.merchant == null) {
+ container = new MerchantMenu(serverPlayer.nextContainerCounter(), serverPlayer.getInventory(), new CraftMerchantCustom(title).getMerchant());
+ } else {
+ container = new MerchantMenu(serverPlayer.nextContainerCounter(), serverPlayer.getInventory(), this.merchant);
+ }
+
+ container.checkReachable = super.checkReachable;
+ container.setTitle(PaperAdventure.asVanilla(this.title));
+ return (V) container.getBukkitView();
+ }
+
+ @Override
+ protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+ throw new UnsupportedOperationException("buildContainer is not supported for CraftMerchantInventoryViewBuilder");
+ }
+
+ @Override
+ public MerchantInventoryViewBuilder copy() {
+ final CraftMerchantInventoryViewBuilder copy = new CraftMerchantInventoryViewBuilder<>(super.handle);
+ copy.checkReachable = super.checkReachable;
+ copy.merchant = this.merchant;
+ copy.title = title;
+ return copy;
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftStandardInventoryViewBuilder.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftStandardInventoryViewBuilder.java
new file mode 100644
index 0000000000..e528facbe0
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/CraftStandardInventoryViewBuilder.java
@@ -0,0 +1,26 @@
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.MenuType;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.view.builder.InventoryViewBuilder;
+
+public class CraftStandardInventoryViewBuilder extends CraftAbstractInventoryViewBuilder {
+
+ public CraftStandardInventoryViewBuilder(final MenuType> handle) {
+ super(handle);
+ }
+
+ @Override
+ protected AbstractContainerMenu buildContainer(final ServerPlayer player) {
+ return super.handle.create(player.nextContainerCounter(), player.getInventory());
+ }
+
+ @Override
+ public InventoryViewBuilder copy() {
+ final CraftStandardInventoryViewBuilder copy = new CraftStandardInventoryViewBuilder<>(handle);
+ copy.title = this.title;
+ return copy;
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/package-info.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/package-info.java
new file mode 100644
index 0000000000..157ce9fd75
--- /dev/null
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/view/builder/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package org.bukkit.craftbukkit.inventory.view.builder;
+
+import org.jspecify.annotations.NullMarked;