diff --git a/.gitignore b/.gitignore
index f5af968..7c81ce8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,18 +13,6 @@
*.tar.gz
*.rar
-# Maven
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-.mvn/wrapper/maven-wrapper.jar
-
# Gradle
.gradle/
build/
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..352faff
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,229 @@
+plugins {
+ id 'java'
+ id 'io.papermc.paperweight.userdev' version '2.0.0-beta.21'
+ id 'com.gradleup.shadow' version '8.3.6'
+}
+
+group = 'cn.infstar'
+version = '1.3.0'
+
+repositories {
+ mavenCentral()
+ maven {
+ name = 'papermc'
+ url = uri('https://repo.papermc.io/repository/maven-public/')
+ }
+}
+
+dependencies {
+ paperweight.paperDevBundle('1.21.11-R0.1-SNAPSHOT')
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
+
+def excludedModules = project.hasProperty('excludeModules') ?
+ project.property('excludeModules').split(',')*.trim() : []
+
+def includeBlocks = !excludedModules.contains('blocks')
+def includePlayer = !excludedModules.contains('player')
+def includeJeiFix = !excludedModules.contains('jei-fix')
+def includeMobDrops = !excludedModules.contains('mob-drops')
+
+println "\n📦 EssentialsC 模块配置:"
+println " ✅ Core (核心)"
+println " ${includeBlocks ? '✅' : '❌'} Blocks (便捷方块)"
+println " ${includePlayer ? '✅' : '❌'} Player (玩家管理)"
+println " ${includeJeiFix ? '✅' : '❌'} JEI Fix (JEI 修复)"
+println " ${includeMobDrops ? '✅' : '❌'} Mob Drops (生物掉落物)"
+println ""
+
+sourceSets {
+ main {
+ java {
+ if (!includeBlocks) {
+ exclude '**/commands/WorkbenchCommand.java'
+ exclude '**/commands/AnvilCommand.java'
+ exclude '**/commands/CartographyTableCommand.java'
+ exclude '**/commands/GrindstoneCommand.java'
+ exclude '**/commands/LoomCommand.java'
+ exclude '**/commands/SmithingTableCommand.java'
+ exclude '**/commands/StonecutterCommand.java'
+ exclude '**/commands/EnderChestCommand.java'
+ exclude '**/commands/BlocksMenuCommand.java'
+ exclude '**/listeners/ShulkerBoxListener.java'
+ }
+
+ if (!includePlayer) {
+ exclude '**/commands/FlyCommand.java'
+ exclude '**/commands/HealCommand.java'
+ exclude '**/commands/FeedCommand.java'
+ exclude '**/commands/VanishCommand.java'
+ exclude '**/commands/SeenCommand.java'
+ exclude '**/commands/HatCommand.java'
+ exclude '**/commands/SuicideCommand.java'
+ exclude '**/commands/RepairCommand.java'
+ }
+
+ if (!includeJeiFix) {
+ exclude '**/listeners/JeiRecipeSyncListener.java'
+ }
+
+ if (!includeMobDrops) {
+ exclude '**/listeners/MobDropListener.java'
+ exclude '**/listeners/MobDropMenuListener.java'
+ exclude '**/commands/MobDropCommand.java'
+ }
+ }
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.encoding = 'UTF-8'
+}
+
+processResources {
+ filteringCharset = 'UTF-8'
+ inputs.property('version', project.version)
+ filesMatching('paper-plugin.yml') {
+ expand('version': project.version)
+ }
+}
+
+shadowJar {
+ enabled = false
+}
+
+// ========== 多版本构建任务 ==========
+
+task shadowJarStandard(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+ from sourceSets.main.output
+ configurations = [project.configurations.runtimeClasspath]
+
+ exclude '**/listeners/MobDropListener.class'
+ exclude '**/listeners/MobDropMenuListener.class'
+ exclude '**/commands/MobDropCommand.class'
+
+ archiveFileName.set("EssentialsC-${project.version}.jar")
+}
+
+task shadowJarAll(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+ from sourceSets.main.output
+ configurations = [project.configurations.runtimeClasspath]
+
+ archiveFileName.set("EssentialsC-all-${project.version}.jar")
+}
+
+task shadowJarLite(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+ from sourceSets.main.output
+ configurations = [project.configurations.runtimeClasspath]
+
+ exclude '**/commands/WorkbenchCommand.class'
+ exclude '**/commands/AnvilCommand.class'
+ exclude '**/commands/CartographyTableCommand.class'
+ exclude '**/commands/GrindstoneCommand.class'
+ exclude '**/commands/LoomCommand.class'
+ exclude '**/commands/SmithingTableCommand.class'
+ exclude '**/commands/StonecutterCommand.class'
+ exclude '**/commands/EnderChestCommand.class'
+ exclude '**/commands/BlocksMenuCommand.class'
+ exclude '**/listeners/ShulkerBoxListener.class'
+
+ archiveFileName.set("EssentialsC-lite-${project.version}.jar")
+}
+
+build {
+ dependsOn shadowJarAll
+}
+
+// ========== 部署任务 ==========
+
+task deployToPaper12111(type: Copy) {
+ group = 'deployment'
+ description = '部署到 Paper 1.21.11 测试服务器(仅完整版)'
+ dependsOn shadowJarAll
+ from shadowJarAll
+ into "${projectDir}/test-server/paper-1.21.11/plugins"
+ doLast {
+ println "✅ 插件已部署到 Paper 1.21.11: ${projectDir}/test-server/paper-1.21.11/plugins"
+ }
+}
+
+task deployToPaper26 {
+ group = 'deployment'
+ description = '部署到 Paper 26.1.2 测试服务器(仅完整版)'
+ dependsOn shadowJarAll
+
+ doFirst {
+ def pluginsDir = file("${projectDir}/test-server/paper-26.1.2/plugins")
+ if (pluginsDir.exists()) {
+ // 删除所有 EssentialsC 相关的 JAR 文件
+ fileTree(pluginsDir).include('EssentialsC*.jar').each { file ->
+ println "🗑️ 删除旧插件: ${file.name}"
+ file.delete()
+ }
+ // 删除配置文件夹
+ def configDir = file("${pluginsDir}/EssentialsC")
+ if (configDir.exists()) {
+ println "🗑️ 删除旧配置文件夹"
+ configDir.deleteDir()
+ }
+ println "✅ 清理完成"
+ } else {
+ println "⚠️ plugins 目录不存在,跳过清理"
+ }
+ }
+
+ doLast {
+ copy {
+ from shadowJarAll.archiveFile
+ into "${projectDir}/test-server/paper-26.1.2/plugins"
+ }
+ println "✅ 插件已部署到 Paper 26.1.2: ${projectDir}/test-server/paper-26.1.2/plugins"
+ }
+}
+
+// Paper 26.1.2 一键构建和部署任务
+task buildAndDeployToPaper26 {
+ group = 'deployment'
+ description = '一键构建并部署到 Paper 26.1.2'
+ dependsOn clean, deployToPaper26
+}
+
+// 默认部署到 Paper 1.21.11
+task deployToTestServer {
+ dependsOn deployToPaper12111
+}
+
+build.finalizedBy deployToTestServer
+
+task buildAllVersions {
+ group = 'build'
+ description = '构建所有版本的插件'
+ dependsOn shadowJarStandard, shadowJarAll, shadowJarLite
+
+ doLast {
+ println "\n🎉 所有版本构建完成!"
+ println "📦 标准版: EssentialsC-${project.version}.jar"
+ println "📦 完整版: EssentialsC-all-${project.version}.jar"
+ println "📦 轻量版: EssentialsC-lite-${project.version}.jar"
+ println "📁 输出目录: ${buildDir}/libs/"
+
+ def standardFile = file("${buildDir}/libs/EssentialsC-${project.version}.jar")
+ def allFile = file("${buildDir}/libs/EssentialsC-all-${project.version}.jar")
+ def liteFile = file("${buildDir}/libs/EssentialsC-lite-${project.version}.jar")
+
+ if (!standardFile.exists()) {
+ throw new GradleException("❌ 标准版文件不存在!")
+ }
+ if (!allFile.exists()) {
+ throw new GradleException("❌ 完整版文件不存在!")
+ }
+ if (!liteFile.exists()) {
+ throw new GradleException("❌ 轻量版文件不存在!")
+ }
+
+ println "✅ 所有文件验证通过!"
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2e11132
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 52cf5ce..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
- 4.0.0
-
- cn.infstar
- essentialsc
- 1.2.0
- jar
-
- essentialsc
-
-
- 21
- UTF-8
- ${project.basedir}/test-server/plugins
-
-
-
- clean package
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.13.0
-
- ${java.version}
- ${java.version}
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.5.3
-
-
- package
-
- shade
-
-
-
-
-
-
-
- src/main/resources
- true
-
-
-
-
-
-
-
- aliyunmaven
- https://maven.aliyun.com/repository/public
-
-
-
- papermc-repo
- https://repo.papermc.io/repository/maven-public/
-
-
-
-
-
- io.papermc.paper
- paper-api
- 1.21.11-R0.1-SNAPSHOT
- provided
-
-
-
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..4705cb6
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ // 阿里云 Gradle 插件镜像(优先)
+ maven {
+ name = 'aliyun-gradle-plugin'
+ url = uri('https://maven.aliyun.com/repository/gradle-plugin')
+ }
+ // Gradle 官方插件仓库
+ gradlePluginPortal()
+ // PaperMC 官方仓库(用于 paperweight 插件)
+ maven {
+ name = 'papermc'
+ url = uri('https://repo.papermc.io/repository/maven-public/')
+ }
+ }
+}
+
+rootProject.name = 'EssentialsC'
diff --git a/src/main/java/cn/infstar/essentialsC/EssentialsC.java b/src/main/java/cn/infstar/essentialsC/EssentialsC.java
index 2ba929c..eabadc0 100644
--- a/src/main/java/cn/infstar/essentialsC/EssentialsC.java
+++ b/src/main/java/cn/infstar/essentialsC/EssentialsC.java
@@ -1,7 +1,6 @@
package cn.infstar.essentialsC;
import cn.infstar.essentialsC.commands.*;
-import cn.infstar.essentialsC.listeners.ShulkerBoxListener;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -15,17 +14,12 @@ public final class EssentialsC extends JavaPlugin {
@Override
public void onEnable() {
- // 初始化语言管理器
langManager = new LangManager(this);
-
- // 注册监听器
+ registerPluginChannels();
registerListeners();
-
- // 注册命令
registerCommands();
- getLogger().info("EssentialsC 插件已启用!");
- getLogger().info("当前语言: " + langManager.getCurrentLanguage());
+ getLogger().info("插件已启用!版本: " + getDescription().getVersion());
}
@Override
@@ -33,55 +27,150 @@ public final class EssentialsC extends JavaPlugin {
getLogger().info("EssentialsC 插件已禁用!");
}
- /**
- * 获取语言管理器实例
- */
public static LangManager getLangManager() {
return langManager;
}
/**
- * 注册所有监听器
+ * 注册 JEI 配方同步所需的插件频道
*/
+ private void registerPluginChannels() {
+ org.bukkit.plugin.messaging.Messenger messenger = getServer().getMessenger();
+ // 注册 Fabric 和 NeoForge 的配方同步频道
+ messenger.registerOutgoingPluginChannel(this, "fabric:recipe_sync");
+ messenger.registerOutgoingPluginChannel(this, "neoforge:recipe_content");
+ }
+
private void registerListeners() {
- // 注册潜影盒右键打开监听器
- new ShulkerBoxListener(this);
- getLogger().info("成功注册监听器!");
+ if (registerListener("cn.infstar.essentialsC.listeners.ShulkerBoxListener")) {
+ getLogger().info("- 潜影盒模块");
+ }
+
+ if (registerListener("cn.infstar.essentialsC.listeners.JeiRecipeSyncListener")) {
+ getLogger().info("- JEI 配方同步");
+ }
+
+ if (registerListener("cn.infstar.essentialsC.listeners.MobDropListener")) {
+ try {
+ Class.forName("cn.infstar.essentialsC.listeners.MobDropMenuListener");
+ new cn.infstar.essentialsC.listeners.MobDropMenuListener(this);
+ } catch (ClassNotFoundException e) {
+ }
+ getLogger().info("- 生物掉落控制");
+ }
+ }
+
+ private boolean registerListener(String className) {
+ try {
+ Class> listenerClass = Class.forName(className);
+ Object listenerInstance = listenerClass.getConstructor(EssentialsC.class).newInstance(this);
+ getServer().getPluginManager().registerEvents((org.bukkit.event.Listener) listenerInstance, this);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
}
private void registerCommands() {
try {
- // 获取 CommandMap
Field bukkitCommandMap = Bukkit.getServer().getClass().getDeclaredField("commandMap");
bukkitCommandMap.setAccessible(true);
org.bukkit.command.CommandMap commandMap = (org.bukkit.command.CommandMap) bukkitCommandMap.get(Bukkit.getServer());
- // 注册所有命令(使用 CMI 风格:独立命令 + 别名)
- registerCommandWithAliases(commandMap, "workbench", new WorkbenchCommand(), "wb");
- registerCommandWithAliases(commandMap, "anvil", new AnvilCommand());
- registerCommandWithAliases(commandMap, "cartographytable", new CartographyTableCommand(), "ct", "cartography");
- registerCommandWithAliases(commandMap, "grindstone", new GrindstoneCommand(), "gs");
- registerCommandWithAliases(commandMap, "loom", new LoomCommand());
- registerCommandWithAliases(commandMap, "smithingtable", new SmithingTableCommand(), "st", "smithing");
- registerCommandWithAliases(commandMap, "stonecutter", new StonecutterCommand(), "sc");
- registerCommandWithAliases(commandMap, "enderchest", new EnderChestCommand(), "ec");
- registerCommandWithAliases(commandMap, "hat", new HatCommand());
- registerCommandWithAliases(commandMap, "suicide", new SuicideCommand(), "die");
- registerCommandWithAliases(commandMap, "fly", new FlyCommand());
- registerCommandWithAliases(commandMap, "heal", new HealCommand());
- registerCommandWithAliases(commandMap, "vanish", new VanishCommand(), "v");
- registerCommandWithAliases(commandMap, "seen", new SeenCommand(), "info");
- registerCommandWithAliases(commandMap, "feed", new FeedCommand());
- registerCommandWithAliases(commandMap, "repair", new RepairCommand(), "rep");
- registerCommandWithAliases(commandMap, "essentialsc", new HelpCommand(), "essc");
+ int commandCount = 0;
- getLogger().info("成功注册所有命令!");
+ if (classExists("cn.infstar.essentialsC.commands.WorkbenchCommand")) {
+ registerCommandWithAliases(commandMap, "workbench", new WorkbenchCommand(), "wb");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.AnvilCommand")) {
+ registerCommandWithAliases(commandMap, "anvil", new AnvilCommand());
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.CartographyTableCommand")) {
+ registerCommandWithAliases(commandMap, "cartographytable", new CartographyTableCommand(), "ct", "cartography");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.GrindstoneCommand")) {
+ registerCommandWithAliases(commandMap, "grindstone", new GrindstoneCommand(), "gs");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.LoomCommand")) {
+ registerCommandWithAliases(commandMap, "loom", new LoomCommand());
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.SmithingTableCommand")) {
+ registerCommandWithAliases(commandMap, "smithingtable", new SmithingTableCommand(), "st", "smithing");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.StonecutterCommand")) {
+ registerCommandWithAliases(commandMap, "stonecutter", new StonecutterCommand(), "sc");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.EnderChestCommand")) {
+ registerCommandWithAliases(commandMap, "enderchest", new EnderChestCommand(), "ec");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.BlocksMenuCommand")) {
+ registerCommandWithAliases(commandMap, "blocks", new BlocksMenuCommand());
+ commandCount++;
+ }
+
+ if (classExists("cn.infstar.essentialsC.commands.FlyCommand")) {
+ registerCommandWithAliases(commandMap, "fly", new FlyCommand());
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.HealCommand")) {
+ registerCommandWithAliases(commandMap, "heal", new HealCommand());
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.FeedCommand")) {
+ registerCommandWithAliases(commandMap, "feed", new FeedCommand());
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.VanishCommand")) {
+ registerCommandWithAliases(commandMap, "vanish", new VanishCommand(), "v");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.SeenCommand")) {
+ registerCommandWithAliases(commandMap, "seen", new SeenCommand(), "info");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.HatCommand")) {
+ registerCommandWithAliases(commandMap, "hat", new HatCommand());
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.SuicideCommand")) {
+ registerCommandWithAliases(commandMap, "suicide", new SuicideCommand(), "die");
+ commandCount++;
+ }
+ if (classExists("cn.infstar.essentialsC.commands.RepairCommand")) {
+ registerCommandWithAliases(commandMap, "repair", new RepairCommand(), "rep");
+ commandCount++;
+ }
+
+ if (classExists("cn.infstar.essentialsC.commands.MobDropCommand")) {
+ registerCommandWithAliases(commandMap, "mobdrops", new MobDropCommand());
+ commandCount++;
+ }
+
+ registerCommandWithAliases(commandMap, "essentialsc", new HelpCommand(), "essc");
+ commandCount++;
} catch (Exception e) {
getLogger().severe("无法注册命令: " + e.getMessage());
e.printStackTrace();
}
}
+ private boolean classExists(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
/**
* 注册命令并支持别名
* @param commandMap Bukkit CommandMap
diff --git a/src/main/java/cn/infstar/essentialsC/commands/BaseCommand.java b/src/main/java/cn/infstar/essentialsC/commands/BaseCommand.java
index 51048e6..a8e8790 100644
--- a/src/main/java/cn/infstar/essentialsC/commands/BaseCommand.java
+++ b/src/main/java/cn/infstar/essentialsC/commands/BaseCommand.java
@@ -33,20 +33,23 @@ public abstract class BaseCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
- if (!(sender instanceof Player player)) {
- sender.sendMessage(getLang().getString("messages.player-only"));
- return true;
+ if (sender instanceof Player player) {
+ if (!player.hasPermission(permission)) {
+ String message = getLang().getString("messages.no-permission",
+ java.util.Map.of("permission", permission));
+ player.sendMessage(message);
+ return true;
+ }
+ return execute(player, args);
+ } else {
+ return executeConsole(sender, args);
}
-
- if (!player.hasPermission(permission)) {
- String message = getLang().getString("messages.no-permission",
- java.util.Map.of("permission", permission));
- player.sendMessage(message);
- return true;
- }
-
- return execute(player, args);
}
protected abstract boolean execute(Player player, String[] args);
+
+ protected boolean executeConsole(CommandSender sender, String[] args) {
+ sender.sendMessage(getLang().getString("messages.player-only"));
+ return true;
+ }
}
diff --git a/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java b/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java
index 009b517..1b606fc 100644
--- a/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java
+++ b/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java
@@ -36,6 +36,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
COMMAND_CACHE.put("feed", new FeedCommand());
COMMAND_CACHE.put("repair", new RepairCommand());
COMMAND_CACHE.put("blocks", new BlocksMenuCommand());
+ COMMAND_CACHE.put("mobdrops", new MobDropCommand());
}
public HelpCommand() {
@@ -44,18 +45,37 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
@Override
protected boolean execute(@NotNull Player player, String[] args) {
+ return handleCommand(player, player, args);
+ }
+
+ @Override
+ protected boolean executeConsole(org.bukkit.command.CommandSender sender, String[] args) {
+ if (args.length > 0 && args[0].equalsIgnoreCase("reload")) {
+ if (!sender.hasPermission("essentialsc.command.reload")) {
+ sender.sendMessage(getLang().getString("messages.no-permission"));
+ return true;
+ }
+ plugin.reloadConfig();
+ EssentialsC.getLangManager().reload();
+ sender.sendMessage(getLang().getString("prefix") + "§a配置已重载!");
+ return true;
+ }
+ sender.sendMessage(getLang().getString("messages.player-only"));
+ return true;
+ }
+
+ private boolean handleCommand(CommandSender sender, Player player, String[] args) {
if (args.length > 0) {
String subCommand = args[0].toLowerCase();
- // 管理相关
if (subCommand.equals("reload")) {
- if (!player.hasPermission("essentialsc.command.reload")) {
- player.sendMessage(getLang().getString("messages.no-permission"));
+ if (!sender.hasPermission("essentialsc.command.reload")) {
+ sender.sendMessage(getLang().getString("messages.no-permission"));
return true;
}
plugin.reloadConfig();
EssentialsC.getLangManager().reload();
- player.sendMessage("§a配置已重载!");
+ sender.sendMessage(getLang().getString("prefix") + "§a配置已重载!");
return true;
}
// 功能方块和其他命令 - 使用别名映射
@@ -70,7 +90,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
// seen 需要特殊处理参数
if (actualCommand.equals("seen")) {
if (args.length < 2) {
- player.sendMessage("§c用法: /essc seen <玩家名>");
+ player.sendMessage(getLang().getString("prefix") + getLang().getString("messages.seen-usage-console"));
return true;
}
COMMAND_CACHE.get("seen").execute(player, new String[]{args[1]});
@@ -79,13 +99,13 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
}
return true;
} else if (subCommand.equals("version") || subCommand.equals("v")) {
- player.sendMessage("§6EssentialsC §fv" + plugin.getDescription().getVersion());
- player.sendMessage("§7运行在 Paper " + Bukkit.getVersion());
+ player.sendMessage(getLang().getString("prefix") + "§6EssentialsC §fv" + plugin.getDescription().getVersion());
+ player.sendMessage(getLang().getString("prefix") + "§7运行在 Paper " + Bukkit.getVersion());
return true;
} else {
- // 未知子命令
- player.sendMessage("§c未知子命令: " + subCommand);
- player.sendMessage("§7使用 §f/essc help §7查看所有可用命令");
+ player.sendMessage(getLang().getString("prefix") + getLang().getString("messages.unknown-subcommand",
+ java.util.Map.of("command", subCommand)));
+ player.sendMessage(getLang().getString("prefix") + getLang().getString("messages.help-usage"));
return true;
}
}
@@ -244,6 +264,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
{"feed", "essentialsc.command.feed"},
{"repair", "essentialsc.command.repair"},
{"rep", "essentialsc.command.repair"},
+ {"mobdrops", "essentialsc.mobdrops.enderman"},
{"version", null},
{"help", null}
};
diff --git a/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java b/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java
new file mode 100644
index 0000000..53bdd12
--- /dev/null
+++ b/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java
@@ -0,0 +1,67 @@
+package cn.infstar.essentialsC.commands;
+
+import cn.infstar.essentialsC.EssentialsC;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.Arrays;
+
+/**
+ * 生物掉落物控制命令
+ * /mobdrops - 打开控制菜单
+ */
+public class MobDropCommand extends BaseCommand {
+
+ public MobDropCommand() {
+ super("essentialsc.mobdrops.enderman");
+ }
+
+ @Override
+ protected boolean execute(Player player, String[] args) {
+ openMobDropMenu(player);
+ return true;
+ }
+
+ /**
+ * 打开生物掉落控制菜单
+ */
+ private void openMobDropMenu(Player player) {
+ // 读取当前配置
+ boolean endermanEnabled = plugin.getConfig().getBoolean("mob-drops.enderman.enabled", true);
+
+ // 创建菜单
+ Inventory menu = Bukkit.createInventory(null, 27, "§6§l生物掉落控制");
+
+ // 末影人控制项
+ ItemStack endermanItem = new ItemStack(Material.ENDER_PEARL);
+ ItemMeta endermanMeta = endermanItem.getItemMeta();
+ endermanMeta.setDisplayName("§d末影人掉落");
+ endermanMeta.setLore(Arrays.asList(
+ "§7当前状态: " + (endermanEnabled ? "§a✅ 开启" : "§c❌ 关闭"),
+ "",
+ "§e点击切换状态"
+ ));
+ endermanItem.setItemMeta(endermanMeta);
+
+ // 放置在中间
+ menu.setItem(13, endermanItem);
+
+ // 装饰物品
+ ItemStack glass = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
+ ItemMeta glassMeta = glass.getItemMeta();
+ glassMeta.setDisplayName(" ");
+ glass.setItemMeta(glassMeta);
+
+ for (int i = 0; i < 27; i++) {
+ if (menu.getItem(i) == null) {
+ menu.setItem(i, glass);
+ }
+ }
+
+ player.openInventory(menu);
+ }
+}
diff --git a/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java b/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java
new file mode 100644
index 0000000..afe9187
--- /dev/null
+++ b/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java
@@ -0,0 +1,391 @@
+package cn.infstar.essentialsC.listeners;
+
+import cn.infstar.essentialsC.EssentialsC;
+import io.netty.buffer.Unpooled;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
+import net.minecraft.network.protocol.common.custom.DiscardedPayload;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.item.crafting.RecipeHolder;
+import net.minecraft.world.item.crafting.RecipeMap;
+import net.minecraft.world.item.crafting.RecipeSerializer;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * JEI 配方同步监听器
+ * 解决 Minecraft 1.21.2+ 配方不同步问题
+ * 支持 Fabric 和 NeoForge 客户端
+ */
+public class JeiRecipeSyncListener implements Listener {
+
+ private final EssentialsC plugin;
+ private final boolean enabled;
+ private final boolean debug;
+ private final boolean sendPlayerMessage;
+
+ public JeiRecipeSyncListener(EssentialsC plugin) {
+ this.plugin = plugin;
+
+ FileConfiguration config = plugin.getConfig();
+ this.enabled = config.getBoolean("jei-sync.enabled", true);
+ this.debug = config.getBoolean("jei-sync.debug", false);
+ this.sendPlayerMessage = config.getBoolean("jei-sync.send-player-message", true);
+ }
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ if (!enabled) {
+ return;
+ }
+
+ Player player = event.getPlayer();
+ String clientBrand = player.getClientBrandName();
+
+ if (debug) {
+ plugin.getLogger().info("========================================");
+ plugin.getLogger().info("玩家 " + player.getName() + " 加入");
+ plugin.getLogger().info("客户端品牌: '" + (clientBrand != null ? clientBrand : "null") + "'");
+ plugin.getLogger().info("JEI 同步功能: " + (enabled ? "启用" : "禁用"));
+ plugin.getLogger().info("========================================");
+ }
+
+ if (clientBrand == null || clientBrand.isEmpty()) {
+ if (debug) {
+ plugin.getLogger().info("跳过 " + player.getName() + ":客户端品牌为空");
+ }
+ return;
+ }
+
+ // 统一转换为小写进行比较,支持更多变体
+ String brandLower = clientBrand.toLowerCase();
+
+ if (brandLower.contains("fabric")) {
+ if (debug) {
+ plugin.getLogger().info("检测到 Fabric 客户端,开始发送配方同步...");
+ }
+ sendPlayerMessage(player, "Fabric");
+ sendFabricRecipeSync(player);
+ } else if (brandLower.contains("neoforge") || brandLower.contains("forge")) {
+ if (debug) {
+ plugin.getLogger().info("检测到 NeoForge/Forge 客户端,开始发送配方同步...");
+ }
+ sendPlayerMessage(player, "NeoForge");
+ sendNeoForgeRecipeSync(player);
+ } else {
+ if (debug) {
+ plugin.getLogger().info("跳过 " + player.getName() + ":不支持的客户端类型 '" + clientBrand + "'");
+ }
+ }
+ }
+
+ /**
+ * 发送提示消息给玩家
+ */
+ private void sendPlayerMessage(Player player, String clientType) {
+ if (!sendPlayerMessage) {
+ return;
+ }
+
+ String messageKey;
+ if (clientType.equalsIgnoreCase("fabric")) {
+ messageKey = "messages.jei-sync-fabric";
+ } else if (clientType.equalsIgnoreCase("neoforge")) {
+ messageKey = "messages.jei-sync-neoforge";
+ } else {
+ return;
+ }
+
+ // 使用统一前缀 + 消息内容
+ String prefix = EssentialsC.getLangManager().getString("prefix");
+ String message = EssentialsC.getLangManager().getString(messageKey);
+ String fullMessage = prefix + " " + message;
+
+ net.kyori.adventure.text.Component component = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacyAmpersand().deserialize(fullMessage);
+ player.sendMessage(component);
+ }
+
+ /**
+ * 发送 Fabric 格式的配方同步数据包
+ */
+ @SuppressWarnings({"unchecked", "deprecation"})
+ private void sendFabricRecipeSync(Player player) {
+ try {
+ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
+ MinecraftServer server = serverPlayer.level().getServer();
+ if (server == null) {
+ if (debug) plugin.getLogger().warning("服务器实例为 null");
+ return;
+ }
+
+ RecipeMap recipeMap = server.getRecipeManager().recipes;
+
+ if (debug) {
+ plugin.getLogger().info("开始构建 Fabric 配方数据");
+ }
+
+ // 创建 Fabric Payload(与参考插件完全一致)
+ var list = new ArrayList();
+ var seen = new HashSet>();
+
+ for (RecipeSerializer> serializer : BuiltInRegistries.RECIPE_SERIALIZER) {
+ if (!seen.add(serializer)) continue;
+
+ List> recipes = new ArrayList<>();
+ for (RecipeHolder> holder : recipeMap.values()) {
+ if (holder.value().getSerializer() == serializer) {
+ recipes.add(holder);
+ }
+ }
+
+ if (!recipes.isEmpty()) {
+ RecipeSerializer> entrySerializer = recipes.get(0).value().getSerializer();
+ list.add(new FabricRecipeEntry(entrySerializer, recipes));
+ }
+ }
+
+ var payload = new FabricRecipeSyncPayload(list);
+
+ if (debug) {
+ plugin.getLogger().info("Fabric 配方条目数: " + list.size());
+ }
+
+ // 构建 buffer
+ RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(
+ Unpooled.buffer(),
+ server.registryAccess()
+ );
+
+ // 使用 CODEC 编码(与参考插件完全一致)
+ var codec = getFabricCodec();
+ codec.encode(buffer, payload);
+
+ // 发送数据包
+ byte[] bytes = new byte[buffer.writerIndex()];
+ buffer.getBytes(0, bytes);
+
+ Identifier id = Identifier.fromNamespaceAndPath("fabric", "recipe_sync");
+ DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes);
+ serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload));
+
+ if (debug) {
+ plugin.getLogger().info("已发送 Fabric 配方同步 [" + id + "], 大小: " + bytes.length + " bytes");
+ }
+
+ } catch (Exception e) {
+ plugin.getLogger().warning("发送 Fabric 配方同步失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 获取 Fabric Codec
+ */
+ @SuppressWarnings({"unchecked", "deprecation"})
+ private net.minecraft.network.codec.StreamCodec getFabricCodec() {
+ return FabricRecipeEntry.CODEC.apply(net.minecraft.network.codec.ByteBufCodecs.list())
+ .map(FabricRecipeSyncPayload::new, FabricRecipeSyncPayload::entries);
+ }
+
+ /**
+ * 发送 NeoForge 格式的配方同步数据包
+ */
+ @SuppressWarnings({"unchecked", "deprecation"})
+ private void sendNeoForgeRecipeSync(Player player) {
+ try {
+ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
+ MinecraftServer server = serverPlayer.level().getServer();
+ if (server == null) {
+ if (debug) plugin.getLogger().warning("服务器实例为 null");
+ return;
+ }
+
+ RecipeMap recipeMap = server.getRecipeManager().recipes;
+
+ if (debug) {
+ plugin.getLogger().info("开始构建 NeoForge 配方数据");
+ }
+
+ // 获取所有配方类型
+ java.util.List> allRecipeTypes =
+ BuiltInRegistries.RECIPE_TYPE.stream().toList();
+
+ if (debug) {
+ plugin.getLogger().info("NeoForge 配方类型数: " + allRecipeTypes.size());
+ }
+
+ // 创建 NeoForge Payload(与参考插件完全一致)
+ var payload = createNeoForgePayload(allRecipeTypes, recipeMap);
+
+ // 构建 buffer
+ RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(
+ Unpooled.buffer(),
+ server.registryAccess()
+ );
+
+ // 使用 STREAM_CODEC 编码(与参考插件完全一致)
+ var streamCodec = getNeoForgeStreamCodec();
+ streamCodec.encode(buffer, payload);
+
+ // 发送数据包
+ byte[] bytes = new byte[buffer.writerIndex()];
+ buffer.getBytes(0, bytes);
+
+ Identifier id = Identifier.fromNamespaceAndPath("neoforge", "recipe_content");
+ DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes);
+ serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload));
+
+ // 发送 Tags 同步(NeoForge 需要)
+ serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket(
+ net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries())
+ ));
+
+ if (debug) {
+ plugin.getLogger().info("已发送 NeoForge 配方同步 [" + id + "], 大小: " + bytes.length + " bytes");
+ }
+
+ } catch (Exception e) {
+ plugin.getLogger().warning("发送 NeoForge 配方同步失败: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 创建 NeoForge 配载对象
+ */
+ private NeoForgeRecipeSyncPayload createNeoForgePayload(
+ java.util.List> recipeTypes,
+ RecipeMap recipeMap) {
+ var recipeTypeSet = new java.util.HashSet<>(recipeTypes);
+
+ if (recipeTypeSet.isEmpty()) {
+ return new NeoForgeRecipeSyncPayload(recipeTypeSet, java.util.List.of());
+ } else {
+ var recipeSubset = recipeMap.values().stream()
+ .filter(h -> recipeTypeSet.contains(h.value().getType()))
+ .toList();
+ return new NeoForgeRecipeSyncPayload(recipeTypeSet, recipeSubset);
+ }
+ }
+
+ /**
+ * 获取 NeoForge StreamCodec
+ */
+ @SuppressWarnings({"unchecked", "deprecation"})
+ private net.minecraft.network.codec.StreamCodec getNeoForgeStreamCodec() {
+ return net.minecraft.network.codec.StreamCodec.composite(
+ net.minecraft.network.codec.ByteBufCodecs.registry(net.minecraft.core.registries.Registries.RECIPE_TYPE)
+ .apply(net.minecraft.network.codec.ByteBufCodecs.collection(java.util.HashSet::new)),
+ NeoForgeRecipeSyncPayload::recipeTypes,
+ RecipeHolder.STREAM_CODEC.apply(net.minecraft.network.codec.ByteBufCodecs.list()),
+ NeoForgeRecipeSyncPayload::recipes,
+ NeoForgeRecipeSyncPayload::new
+ );
+ }
+
+ /**
+ * Fabric 配方条目
+ */
+ @SuppressWarnings("deprecation")
+ private static class FabricRecipeEntry {
+ final Object serializer; // 使用 Object 避免 NMS 类型不兼容
+ final List> recipes;
+
+ FabricRecipeEntry(Object serializer, List> recipes) {
+ this.serializer = serializer;
+ this.recipes = recipes;
+ }
+
+ static final net.minecraft.network.codec.StreamCodec CODEC =
+ net.minecraft.network.codec.StreamCodec.ofMember(
+ FabricRecipeEntry::write,
+ FabricRecipeEntry::read
+ );
+
+ @SuppressWarnings("unchecked")
+ private static FabricRecipeEntry read(RegistryFriendlyByteBuf buf) {
+ Identifier recipeSerializerId = buf.readIdentifier();
+ RecipeSerializer> recipeSerializer = BuiltInRegistries.RECIPE_SERIALIZER.getValue(recipeSerializerId);
+
+ if (recipeSerializer == null) {
+ throw new RuntimeException("Tried syncing unsupported packet serializer '" + recipeSerializerId + "'!");
+ }
+
+ int count = buf.readVarInt();
+ var list = new ArrayList>();
+
+ for (int i = 0; i < count; i++) {
+ net.minecraft.resources.ResourceKey> id =
+ buf.readResourceKey(net.minecraft.core.registries.Registries.RECIPE);
+
+ // 使用反射获取 streamCodec,避免 NMS 类型不兼容
+ try {
+ var streamCodecMethod = recipeSerializer.getClass().getMethod("streamCodec");
+ var streamCodec = streamCodecMethod.invoke(recipeSerializer);
+ net.minecraft.world.item.crafting.Recipe> recipe =
+ ((net.minecraft.network.codec.StreamCodec>) streamCodec)
+ .decode(buf);
+ list.add(new RecipeHolder<>(id, recipe));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to decode recipe: " + e.getMessage(), e);
+ }
+ }
+
+ return new FabricRecipeEntry(recipeSerializer, list);
+ }
+
+ private void write(RegistryFriendlyByteBuf buf) {
+ // 使用反射获取 key,避免 NMS 类型不兼容
+ try {
+ var getKeyMethod = BuiltInRegistries.RECIPE_SERIALIZER.getClass().getMethod("getKey", Object.class);
+ Identifier identifier = (Identifier) getKeyMethod.invoke(BuiltInRegistries.RECIPE_SERIALIZER, this.serializer);
+ buf.writeIdentifier(identifier);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to get serializer key: " + e.getMessage(), e);
+ }
+
+ buf.writeVarInt(this.recipes.size());
+
+ // 使用反射获取 streamCodec,避免 NMS 类型不兼容
+ try {
+ var streamCodecMethod = this.serializer.getClass().getMethod("streamCodec");
+ @SuppressWarnings("unchecked")
+ var codec = (net.minecraft.network.codec.StreamCodec>)
+ streamCodecMethod.invoke(this.serializer);
+
+ for (RecipeHolder> recipe : this.recipes) {
+ buf.writeResourceKey(recipe.id());
+ codec.encode(buf, recipe.value());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to encode recipe: " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Fabric 配方同步 Payload
+ */
+ private record FabricRecipeSyncPayload(List entries) {
+ }
+
+ /**
+ * NeoForge 配方同步 Payload
+ */
+ private record NeoForgeRecipeSyncPayload(
+ java.util.Set> recipeTypes,
+ java.util.List> recipes) {
+ }
+}
diff --git a/src/main/java/cn/infstar/essentialsC/listeners/MobDropListener.java b/src/main/java/cn/infstar/essentialsC/listeners/MobDropListener.java
new file mode 100644
index 0000000..b33ac5b
--- /dev/null
+++ b/src/main/java/cn/infstar/essentialsC/listeners/MobDropListener.java
@@ -0,0 +1,64 @@
+package cn.infstar.essentialsC.listeners;
+
+import cn.infstar.essentialsC.EssentialsC;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.EntityType;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDeathEvent;
+
+/**
+ * 生物掉落物控制监听器
+ * 当前仅支持末影人
+ */
+public class MobDropListener implements Listener {
+
+ private final EssentialsC plugin;
+ private boolean endermanDropEnabled;
+
+ public MobDropListener(EssentialsC plugin) {
+ this.plugin = plugin;
+ loadConfig();
+
+ // 注册监听器
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ /**
+ * 加载配置
+ */
+ private void loadConfig() {
+ FileConfiguration config = plugin.getConfig();
+ config.addDefault("mob-drops.enderman.enabled", true);
+ config.options().copyDefaults(true);
+
+ try {
+ config.save(plugin.getDataFolder().toPath().resolve("config.yml").toFile());
+ } catch (Exception e) {
+ plugin.getLogger().warning("无法保存配置文件: " + e.getMessage());
+ }
+
+ this.endermanDropEnabled = config.getBoolean("mob-drops.enderman.enabled", true);
+ }
+
+ @EventHandler
+ public void onEntityDeath(EntityDeathEvent event) {
+ if (event.getEntityType() != EntityType.ENDERMAN) {
+ return;
+ }
+
+ boolean enabled = plugin.getConfig().getBoolean("mob-drops.enderman.enabled", true);
+
+ if (!enabled) {
+ event.getDrops().clear();
+ }
+ }
+
+ /**
+ * 重新加载配置
+ */
+ public void reload() {
+ loadConfig();
+ plugin.getLogger().info("生物掉落物配置已重载(末影人: " + (endermanDropEnabled ? "开启" : "关闭") + ")");
+ }
+}
diff --git a/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java b/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java
new file mode 100644
index 0000000..28f0f03
--- /dev/null
+++ b/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java
@@ -0,0 +1,101 @@
+package cn.infstar.essentialsC.listeners;
+
+import cn.infstar.essentialsC.EssentialsC;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.Arrays;
+
+public class MobDropMenuListener implements Listener {
+
+ private final EssentialsC plugin;
+
+ public MobDropMenuListener(EssentialsC plugin) {
+ this.plugin = plugin;
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @EventHandler
+ public void onInventoryClick(InventoryClickEvent event) {
+ if (!event.getView().getTitle().equals("§6§l生物掉落控制")) {
+ return;
+ }
+
+ event.setCancelled(true);
+
+ if (!(event.getWhoClicked() instanceof Player player)) {
+ return;
+ }
+
+ ItemStack clickedItem = event.getCurrentItem();
+ if (clickedItem == null) {
+ return;
+ }
+
+ if (event.getSlot() == 13) {
+ toggleEndermanDrops(player);
+ Bukkit.getScheduler().runTaskLater(plugin, () -> openMobDropMenu(player), 2L);
+ }
+ }
+
+ private void toggleEndermanDrops(Player player) {
+ FileConfiguration config = plugin.getConfig();
+ boolean currentValue = config.getBoolean("mob-drops.enderman.enabled", true);
+ boolean newValue = !currentValue;
+
+ config.set("mob-drops.enderman.enabled", newValue);
+
+ try {
+ config.save(plugin.getDataFolder().toPath().resolve("config.yml").toFile());
+ } catch (Exception e) {
+ player.sendMessage(EssentialsC.getLangManager().getString("prefix") +
+ EssentialsC.getLangManager().getString("messages.mobdrop-save-failed",
+ java.util.Map.of("error", e.getMessage())));
+ return;
+ }
+
+ String status = newValue ? "§a开启" : "§c关闭";
+ player.sendMessage(EssentialsC.getLangManager().getString("prefix") +
+ EssentialsC.getLangManager().getString("messages.mobdrop-toggled",
+ java.util.Map.of("status", status)));
+ }
+
+ private void openMobDropMenu(Player player) {
+ boolean endermanEnabled = plugin.getConfig().getBoolean("mob-drops.enderman.enabled", true);
+
+ Inventory menu = Bukkit.createInventory(null, 27, "§6§l生物掉落控制");
+
+ ItemStack endermanItem = new ItemStack(Material.ENDER_PEARL);
+ ItemMeta endermanMeta = endermanItem.getItemMeta();
+ endermanMeta.setDisplayName("§d末影人掉落");
+ endermanMeta.setLore(Arrays.asList(
+ "§7当前状态: " + (endermanEnabled ? "§a✅ 开启" : "§c❌ 关闭"),
+ "",
+ "§e点击切换状态"
+ ));
+ endermanItem.setItemMeta(endermanMeta);
+
+ menu.setItem(13, endermanItem);
+
+ ItemStack glass = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
+ ItemMeta glassMeta = glass.getItemMeta();
+ glassMeta.setDisplayName(" ");
+ glass.setItemMeta(glassMeta);
+
+ for (int i = 0; i < 27; i++) {
+ if (menu.getItem(i) == null) {
+ menu.setItem(i, glass);
+ }
+ }
+
+ player.openInventory(menu);
+ }
+}
diff --git a/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java b/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java
index 23c2c13..afc9e55 100644
--- a/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java
+++ b/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java
@@ -14,6 +14,7 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
+import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
@@ -47,17 +48,57 @@ public class ShulkerBoxListener implements Listener {
Material.BLACK_SHULKER_BOX
);
+ /**
+ * 潜影盒 Inventory Holder - 用于识别自定义 inventory
+ */
+ private static class ShulkerBoxHolder implements org.bukkit.inventory.InventoryHolder {
+ private final Inventory inventory;
+ private final ItemStack shulkerBoxItem;
+ private final ItemStack[] currentContents; // 实时追踪的物品内容
+
+ ShulkerBoxHolder(ItemStack shulkerBoxItem, String title) {
+ this.shulkerBoxItem = shulkerBoxItem;
+ this.inventory = Bukkit.createInventory(this, 27, title);
+ this.currentContents = new ItemStack[27];
+ }
+
+ @Override
+ public @NotNull Inventory getInventory() {
+ return this.inventory;
+ }
+
+ public ItemStack getShulkerBoxItem() {
+ return shulkerBoxItem;
+ }
+
+ /**
+ * 更新指定槽位的物品(供外部调用)
+ */
+ public void updateSlot(int slot, ItemStack item) {
+ if (slot >= 0 && slot < 27) {
+ currentContents[slot] = item;
+ }
+ }
+
+ /**
+ * 获取当前追踪的所有物品内容
+ */
+ public ItemStack[] getCurrentContents() {
+ return currentContents.clone();
+ }
+ }
+
/**
* 潜影盒数据记录
*/
private static class ShulkerBoxData {
+ int slotIndex; // 玩家背包中的槽位索引
ItemStack originalSnapshot; // 打开时的物品快照(用于验证)
- ItemStack currentItem; // 当前物品引用(用于更新)
int totalItems; // 打开时的物品总数(用于防刷)
- ShulkerBoxData(ItemStack snapshot, ItemStack current, int items) {
+ ShulkerBoxData(int slot, ItemStack snapshot, int items) {
+ this.slotIndex = slot;
this.originalSnapshot = snapshot;
- this.currentItem = current;
this.totalItems = items;
}
}
@@ -94,8 +135,18 @@ public class ShulkerBoxListener implements Listener {
// 取消默认行为(防止放置潜影盒)
event.setCancelled(true);
- // 打开潜影盒
- openShulkerBox(player, item);
+ // 查找物品在玩家背包中的槽位
+ int slotIndex = -1;
+ for (int i = 0; i < player.getInventory().getSize(); i++) {
+ ItemStack invItem = player.getInventory().getItem(i);
+ if (invItem != null && invItem.isSimilar(item)) {
+ slotIndex = i;
+ break;
+ }
+ }
+
+ // 打开潜影盒(传入槽位索引)
+ openShulkerBox(player, item, slotIndex);
}
@EventHandler
@@ -104,6 +155,13 @@ public class ShulkerBoxListener implements Listener {
return;
}
+ Inventory closedInventory = event.getInventory();
+
+ // 检查是否是潜影盒 inventory
+ if (!(closedInventory.getHolder(false) instanceof ShulkerBoxHolder holder)) {
+ return;
+ }
+
UUID playerId = player.getUniqueId();
ShulkerBoxData data = openShulkerBoxes.remove(playerId);
@@ -111,58 +169,121 @@ public class ShulkerBoxListener implements Listener {
return;
}
- Inventory closedInventory = event.getInventory();
- ItemStack currentItem = data.currentItem;
-
- // 验证物品是否还存在
- if (currentItem == null || currentItem.getType().isAir()) {
- // 物品已不存在,丢弃 inventory 中的所有物品
- for (ItemStack item : closedInventory.getContents()) {
- if (item != null && !item.getType().isAir()) {
- player.getWorld().dropItemNaturally(player.getLocation(), item);
- }
- }
- return;
- }
-
- // 更新潜影盒物品中的内容
- if (currentItem.getItemMeta() instanceof BlockStateMeta blockStateMeta) {
- if (blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox) {
- // 将 inventory 的内容复制回潜影盒
- ItemStack[] contents = closedInventory.getContents();
- for (int i = 0; i < 27 && i < contents.length; i++) {
- shulkerBox.getInventory().setItem(i, contents[i]);
- }
-
- // 更新物品元数据
- blockStateMeta.setBlockState(shulkerBox);
- currentItem.setItemMeta(blockStateMeta);
- }
- }
+ plugin.getLogger().info("=== 潜影盒关闭(数据已在点击时实时保存) ===");
}
@EventHandler
- public void onInventoryClick(InventoryClickEvent event) {
+ public void onInventoryOpen(org.bukkit.event.inventory.InventoryOpenEvent event) {
+ if (!(event.getPlayer() instanceof Player player)) {
+ return;
+ }
+
+ Inventory openedInventory = event.getInventory();
+ if (openedInventory.getHolder(false) instanceof ShulkerBoxHolder) {
+ plugin.getLogger().info("[Open] ✅ 潜影盒 inventory 已打开");
+ }
+ }
+
+ @EventHandler(priority = org.bukkit.event.EventPriority.LOWEST)
+ public void onInventoryClickDebug(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
- // 检查是否是玩家打开的潜影盒(使用 get 避免两次查找)
- if (openShulkerBoxes.get(player.getUniqueId()) == null) {
+ Inventory clickedInventory = event.getClickedInventory();
+ if (clickedInventory == null) {
return;
}
- // 获取点击的物品
+ if (clickedInventory.getHolder(false) instanceof ShulkerBoxHolder) {
+ plugin.getLogger().info("[Click-LOWEST] 检测到点击事件 | 槽位: " + event.getSlot() +
+ " | 物品: " + (event.getCurrentItem() != null ? event.getCurrentItem().getType() : "null"));
+ }
+ }
+ if (!(event.getWhoClicked() instanceof Player player)) {
+ return;
+ }
+
+ Inventory clickedInventory = event.getClickedInventory();
+ if (clickedInventory == null) {
+ return;
+ }
+
+ // 检查是否是潜影盒 inventory
+ if (!(clickedInventory.getHolder(false) instanceof ShulkerBoxHolder holder)) {
+ return;
+ }
+
+ UUID playerId = player.getUniqueId();
+ ShulkerBoxData data = openShulkerBoxes.get(playerId);
+ if (data == null) {
+ return;
+ }
+
+ // 阻止嵌套潜影盒
ItemStack clickedItem = event.getCurrentItem();
- if (clickedItem == null) {
+ if (clickedItem != null && isShulkerBox(clickedItem)) {
+ event.setCancelled(true);
+ player.sendMessage(EssentialsC.getLangManager().getString("prefix") +
+ EssentialsC.getLangManager().getString("messages.shulkerbox-nested"));
return;
}
- // 检查是否是潜影盒,如果是则阻止放置
- if (isShulkerBox(clickedItem)) {
- event.setCancelled(true);
- player.sendMessage("§c不能在潜影盒中放入另一个潜影盒!");
- }
+ // ✅ CMILib 方式:延迟 1 tick 后立即保存到潜影盒 NBT
+ plugin.getServer().getScheduler().runTask(plugin, () -> {
+ plugin.getLogger().info("[Click] === 开始处理点击事件 ===");
+
+ // 读取当前 inventory 内容
+ ItemStack[] contents = clickedInventory.getContents();
+
+ // 调试
+ int nonEmpty = 0;
+ StringBuilder items = new StringBuilder();
+ for (int i = 0; i < contents.length; i++) {
+ if (contents[i] != null && !contents[i].getType().isAir()) {
+ nonEmpty++;
+ items.append(String.format("\n [%d] %s x%d", i, contents[i].getType(), contents[i].getAmount()));
+ }
+ }
+ plugin.getLogger().info("[Click] 非空槽位: " + nonEmpty);
+ plugin.getLogger().info("[Click] 物品详情:" + items);
+
+ // 获取玩家背包中的潜影盒物品
+ ItemStack shulkerItem = null;
+ if (data.slotIndex >= 0 && data.slotIndex < player.getInventory().getSize()) {
+ shulkerItem = player.getInventory().getItem(data.slotIndex);
+ plugin.getLogger().info("[Click] 槽位 " + data.slotIndex + " 物品: " +
+ (shulkerItem != null ? shulkerItem.getType() : "null"));
+ }
+
+ if (shulkerItem == null || shulkerItem.getType().isAir()) {
+ plugin.getLogger().warning("[Click] ❌ 潜影盒物品不存在");
+ return;
+ }
+
+ // 更新潜影盒的 BlockState
+ if (shulkerItem.getItemMeta() instanceof BlockStateMeta blockStateMeta) {
+ if (blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox) {
+ // 设置 inventory 内容
+ for (int i = 0; i < 27 && i < contents.length; i++) {
+ shulkerBox.getInventory().setItem(i, contents[i]);
+ }
+
+ // 更新元数据
+ blockStateMeta.setBlockState(shulkerBox);
+ shulkerItem.setItemMeta(blockStateMeta);
+
+ // 写回玩家背包
+ player.getInventory().setItem(data.slotIndex, shulkerItem);
+
+ plugin.getLogger().info("[Click] ✅ 已实时保存 " + nonEmpty + " 个物品槽");
+ } else {
+ plugin.getLogger().warning("[Click] ❌ BlockState 不是 ShulkerBox");
+ }
+ } else {
+ plugin.getLogger().warning("[Click] ❌ 没有 BlockStateMeta");
+ }
+ });
}
@@ -177,7 +298,7 @@ public class ShulkerBoxListener implements Listener {
/**
* 打开潜影盒
*/
- private void openShulkerBox(Player player, ItemStack shulkerBox) {
+ private void openShulkerBox(Player player, ItemStack shulkerBox, int slotIndex) {
// 获取潜影盒的 BlockStateMeta
if (!(shulkerBox.getItemMeta() instanceof BlockStateMeta blockStateMeta)) {
return;
@@ -216,17 +337,18 @@ public class ShulkerBoxListener implements Listener {
}
}
- // 创建一个新的 inventory(基于潜影盒的内容)
- Inventory inventory = Bukkit.createInventory(null, 27, title);
+ // 创建 ShulkerBoxHolder(会自动创建 inventory)
+ ShulkerBoxHolder holder = new ShulkerBoxHolder(shulkerBox, title);
+ Inventory inventory = holder.getInventory();
- // 复制潜影盒的内容到新 inventory
+ // 复制潜影盒的内容到 inventory
ItemStack[] contents = shulkerBoxBlock.getInventory().getContents();
for (int i = 0; i < 27 && i < contents.length; i++) {
inventory.setItem(i, contents[i]);
}
- // 记录玩家打开的潜影盒(包含快照和当前引用)
- openShulkerBoxes.put(player.getUniqueId(), new ShulkerBoxData(snapshot, shulkerBox, totalItems));
+ // 记录玩家打开的潜影盒(保存槽位索引和快照)
+ openShulkerBoxes.put(player.getUniqueId(), new ShulkerBoxData(slotIndex, snapshot, totalItems));
// 打开 inventory
player.openInventory(inventory);
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 554ceb5..26f6c22 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -85,3 +85,20 @@ shulkerbox:
# 支持颜色代码(使用 & 符号)
# 留空则使用 "Shulker Box"(客户端语言)
default-title: "&e潜影盒"
+
+# JEI/REI 配方同步设置(MC 1.21.2+)
+# 解决 Fabric/NeoForge 客户端配方不同步问题
+jei-sync:
+ # 是否启用 JEI 配方同步功能
+ enabled: true
+ # 是否在控制台显示同步日志
+ debug: true
+ # 是否向玩家发送同步提示消息
+ send-player-message: true
+
+# 生物掉落物控制
+mob-drops:
+ # 末影人掉落物控制
+ enderman:
+ # 是否允许末影人死亡后掉落物品和经验
+ enabled: true
diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml
index 9d3f444..f110f8a 100644
--- a/src/main/resources/lang/zh_CN.yml
+++ b/src/main/resources/lang/zh_CN.yml
@@ -2,7 +2,7 @@
# 中文语言文件
# 插件前缀
-prefix: "&6[EssentialsC] &r"
+prefix: "&7[&6EssentialsC&7]&f:"
# 命令消息
messages:
@@ -33,6 +33,14 @@ messages:
no-permission-repair-all: "&c你没有权限修复所有物品!"
player-not-found: "&c未找到玩家: {player}"
no-permission-others: "&c你没有权限治疗其他玩家!"
+ seen-usage-console: "&c用法: /seen <玩家名>"
+ unknown-subcommand: "&c未知子命令: {command}"
+ help-usage: "&7使用 §f/essc help &7查看所有可用命令"
+ mobdrop-save-failed: "&c保存配置失败: {error}"
+ mobdrop-toggled: "&a末影人掉落已{status}!"
+ shulkerbox-nested: "&c不能在潜影盒中放入另一个潜影盒!"
+ jei-sync-fabric: "&6JEI-FIX&8(&bFabric&8):&e正在同步合成配方..."
+ jei-sync-neoforge: "&6JEI-FIX&8(&bNeoForge&8):&e正在同步合成配方..."
# 帮助命令
help:
diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml
index 4e29c7a..03a41f2 100644
--- a/src/main/resources/paper-plugin.yml
+++ b/src/main/resources/paper-plugin.yml
@@ -1,10 +1,11 @@
name: EssentialsC
description: 精简版基础插件
-version: '${version}'
+version: ${version}
main: cn.infstar.essentialsC.EssentialsC
api-version: '1.21'
load: POSTWORLD
+folia-supported: true
authors: [ Coldsmiles_7 ]
website: www.infstar.cn
@@ -73,6 +74,9 @@ permissions:
essentialsc.shulkerbox.open:
description: Allows right-click to open shulker boxes without placing them
default: op
+ essentialsc.mobdrops.enderman:
+ description: Allows control of enderman drops
+ default: op
essentialsc.*:
description: All EssentialsC permissions
default: false
@@ -97,3 +101,4 @@ permissions:
essentialsc.command.repair: true
essentialsc.command.help: true
essentialsc.shulkerbox.open: true
+ essentialsc.mobdrops.enderman: true