From 19ddd3fa9947b17add26e0872d1d14d9d4707242 Mon Sep 17 00:00:00 2001 From: Coldsmile Date: Sun, 2 Aug 2026 03:36:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E6=80=A7=E5=B9=B6=E9=87=8D=E6=9E=84=E6=BD=9C=E5=BD=B1=E7=9B=92?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 10 +- .github/workflows/release.yml | 16 +- build.gradle | 2 +- .../compat/jei/JeiRecipeSyncAdapter.java | 2 + .../cn/infstar/essentialsC/EssentialsC.java | 9 + .../essentialsC/admin/AdminModeManager.java | 39 +- .../commands/BlocksMenuCommand.java | 22 +- .../essentialsC/commands/CommandRegistry.java | 3 +- .../essentialsC/commands/GlowCommand.java | 7 +- .../essentialsC/commands/HealCommand.java | 5 +- .../essentialsC/commands/HelpCommand.java | 15 +- .../commands/MaintenanceCommand.java | 24 +- .../essentialsC/commands/MobDropCommand.java | 14 +- .../commands/NightVisionCommand.java | 7 +- .../essentialsC/commands/SeenCommand.java | 7 + .../essentialsC/commands/TpIgnoreCommand.java | 11 +- .../essentialsC/commands/VanishCommand.java | 15 +- .../listeners/JeiRecipeSyncListener.java | 8 + .../listeners/MobDropMenuListener.java | 1 + .../listeners/ShulkerBoxListener.java | 554 ++++++++++++++---- .../listeners/ShulkerBoxSessionPolicy.java | 17 + .../essentialsC/listeners/VanishListener.java | 33 ++ .../maintenance/MaintenanceManager.java | 74 ++- .../skinbridge/SkinBridgeManager.java | 177 +++++- .../teleport/TeleportRequestManager.java | 37 +- .../essentialsC/util/AtomicYamlWriter.java | 6 +- src/main/resources/config.yml | 2 + src/main/resources/lang/en_US.yml | 5 + src/main/resources/lang/zh_CN.yml | 5 + .../ConfigurationResourcesTest.java | 21 + .../ShulkerBoxSessionPolicyTest.java | 25 + .../util/AtomicYamlWriterTest.java | 61 ++ .../jei/JeiRecipeSyncAdapterImpl.java | 54 +- .../v26_1_2/jei/JeiRecipeSyncAdapterImpl.java | 54 +- .../v26_2/jei/JeiRecipeSyncAdapterImpl.java | 54 +- 35 files changed, 1167 insertions(+), 229 deletions(-) create mode 100644 src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicy.java create mode 100644 src/test/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicyTest.java create mode 100644 src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a49b915..80861bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,11 @@ jobs: - name: 验证 Gradle Wrapper uses: gradle/actions/wrapper-validation@v4 - - name: 构建插件 + - name: 配置 Gradle uses: gradle/actions/setup-gradle@v4 - with: - arguments: clean build + + - name: 授予执行权限 + run: chmod +x gradlew + + - name: 构建插件 + run: ./gradlew clean build --no-daemon diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6180358..1a025b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,18 +16,28 @@ jobs: - name: 检出代码 uses: actions/checkout@v4 - - name: 设置 Java 21 + - name: 设置 Java 25 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: gradle - name: 授予执行权限 run: chmod +x gradlew + - name: 配置 Gradle + uses: gradle/actions/setup-gradle@v4 + - name: 构建插件 - run: ./gradlew build + shell: bash + run: | + VERSION="${GITHUB_REF_NAME#v}" + if [[ "$VERSION" == "$GITHUB_REF_NAME" || ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "无效的发布标签: $GITHUB_REF_NAME" >&2 + exit 1 + fi + ./gradlew clean build --no-daemon -PpluginVersion="$VERSION" - name: 创建发行版 uses: softprops/action-gh-release@v2 diff --git a/build.gradle b/build.gradle index 0c37481..96aa47e 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { } group = 'cn.infstar' -version = '1.4.0' +version = providers.gradleProperty('pluginVersion').orElse('1.4.0').get() repositories { mavenCentral() diff --git a/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/JeiRecipeSyncAdapter.java b/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/JeiRecipeSyncAdapter.java index 060f546..ac44e7b 100644 --- a/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/JeiRecipeSyncAdapter.java +++ b/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/JeiRecipeSyncAdapter.java @@ -9,4 +9,6 @@ public interface JeiRecipeSyncAdapter { void sendFabricRecipeSync(Player player, Logger logger, boolean debug) throws Exception; void sendNeoForgeRecipeSync(Player player, Logger logger, boolean debug) throws Exception; + + void clearCache(); } diff --git a/src/main/java/cn/infstar/essentialsC/EssentialsC.java b/src/main/java/cn/infstar/essentialsC/EssentialsC.java index 6b74955..3e0b56f 100644 --- a/src/main/java/cn/infstar/essentialsC/EssentialsC.java +++ b/src/main/java/cn/infstar/essentialsC/EssentialsC.java @@ -80,6 +80,9 @@ public final class EssentialsC extends JavaPlugin { if (skinBridgeManager != null) { skinBridgeManager.shutdown(); } + if (shulkerBoxListener != null) { + shulkerBoxListener.shutdown(); + } if (vanishListener != null) { vanishListener.shutdown(); } @@ -225,6 +228,7 @@ public final class EssentialsC extends JavaPlugin { private void refreshBlocks() { if (!moduleManager.isEnabled(ModuleManager.BLOCKS)) { if (shulkerBoxListener != null) { + shulkerBoxListener.shutdown(); HandlerList.unregisterAll(shulkerBoxListener); shulkerBoxListener = null; } @@ -419,5 +423,10 @@ public final class EssentialsC extends JavaPlugin { } return List.of(); } + + @Override + public String permission() { + return "essentialsc".equals(name) ? null : executor.getPermission(); + } } } diff --git a/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java b/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java index 1bddf4d..edf90fa 100644 --- a/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java +++ b/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java @@ -94,9 +94,22 @@ public final class AdminModeManager implements Listener { } saveProfile(player, playerPath + ".admin"); + if (!saveData()) { + activePlayers.add(player.getUniqueId()); + startActionBarTask(); + sendLangMessage(player, "admin-mode.messages.save-failed"); + return; + } restoreNormalProfile(player); data.set(playerPath + ".active", false); - saveData(); + if (!saveData()) { + data.set(playerPath + ".active", true); + loadProfile(player, playerPath + ".admin"); + activePlayers.add(player.getUniqueId()); + startActionBarTask(); + sendLangMessage(player, "admin-mode.messages.save-failed"); + return; + } sendLangMessage(player, "admin-mode.messages.crash-restored"); } @@ -129,27 +142,43 @@ public final class AdminModeManager implements Listener { player.setAllowFlight(true); player.setFlying(true); player.setFlySpeed(getAdminFlySpeed()); - saveData(); sendLangMessage(player, "admin-mode.messages.enabled"); sendActionBar(player); startActionBarTask(); } - private void disable(Player player, boolean notify) { + private boolean disable(Player player, boolean notify) { String playerPath = getPlayerPath(player); player.closeInventory(); saveProfile(player, playerPath + ".admin"); + data.set(playerPath + ".active", true); + if (!saveData()) { + if (notify) { + sendLangMessage(player, "admin-mode.messages.save-failed"); + } + return false; + } restoreNormalProfile(player); - activePlayers.remove(player.getUniqueId()); data.set(playerPath + ".active", false); - saveData(); + if (!saveData()) { + data.set(playerPath + ".active", true); + loadProfile(player, playerPath + ".admin"); + activePlayers.add(player.getUniqueId()); + if (notify) { + sendLangMessage(player, "admin-mode.messages.save-failed"); + } + return false; + } + + activePlayers.remove(player.getUniqueId()); if (notify) { sendLangMessage(player, "admin-mode.messages.disabled"); } stopActionBarTaskIfIdle(); + return true; } private void restoreNormalProfile(Player player) { diff --git a/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java b/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java index 9eeba29..4c4a6ba 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java @@ -1,7 +1,6 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Sound; @@ -16,6 +15,8 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import java.util.ArrayList; import java.util.List; @@ -25,8 +26,6 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { private static final int MENU_SIZE = 36; private static final int[] DIVIDER_SLOTS = {4, 13, 22, 31}; - private static boolean listenerRegistered = false; - private final NamespacedKey blockKey; private static final class BlocksMenuHolder implements InventoryHolder { @@ -45,10 +44,7 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { public BlocksMenuCommand() { super("essentialsc.command.blocks"); addConfigDefaults(); - if (!listenerRegistered) { - plugin.getServer().getPluginManager().registerEvents(this, plugin); - listenerRegistered = true; - } + plugin.getServer().getPluginManager().registerEvents(this, plugin); this.blockKey = new NamespacedKey(plugin, "block_key"); } @@ -170,8 +166,10 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { ItemStack item = new ItemStack(menuItem.material()); ItemMeta meta = item.getItemMeta(); if (meta != null) { - meta.setDisplayName(menuItem.name()); - meta.setLore(menuItem.lore().isEmpty() ? null : menuItem.lore()); + meta.displayName(legacyComponent(menuItem.name())); + meta.lore(menuItem.lore().isEmpty() ? null : menuItem.lore().stream() + .map(this::legacyComponent) + .toList()); if (menuItem.commandKey() != null && !menuItem.commandKey().isBlank()) { meta.getPersistentDataContainer().set(blockKey, PersistentDataType.STRING, menuItem.commandKey()); } @@ -184,7 +182,7 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { ItemStack divider = new ItemStack(Material.GRAY_STAINED_GLASS_PANE); ItemMeta meta = divider.getItemMeta(); if (meta != null) { - meta.setDisplayName(" "); + meta.displayName(Component.text(" ")); divider.setItemMeta(meta); } @@ -250,8 +248,8 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { } } - private String translateColor(String text) { - return text == null ? "" : ChatColor.translateAlternateColorCodes('&', text); + private Component legacyComponent(String text) { + return LegacyComponentSerializer.legacySection().deserialize(text == null ? "" : text); } private void addConfigDefaults() { diff --git a/src/main/java/cn/infstar/essentialsC/commands/CommandRegistry.java b/src/main/java/cn/infstar/essentialsC/commands/CommandRegistry.java index 08a3436..65a24fb 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/CommandRegistry.java +++ b/src/main/java/cn/infstar/essentialsC/commands/CommandRegistry.java @@ -176,8 +176,7 @@ public final class CommandRegistry { } } - public static void clearCache() { - COMMAND_CACHE.clear(); + public static void clearInitializationFailures() { UNAVAILABLE_COMMANDS.clear(); } diff --git a/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java b/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java index b9c139c..895fe9c 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java @@ -54,6 +54,11 @@ public class GlowCommand extends BaseCommand { private boolean isPluginGlowEnabled(Player player) { Byte value = player.getPersistentDataContainer().get(enabledKey, PersistentDataType.BYTE); - return value != null && value == (byte) 1; + boolean enabled = value != null && value == (byte) 1; + if (enabled && !player.isGlowing()) { + player.getPersistentDataContainer().remove(enabledKey); + return false; + } + return enabled; } } diff --git a/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java b/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java index c33924a..2a3a11b 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java @@ -1,6 +1,8 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Bukkit; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; import org.bukkit.entity.Player; import java.util.Map; @@ -37,7 +39,8 @@ public class HealCommand extends BaseCommand { } private void healPlayer(Player player) { - player.setHealth(player.getMaxHealth()); + AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH); + player.setHealth(maxHealth == null ? player.getHealth() : maxHealth.getValue()); player.setFoodLevel(20); player.setSaturation(20f); player.clearActivePotionEffects(); diff --git a/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java b/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java index 27494fa..d62424f 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java @@ -20,6 +20,17 @@ public class HelpCommand extends BaseCommand implements TabCompleter { super("essentialsc.command.help"); } + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length > 0) { + if (sender instanceof Player player) { + return handleCommand(sender, player, args); + } + return executeConsole(sender, args); + } + return super.onCommand(sender, command, label, args); + } + @Override protected boolean execute(Player player, String[] args) { return handleCommand(player, player, args); @@ -36,7 +47,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter { EssentialsC.getLangManager().reload(); plugin.getFeatureConfigManager().reload(); plugin.getModuleManager().reload(); - CommandRegistry.clearCache(); + CommandRegistry.clearInitializationFailures(); plugin.reloadRuntimeModules(); sender.sendMessage(getLang().getPrefixedString("messages.config-reloaded")); return true; @@ -58,7 +69,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter { EssentialsC.getLangManager().reload(); plugin.getFeatureConfigManager().reload(); plugin.getModuleManager().reload(); - CommandRegistry.clearCache(); + CommandRegistry.clearInitializationFailures(); plugin.reloadRuntimeModules(); sender.sendMessage(getLang().getPrefixedString("messages.config-reloaded")); return true; diff --git a/src/main/java/cn/infstar/essentialsC/commands/MaintenanceCommand.java b/src/main/java/cn/infstar/essentialsC/commands/MaintenanceCommand.java index b824cb2..e40630f 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/MaintenanceCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/MaintenanceCommand.java @@ -41,12 +41,18 @@ public class MaintenanceCommand extends BaseCommand implements TabCompleter { switch (args[0].toLowerCase(Locale.ROOT)) { case "on", "enable", "enabled" -> { - maintenanceManager.setEnabled(true); + if (maintenanceManager.setEnabled(true) == MaintenanceManager.OperationResult.SAVE_FAILED) { + sendSaveFailed(sender); + return true; + } sender.sendMessage(getLang().getPrefixedString("maintenance.messages.enabled")); return true; } case "off", "disable", "disabled" -> { - maintenanceManager.setEnabled(false); + if (maintenanceManager.setEnabled(false) == MaintenanceManager.OperationResult.SAVE_FAILED) { + sendSaveFailed(sender); + return true; + } sender.sendMessage(getLang().getPrefixedString("maintenance.messages.disabled")); return true; } @@ -62,7 +68,10 @@ public class MaintenanceCommand extends BaseCommand implements TabCompleter { } String target = args[1]; - if (maintenanceManager.addWhitelistEntry(target)) { + MaintenanceManager.OperationResult result = maintenanceManager.addWhitelistEntry(target); + if (result == MaintenanceManager.OperationResult.SAVE_FAILED) { + sendSaveFailed(sender); + } else if (result == MaintenanceManager.OperationResult.SUCCESS) { sender.sendMessage(getLang().getPrefixedString("maintenance.messages.whitelist-added", Map.of("player", target))); } else { @@ -78,7 +87,10 @@ public class MaintenanceCommand extends BaseCommand implements TabCompleter { } String target = args[1]; - if (maintenanceManager.removeWhitelistEntry(target)) { + MaintenanceManager.OperationResult result = maintenanceManager.removeWhitelistEntry(target); + if (result == MaintenanceManager.OperationResult.SAVE_FAILED) { + sendSaveFailed(sender); + } else if (result == MaintenanceManager.OperationResult.SUCCESS) { sender.sendMessage(getLang().getPrefixedString("maintenance.messages.whitelist-removed", Map.of("player", target))); } else { @@ -123,6 +135,10 @@ public class MaintenanceCommand extends BaseCommand implements TabCompleter { ))); } + private void sendSaveFailed(CommandSender sender) { + sender.sendMessage(getLang().getPrefixedString("maintenance.messages.save-failed")); + } + @Override public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission(getPermission())) { diff --git a/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java b/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java index dd4c6ab..a920630 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java @@ -8,6 +8,8 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import java.util.List; import java.util.Map; @@ -52,12 +54,12 @@ public class MobDropCommand extends BaseCommand { ItemStack endermanItem = new ItemStack(Material.ENDER_PEARL); ItemMeta endermanMeta = endermanItem.getItemMeta(); if (endermanMeta != null) { - endermanMeta.setDisplayName(lang.getString("mobdrops-menu.enderman.name")); - endermanMeta.setLore(List.of( + endermanMeta.displayName(legacyComponent(lang.getString("mobdrops-menu.enderman.name"))); + endermanMeta.lore(List.of( lang.getString("mobdrops-menu.enderman.status", Map.of("status", status)), "", lang.getString("mobdrops-menu.enderman.toggle") - )); + ).stream().map(MobDropCommand::legacyComponent).toList()); endermanItem.setItemMeta(endermanMeta); } menu.setItem(ENDERMAN_SLOT, endermanItem); @@ -65,7 +67,7 @@ public class MobDropCommand extends BaseCommand { ItemStack glass = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); ItemMeta glassMeta = glass.getItemMeta(); if (glassMeta != null) { - glassMeta.setDisplayName(" "); + glassMeta.displayName(Component.text(" ")); glass.setItemMeta(glassMeta); } @@ -82,6 +84,10 @@ public class MobDropCommand extends BaseCommand { return ENDERMAN_SLOT; } + private static Component legacyComponent(String text) { + return LegacyComponentSerializer.legacySection().deserialize(text == null ? "" : text); + } + private void openMobDropMenu(Player player) { openMobDropMenu(plugin, player); } diff --git a/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java b/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java index 5ff1d71..59593e4 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java @@ -56,6 +56,11 @@ public class NightVisionCommand extends BaseCommand { private boolean isPluginNightVisionEnabled(Player player) { Byte value = player.getPersistentDataContainer().get(enabledKey, PersistentDataType.BYTE); - return value != null && value == (byte) 1; + boolean enabled = value != null && value == (byte) 1; + if (enabled && !player.hasPotionEffect(PotionEffectType.NIGHT_VISION)) { + player.getPersistentDataContainer().remove(enabledKey); + return false; + } + return enabled; } } diff --git a/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java b/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java index 15ed1fa..3608b2a 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java @@ -21,6 +21,13 @@ public class SeenCommand extends BaseCommand { return true; } + Player onlineTarget = Bukkit.getPlayerExact(args[0]); + if (onlineTarget != null && VanishCommand.isVanished(onlineTarget) + && !player.hasPermission(VanishCommand.SEE_PERMISSION)) { + player.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0]))); + return true; + } + OfflinePlayer target = Bukkit.getOfflinePlayer(args[0]); if (!target.hasPlayedBefore() && !target.isOnline()) { player.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0]))); diff --git a/src/main/java/cn/infstar/essentialsC/commands/TpIgnoreCommand.java b/src/main/java/cn/infstar/essentialsC/commands/TpIgnoreCommand.java index dbcefe9..712bcc2 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/TpIgnoreCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/TpIgnoreCommand.java @@ -17,10 +17,13 @@ public final class TpIgnoreCommand extends BaseCommand { return true; } - boolean ignoring = manager.toggleIgnoringRequests(player); - player.sendMessage(getLang().getPrefixedComponent(ignoring - ? "tpa.messages.ignore-enabled" - : "tpa.messages.ignore-disabled")); + TeleportRequestManager.ToggleIgnoreResult result = manager.toggleIgnoringRequests(player); + String messagePath = switch (result) { + case ENABLED -> "tpa.messages.ignore-enabled"; + case DISABLED -> "tpa.messages.ignore-disabled"; + case SAVE_FAILED -> "tpa.messages.ignore-save-failed"; + }; + player.sendMessage(getLang().getPrefixedComponent(messagePath)); return true; } } diff --git a/src/main/java/cn/infstar/essentialsC/commands/VanishCommand.java b/src/main/java/cn/infstar/essentialsC/commands/VanishCommand.java index f05c78b..1aa63d8 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/VanishCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/VanishCommand.java @@ -27,14 +27,23 @@ public class VanishCommand extends BaseCommand { if (vanishedPlayers.contains(uuid)) { vanishedPlayers.remove(uuid); + if (!saveState(plugin)) { + vanishedPlayers.add(uuid); + player.sendMessage(getLang().getPrefixedString("messages.vanish-save-failed")); + return true; + } showPlayerToAll(plugin, player); player.sendMessage(getLang().getPrefixedString("messages.vanish-disabled")); } else { vanishedPlayers.add(uuid); + if (!saveState(plugin)) { + vanishedPlayers.remove(uuid); + player.sendMessage(getLang().getPrefixedString("messages.vanish-save-failed")); + return true; + } hidePlayerFromAll(plugin, player); player.sendMessage(getLang().getPrefixedString("messages.vanish-enabled")); } - saveState(plugin); return true; } @@ -111,7 +120,7 @@ public class VanishCommand extends BaseCommand { return vanishedPlayers.contains(player.getUniqueId()); } - private static void saveState(EssentialsC plugin) { + private static boolean saveState(EssentialsC plugin) { if (stateFile == null) { stateFile = new File(plugin.getDataFolder(), "vanished-players.yml"); } @@ -119,8 +128,10 @@ public class VanishCommand extends BaseCommand { state.set("players", vanishedPlayers.stream().map(UUID::toString).sorted().toList()); try { AtomicYamlWriter.save(state, stateFile); + return true; } catch (Exception exception) { plugin.getLogger().warning("保存 vanished-players.yml 失败: " + exception.getMessage()); + return false; } } } diff --git a/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java b/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java index f7fc11e..c963a17 100644 --- a/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java +++ b/src/main/java/cn/infstar/essentialsC/listeners/JeiRecipeSyncListener.java @@ -2,6 +2,7 @@ package cn.infstar.essentialsC.listeners; import cn.infstar.essentialsC.EssentialsC; import cn.infstar.essentialsC.compat.jei.JeiRecipeSyncAdapter; +import io.papermc.paper.event.server.ServerResourcesReloadedEvent; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -52,6 +53,13 @@ public class JeiRecipeSyncListener implements Listener { plugin.getServer().getScheduler().runTaskLater(plugin, () -> detectAndSync(player), brandCheckDelayTicks); } + @EventHandler + public void onServerResourcesReloaded(ServerResourcesReloadedEvent event) { + if (adapter != null) { + adapter.clearCache(); + } + } + private void detectAndSync(Player player) { if (!player.isOnline()) { return; diff --git a/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java b/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java index 4a3e412..f5e531f 100644 --- a/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java +++ b/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java @@ -53,6 +53,7 @@ public class MobDropMenuListener implements Listener { try { AtomicYamlWriter.save(config, plugin.getDataFolder().toPath().resolve("config.yml").toFile()); } catch (Exception e) { + config.set("mob-drops.enderman.allow-drops", !newValue); player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.mobdrop-save-failed", Map.of("error", e.getMessage()))); return; diff --git a/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java b/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java index 406bc63..d3baa49 100644 --- a/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java +++ b/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxListener.java @@ -1,8 +1,10 @@ package cn.infstar.essentialsC.listeners; import cn.infstar.essentialsC.EssentialsC; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.Sound; import org.bukkit.block.ShulkerBox; import org.bukkit.entity.Player; @@ -10,13 +12,18 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; +import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.Inventory; @@ -26,18 +33,20 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.BlockStateMeta; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; - +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; public class ShulkerBoxListener implements Listener { private static final int SHULKER_SIZE = 27; + private static final int OFF_HAND_SLOT = 40; private static final Set SHULKER_BOX_MATERIALS = Set.of( Material.SHULKER_BOX, @@ -60,38 +69,47 @@ public class ShulkerBoxListener implements Listener { ); private final EssentialsC plugin; + private final NamespacedKey sessionKey; private final Map openShulkerBoxes = new HashMap<>(); private static final class ShulkerBoxHolder implements InventoryHolder { private final Inventory inventory; private ShulkerBoxHolder(Component title) { - this.inventory = Bukkit.createInventory(this, SHULKER_SIZE, title); + this.inventory = Bukkit.createInventory(this, InventoryType.SHULKER_BOX, title); } @Override public Inventory getInventory() { - return this.inventory; + return inventory; } } private static final class OpenShulkerSession { - private final ItemStack sourceItem; - private final EquipmentSlot sourceHand; + private final String token; + private final Inventory inventory; private final int preferredSlot; + private boolean syncScheduled; - private OpenShulkerSession(ItemStack sourceItem, EquipmentSlot sourceHand, int preferredSlot) { - this.sourceItem = sourceItem; - this.sourceHand = sourceHand; + private OpenShulkerSession(String token, Inventory inventory, int preferredSlot) { + this.token = token; + this.inventory = inventory; this.preferredSlot = preferredSlot; } } - public ShulkerBoxListener(EssentialsC plugin) { - this.plugin = plugin; + private record LocatedSource(ItemStack item, Consumer save) { } - @EventHandler(priority = EventPriority.HIGHEST) + public ShulkerBoxListener(EssentialsC plugin) { + this.plugin = plugin; + this.sessionKey = new NamespacedKey(plugin, "open_shulker_session"); + for (Player player : Bukkit.getOnlinePlayers()) { + clearStaleSessionTokens(player); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onPlayerInteract(PlayerInteractEvent event) { if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) { return; @@ -111,21 +129,15 @@ public class ShulkerBoxListener implements Listener { if (!isShulkerBox(sourceItem)) { return; } + + event.setUseItemInHand(org.bukkit.event.Event.Result.DENY); + event.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY); + if (sourceItem.getAmount() != 1) { player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.shulkerbox-unstack-first")); return; } - if (!(sourceItem.getItemMeta() instanceof BlockStateMeta blockStateMeta)) { - return; - } - if (!(blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox)) { - return; - } - - event.setUseItemInHand(org.bukkit.event.Event.Result.DENY); - event.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY); - ItemStack sourceSnapshot = sourceItem.clone(); plugin.getServer().getScheduler().runTask(plugin, () -> { if (!player.isOnline() || openShulkerBoxes.containsKey(player.getUniqueId())) { @@ -137,7 +149,7 @@ public class ShulkerBoxListener implements Listener { return; } - openShulkerBox(player, hand, sourceSnapshot, shulkerBox); + openShulkerBox(player, hand, currentItem); }); } @@ -147,14 +159,33 @@ public class ShulkerBoxListener implements Listener { return; } - InventoryView view = event.getView(); - Inventory topInventory = view.getTopInventory(); - if (!(topInventory.getHolder(false) instanceof ShulkerBoxHolder)) { + OpenShulkerSession session = openShulkerBoxes.get(player.getUniqueId()); + if (session == null || event.getView().getTopInventory() != session.inventory) { return; } - int topSize = topInventory.getSize(); - boolean clickTopInventory = event.getRawSlot() >= 0 && event.getRawSlot() < topSize; + if (hasSessionToken(event.getCurrentItem(), session.token) + || hasSessionToken(event.getCursor(), session.token)) { + event.setCancelled(true); + return; + } + + if (event.getClick() == ClickType.NUMBER_KEY) { + ItemStack hotbarItem = player.getInventory().getItem(event.getHotbarButton()); + if (hasSessionToken(hotbarItem, session.token)) { + event.setCancelled(true); + return; + } + } + + if (event.getClick() == ClickType.SWAP_OFFHAND + && hasSessionToken(player.getInventory().getItemInOffHand(), session.token)) { + event.setCancelled(true); + return; + } + + int topSize = session.inventory.getSize(); + boolean clickTopInventory = ShulkerBoxSessionPolicy.isTopSlot(event.getRawSlot(), topSize); if (clickTopInventory && isShulkerBox(event.getCursor())) { event.setCancelled(true); @@ -171,43 +202,46 @@ public class ShulkerBoxListener implements Listener { } } - if (clickTopInventory && event.getClick() == ClickType.SWAP_OFFHAND) { - ItemStack offHandItem = player.getInventory().getItemInOffHand(); - if (isShulkerBox(offHandItem)) { - event.setCancelled(true); - sendNestedMessage(player); - return; - } + if (clickTopInventory && event.getClick() == ClickType.SWAP_OFFHAND + && isShulkerBox(player.getInventory().getItemInOffHand())) { + event.setCancelled(true); + sendNestedMessage(player); + return; } if (event.isShiftClick() && isShulkerBox(event.getCurrentItem())) { event.setCancelled(true); sendNestedMessage(player); + return; } + + scheduleSynchronization(player, session); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onInventoryDrag(InventoryDragEvent event) { - InventoryView view = event.getView(); - Inventory topInventory = view.getTopInventory(); - if (!(topInventory.getHolder(false) instanceof ShulkerBoxHolder)) { + if (!(event.getWhoClicked() instanceof Player player)) { return; } - if (!isShulkerBox(event.getOldCursor())) { + OpenShulkerSession session = openShulkerBoxes.get(player.getUniqueId()); + if (session == null || event.getView().getTopInventory() != session.inventory) { return; } - int topSize = topInventory.getSize(); - for (int rawSlot : event.getRawSlots()) { - if (rawSlot >= 0 && rawSlot < topSize) { - event.setCancelled(true); - if (event.getWhoClicked() instanceof Player player) { - sendNestedMessage(player); - } - return; - } + if (hasSessionToken(event.getOldCursor(), session.token)) { + event.setCancelled(true); + return; } + + if (isShulkerBox(event.getOldCursor()) + && ShulkerBoxSessionPolicy.touchesTopInventory(event.getRawSlots(), session.inventory.getSize())) { + event.setCancelled(true); + sendNestedMessage(player); + return; + } + + scheduleSynchronization(player, session); } @EventHandler @@ -221,111 +255,436 @@ public class ShulkerBoxListener implements Listener { } } + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerDropItem(PlayerDropItemEvent event) { + OpenShulkerSession session = openShulkerBoxes.get(event.getPlayer().getUniqueId()); + if (session != null && hasSessionToken(event.getItemDrop().getItemStack(), session.token)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) { + OpenShulkerSession session = openShulkerBoxes.get(event.getPlayer().getUniqueId()); + if (session == null) { + return; + } + + if (hasSessionToken(event.getMainHandItem(), session.token) + || hasSessionToken(event.getOffHandItem(), session.token)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerDeath(PlayerDeathEvent event) { + Player player = event.getPlayer(); + OpenShulkerSession session = openShulkerBoxes.remove(player.getUniqueId()); + if (session == null) { + return; + } + + boolean saved; + if (event.getKeepInventory()) { + saved = writeSessionToPlayer(player, session, true); + } else { + saved = writeSessionToDrops(event.getDrops(), session); + if (!saved) { + saved = writeSessionToPlayer(player, session, true); + } + } + + if (!saved) { + handleLostSource(player, session, "玩家死亡时未找到唯一的源物品"); + } + } + @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - commitOpenShulker(player, player.getOpenInventory().getTopInventory()); + commitCurrentSession(event.getPlayer()); } @EventHandler public void onPlayerKick(PlayerKickEvent event) { - Player player = event.getPlayer(); - commitOpenShulker(player, player.getOpenInventory().getTopInventory()); + commitCurrentSession(event.getPlayer()); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!openShulkerBoxes.containsKey(event.getPlayer().getUniqueId())) { + clearStaleSessionTokens(event.getPlayer()); + } } @EventHandler public void onPluginDisable(PluginDisableEvent event) { - if (event.getPlugin() != plugin) { + if (event.getPlugin() == plugin) { + shutdown(); + } + } + + public void shutdown() { + for (UUID playerId : List.copyOf(openShulkerBoxes.keySet())) { + Player player = Bukkit.getPlayer(playerId); + if (player == null) { + openShulkerBoxes.remove(playerId); + plugin.getLogger().warning("潜影盒会话关闭失败: 玩家 " + playerId + " 已离线。"); + continue; + } + + OpenShulkerSession session = openShulkerBoxes.get(playerId); + boolean viewingSession = session != null && player.getOpenInventory().getTopInventory() == session.inventory; + commitCurrentSession(player); + if (viewingSession) { + player.closeInventory(); + } + } + } + + private void openShulkerBox(Player player, EquipmentSlot hand, ItemStack sourceItem) { + if (!(sourceItem.getItemMeta() instanceof BlockStateMeta blockStateMeta) + || !(blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox)) { + sendOpenFailedMessage(player); return; } - for (Player player : Bukkit.getOnlinePlayers()) { - commitOpenShulker(player, player.getOpenInventory().getTopInventory()); + String token = UUID.randomUUID().toString(); + ItemStack taggedSource = sourceItem.clone(); + setSessionToken(taggedSource, token); + setItemInHand(player, hand, taggedSource); + + ShulkerBoxHolder holder; + try { + holder = new ShulkerBoxHolder(resolveTitle(sourceItem)); + holder.getInventory().setContents(cloneContents(shulkerBox.getInventory().getContents())); + } catch (RuntimeException exception) { + clearTokenFromPlayer(player, token); + plugin.getLogger().warning("打开潜影盒失败: " + exception.getMessage()); + sendOpenFailedMessage(player); + return; + } + + int preferredSlot = hand == EquipmentSlot.HAND ? player.getInventory().getHeldItemSlot() : OFF_HAND_SLOT; + OpenShulkerSession session = new OpenShulkerSession(token, holder.getInventory(), preferredSlot); + openShulkerBoxes.put(player.getUniqueId(), session); + + try { + InventoryView openedView = player.openInventory(session.inventory); + if (openedView == null + || openedView.getTopInventory() != session.inventory + || player.getOpenInventory().getTopInventory() != session.inventory) { + rollbackFailedOpen(player, session); + return; + } + } catch (RuntimeException exception) { + plugin.getLogger().warning("打开潜影盒失败: " + exception.getMessage()); + rollbackFailedOpen(player, session); + return; + } + + player.playSound(player.getLocation(), Sound.BLOCK_SHULKER_BOX_OPEN, 0.8F, 1.0F); + } + + private void rollbackFailedOpen(Player player, OpenShulkerSession session) { + openShulkerBoxes.remove(player.getUniqueId(), session); + clearTokenFromPlayer(player, session.token); + sendOpenFailedMessage(player); + } + + private void scheduleSynchronization(Player player, OpenShulkerSession session) { + if (session.syncScheduled) { + return; + } + + session.syncScheduled = true; + plugin.getServer().getScheduler().runTask(plugin, () -> { + session.syncScheduled = false; + if (openShulkerBoxes.get(player.getUniqueId()) != session) { + return; + } + + if (!writeSessionToPlayer(player, session, false)) { + openShulkerBoxes.remove(player.getUniqueId(), session); + handleLostSource(player, session, "同步期间未找到唯一的源物品"); + if (player.getOpenInventory().getTopInventory() == session.inventory) { + player.closeInventory(); + } + } + }); + } + + private void commitCurrentSession(Player player) { + OpenShulkerSession session = openShulkerBoxes.get(player.getUniqueId()); + if (session != null) { + commitOpenShulker(player, session.inventory); } } private boolean commitOpenShulker(Player player, Inventory inventory) { - if (!(inventory.getHolder(false) instanceof ShulkerBoxHolder)) { + OpenShulkerSession session = openShulkerBoxes.get(player.getUniqueId()); + if (session == null || session.inventory != inventory) { return false; } - OpenShulkerSession session = openShulkerBoxes.remove(player.getUniqueId()); - if (session == null) { + openShulkerBoxes.remove(player.getUniqueId(), session); + boolean saved = writeSessionToPlayer(player, session, true); + if (!saved) { + handleLostSource(player, session, "关闭期间未找到唯一的源物品"); + } + return saved; + } + + private boolean writeSessionToPlayer(Player player, OpenShulkerSession session, boolean clearToken) { + List sources = findSources(player, session); + if (sources.size() != 1) { + clearSessionTokens(sources); return false; } - ItemStack updatedShulker = session.sourceItem.clone(); - writeInventoryBack(updatedShulker, inventory.getContents()); - restoreItemToPlayer(player, session, updatedShulker); + LocatedSource source = sources.getFirst(); + if (source.item().getAmount() != 1) { + clearSessionTokens(sources); + return false; + } + + ItemStack updatedItem = source.item().clone(); + if (!writeInventoryBack(updatedItem, session.inventory.getContents(), clearToken)) { + clearSessionTokens(sources); + return false; + } + + source.save().accept(updatedItem); return true; } - private void openShulkerBox(Player player, EquipmentSlot hand, ItemStack sourceItem, ShulkerBox shulkerBox) { - removeItemFromHand(player, hand); + private boolean writeSessionToDrops(List drops, OpenShulkerSession session) { + List matchingIndexes = new ArrayList<>(); + for (int index = 0; index < drops.size(); index++) { + if (hasSessionToken(drops.get(index), session.token)) { + matchingIndexes.add(index); + } + } - ShulkerBoxHolder holder = new ShulkerBoxHolder(resolveTitle(sourceItem)); - holder.getInventory().setContents(cloneContents(shulkerBox.getInventory().getContents())); + if (matchingIndexes.size() != 1) { + clearTokensFromDrops(drops, matchingIndexes); + return false; + } - int preferredSlot = hand == EquipmentSlot.HAND ? player.getInventory().getHeldItemSlot() : -1; - openShulkerBoxes.put(player.getUniqueId(), new OpenShulkerSession(sourceItem, hand, preferredSlot)); - player.openInventory(holder.getInventory()); - player.playSound(player.getLocation(), Sound.BLOCK_SHULKER_BOX_OPEN, 0.8F, 1.0F); + int sourceIndex = matchingIndexes.getFirst(); + ItemStack sourceItem = drops.get(sourceIndex); + if (sourceItem.getAmount() != 1) { + clearTokensFromDrops(drops, matchingIndexes); + return false; + } + + ItemStack updatedItem = sourceItem.clone(); + if (!writeInventoryBack(updatedItem, session.inventory.getContents(), true)) { + clearTokensFromDrops(drops, matchingIndexes); + return false; + } + + drops.set(sourceIndex, updatedItem); + return true; } - private void writeInventoryBack(ItemStack shulkerItem, ItemStack[] contents) { + private boolean writeInventoryBack(ItemStack shulkerItem, ItemStack[] contents, boolean clearToken) { + if (containsShulkerBox(contents)) { + plugin.getLogger().warning("保存潜影盒内容失败: 虚拟容器中检测到嵌套潜影盒。"); + return false; + } + if (!(shulkerItem.getItemMeta() instanceof BlockStateMeta blockStateMeta)) { plugin.getLogger().warning("保存潜影盒内容失败: 缺少 BlockStateMeta。"); - return; + return false; } if (!(blockStateMeta.getBlockState() instanceof ShulkerBox shulkerBox)) { plugin.getLogger().warning("保存潜影盒内容失败: 方块状态不是 ShulkerBox。"); - return; + return false; } - shulkerBox.getInventory().setContents(cloneContents(contents)); - blockStateMeta.setBlockState(shulkerBox); - shulkerItem.setItemMeta(blockStateMeta); + try { + shulkerBox.getInventory().setContents(cloneContents(contents)); + blockStateMeta.setBlockState(shulkerBox); + if (clearToken) { + blockStateMeta.getPersistentDataContainer().remove(sessionKey); + } + shulkerItem.setItemMeta(blockStateMeta); + return true; + } catch (RuntimeException exception) { + plugin.getLogger().warning("保存潜影盒内容失败: " + exception.getMessage()); + return false; + } } - private void restoreItemToPlayer(Player player, OpenShulkerSession session, ItemStack shulkerItem) { + private List findSources(Player player, OpenShulkerSession session) { + List sources = new ArrayList<>(); PlayerInventory inventory = player.getInventory(); - if (session.sourceHand == EquipmentSlot.OFF_HAND) { - if (isEmpty(inventory.getItemInOffHand())) { - inventory.setItemInOffHand(shulkerItem); - return; - } - } else if (session.preferredSlot >= 0 && isEmpty(inventory.getItem(session.preferredSlot))) { - inventory.setItem(session.preferredSlot, shulkerItem); - return; + if (session.preferredSlot >= 0 && session.preferredSlot < inventory.getSize()) { + addSourceIfMatching( + sources, + inventory.getItem(session.preferredSlot), + item -> inventory.setItem(session.preferredSlot, item), + session.token + ); } - Map leftovers = inventory.addItem(shulkerItem); - for (ItemStack leftover : leftovers.values()) { - player.getWorld().dropItemNaturally(player.getLocation(), leftover); + for (int slot = 0; slot < inventory.getSize(); slot++) { + if (slot == session.preferredSlot) { + continue; + } + int inventorySlot = slot; + addSourceIfMatching( + sources, + inventory.getItem(inventorySlot), + item -> inventory.setItem(inventorySlot, item), + session.token + ); } + + addSourceIfMatching(sources, player.getItemOnCursor(), player::setItemOnCursor, session.token); + return sources; + } + + private void addSourceIfMatching( + List sources, + ItemStack item, + Consumer save, + String token + ) { + if (hasSessionToken(item, token)) { + sources.add(new LocatedSource(item, save)); + } + } + + private void clearSessionTokens(List sources) { + for (LocatedSource source : sources) { + ItemStack cleanedItem = source.item().clone(); + clearSessionToken(cleanedItem); + source.save().accept(cleanedItem); + } + } + + private void clearTokensFromDrops(List drops, List indexes) { + for (int index : indexes) { + ItemStack cleanedItem = drops.get(index).clone(); + clearSessionToken(cleanedItem); + drops.set(index, cleanedItem); + } + } + + private void clearTokenFromPlayer(Player player, String token) { + PlayerInventory inventory = player.getInventory(); + for (int slot = 0; slot < inventory.getSize(); slot++) { + ItemStack item = inventory.getItem(slot); + if (!hasSessionToken(item, token)) { + continue; + } + ItemStack cleanedItem = item.clone(); + clearSessionToken(cleanedItem); + inventory.setItem(slot, cleanedItem); + } + + ItemStack cursor = player.getItemOnCursor(); + if (hasSessionToken(cursor, token)) { + ItemStack cleanedCursor = cursor.clone(); + clearSessionToken(cleanedCursor); + player.setItemOnCursor(cleanedCursor); + } + } + + private void clearStaleSessionTokens(Player player) { + PlayerInventory inventory = player.getInventory(); + for (int slot = 0; slot < inventory.getSize(); slot++) { + ItemStack item = inventory.getItem(slot); + if (!hasAnySessionToken(item)) { + continue; + } + ItemStack cleanedItem = item.clone(); + clearSessionToken(cleanedItem); + inventory.setItem(slot, cleanedItem); + } + + ItemStack cursor = player.getItemOnCursor(); + if (hasAnySessionToken(cursor)) { + ItemStack cleanedCursor = cursor.clone(); + clearSessionToken(cleanedCursor); + player.setItemOnCursor(cleanedCursor); + } + } + + private void handleLostSource(Player player, OpenShulkerSession session, String reason) { + clearTokenFromPlayer(player, session.token); + plugin.getLogger().warning("潜影盒会话已终止: " + player.getName() + " (" + player.getUniqueId() + ")," + reason + "。"); + if (player.isOnline()) { + player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.shulkerbox-session-invalid")); + } + } + + private void setSessionToken(ItemStack item, String token) { + ItemMeta itemMeta = item.getItemMeta(); + itemMeta.getPersistentDataContainer().set(sessionKey, PersistentDataType.STRING, token); + item.setItemMeta(itemMeta); + } + + private void clearSessionToken(ItemStack item) { + ItemMeta itemMeta = item.getItemMeta(); + if (itemMeta == null) { + return; + } + itemMeta.getPersistentDataContainer().remove(sessionKey); + item.setItemMeta(itemMeta); + } + + private boolean hasSessionToken(ItemStack item, String token) { + if (!isShulkerBox(item)) { + return false; + } + ItemMeta itemMeta = item.getItemMeta(); + if (itemMeta == null) { + return false; + } + String storedToken = itemMeta.getPersistentDataContainer().get(sessionKey, PersistentDataType.STRING); + return token.equals(storedToken); + } + + private boolean hasAnySessionToken(ItemStack item) { + if (!isShulkerBox(item)) { + return false; + } + ItemMeta itemMeta = item.getItemMeta(); + return itemMeta != null && itemMeta.getPersistentDataContainer().has(sessionKey, PersistentDataType.STRING); } private ItemStack[] cloneContents(ItemStack[] contents) { ItemStack[] copied = new ItemStack[SHULKER_SIZE]; - for (int i = 0; i < SHULKER_SIZE && i < contents.length; i++) { - copied[i] = contents[i] == null ? null : contents[i].clone(); + for (int index = 0; index < SHULKER_SIZE && index < contents.length; index++) { + copied[index] = contents[index] == null ? null : contents[index].clone(); } return copied; } + private boolean containsShulkerBox(ItemStack[] contents) { + for (ItemStack item : contents) { + if (isShulkerBox(item)) { + return true; + } + } + return false; + } + private ItemStack getItemFromHand(Player player, EquipmentSlot hand) { return hand == EquipmentSlot.OFF_HAND ? player.getInventory().getItemInOffHand() : player.getInventory().getItemInMainHand(); } - private void removeItemFromHand(Player player, EquipmentSlot hand) { + private void setItemInHand(Player player, EquipmentSlot hand, ItemStack item) { if (hand == EquipmentSlot.OFF_HAND) { - player.getInventory().setItemInOffHand(null); + player.getInventory().setItemInOffHand(item); } else { - player.getInventory().setItem(player.getInventory().getHeldItemSlot(), null); + player.getInventory().setItem(player.getInventory().getHeldItemSlot(), item); } } @@ -336,7 +695,6 @@ public class ShulkerBoxListener implements Listener { if (displayName != null) { return displayName; } - return LegacyComponentSerializer.legacySection().deserialize(itemMeta.getDisplayName()); } return Component.translatable(shulkerBox.getType().getItemTranslationKey()); @@ -346,12 +704,12 @@ public class ShulkerBoxListener implements Listener { player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.shulkerbox-nested")); } - private boolean isShulkerBox(ItemStack item) { - return item != null && !item.getType().isAir() && SHULKER_BOX_MATERIALS.contains(item.getType()); + private void sendOpenFailedMessage(Player player) { + player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.shulkerbox-open-failed")); } - private boolean isEmpty(ItemStack item) { - return item == null || item.getType().isAir(); + private boolean isShulkerBox(ItemStack item) { + return item != null && !item.getType().isAir() && SHULKER_BOX_MATERIALS.contains(item.getType()); } private boolean isSameShulkerItem(ItemStack currentItem, ItemStack sourceSnapshot) { diff --git a/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicy.java b/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicy.java new file mode 100644 index 0000000..e5fade6 --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicy.java @@ -0,0 +1,17 @@ +package cn.infstar.essentialsC.listeners; + +import java.util.Set; + +final class ShulkerBoxSessionPolicy { + + private ShulkerBoxSessionPolicy() { + } + + static boolean isTopSlot(int rawSlot, int topInventorySize) { + return rawSlot >= 0 && rawSlot < topInventorySize; + } + + static boolean touchesTopInventory(Set rawSlots, int topInventorySize) { + return rawSlots.stream().anyMatch(rawSlot -> isTopSlot(rawSlot, topInventorySize)); + } +} diff --git a/src/main/java/cn/infstar/essentialsC/listeners/VanishListener.java b/src/main/java/cn/infstar/essentialsC/listeners/VanishListener.java index 4c8eee9..45e1699 100644 --- a/src/main/java/cn/infstar/essentialsC/listeners/VanishListener.java +++ b/src/main/java/cn/infstar/essentialsC/listeners/VanishListener.java @@ -9,10 +9,17 @@ import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.scheduler.BukkitTask; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + public final class VanishListener implements Listener { private final EssentialsC plugin; private final BukkitTask permissionTask; + private final Map observerSeePermissions = new HashMap<>(); public VanishListener(EssentialsC plugin) { this.plugin = plugin; @@ -23,6 +30,7 @@ public final class VanishListener implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); + observerSeePermissions.put(player.getUniqueId(), player.hasPermission(VanishCommand.SEE_PERMISSION)); if (VanishCommand.isVanished(player) && !player.hasPermission("essentialsc.command.vanish")) { VanishCommand.restoreVisibility(plugin, player, false); } @@ -35,6 +43,7 @@ public final class VanishListener implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { + observerSeePermissions.remove(event.getPlayer().getUniqueId()); Player player = event.getPlayer(); if (VanishCommand.isVanished(player)) { event.quitMessage(null); @@ -46,10 +55,34 @@ public final class VanishListener implements Listener { } private void checkPermissions() { + Set onlinePlayers = new HashSet<>(); for (Player player : plugin.getServer().getOnlinePlayers()) { + onlinePlayers.add(player.getUniqueId()); if (VanishCommand.isVanished(player) && !player.hasPermission("essentialsc.command.vanish")) { VanishCommand.restoreVisibility(plugin, player, true); } } + observerSeePermissions.keySet().removeIf(uuid -> !onlinePlayers.contains(uuid)); + + for (Player observer : plugin.getServer().getOnlinePlayers()) { + boolean canSeeVanished = observer.hasPermission(VanishCommand.SEE_PERMISSION); + Boolean previous = observerSeePermissions.put(observer.getUniqueId(), canSeeVanished); + if (previous == null || previous != canSeeVanished) { + refreshObserverVisibility(observer, canSeeVanished); + } + } + } + + private void refreshObserverVisibility(Player observer, boolean canSeeVanished) { + for (Player player : plugin.getServer().getOnlinePlayers()) { + if (observer.equals(player) || !VanishCommand.isVanished(player)) { + continue; + } + if (canSeeVanished) { + observer.showPlayer(plugin, player); + } else { + observer.hidePlayer(plugin, player); + } + } } } diff --git a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java index 3e94d73..c8417b5 100644 --- a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java +++ b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java @@ -10,6 +10,7 @@ import org.bukkit.boss.BossBar; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import java.io.File; import java.util.ArrayList; @@ -67,13 +68,21 @@ public final class MaintenanceManager { return config.getBoolean("enabled", false); } - public void setEnabled(boolean enabled) { + public OperationResult setEnabled(boolean enabled) { + if (isEnabled() == enabled) { + return OperationResult.UNCHANGED; + } + boolean previous = isEnabled(); config.set("enabled", enabled); - save(); + if (!save()) { + config.set("enabled", previous); + return OperationResult.SAVE_FAILED; + } refreshBossBar(); if (enabled) { kickUnauthorizedPlayers(); } + return OperationResult.SUCCESS; } public String getBypassPermission() { @@ -105,10 +114,10 @@ public final class MaintenanceManager { || getWhitelistNames().contains(normalizeName(player.getName())); } - public boolean addWhitelistEntry(String input) { + public OperationResult addWhitelistEntry(String input) { String normalized = normalizeEntry(input); if (normalized.isEmpty()) { - return false; + return OperationResult.UNCHANGED; } UUID uuid = parseUuid(normalized); @@ -124,10 +133,10 @@ public final class MaintenanceManager { return addWhitelistName(input); } - public boolean removeWhitelistEntry(String input) { + public OperationResult removeWhitelistEntry(String input) { String normalized = normalizeEntry(input); if (normalized.isEmpty()) { - return false; + return OperationResult.UNCHANGED; } UUID uuid = parseUuid(normalized); @@ -136,8 +145,11 @@ public final class MaintenanceManager { } Player onlinePlayer = Bukkit.getPlayerExact(normalized); - if (onlinePlayer != null && removeWhitelistUuid(onlinePlayer.getUniqueId().toString())) { - return true; + if (onlinePlayer != null) { + OperationResult uuidResult = removeWhitelistUuid(onlinePlayer.getUniqueId().toString()); + if (uuidResult != OperationResult.UNCHANGED) { + return uuidResult; + } } return removeWhitelistName(input); @@ -202,7 +214,7 @@ public final class MaintenanceManager { String message = getKickMessage(); for (Player player : List.copyOf(plugin.getServer().getOnlinePlayers())) { if (!canJoin(player)) { - player.kickPlayer(message); + player.kick(LegacyComponentSerializer.legacySection().deserialize(message)); } } } @@ -248,27 +260,28 @@ public final class MaintenanceManager { return Math.max(0.0D, Math.min(1.0D, progress)); } - private boolean addWhitelistUuid(String uuid) { + private OperationResult addWhitelistUuid(String uuid) { return updateWhitelist("whitelist.uuids", uuid.toLowerCase(Locale.ROOT), true); } - private boolean addWhitelistName(String name) { + private OperationResult addWhitelistName(String name) { return updateWhitelist("whitelist.names", normalizeName(name), true); } - private boolean removeWhitelistUuid(String uuid) { + private OperationResult removeWhitelistUuid(String uuid) { return updateWhitelist("whitelist.uuids", uuid.toLowerCase(Locale.ROOT), false); } - private boolean removeWhitelistName(String name) { + private OperationResult removeWhitelistName(String name) { return updateWhitelist("whitelist.names", normalizeName(name), false); } - private boolean updateWhitelist(String path, String value, boolean add) { + private OperationResult updateWhitelist(String path, String value, boolean add) { if (value == null || value.isBlank()) { - return false; + return OperationResult.UNCHANGED; } + List previousEntries = new ArrayList<>(config.getStringList(path)); Set entries = path.endsWith(".names") ? getWhitelistNames() : getWhitelistUuids(); boolean changed; if (add) { @@ -277,15 +290,20 @@ public final class MaintenanceManager { changed = entries.remove(value); } - if (changed) { - config.set(path, new ArrayList<>(entries)); - save(); - refreshBossBar(); - if (isEnabled()) { - kickUnauthorizedPlayers(); - } + if (!changed) { + return OperationResult.UNCHANGED; } - return changed; + + config.set(path, new ArrayList<>(entries)); + if (!save()) { + config.set(path, previousEntries); + return OperationResult.SAVE_FAILED; + } + refreshBossBar(); + if (isEnabled()) { + kickUnauthorizedPlayers(); + } + return OperationResult.SUCCESS; } private Set getWhitelistUuids() { @@ -340,11 +358,19 @@ public final class MaintenanceManager { return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text); } - private void save() { + private boolean save() { try { AtomicYamlWriter.save(config, configFile); + return true; } catch (Exception e) { plugin.getLogger().warning("保存 maintenance.yml 失败: " + e.getMessage()); + return false; } } + + public enum OperationResult { + SUCCESS, + UNCHANGED, + SAVE_FAILED + } } diff --git a/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java b/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java index 656001b..cac32c3 100644 --- a/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java +++ b/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java @@ -16,6 +16,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; import java.io.File; import java.net.URI; @@ -37,6 +38,9 @@ import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; @@ -55,8 +59,11 @@ public final class SkinBridgeManager implements Listener { private final ConcurrentMap cache = new ConcurrentHashMap<>(); private final ConcurrentMap generatedSkinCache = new ConcurrentHashMap<>(); private final ConcurrentMap pendingLookups = new ConcurrentHashMap<>(); + private final ConcurrentMap scheduledLookups = new ConcurrentHashMap<>(); + private final ConcurrentMap> runningLookups = new ConcurrentHashMap<>(); private final ConcurrentMap forceRefreshCooldowns = new ConcurrentHashMap<>(); private final ConcurrentMap loginSkinUrls = new ConcurrentHashMap<>(); + private final ConcurrentMap> pendingGenerations = new ConcurrentHashMap<>(); private final AtomicLong configurationGeneration = new AtomicLong(); private volatile List providers = List.of(); @@ -66,6 +73,7 @@ public final class SkinBridgeManager implements Listener { private volatile boolean logDetectionResults; private volatile int requestTimeoutSeconds; private volatile int cacheMinutes; + private volatile int maxGeneratedCacheEntries; private volatile int forceRefreshCooldownSeconds; private volatile boolean requireCurrentTextureMatch; private volatile long joinDelayTicks; @@ -94,6 +102,10 @@ public final class SkinBridgeManager implements Listener { public void reload() { configurationGeneration.incrementAndGet(); + cancelScheduledLookups(); + cancelRunningLookups(); + pendingLookups.clear(); + pendingGenerations.clear(); FileConfiguration config = plugin.getConfig(); debug = plugin.getConfig().getBoolean("debug", false); @@ -107,6 +119,7 @@ public final class SkinBridgeManager implements Listener { long minimumSubmitIntervalMillis = clamp( config.getLong("skin-bridge.mineskin.minimum-submit-interval-millis", 1000L), 0L, 10000L); cacheMinutes = clamp(config.getInt("skin-bridge.cache-minutes", 120), 5, 10080); + maxGeneratedCacheEntries = clamp(config.getInt("skin-bridge.max-generated-cache-entries", 500), 10, 10_000); forceRefreshCooldownSeconds = clamp(config.getInt("skin-bridge.force-refresh-cooldown-seconds", 30), 0, 3600); requireCurrentTextureMatch = config.getBoolean("skin-bridge.require-current-texture-match", true); joinDelayTicks = clamp(config.getLong("skin-bridge.join-delay-ticks", 20L), 0, 200); @@ -124,11 +137,14 @@ public final class SkinBridgeManager implements Listener { public void shutdown() { configurationGeneration.incrementAndGet(); + cancelScheduledLookups(); + cancelRunningLookups(); executor.shutdownNow(); saveGeneratedSkinCache(); cache.clear(); generatedSkinCache.clear(); pendingLookups.clear(); + pendingGenerations.clear(); forceRefreshCooldowns.clear(); loginSkinUrls.clear(); } @@ -145,7 +161,11 @@ public final class SkinBridgeManager implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - loginSkinUrls.remove(event.getPlayer().getUniqueId()); + UUID playerId = event.getPlayer().getUniqueId(); + loginSkinUrls.remove(playerId); + pendingLookups.remove(playerId); + cancelScheduledLookup(playerId); + cancelRunningLookup(playerId); } public SyncResult queueSync(Player player, boolean force) { @@ -200,8 +220,18 @@ public final class SkinBridgeManager implements Listener { String loginSkinUrl = loginSkinUrls.computeIfAbsent(playerId, ignored -> getCurrentSkinUrl(player).orElse("")); String currentSkinUrl = loginSkinUrl.isEmpty() ? null : loginSkinUrl; - Bukkit.getScheduler().runTaskLater(plugin, - () -> startLookup(playerId, playerName, currentSkinUrl, lookupGeneration), joinDelayTicks); + BukkitRunnable scheduledLookup = new BukkitRunnable() { + @Override + public void run() { + scheduledLookups.remove(playerId, this); + startLookup(playerId, playerName, currentSkinUrl, lookupGeneration); + } + }; + BukkitRunnable previousLookup = scheduledLookups.put(playerId, scheduledLookup); + if (previousLookup != null) { + previousLookup.cancel(); + } + scheduledLookup.runTaskLater(plugin, joinDelayTicks); sendPlayerNotification(playerId, "skin-bridge.notifications.detecting", Map.of()); return SyncResult.QUEUED; } @@ -241,15 +271,18 @@ public final class SkinBridgeManager implements Listener { } private void startLookup(UUID playerId, String playerName, String currentSkinUrl, long lookupGeneration) { - if (executor.isShutdown() || lookupGeneration != configurationGeneration.get()) { + Player player = Bukkit.getPlayer(playerId); + if (executor.isShutdown() || lookupGeneration != configurationGeneration.get() + || !isLookupActive(playerId, lookupGeneration) || player == null || !player.isOnline()) { pendingLookups.remove(playerId, lookupGeneration); return; } + LookupKey lookupKey = new LookupKey(playerId, lookupGeneration); try { - executor.execute(() -> { + Future lookupTask = executor.submit(() -> { try { CachedLookup resolved = resolve(playerId, playerName, currentSkinUrl); - if (lookupGeneration != configurationGeneration.get()) { + if (lookupGeneration != configurationGeneration.get() || !isLookupActive(playerId, lookupGeneration)) { return; } cache.put(playerId, resolved); @@ -262,6 +295,9 @@ public final class SkinBridgeManager implements Listener { } } } catch (Exception exception) { + if (!isLookupActive(playerId, lookupGeneration) || exception instanceof InterruptedException) { + return; + } plugin.getLogger().warning("SkinBridge 查询 " + playerName + " 的皮肤资料失败: " + exception.getMessage()); sendPlayerNotification(playerId, "skin-bridge.notifications.failed", Map.of()); if (debug) { @@ -269,8 +305,15 @@ public final class SkinBridgeManager implements Listener { } } finally { pendingLookups.remove(playerId, lookupGeneration); + runningLookups.remove(lookupKey); } }); + runningLookups.put(lookupKey, lookupTask); + if (!isLookupActive(playerId, lookupGeneration)) { + runningLookups.remove(lookupKey, lookupTask); + lookupTask.cancel(true); + executor.purge(); + } } catch (RejectedExecutionException exception) { pendingLookups.remove(playerId, lookupGeneration); plugin.getLogger().warning("SkinBridge 查询队列拒绝了玩家任务: " + playerName); @@ -325,17 +368,7 @@ public final class SkinBridgeManager implements Listener { plugin.getLogger().info("SkinBridge 已识别玩家 " + playerName + " 的皮肤来源: " + provider.name()); } SkinCacheKey cacheKey = new SkinCacheKey(matchedProfile.skinUrl(), matchedProfile.model()); - GeneratedCacheEntry generatedEntry = generatedSkinCache.get(cacheKey); - GeneratedSkin generatedSkin; - if (generatedEntry != null && !generatedEntry.hasExpired()) { - generatedSkin = generatedEntry.skin(); - } else { - generatedSkinCache.remove(cacheKey); - generatedSkin = currentGateway.generateSkin(matchedProfile.skinUrl(), matchedProfile.model()); - generatedSkinCache.put(cacheKey, new GeneratedCacheEntry(generatedSkin, - System.currentTimeMillis() + Duration.ofMinutes(cacheMinutes).toMillis())); - saveGeneratedSkinCache(); - } + GeneratedSkin generatedSkin = getOrGenerateSkin(cacheKey, currentGateway); return cached(provider.name(), generatedSkin, State.EXTERNAL); } @@ -472,12 +505,14 @@ public final class SkinBridgeManager implements Listener { plugin.getLogger().warning("忽略无效的 SkinBridge 缓存记录: " + id); } } + trimGeneratedSkinCache(); } private synchronized void saveGeneratedSkinCache() { FileConfiguration config = new YamlConfiguration(); long now = System.currentTimeMillis(); generatedSkinCache.entrySet().removeIf(entry -> entry.getValue().expiresAtMillis() <= now); + trimGeneratedSkinCache(); generatedSkinCache.forEach((key, entry) -> { String path = "entries." + cacheId(key); config.set(path + ".skin-url", key.skinUrl()); @@ -503,6 +538,111 @@ public final class SkinBridgeManager implements Listener { } } + private GeneratedSkin getOrGenerateSkin(SkinCacheKey cacheKey, SkinBridgeGateway currentGateway) throws Exception { + GeneratedCacheEntry cachedEntry = generatedSkinCache.get(cacheKey); + if (cachedEntry != null && !cachedEntry.hasExpired()) { + return cachedEntry.skin(); + } + if (cachedEntry != null) { + generatedSkinCache.remove(cacheKey, cachedEntry); + } + + CompletableFuture created = new CompletableFuture<>(); + CompletableFuture running = pendingGenerations.putIfAbsent(cacheKey, created); + if (running != null) { + return awaitGeneratedSkin(running); + } + + try { + GeneratedSkin generated = currentGateway.generateSkin(cacheKey.skinUrl(), cacheKey.model()); + cacheGeneratedSkin(cacheKey, generated); + created.complete(generated); + return generated; + } catch (Exception exception) { + created.completeExceptionally(exception); + throw exception; + } catch (Error error) { + created.completeExceptionally(error); + throw error; + } finally { + pendingGenerations.remove(cacheKey, created); + } + } + + private GeneratedSkin awaitGeneratedSkin(CompletableFuture running) throws Exception { + try { + return running.get(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw exception; + } catch (ExecutionException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof Exception nested) { + throw nested; + } + if (cause instanceof Error error) { + throw error; + } + throw new IllegalStateException("MineSkin 生成任务失败。", cause); + } + } + + private synchronized void cacheGeneratedSkin(SkinCacheKey cacheKey, GeneratedSkin generatedSkin) { + generatedSkinCache.put(cacheKey, new GeneratedCacheEntry(generatedSkin, + System.currentTimeMillis() + Duration.ofMinutes(cacheMinutes).toMillis())); + trimGeneratedSkinCache(); + saveGeneratedSkinCache(); + } + + private void trimGeneratedSkinCache() { + long now = System.currentTimeMillis(); + generatedSkinCache.entrySet().removeIf(entry -> entry.getValue().expiresAtMillis() <= now); + int excessEntries = generatedSkinCache.size() - maxGeneratedCacheEntries; + if (excessEntries <= 0) { + return; + } + generatedSkinCache.entrySet().stream() + .sorted(Comparator.comparingLong(entry -> entry.getValue().expiresAtMillis())) + .limit(excessEntries) + .map(Map.Entry::getKey) + .forEach(generatedSkinCache::remove); + } + + private boolean isLookupActive(UUID playerId, long lookupGeneration) { + return Long.valueOf(lookupGeneration).equals(pendingLookups.get(playerId)); + } + + private void cancelScheduledLookup(UUID playerId) { + BukkitRunnable scheduledLookup = scheduledLookups.remove(playerId); + if (scheduledLookup != null) { + scheduledLookup.cancel(); + } + } + + private void cancelScheduledLookups() { + for (UUID playerId : List.copyOf(scheduledLookups.keySet())) { + cancelScheduledLookup(playerId); + } + } + + private void cancelRunningLookup(UUID playerId) { + runningLookups.forEach((lookupKey, lookupTask) -> { + if (lookupKey.playerId().equals(playerId) && runningLookups.remove(lookupKey, lookupTask)) { + lookupTask.cancel(true); + } + }); + executor.purge(); + } + + private void cancelRunningLookups() { + runningLookups.forEach((lookupKey, lookupTask) -> { + if (runningLookups.remove(lookupKey, lookupTask)) { + lookupTask.cancel(true); + } + }); + executor.purge(); + } + private List loadProviders(FileConfiguration config) { ConfigurationSection providersSection = config.getConfigurationSection("skin-bridge.providers"); if (providersSection == null) { @@ -655,6 +795,9 @@ public final class SkinBridgeManager implements Listener { private record SkinCacheKey(String skinUrl, SkinModel model) { } + private record LookupKey(UUID playerId, long generation) { + } + private record GeneratedCacheEntry(GeneratedSkin skin, long expiresAtMillis) { private boolean hasExpired() { return System.currentTimeMillis() >= expiresAtMillis; diff --git a/src/main/java/cn/infstar/essentialsC/teleport/TeleportRequestManager.java b/src/main/java/cn/infstar/essentialsC/teleport/TeleportRequestManager.java index 986a517..f95103b 100644 --- a/src/main/java/cn/infstar/essentialsC/teleport/TeleportRequestManager.java +++ b/src/main/java/cn/infstar/essentialsC/teleport/TeleportRequestManager.java @@ -230,24 +230,24 @@ public final class TeleportRequestManager implements Listener { } public List getIncomingRequesterNames(Player target, String partial) { - String partialLower = partial == null ? "" : partial.toLowerCase(); + String partialLower = partial == null ? "" : partial.toLowerCase(Locale.ROOT); return requests.getOrDefault(target.getUniqueId(), new ArrayDeque<>()).stream() .filter(request -> !request.hasExpired()) .map(TeleportRequest::requesterName) - .filter(name -> name.toLowerCase().startsWith(partialLower)) + .filter(name -> name.toLowerCase(Locale.ROOT).startsWith(partialLower)) .distinct() .sorted(String.CASE_INSENSITIVE_ORDER) .toList(); } public List getOutgoingTargetNames(Player requester, String partial) { - String partialLower = partial == null ? "" : partial.toLowerCase(); + String partialLower = partial == null ? "" : partial.toLowerCase(Locale.ROOT); return requests.values().stream() .flatMap(Deque::stream) .filter(request -> request.requesterId().equals(requester.getUniqueId())) .filter(request -> !request.hasExpired()) .map(TeleportRequest::targetName) - .filter(name -> name.toLowerCase().startsWith(partialLower)) + .filter(name -> name.toLowerCase(Locale.ROOT).startsWith(partialLower)) .distinct() .sorted(String.CASE_INSENSITIVE_ORDER) .toList(); @@ -344,7 +344,7 @@ public final class TeleportRequestManager implements Listener { .orElse(false); } - public boolean toggleIgnoringRequests(Player player) { + public ToggleIgnoreResult toggleIgnoringRequests(Player player) { UUID uuid = player.getUniqueId(); boolean nowIgnoring; if (ignoringRequests.contains(uuid)) { @@ -354,8 +354,15 @@ public final class TeleportRequestManager implements Listener { ignoringRequests.add(uuid); nowIgnoring = true; } - saveIgnoringRequests(); - return nowIgnoring; + if (!saveIgnoringRequests()) { + if (nowIgnoring) { + ignoringRequests.remove(uuid); + } else { + ignoringRequests.add(uuid); + } + return ToggleIgnoreResult.SAVE_FAILED; + } + return nowIgnoring ? ToggleIgnoreResult.ENABLED : ToggleIgnoreResult.DISABLED; } public void playRequestReceivedSound(Player player) { @@ -395,11 +402,11 @@ public final class TeleportRequestManager implements Listener { return exactMatch; } - String playerNameLower = playerName.toLowerCase(); + String playerNameLower = playerName.toLowerCase(Locale.ROOT); return Bukkit.getOnlinePlayers().stream() .map(Player.class::cast) .filter(filter) - .filter(player -> player.getName().toLowerCase().startsWith(playerNameLower)) + .filter(player -> player.getName().toLowerCase(Locale.ROOT).startsWith(playerNameLower)) .findFirst(); } @@ -750,7 +757,7 @@ public final class TeleportRequestManager implements Listener { placeholders.put("requester", request.requesterName()); placeholders.put("target", request.targetName()); placeholders.put("seconds", String.valueOf(timeoutSeconds)); - placeholders.put("type", request.type().name().toLowerCase()); + placeholders.put("type", request.type().name().toLowerCase(Locale.ROOT)); return placeholders; } @@ -888,18 +895,26 @@ public final class TeleportRequestManager implements Listener { } } - private void saveIgnoringRequests() { + private boolean saveIgnoringRequests() { FileConfiguration ignoreConfig = new YamlConfiguration(); for (UUID uuid : ignoringRequests) { ignoreConfig.set("ignored." + uuid, true); } try { AtomicYamlWriter.save(ignoreConfig, ignoreFile); + return true; } catch (Exception e) { plugin.getLogger().warning("保存 teleport-ignore.yml 失败: " + e.getMessage()); + return false; } } + public enum ToggleIgnoreResult { + ENABLED, + DISABLED, + SAVE_FAILED + } + public enum CreateRequestStatus { SUCCESS, DUPLICATE, diff --git a/src/main/java/cn/infstar/essentialsC/util/AtomicYamlWriter.java b/src/main/java/cn/infstar/essentialsC/util/AtomicYamlWriter.java index 3097f47..d5d7acc 100644 --- a/src/main/java/cn/infstar/essentialsC/util/AtomicYamlWriter.java +++ b/src/main/java/cn/infstar/essentialsC/util/AtomicYamlWriter.java @@ -22,7 +22,11 @@ public final class AtomicYamlWriter { Files.createDirectories(parent); } - Path temporary = Files.createTempFile(parent, targetFile.getName(), ".tmp"); + String temporaryPrefix = targetFile.getName(); + if (temporaryPrefix.length() < 3) { + temporaryPrefix = (temporaryPrefix + "___").substring(0, 3); + } + Path temporary = Files.createTempFile(parent, temporaryPrefix, ".tmp"); try { Files.writeString(temporary, configuration.saveToString(), StandardCharsets.UTF_8); try { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ce3173b..816fefc 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -79,6 +79,8 @@ skin-bridge: join-delay-ticks: 20 # 检测结果与生成皮肤的内存缓存时间,单位为分钟。 cache-minutes: 120 + # 持久化 MineSkin 纹理缓存的最大条目数,超出时优先清理最早过期的记录。 + max-generated-cache-entries: 500 # 强制刷新同一玩家皮肤的冷却时间,单位为秒。 force-refresh-cooldown-seconds: 30 # 仅当 Provider 纹理与玩家登录时携带的纹理一致时,才确认其来自该皮肤站。 diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml index 8056507..09355b4 100644 --- a/src/main/resources/lang/en_US.yml +++ b/src/main/resources/lang/en_US.yml @@ -28,6 +28,7 @@ messages: glow-usage: '<#FF3300>Error: <#FF7E5E>Usage: /glow [on|off|toggle]' vanish-enabled: '<#00FB9A>You are now vanished.' vanish-disabled: '<#FF7E5E>You are no longer vanished.' + vanish-save-failed: '<#FF3300>Error: <#FF7E5E>Failed to save vanish state; no changes were applied.' vanish-permission-removed: '<#FFC43B>Your vanish permission was removed, so vanish mode was disabled.' seen-usage: '<#FF3300>Error: <#FF7E5E>Usage: /seen [player]' seen-usage-console: '<#FF3300>Error: <#FF7E5E>Usage: /seen [player]' @@ -59,6 +60,8 @@ messages: mobdrop-toggled: '<#00FB9A>Enderman drops are now {status}.' shulkerbox-nested: '<#FF3300>Error: <#FF7E5E>You cannot put a shulker box inside another shulker box.' shulkerbox-unstack-first: '<#FF3300>Error: <#FF7E5E>Please unstack the shulker box before quick opening it.' + shulkerbox-open-failed: '<#FF3300>Error: <#FF7E5E>Failed to open the shulker box. Please try again.' + shulkerbox-session-invalid: '<#FF3300>Error: <#FF7E5E>The shulker box session became invalid. Saving was stopped to prevent item duplication.' jei-sync-fabric: '<#00FB9A>Synchronizing recipes for your Fabric client…' jei-sync-neoforge: '<#00FB9A>Synchronizing recipes for your NeoForge client…' @@ -138,6 +141,7 @@ tpa: teleport-failed: '<#FF3300>Error: <#FF7E5E>Teleportation could not be completed. Please try again.' ignore-enabled: '<#00FB9A>Now <#00FB9A>ignoring <#00FB9A>incoming teleport requests.' ignore-disabled: '<#00FB9A>Now <#00FB9A>listening <#00FB9A>to incoming teleport requests.' + ignore-save-failed: '<#FF3300>Error: <#FF7E5E>Failed to save the teleport request ignore state; no changes were applied.' ignore-notification: 'You are currently ignoring teleport requests.' tpaall-sent: '<#00FB9A>You sent a teleport request to every player asking them to teleport to you.' tpaall-no-targets: '<#FF3300>Error: <#FF7E5E>There are no other players online.' @@ -260,6 +264,7 @@ maintenance: enabled: '<#00FB9A>Maintenance mode enabled.' disabled: '<#FF7E5E>Maintenance mode disabled.' reloaded: '<#00FB9A>Maintenance configuration reloaded.' + save-failed: '<#FF3300>Error: <#FF7E5E>Failed to save maintenance configuration; no changes were applied.' status: 'Maintenance mode status: {status} | Whitelist: {whitelist_count}' usage: '<#FF3300>Error: <#FF7E5E>Usage: /maintenance [on|off|status|reload|add|remove|list]' add-usage: '<#FF3300>Error: <#FF7E5E>Usage: /maintenance add [player|uuid]' diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml index a544181..02bd0f9 100644 --- a/src/main/resources/lang/zh_CN.yml +++ b/src/main/resources/lang/zh_CN.yml @@ -28,6 +28,7 @@ messages: glow-usage: '<#FF3300>错误: <#FF7E5E>用法: /glow [on|off|toggle]' vanish-enabled: '<#00FB9A>你已进入隐身模式.' vanish-disabled: '<#FF7E5E>你已退出隐身模式.' + vanish-save-failed: '<#FF3300>错误: <#FF7E5E>无法保存隐身状态,本次操作未生效.' vanish-permission-removed: '<#FFC43B>你的隐身权限已被移除,隐身模式已自动关闭.' seen-usage: '<#FF3300>错误: <#FF7E5E>用法: /seen [玩家]' seen-usage-console: '<#FF3300>错误: <#FF7E5E>用法: /seen [玩家]' @@ -59,6 +60,8 @@ messages: mobdrop-toggled: '<#00FB9A>末影人掉落已切换为 {status}.' shulkerbox-nested: '<#FF3300>错误: <#FF7E5E>不能将潜影盒放入另一个潜影盒.' shulkerbox-unstack-first: '<#FF3300>错误: <#FF7E5E>请先将潜影盒拆分为单个后再快捷打开.' + shulkerbox-open-failed: '<#FF3300>错误: <#FF7E5E>潜影盒打开失败,请稍后重试.' + shulkerbox-session-invalid: '<#FF3300>错误: <#FF7E5E>潜影盒会话异常,已停止保存以避免物品复制.' jei-sync-fabric: '<#00FB9A>正在为 Fabric 客户端同步配方…' jei-sync-neoforge: '<#00FB9A>正在为 NeoForge 客户端同步配方…' @@ -138,6 +141,7 @@ tpa: teleport-failed: '<#FF3300>错误: <#FF7E5E>无法完成传送,请稍后重试' ignore-enabled: '<#00FB9A>你已 <#00FB9A>忽略 <#00FB9A>传送请求' ignore-disabled: '<#00FB9A>你已 <#00FB9A>恢复接收 <#00FB9A>传送请求' + ignore-save-failed: '<#FF3300>错误: <#FF7E5E>无法保存传送请求忽略状态,本次操作未生效.' ignore-notification: '你当前正在忽略传送请求.' tpaall-sent: '<#00FB9A>你已向所有玩家发送传送请求,请求他们传送到你的位置' tpaall-no-targets: '<#FF3300>错误: <#FF7E5E>当前没有其他在线玩家.' @@ -260,6 +264,7 @@ maintenance: enabled: '<#00FB9A>维护模式已开启.' disabled: '<#FF7E5E>维护模式已关闭.' reloaded: '<#00FB9A>维护模式配置已重载.' + save-failed: '<#FF3300>错误: <#FF7E5E>维护模式配置保存失败,操作未生效.' status: '维护模式当前状态: {status} | 白名单: {whitelist_count}' usage: '<#FF3300>错误: <#FF7E5E>用法: /maintenance [on|off|status|reload|add|remove|list]' add-usage: '<#FF3300>错误: <#FF7E5E>用法: /maintenance add [玩家|UUID]' diff --git a/src/test/java/cn/infstar/essentialsC/ConfigurationResourcesTest.java b/src/test/java/cn/infstar/essentialsC/ConfigurationResourcesTest.java index 56ccfba..b5f8ced 100644 --- a/src/test/java/cn/infstar/essentialsC/ConfigurationResourcesTest.java +++ b/src/test/java/cn/infstar/essentialsC/ConfigurationResourcesTest.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.api.Test; import java.io.InputStream; import java.io.InputStreamReader; @@ -10,6 +11,7 @@ import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class ConfigurationResourcesTest { @@ -32,10 +34,29 @@ class ConfigurationResourcesTest { if (resourcePath.equals("config.yml")) { assertEquals(2, configuration.getInt("config-version")); assertEquals(5, configuration.getInt("tpa.max-pending-requests")); + assertEquals(500, configuration.getInt("skin-bridge.max-generated-cache-entries")); + assertTrue(configuration.getString("skin-bridge.mineskin.api-key", "").isBlank()); } if (resourcePath.equals("paper-plugin.yml")) { assertEquals("1.21.11", configuration.getString("api-version")); } } } + + @Test + void bundledLanguagesContainTheSameKeys() throws Exception { + YamlConfiguration chinese = loadResource("lang/zh_CN.yml"); + YamlConfiguration english = loadResource("lang/en_US.yml"); + + assertEquals(chinese.getKeys(true), english.getKeys(true)); + } + + private YamlConfiguration loadResource(String resourcePath) throws Exception { + try (InputStream input = getClass().getClassLoader().getResourceAsStream(resourcePath)) { + assertNotNull(input, resourcePath); + YamlConfiguration configuration = new YamlConfiguration(); + configuration.load(new InputStreamReader(input, StandardCharsets.UTF_8)); + return configuration; + } + } } diff --git a/src/test/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicyTest.java b/src/test/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicyTest.java new file mode 100644 index 0000000..045896d --- /dev/null +++ b/src/test/java/cn/infstar/essentialsC/listeners/ShulkerBoxSessionPolicyTest.java @@ -0,0 +1,25 @@ +package cn.infstar.essentialsC.listeners; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ShulkerBoxSessionPolicyTest { + + @Test + void identifiesOnlySlotsInsideTopInventory() { + assertFalse(ShulkerBoxSessionPolicy.isTopSlot(-999, 27)); + assertTrue(ShulkerBoxSessionPolicy.isTopSlot(0, 27)); + assertTrue(ShulkerBoxSessionPolicy.isTopSlot(26, 27)); + assertFalse(ShulkerBoxSessionPolicy.isTopSlot(27, 27)); + } + + @Test + void detectsDragsThatTouchTopInventory() { + assertTrue(ShulkerBoxSessionPolicy.touchesTopInventory(Set.of(5, 30), 27)); + assertFalse(ShulkerBoxSessionPolicy.touchesTopInventory(Set.of(27, 35), 27)); + } +} diff --git a/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java b/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java new file mode 100644 index 0000000..8c822fb --- /dev/null +++ b/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java @@ -0,0 +1,61 @@ +package cn.infstar.essentialsC.util; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AtomicYamlWriterTest { + + @TempDir + Path temporaryDirectory; + + @Test + void createsParentDirectoriesAndWritesUtf8Yaml() throws Exception { + Path target = temporaryDirectory.resolve("nested/data.yml"); + YamlConfiguration configuration = new YamlConfiguration(); + configuration.set("message", "中文内容"); + + AtomicYamlWriter.save(configuration, target.toFile()); + + assertTrue(Files.exists(target)); + assertTrue(Files.readString(target, StandardCharsets.UTF_8).contains("中文内容")); + try (var files = Files.list(target.getParent())) { + assertFalse(files.anyMatch(path -> path.getFileName().toString().endsWith(".tmp"))); + } + } + + @Test + void replacesExistingYamlWithoutRetainingOldValues() throws Exception { + Path target = temporaryDirectory.resolve("state.yml"); + YamlConfiguration initial = new YamlConfiguration(); + initial.set("old-value", true); + AtomicYamlWriter.save(initial, target.toFile()); + + YamlConfiguration replacement = new YamlConfiguration(); + replacement.set("new-value", 42); + AtomicYamlWriter.save(replacement, target.toFile()); + + YamlConfiguration loaded = YamlConfiguration.loadConfiguration(target.toFile()); + assertFalse(loaded.contains("old-value")); + assertEquals(42, loaded.getInt("new-value")); + } + + @Test + void supportsShortTargetFileNames() throws Exception { + Path target = temporaryDirectory.resolve("x"); + YamlConfiguration configuration = new YamlConfiguration(); + configuration.set("written", true); + + AtomicYamlWriter.save(configuration, target.toFile()); + + assertTrue(YamlConfiguration.loadConfiguration(target.toFile()).getBoolean("written")); + } +} diff --git a/versions/v1_21_11/src/main/java/cn/infstar/essentialsC/v1_21_11/jei/JeiRecipeSyncAdapterImpl.java b/versions/v1_21_11/src/main/java/cn/infstar/essentialsC/v1_21_11/jei/JeiRecipeSyncAdapterImpl.java index 78297da..e23042e 100644 --- a/versions/v1_21_11/src/main/java/cn/infstar/essentialsC/v1_21_11/jei/JeiRecipeSyncAdapterImpl.java +++ b/versions/v1_21_11/src/main/java/cn/infstar/essentialsC/v1_21_11/jei/JeiRecipeSyncAdapterImpl.java @@ -22,6 +22,9 @@ import java.util.logging.Logger; public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { + private DiscardedPayload fabricPayloadCache; + private DiscardedPayload neoForgePayloadCache; + @Override @SuppressWarnings({"unchecked", "deprecation"}) public void sendFabricRecipeSync(Player player, Logger logger, boolean debug) { @@ -33,6 +36,10 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } return; } + if (fabricPayloadCache != null) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(fabricPayloadCache)); + return; + } RecipeMap recipeMap = server.getRecipeManager().recipes; if (debug) { @@ -66,13 +73,18 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), server.registryAccess()); - getFabricCodec().encode(buffer, payload); - - byte[] bytes = new byte[buffer.writerIndex()]; - buffer.getBytes(0, bytes); + byte[] bytes; + try { + getFabricCodec().encode(buffer, payload); + bytes = new byte[buffer.writerIndex()]; + buffer.getBytes(0, bytes); + } finally { + buffer.release(); + } Identifier id = Identifier.fromNamespaceAndPath("fabric", "recipe_sync"); DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes); + fabricPayloadCache = discardedPayload; serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload)); if (debug) { @@ -91,6 +103,11 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } return; } + if (neoForgePayloadCache != null) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(neoForgePayloadCache)); + sendTags(serverPlayer, server); + return; + } RecipeMap recipeMap = server.getRecipeManager().recipes; if (debug) { @@ -105,23 +122,38 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { var payload = createNeoForgePayload(allRecipeTypes, recipeMap); RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), server.registryAccess()); - getNeoForgeStreamCodec().encode(buffer, payload); - - byte[] bytes = new byte[buffer.writerIndex()]; - buffer.getBytes(0, bytes); + byte[] bytes; + try { + getNeoForgeStreamCodec().encode(buffer, payload); + bytes = new byte[buffer.writerIndex()]; + buffer.getBytes(0, bytes); + } finally { + buffer.release(); + } Identifier id = Identifier.fromNamespaceAndPath("neoforge", "recipe_content"); DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes); + neoForgePayloadCache = discardedPayload; serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload)); - serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket( - net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries()) - )); + sendTags(serverPlayer, server); if (debug) { logger.info("已发送 NeoForge 配方同步 [" + id + "], 大小: " + bytes.length + " bytes"); } } + @Override + public void clearCache() { + fabricPayloadCache = null; + neoForgePayloadCache = null; + } + + private void sendTags(ServerPlayer serverPlayer, MinecraftServer server) { + serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket( + net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries()) + )); + } + @SuppressWarnings({"unchecked", "deprecation"}) private net.minecraft.network.codec.StreamCodec getFabricCodec() { return FabricRecipeEntry.CODEC.apply(net.minecraft.network.codec.ByteBufCodecs.list()) diff --git a/versions/v26_1_2/src/main/java/cn/infstar/essentialsC/v26_1_2/jei/JeiRecipeSyncAdapterImpl.java b/versions/v26_1_2/src/main/java/cn/infstar/essentialsC/v26_1_2/jei/JeiRecipeSyncAdapterImpl.java index 3a1f060..f40b85f 100644 --- a/versions/v26_1_2/src/main/java/cn/infstar/essentialsC/v26_1_2/jei/JeiRecipeSyncAdapterImpl.java +++ b/versions/v26_1_2/src/main/java/cn/infstar/essentialsC/v26_1_2/jei/JeiRecipeSyncAdapterImpl.java @@ -22,6 +22,9 @@ import java.util.logging.Logger; public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { + private DiscardedPayload fabricPayloadCache; + private DiscardedPayload neoForgePayloadCache; + @Override @SuppressWarnings({"unchecked", "deprecation"}) public void sendFabricRecipeSync(Player player, Logger logger, boolean debug) { @@ -33,6 +36,10 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } return; } + if (fabricPayloadCache != null) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(fabricPayloadCache)); + return; + } RecipeMap recipeMap = server.getRecipeManager().recipes; if (debug) { @@ -66,13 +73,18 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), server.registryAccess()); - getFabricCodec().encode(buffer, payload); - - byte[] bytes = new byte[buffer.writerIndex()]; - buffer.getBytes(0, bytes); + byte[] bytes; + try { + getFabricCodec().encode(buffer, payload); + bytes = new byte[buffer.writerIndex()]; + buffer.getBytes(0, bytes); + } finally { + buffer.release(); + } Identifier id = Identifier.fromNamespaceAndPath("fabric", "recipe_sync"); DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes); + fabricPayloadCache = discardedPayload; serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload)); if (debug) { @@ -91,6 +103,11 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } return; } + if (neoForgePayloadCache != null) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(neoForgePayloadCache)); + sendTags(serverPlayer, server); + return; + } RecipeMap recipeMap = server.getRecipeManager().recipes; if (debug) { @@ -105,23 +122,38 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { var payload = createNeoForgePayload(allRecipeTypes, recipeMap); RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), server.registryAccess()); - getNeoForgeStreamCodec().encode(buffer, payload); - - byte[] bytes = new byte[buffer.writerIndex()]; - buffer.getBytes(0, bytes); + byte[] bytes; + try { + getNeoForgeStreamCodec().encode(buffer, payload); + bytes = new byte[buffer.writerIndex()]; + buffer.getBytes(0, bytes); + } finally { + buffer.release(); + } Identifier id = Identifier.fromNamespaceAndPath("neoforge", "recipe_content"); DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes); + neoForgePayloadCache = discardedPayload; serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload)); - serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket( - net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries()) - )); + sendTags(serverPlayer, server); if (debug) { logger.info("已发送 NeoForge 配方同步 [" + id + "], 大小: " + bytes.length + " bytes"); } } + @Override + public void clearCache() { + fabricPayloadCache = null; + neoForgePayloadCache = null; + } + + private void sendTags(ServerPlayer serverPlayer, MinecraftServer server) { + serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket( + net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries()) + )); + } + @SuppressWarnings({"unchecked", "deprecation"}) private net.minecraft.network.codec.StreamCodec getFabricCodec() { return FabricRecipeEntry.CODEC.apply(net.minecraft.network.codec.ByteBufCodecs.list()) diff --git a/versions/v26_2/src/main/java/cn/infstar/essentialsC/v26_2/jei/JeiRecipeSyncAdapterImpl.java b/versions/v26_2/src/main/java/cn/infstar/essentialsC/v26_2/jei/JeiRecipeSyncAdapterImpl.java index cb6ac43..5524fb0 100644 --- a/versions/v26_2/src/main/java/cn/infstar/essentialsC/v26_2/jei/JeiRecipeSyncAdapterImpl.java +++ b/versions/v26_2/src/main/java/cn/infstar/essentialsC/v26_2/jei/JeiRecipeSyncAdapterImpl.java @@ -22,6 +22,9 @@ import java.util.logging.Logger; public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { + private DiscardedPayload fabricPayloadCache; + private DiscardedPayload neoForgePayloadCache; + @Override @SuppressWarnings({"unchecked", "deprecation"}) public void sendFabricRecipeSync(Player player, Logger logger, boolean debug) { @@ -33,6 +36,10 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } return; } + if (fabricPayloadCache != null) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(fabricPayloadCache)); + return; + } RecipeMap recipeMap = server.getRecipeManager().recipes; if (debug) { @@ -66,13 +73,18 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), server.registryAccess()); - getFabricCodec().encode(buffer, payload); - - byte[] bytes = new byte[buffer.writerIndex()]; - buffer.getBytes(0, bytes); + byte[] bytes; + try { + getFabricCodec().encode(buffer, payload); + bytes = new byte[buffer.writerIndex()]; + buffer.getBytes(0, bytes); + } finally { + buffer.release(); + } Identifier id = Identifier.fromNamespaceAndPath("fabric", "recipe_sync"); DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes); + fabricPayloadCache = discardedPayload; serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload)); if (debug) { @@ -91,6 +103,11 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { } return; } + if (neoForgePayloadCache != null) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(neoForgePayloadCache)); + sendTags(serverPlayer, server); + return; + } RecipeMap recipeMap = server.getRecipeManager().recipes; if (debug) { @@ -105,23 +122,38 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { var payload = createNeoForgePayload(allRecipeTypes, recipeMap); RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), server.registryAccess()); - getNeoForgeStreamCodec().encode(buffer, payload); - - byte[] bytes = new byte[buffer.writerIndex()]; - buffer.getBytes(0, bytes); + byte[] bytes; + try { + getNeoForgeStreamCodec().encode(buffer, payload); + bytes = new byte[buffer.writerIndex()]; + buffer.getBytes(0, bytes); + } finally { + buffer.release(); + } Identifier id = Identifier.fromNamespaceAndPath("neoforge", "recipe_content"); DiscardedPayload discardedPayload = new DiscardedPayload(id, bytes); + neoForgePayloadCache = discardedPayload; serverPlayer.connection.send(new ClientboundCustomPayloadPacket(discardedPayload)); - serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket( - net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries()) - )); + sendTags(serverPlayer, server); if (debug) { logger.info("已发送 NeoForge 配方同步 [" + id + "], 大小: " + bytes.length + " bytes"); } } + @Override + public void clearCache() { + fabricPayloadCache = null; + neoForgePayloadCache = null; + } + + private void sendTags(ServerPlayer serverPlayer, MinecraftServer server) { + serverPlayer.connection.send(new net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket( + net.minecraft.tags.TagNetworkSerialization.serializeTagsToNetwork(server.registries()) + )); + } + @SuppressWarnings({"unchecked", "deprecation"}) private net.minecraft.network.codec.StreamCodec getFabricCodec() { return FabricRecipeEntry.CODEC.apply(net.minecraft.network.codec.ByteBufCodecs.list())