本课作品
用 PDC 识别的矿工罗盘
名字给玩家看,身份数据给程序认。制作带说明的原版罗盘,用材料、命名空间、类型和值四项检查识别它,再让主手右键只显示一次任务。 前置背包:命令、事件、空值检查与List。新项目BlockAcademyL37沿用第31课构建脚本,继续锁定Java21和普通Paper1.21.11。
开始前:建议先完成第 31–36 课
01 / 13
开始前:先认清运行位置
前30课打好了普通 Java 程序的基础。这一阶段把代码接到 Paper 服务器:Minecraft 客户端负责进入世界,服务器加载插件,事件和命令把玩家动作交给你的方法。
本书固定使用 JDK 21 与 Paper 1.21.11,客户端版本也保持一致。下载页面上的最新版本可能已经变了,按版本号选择。本书采用普通 plugin.yml 与 Bukkit API;本阶段没有声称支持 Folia。
图框中的短例是局部片段,先读本节的位置说明,不要各自保存成插件。完整文件会明确给出文件名;同一文件分成多段时,按编号连接,包名、import 与外层类只出现一次。
- 预测:先写成功条件和预期消息。
- 运行:构建成功后,在专用本机测试服安装并重启。
- 核对:普通玩家、管理身份与控制台分别走对应路径。
02 / 13
物品本体和说明书分开改
普通罗盘在铁砧里改名“超级矿工罗盘”,就来申请特殊能力。门卫看了看名字差点放行。显示名能换,可靠识别要查看插件写入的身份标记。
ItemStack 是物品,ItemMeta 是说明和附加数据。制作时写入 NamespacedKey 对应的 PDC 标记;识别时检查材料、数据类型和值。名字负责让玩家读懂,标记负责让程序认对。
- ItemStack物品本体
- ItemMeta名称说明与附加数据
- setItemMeta把修改写回本体
先制作可读的罗盘 · 位置见本节说明
ItemStack item = new ItemStack(Material.COMPASS);
var meta = item.getItemMeta();
meta.displayName(Component.text("矿工罗盘"));
meta.lore(List.of(Component.text("主手右键查看课堂任务")));
item.setItemMeta(meta);故意漏掉最后一步 · 故意错误,先预测再修复
// 若忘了setItemMeta,检查名称和说明是否真的保存。03 / 13
铁砧能改名 不能证明身份
把普通罗盘改名为“矿工罗盘”,看起来像真的,但名字只是标签纸。程序要检查藏在物品数据里的PDC标记。显示名负责让玩家认得,数据键负责让程序认得。
- 显示名称方便玩家理解
- NamespacedKey稳定的数据位置
- BYTE值1本章身份标记
为本插件创建一个键 · 位置见本节说明
compassKey = new NamespacedKey(this, "miner_compass");写入类型明确的标记 · 位置见本节说明
meta.getPersistentDataContainer().set(
compassKey, PersistentDataType.BYTE, (byte) 1);| 信息 | 用途 |
|---|---|
| 显示名称与lore | 让玩家看懂 |
| NamespacedKey | 命名空间加键名,减少冲突 |
| BYTE类型和值1 | 本课约定的罗盘身份 |
04 / 13
把制作和识别放进同一个类
新建CompassItems.java,放进de.teacherma.lesson37包。制作和识别使用同一个键与同一种类型,避免一边写miner_compass,另一边却找mining_compass,像把钥匙送错了门。
- 创建罗盘选择原版材料
- 写入元数据名称、lore和PDC
- 写回物品setItemMeta再返回
完整CompassItems.java · 完整文件
package de.teacherma.lesson37;
import java.util.List;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
public final class CompassItems {
private CompassItems() { }
public static ItemStack create(NamespacedKey key) {
ItemStack item = new ItemStack(Material.COMPASS);
var meta = item.getItemMeta();
meta.displayName(Component.text("矿工罗盘"));
meta.lore(List.of(Component.text("主手右键查看课堂任务")));
meta.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte) 1);
item.setItemMeta(meta);
return item;
}
public static boolean isOurs(ItemStack item, NamespacedKey key) {
if (item == null || item.getType() != Material.COMPASS || !item.hasItemMeta())
return false;
Byte marker = item.getItemMeta().getPersistentDataContainer()
.get(key, PersistentDataType.BYTE);
return Byte.valueOf((byte) 1).equals(marker);
}
}05 / 13
检查材料 类型和值
空手时item可能为null。普通罗盘可能没有meta;有meta也未必有我们的标记。识别函数按顺序排除不匹配的情况,最后比较BYTE值是不是1。只检查“键存在”,会把别的标记值也收进来。
- 材料COMPASS先排除不相关物品
- 键和类型读取对应BYTE标记
- 值等于1才认作本章罗盘
安全比较可能为空的值 · 位置见本节说明
Byte marker = meta.getPersistentDataContainer()
.get(compassKey, PersistentDataType.BYTE);
boolean ours = Byte.valueOf((byte) 1).equals(marker);删除自定义身份的片段 · 位置见本节说明
meta.getPersistentDataContainer().remove(compassKey);
item.setItemMeta(meta);| 物品数据 | 识别结果 |
|---|---|
| 普通罗盘或无标记 | false |
| 同键STRING文字"1" | false |
| 同键BYTE值2 | false |
| COMPASS加同键BYTE值1 | true |
06 / 13
两只手不要抢着汇报
交互事件可能分别为两只手触发。若副手也拿着罗盘,同一动作就可能出现两条任务提示。我们只接受EquipmentSlot.HAND,也就是主手;不是靠“发消息后删一条”补救。
停下来想一想:真罗盘被玩家改名后还该识别吗?普通罗盘改成同名后应该获得能力吗?
- 主手EquipmentSlot.HAND
- 右键动作空气和方块都接受
- 物品匹配三关通过后提示一次
沿箭头进入下一步前,必须满足前一步条件;被拒绝的分支提前返回。
先筛选主手 · 位置见本节说明
if (event.getHand() != EquipmentSlot.HAND) return;两种右键都允许 · 位置见本节说明
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_AIR
&& action != Action.RIGHT_CLICK_BLOCK) return;- 主手持有本课罗盘右键空气:显示一次任务。
- 主手右键方块:显示任务,方块原有交互仍可继续。
- 副手持有罗盘或左键挥动:本课不显示任务。
07 / 13
读任务不代表批准交互
对空气右键时,原版可能预测“没有动作”而把事件标为取消。因此这个只读提示监听器没有设置ignoreCancelled=true。它只看信息并发提示,不恢复取消,也不授权方块操作。
- 只读观察只发送任务文字
- 原有交互不被本章取消或恢复
- 未来有副作用须重新设计许可策略
只读观察方法的入口 · 位置见本节说明
@EventHandler(priority = EventPriority.MONITOR)
public void onUse(PlayerInteractEvent event) {
// 主手、动作、物品都匹配后,只发送任务文字。
}不要为提示解封保护 · 故意错误,先预测再修复
event.setCancelled(false); // 本课不要这样做08 / 13
给罗盘项目登记命令
项目名设为BlockAcademyL37。resources目录只需要plugin.yml,本课没有config.yml。主类和物品工具类都在lesson37包,构建脚本沿用第31课。
- CompassItems制作与识别共用规则
- CompassPlugin登记命令与监听器
- plugin.yml声明领取命令和权限
完整plugin.yml · 完整文件
name: BlockAcademyL37
version: 1.0.0
main: de.teacherma.lesson37.CompassPlugin
api-version: '1.21.11'
author: TeacherMa
website: https://www.teacher-ma.de
commands:
minercompass:
description: 领取课堂矿工罗盘
usage: /minercompass
permissions:
blockacademy.compass.give:
default: op完整项目中的Java文件 · 完整文件
src/main/java/de/teacherma/lesson37/
CompassItems.java
CompassPlugin.java09 / 13
罗盘主类上半部
新建CompassPlugin.java。本页先声明依赖、创建一次键、注册命令和监听器;下一页继续命令与交互逻辑。不要在两页中间加一个新的class。
- 启用时创建稳定键
- 命令登记领取入口可找到
- 监听登记主手右键能收到通知
主类第1部分 · 顺序接在同一源文件
package de.teacherma.lesson37;
import java.util.Objects;
import net.kyori.adventure.text.Component;
import org.bukkit.NamespacedKey;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.plugin.java.JavaPlugin;
public final class CompassPlugin extends JavaPlugin implements Listener {
private NamespacedKey compassKey;
@Override
public void onEnable() {
compassKey = new NamespacedKey(this, "miner_compass");
Objects.requireNonNull(getCommand("minercompass")).setExecutor(this);
getServer().getPluginManager().registerEvents(this, this);
}10 / 13
罗盘主类下半部
紧接上一页。领取前检查发送者、权限与参数;背包装不下的罗盘放到脚边。交互方法只做三次筛选和一次提示,短得足够让你逐行解释。
- 领取先检查身份、权限与参数
- 剩余满背包如实处理
- 观察不改方块,只发提示
主类第2部分 · 顺序接在同一源文件
@Override
public boolean onCommand(CommandSender sender, Command command,
String label, String[] args) {
if (!(sender instanceof Player p)) {
sender.sendMessage("请在游戏中领取罗盘"); return true;
}
if (!p.hasPermission("blockacademy.compass.give")) {
p.sendMessage("请老师授权后领取"); return true;
}
if (args.length != 0) { p.sendMessage("用法: /minercompass"); return true; }
var left = p.getInventory().addItem(CompassItems.create(compassKey));
left.values().forEach(item -> p.getWorld().dropItemNaturally(p.getLocation(), item));
p.sendMessage(left.isEmpty() ? "罗盘已入包,主手右键试试" : "罗盘在脚边");
return true;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onUse(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) return;
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) return;
if (!CompassItems.isOurs(event.getItem(), compassKey)) return;
event.getPlayer().sendMessage(Component.text("课堂任务:准备工具,参加矿石冲刺赛!"));
}
}11 / 13
真假罗盘一起参加考试
只测试真罗盘会漏掉大半问题。准备一个普通罗盘、一个改名罗盘和本课罗盘,让“假学生”也来考试。记录预期和实际,不要只在聊天栏看到一句话就宣布毕业。
- 真物品改名后仍可识别
- 同名假物品没有PDC就不识别
- 动作边界空手、副手和左键不提示
| 测试 | 预期 |
|---|---|
| 真罗盘主手右键空气 | 显示一次任务 |
| 真罗盘主手右键箱子 | 提示出现,箱子交互不被本课改变 |
| 同名普通罗盘 | 不显示任务 |
| 空手或左键 | 不显示任务 |
| 真罗盘只放副手 | 不显示任务 |
| 满背包领取 | 剩余罗盘放脚边并提示 |
| 保存物品后正常重启 | 同名同键插件仍识别标签 |
12 / 13
拓展挑战与自己的验收记录
留下源代码、实际运行结果和一条问题记录。先用自己的话解释,再运行验证;结果与预测不同的地方,往往最值得讨论。
- 木镐增加两行lore
- 铁镐测试键、类型和值
- 钻石镐设计三件不同标签的工具
- 木镐:给罗盘增加两行lore,解释为什么要setItemMeta。
- 铁镐:制作材料、键、类型和值四项识别测试,普通物品不能误触发。
- 钻石镐:设计矿工三件套,用不同标签区分功能;写明领取权限与重启后的行为。
- 验收要求:测试真物品、同名假物品、空手、副手以及改名真物品;一次操作不能因两只手重复响应。
| 我能独立完成 | 已掌握 | 再练一次 |
|---|---|---|
| 能创建并保存ItemMeta | □ | □ |
| 会读写PDC身份标记 | □ | □ |
| 能处理主手与右键动作 | □ | □ |
13 / 13
把证据留给下一次冒险
把“能编译”“数字规则通过”和“游戏实际表现正确”分成三栏。没有运行的场景写待测;发现 Bug 时记录触发条件、预期、实际、最小修复与复测结果。
| 类别 | 你选择的操作 | 留下的证据 |
|---|---|---|
| 正常 | 本课的一次成功路径 | 版本与实际输出 |
| 边界 | 结束前一刻、最大值或满背包 | 与合同逐项比较 |
| 拒绝 | 错误参数、无权限或不相关事件 | 反馈清楚,状态未误改 |
- 解释本课每条箭头为什么从这里指向那里;并列卡片没有先后含义。
- 先改一个规则,再同时修改说明和预期,用同一输入复测。
TAKE IT WITH YOU
下载本课 PDF
完整课文、代码、图解与练习,选择你需要的语言。
