项目背景
ThePit 是一个 Minecraft 竞技 PvP 竞技场插件,运行在 1.20.1 Arclight(Forge+Bukkit 混合端)上。今天修复了三个核心问题:CPS 显示异常、任务系统无法触发、数据库访问权限错误。
技术栈
- 语言: Java 17
- 服务端: Arclight 1.20.1 (Forge + Bukkit)
- 构建工具: Gradle 8.1.1
- 数据库: SQLite / MySQL (HikariCP 连接池)
- 依赖库: Paper API, ProtocolLib 5.2.0, PlaceholderAPI 2.11.5
问题一:CPS 显示为 0
玩家无论如何点击左键,计分板上的 CPS 始终显示为 0。
原始代码使用 Bukkit 的 PlayerInteractEvent 监听左键点击,但 Arclight 混合端上此事件触发不可靠。Forge 和 Bukkit 的事件系统存在兼容性问题。
解决方案:ProtocolLib 数据包监听
使用 ProtocolLib 直接监听客户端的手臂挥动数据包(ARM_ANIMATION),绕过 Bukkit 事件系统:
public class CPSListener implements Listener {
public CPSListener(Plugin plugin) {
ProtocolLibrary.getProtocolManager().addPacketListener(
new PacketAdapter(plugin, ListenerPriority.NORMAL,
PacketType.Play.Client.ARM_ANIMATION) {
@Override
public void onPacketReceiving(PacketEvent event) {
Player player = event.getPlayer();
if (player != null) {
CPSManager.recordClick(player);
}
}
}
);
}
}
ARM_ANIMATION 对应 ServerboundSwingPacket,每次玩家挥动手臂都会发送,在所有服务端类型上都可靠。
问题二:任务系统无法触发
- 任务界面只显示日常任务,周任务标题占用了错误的槽位
- 没有事件监听器连接游戏事件到任务进度
- 任务进度只存在内存中,重启丢失
解决方案
1. 事件监听器:创建 QuestListener,使用 MONITOR 优先级确保在金币发放和连杀更新之后执行。连杀任务延迟 1 tick 读取,避免数据竞争。
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerKill(PlayerRespawnEvent event) {
Player killer = event.getPlayer().getKiller();
if (killer != null) {
questManager.updateQuestProgress(killer, QuestType.KILL_PLAYERS, 1);
Bukkit.getScheduler().runTaskLater(plugin, () -> {
int streak = new PlayerStreak().getStreak(killer);
questManager.setProgress(killer, "weekly_streak_5", streak);
}, 1L);
}
}
2. 数据库持久化:使用 PreparedStatement 和 UUID 关联存储任务进度。
CREATE TABLE ThePitQuests (
Player varchar(30), UUID varchar(50), QuestID varchar(50),
Progress INT(11), LastResetDay INT(11),
LastResetWeek INT(11), LastResetYear INT(11)
);
3. 自动重置:每次读取进度时检查日期/周数,过期自动重置。日任务每天重置,周任务每周重置,无需定时任务。
事件优先级
GlobalEvents– HIGHEST:发放金币PlayerStreak– 默认:更新连杀数QuestListener– MONITOR:更新任务进度
问题三:数据库反射访问错误
java.lang.IllegalAccessException: class QuestManager cannot access
a member of class SQLite with modifiers "private"
反射访问私有字段 connection 时缺少 setAccessible(true)。Java 安全机制默认禁止访问私有成员。
Field field = SQLite.getInstance().getClass()
.getDeclaredField("connection");
field.setAccessible(true); // 关键修复
return (Connection) field.get(SQLite.getInstance());
总结
- CPS:ProtocolLib 数据包监听解决混合端兼容性
- 任务系统:事件监听 + 数据库持久化 + 按需自动重置
- 数据库:反射权限修复
setAccessible(true)
项目已可部署使用,后续可考虑 QuestManager 单例化和连接池优化。
