Skills/nms/nms-custom-menu/SKILL.md
繼承 AbstractContainerMenu 建立自定義容器 GUI,支援 slot 事件攔截與資料同步(Paper NMS + Mojang-mapped)/ Build custom container GUIs by extending AbstractContainerMenu with slot event handling
npx skillsauth add MrPippi/MPS nms-custom-menuInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
nms-custom-menu
透過繼承 NMS AbstractContainerMenu 實作自定義容器 GUI,支援 slot 操作攔截、資料同步(ContainerData)、與 Bukkit InventoryView 整合,比純 Bukkit API 更靈活。
| 參數 | 範例 | 說明 |
|------|------|------|
| package_name | com.example.gui | 產出類別所在 package |
| menu_class_name | ShopMenu | Menu 類名稱 |
| menu_type | GENERIC_9x3 | MenuType(如 GENERIC_9x3、ANVIL) |
| rows | 3 | 列數(GENERIC_9xN 時使用) |
CustomMenu.java — AbstractContainerMenu 實作CustomMenuProvider.java — MenuProvider(供 ServerPlayer.openMenu 使用)CustomMenuListener.java — Bukkit 事件橋接(InventoryClickEvent 等)參見 Skills/paper-nms/PLATFORM.md。關鍵依賴:
dependencies {
paperweight.paperDevBundle('1.21.1-R0.1-SNAPSHOT')
}
CustomMenu.javapackage com.example.gui;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventoryCustom;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventoryView;
import org.bukkit.inventory.InventoryView;
@SuppressWarnings("UnstableApiUsage")
public class CustomMenu extends AbstractContainerMenu {
private static final int ROWS = 3;
private static final int SIZE = ROWS * 9;
private final net.minecraft.world.Container menuInventory;
private final Inventory playerInventory;
private CraftInventoryView bukkitView;
public CustomMenu(int syncId, Inventory playerInventory) {
super(MenuType.GENERIC_9x3, syncId);
this.playerInventory = playerInventory;
this.menuInventory = new net.minecraft.world.SimpleContainer(SIZE);
// 注册 GUI slot(上方容器區)
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < 9; col++) {
int index = col + row * 9;
addSlot(new Slot(menuInventory, index, 8 + col * 18, 18 + row * 18));
}
}
// 注册玩家物品欄 slot(下方區)
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 9; col++) {
addSlot(new Slot(playerInventory, col + row * 9 + 9,
8 + col * 18, 103 + row * 18));
}
}
// 快捷欄
for (int col = 0; col < 9; col++) {
addSlot(new Slot(playerInventory, col, 8 + col * 18, 161));
}
}
/** 控制哪些 slot 可以被取出(回傳 false = 禁止取出)。 */
@Override
public boolean stillValid(Player player) {
return true;
}
/** 攔截 shift-click 邏輯。 */
@Override
public ItemStack quickMoveStack(Player player, int slotIndex) {
return ItemStack.EMPTY; // 禁止 shift-click
}
/** 取得 Bukkit InventoryView(供 Bukkit 事件使用)。 */
@Override
public InventoryView getBukkitView() {
if (bukkitView == null) {
CraftInventory craftInventory = new CraftInventoryCustom(null, menuInventory);
bukkitView = new CraftInventoryView(
(org.bukkit.entity.HumanEntity) playerInventory.player.getBukkitEntity(),
craftInventory, this);
}
return bukkitView;
}
}
CustomMenuProvider.javapackage com.example.gui;
import net.minecraft.network.chat.Component;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
@SuppressWarnings("UnstableApiUsage")
public class CustomMenuProvider implements MenuProvider {
private final String title;
public CustomMenuProvider(String title) {
this.title = title;
}
@Override
public Component getDisplayName() {
return Component.literal(title);
}
@Override
public AbstractContainerMenu createMenu(int syncId, Inventory inventory, Player player) {
return new CustomMenu(syncId, inventory);
}
}
CustomMenuListener.java(Bukkit 事件橋接)package com.example.gui;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.InventoryView;
public class CustomMenuListener implements Listener {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
InventoryView view = event.getView();
if (!(view.getTopInventory().getHolder() instanceof CustomMenuHolder holder)) return;
event.setCancelled(true); // 預設取消所有點擊
int slot = event.getRawSlot();
if (slot >= 0 && slot < 27) {
holder.handleSlotClick(slot, event.getWhoClicked());
}
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
InventoryView view = event.getView();
if (view.getTopInventory().getHolder() instanceof CustomMenuHolder holder) {
holder.onClose(event.getPlayer());
}
}
}
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
@SuppressWarnings("UnstableApiUsage")
public static void openMenu(Player player, String title) {
ServerPlayer nms = ((CraftPlayer) player).getHandle();
nms.openMenu(new CustomMenuProvider(title));
}
src/main/java/com/example/
├── MyNmsPlugin.java
└── gui/
├── CustomMenu.java
├── CustomMenuProvider.java
├── CustomMenuHolder.java
└── CustomMenuListener.java
nms.openMenu() 及所有 GUI 操作必須在主執行緒呼叫Skills/_shared/nms-threading.md| 錯誤 | 原因 | 解法 |
|------|------|------|
| GUI 打開後立即關閉 | stillValid() 回傳 false | 確認玩家距離或條件正確 |
| slot index 超出範圍 | container size 不匹配 | 確認 slot 注册數量與 MenuType 相符 |
| getBukkitView() NPE | playerInventory.player 未初始化 | 確認在 PlayerJoinEvent 後開啟 |
| shift-click 穿越 GUI | quickMoveStack 未正確實作 | 回傳 ItemStack.EMPTY 禁止操作 |
development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
tools
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag