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