diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 46f1477..7271705 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,9 +5,24 @@ updates: schedule: interval: weekly open-pull-requests-limit: 5 + groups: + gradle-dependencies: + patterns: + - "*" + ignore: + # 最低 Paper API 由版本兼容架构固定管理,不能自动升级为最新服务端版本。 + - dependency-name: "io.papermc.paper:paper-api" + # 稳定发布不接受 paperweight 快照版本。 + - dependency-name: "io.papermc.paperweight.userdev" + versions: + - "*-SNAPSHOT" - package-ecosystem: github-actions directory: / schedule: interval: weekly open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80861bf..b57297f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,3 +36,44 @@ jobs: - name: 构建插件 run: ./gradlew clean build --no-daemon + + - name: 上传测试产物 + uses: actions/upload-artifact@v4 + with: + name: EssentialsC-test-build + path: build/libs/EssentialsC-*.jar + if-no-files-found: error + + paper-smoke-test: + name: Paper ${{ matrix.paper }} / Java ${{ matrix.java }} + needs: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - paper: '1.21.11' + java: '21' + - paper: '26.1.2' + java: '25' + - paper: '26.2' + java: '25' + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 设置 Java ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: temurin + + - name: 下载插件产物 + uses: actions/download-artifact@v4 + with: + name: EssentialsC-test-build + path: build/libs + + - name: 启动 Paper 并验证插件 + run: bash scripts/smoke-test-paper.sh '${{ matrix.paper }}' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7e6edbc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# 更新日志 + +## 1.4.0 - 2026-08-02 + +### 新增 + +- 按 HuskHomes 的单服逻辑完整实现 TPA 请求、接受、拒绝、忽略、预热、冷却、位置快照、音效与相关权限。 +- 新增维护模式,包括 MOTD、BossBar、登录拦截、白名单、管理员通知和 LuckPerms 异步权限检查。 +- 新增 SkinBridge,可识别 InfstarMC、LittleSkin 与自定义 Blessing Skin Provider,并通过 MineSkin 与 Paper Profile API 同步皮肤。 +- 新增管理模式独立状态存储、玩家功能状态管理与 JEI 配方同步版本适配。 + +### 优化 + +- 按便捷方块、管理、修复与皮肤功能重新整理运行时模块。 +- 完善潜影盒会话保护、TPA 请求队列、配置迁移、数据原子写入与持久化错误处理。 +- 适配 Paper `1.21.11`、`26.1.2` 与 `26.2`,最终插件保持 Java 21 字节码兼容。 +- 迁移 Paper 弃用 API,并将弃用警告设为构建错误。 +- 统一 HuskHomes 风格的中英文消息、颜色和配置注释。 + +### 注意事项 + +- 最低服务端版本为 Paper `1.21.11`,不支持 Folia 或 Fabric 服务端。 +- LuckPerms 为推荐软依赖;未安装时维护模式会在玩家加入后复核绕过权限。 +- SkinBridge 默认关闭,启用前必须配置有效的 MineSkin API Key。 +- 主配置版本为 `2`。 diff --git a/README.md b/README.md index 997f303..a90c5c4 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,10 @@ 1. 从 [Releases](https://github.com/Coldsmiles/EssentialsC/releases) 下载所需版本。 2. 将插件放入服务端的 `plugins/` 目录。 -3. 启动一次服务端以生成配置文件。 -4. 按需修改 `plugins/EssentialsC/` 下的配置文件与 `lang/` 语言文件。 -5. 如有需要,使用权限插件为玩家授权。 +3. 建议安装 LuckPerms;维护模式会优先通过其缓存异步检查登录权限,未安装时会在玩家加入后立即复核。 +4. 启动一次服务端以生成配置文件。 +5. 按需修改 `plugins/EssentialsC/` 下的配置文件与 `lang/` 语言文件。 +6. 使用权限插件为玩家分配所需权限。 ## 配置说明 @@ -113,6 +114,8 @@ SkinBridge 默认关闭。使用前需启用 `modules.yml` 中的 `modules.skin- 主配置当前使用 `config-version: 2`。SkinBridge 直接配置在 `config.yml` 中,便捷菜单配置仍保存在独立文件中。 +版本变化与升级说明见 [CHANGELOG.md](CHANGELOG.md)。 + ## 权限示例 常用权限节点: diff --git a/build.gradle b/build.gradle index 96aa47e..234fc6a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.language.jvm.tasks.ProcessResources import org.gradle.api.tasks.testing.Test +import java.util.zip.ZipFile plugins { id 'java' @@ -21,6 +22,7 @@ repositories { dependencies { paperweight.paperDevBundle('1.21.11-R0.1-SNAPSHOT') + compileOnly 'net.luckperms:api:5.5' implementation project(':compat-api') implementation 'com.google.code.gson:gson:2.11.0' testImplementation platform('org.junit:junit-bom:5.13.4') @@ -34,8 +36,11 @@ java { paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.getMOJANG_PRODUCTION() -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' +allprojects { + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.compilerArgs.addAll(['-Xlint:deprecation', '-Werror']) + } } tasks.withType(ProcessResources).configureEach { @@ -79,6 +84,39 @@ tasks.named('assemble').configure { dependsOn(tasks.named('shadowJar')) } +tasks.register('verifyJava21Bytecode') { + group = 'verification' + description = '验证最终插件中的全部 class 均可由 Java 21 加载。' + dependsOn(tasks.named('shadowJar')) + + doLast { + def artifact = tasks.named('shadowJar', ShadowJar).get().archiveFile.get().asFile + def unsupportedClasses = [] + new ZipFile(artifact).withCloseable { zip -> + zip.entries().each { entry -> + if (!entry.directory && entry.name.endsWith('.class')) { + zip.getInputStream(entry).withCloseable { input -> + byte[] header = input.readNBytes(8) + if (header.length == 8) { + int majorVersion = ((header[6] & 0xff) << 8) | (header[7] & 0xff) + if (majorVersion > 65) { + unsupportedClasses.add("${entry.name} (class ${majorVersion})") + } + } + } + } + } + } + if (!unsupportedClasses.isEmpty()) { + throw new GradleException("检测到无法由 Java 21 加载的类:\n" + unsupportedClasses.join('\n')) + } + } +} + +tasks.named('check').configure { + dependsOn(tasks.named('verifyJava21Bytecode')) +} + def registerTestServerDeployTask = { String taskName, String serverPath, String serverName -> tasks.register(taskName, Copy) { group = 'deployment' diff --git a/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/RecipeGrouping.java b/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/RecipeGrouping.java new file mode 100644 index 0000000..a8fe22b --- /dev/null +++ b/compat-api/src/main/java/cn/infstar/essentialsC/compat/jei/RecipeGrouping.java @@ -0,0 +1,22 @@ +package cn.infstar.essentialsC.compat.jei; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public final class RecipeGrouping { + + private RecipeGrouping() { + } + + public static Map> groupBy(Iterable values, + Function keyFunction) { + Map> grouped = new LinkedHashMap<>(); + for (V value : values) { + grouped.computeIfAbsent(keyFunction.apply(value), ignored -> new ArrayList<>()).add(value); + } + return grouped; + } +} diff --git a/scripts/smoke-test-paper.sh b/scripts/smoke-test-paper.sh new file mode 100644 index 0000000..46e5cee --- /dev/null +++ b/scripts/smoke-test-paper.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +paper_version="${1:?必须提供 Paper 版本}" +workspace="$(mktemp -d)" +server_pid="" + +cleanup() { + if [[ -n "$server_pid" ]] && kill -0 "$server_pid" 2>/dev/null; then + kill -TERM "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + rm -rf "$workspace" +} +trap cleanup EXIT + +metadata_url="https://fill.papermc.io/v3/projects/paper/versions/${paper_version}/builds/latest" +download_url="$(curl --fail --silent --show-error --location "$metadata_url" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["downloads"]["server:default"]["url"])')" + +curl --fail --silent --show-error --location "$download_url" --output "$workspace/paper.jar" +mkdir -p "$workspace/plugins" +cp build/libs/EssentialsC-*.jar "$workspace/plugins/EssentialsC.jar" +printf 'eula=true\n' > "$workspace/eula.txt" +printf 'online-mode=false\nserver-port=0\nenable-query=false\n' > "$workspace/server.properties" + +( + cd "$workspace" + java -Xms512M -Xmx1G -jar paper.jar --nogui > server.log 2>&1 +) & +server_pid=$! + +for _ in $(seq 1 120); do + if grep -Fq 'EssentialsC v1.4.0 已启用' "$workspace/server.log"; then + if grep -Eq 'UnsupportedClassVersionError|Error occurred while enabling EssentialsC|Could not load.*EssentialsC' "$workspace/server.log"; then + cat "$workspace/server.log" + exit 1 + fi + echo "Paper ${paper_version} 启动验证通过。" + exit 0 + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + cat "$workspace/server.log" + exit 1 + fi + sleep 1 +done + +cat "$workspace/server.log" +echo "Paper ${paper_version} 启动验证超时。" >&2 +exit 1 diff --git a/src/main/java/cn/infstar/essentialsC/EssentialsC.java b/src/main/java/cn/infstar/essentialsC/EssentialsC.java index 3e0b56f..5a12a76 100644 --- a/src/main/java/cn/infstar/essentialsC/EssentialsC.java +++ b/src/main/java/cn/infstar/essentialsC/EssentialsC.java @@ -2,6 +2,7 @@ package cn.infstar.essentialsC; import cn.infstar.essentialsC.admin.AdminModeManager; import cn.infstar.essentialsC.commands.BaseCommand; +import cn.infstar.essentialsC.commands.BlocksMenuCommand; import cn.infstar.essentialsC.commands.CommandRegistry; import cn.infstar.essentialsC.commands.HelpCommand; import cn.infstar.essentialsC.commands.PaperCommand; @@ -13,6 +14,7 @@ import cn.infstar.essentialsC.listeners.ShulkerBoxListener; import cn.infstar.essentialsC.listeners.VanishListener; import cn.infstar.essentialsC.maintenance.MaintenanceListener; import cn.infstar.essentialsC.maintenance.MaintenanceManager; +import cn.infstar.essentialsC.player.PlayerStateManager; import cn.infstar.essentialsC.skinbridge.SkinBridgeManager; import cn.infstar.essentialsC.teleport.TeleportRequestManager; import cn.infstar.essentialsC.tpsbar.TpsBarManager; @@ -40,9 +42,11 @@ public final class EssentialsC extends JavaPlugin { private AdminModeManager adminModeManager; private MaintenanceManager maintenanceManager; private TeleportRequestManager teleportRequestManager; + private PlayerStateManager playerStateManager; private MaintenanceListener maintenanceListener; private TpsBarService tpsBarManager; private ShulkerBoxListener shulkerBoxListener; + private BlocksMenuCommand blocksMenuCommandListener; private JeiRecipeSyncListener jeiRecipeSyncListener; private MobDropListener mobDropListener; private MobDropMenuListener mobDropMenuListener; @@ -53,9 +57,9 @@ public final class EssentialsC extends JavaPlugin { @Override public void onEnable() { + featureConfigManager = new FeatureConfigManager(this); langManager = new LangManager(this); moduleManager = new ModuleManager(this); - featureConfigManager = new FeatureConfigManager(this); reloadRuntimeModules(); registerCommands(); @@ -77,6 +81,9 @@ public final class EssentialsC extends JavaPlugin { if (teleportRequestManager != null) { teleportRequestManager.shutdown(); } + if (playerStateManager != null) { + playerStateManager.shutdown(); + } if (skinBridgeManager != null) { skinBridgeManager.shutdown(); } @@ -116,6 +123,10 @@ public final class EssentialsC extends JavaPlugin { return teleportRequestManager; } + public PlayerStateManager getPlayerStateManager() { + return playerStateManager; + } + public TpsBarService getTpsBarManager() { return tpsBarManager; } @@ -137,6 +148,10 @@ public final class EssentialsC extends JavaPlugin { } private void refreshCorePlayerFeatures() { + if (playerStateManager == null) { + playerStateManager = new PlayerStateManager(this); + getServer().getPluginManager().registerEvents(playerStateManager, this); + } if (vanishListener == null) { vanishListener = new VanishListener(this); getServer().getPluginManager().registerEvents(vanishListener, this); @@ -189,7 +204,7 @@ public final class EssentialsC extends JavaPlugin { maintenanceManager.reload(); } if (maintenanceListener == null) { - maintenanceListener = new MaintenanceListener(this); + maintenanceListener = new MaintenanceListener(this, maintenanceManager); getServer().getPluginManager().registerEvents(maintenanceListener, this); } setModuleStatus("维护模式", true, maintenanceManager.isEnabled() ? "当前开启" : "当前关闭"); @@ -227,6 +242,10 @@ public final class EssentialsC extends JavaPlugin { private void refreshBlocks() { if (!moduleManager.isEnabled(ModuleManager.BLOCKS)) { + if (blocksMenuCommandListener != null) { + HandlerList.unregisterAll(blocksMenuCommandListener); + blocksMenuCommandListener = null; + } if (shulkerBoxListener != null) { shulkerBoxListener.shutdown(); HandlerList.unregisterAll(shulkerBoxListener); @@ -240,6 +259,13 @@ public final class EssentialsC extends JavaPlugin { shulkerBoxListener = new ShulkerBoxListener(this); getServer().getPluginManager().registerEvents(shulkerBoxListener, this); } + if (blocksMenuCommandListener == null) { + BaseCommand command = CommandRegistry.getCommand("blocks"); + if (command instanceof BlocksMenuCommand blocksMenuCommand) { + blocksMenuCommandListener = blocksMenuCommand; + getServer().getPluginManager().registerEvents(blocksMenuCommandListener, this); + } + } setModuleStatus("便捷方块", true, "命令和潜影盒监听器已启用"); } @@ -283,6 +309,7 @@ public final class EssentialsC extends JavaPlugin { } if (mobDropMenuListener == null) { mobDropMenuListener = new MobDropMenuListener(this); + getServer().getPluginManager().registerEvents(mobDropMenuListener, this); } setModuleStatus("生物掉落", true, "末影人掉落控制已启用"); } @@ -323,11 +350,13 @@ public final class EssentialsC extends JavaPlugin { unregisterListener(adminModeManager); unregisterListener(maintenanceListener); unregisterListener(shulkerBoxListener); + unregisterListener(blocksMenuCommandListener); unregisterListener(jeiRecipeSyncListener); unregisterListener(mobDropListener); unregisterListener(mobDropMenuListener); unregisterListener(vanishListener); unregisterListener(teleportRequestManager); + unregisterListener(playerStateManager); unregisterListener(skinBridgeManager); if (tpsBarManager instanceof Listener listener) { unregisterListener(listener); @@ -350,7 +379,7 @@ public final class EssentialsC extends JavaPlugin { .count(); long disabledCount = moduleStatus.size() - enabledCount; - getLogger().info("EssentialsC v" + getDescription().getVersion() + getLogger().info("EssentialsC v" + getPluginMeta().getVersion() + " 已启用 | 模块: " + enabledCount + " 启用, " + disabledCount + " 关闭"); if (!getConfig().getBoolean("debug", false)) { diff --git a/src/main/java/cn/infstar/essentialsC/FeatureConfigManager.java b/src/main/java/cn/infstar/essentialsC/FeatureConfigManager.java index 3436b6d..0777f7f 100644 --- a/src/main/java/cn/infstar/essentialsC/FeatureConfigManager.java +++ b/src/main/java/cn/infstar/essentialsC/FeatureConfigManager.java @@ -2,6 +2,7 @@ package cn.infstar.essentialsC; import cn.infstar.essentialsC.util.AtomicYamlWriter; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -10,31 +11,48 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; /** - * 管理独立功能配置,并负责迁移旧版配置结构。 + * 管理主配置与独立功能配置,并负责按顺序执行配置迁移。 */ public final class FeatureConfigManager { private static final int MAIN_CONFIG_VERSION = 2; private final EssentialsC plugin; + private final File mainConfigFile; private final File blocksMenuFile; private FileConfiguration blocksMenuConfig; public FeatureConfigManager(EssentialsC plugin) { this.plugin = plugin; + this.mainConfigFile = new File(plugin.getDataFolder(), "config.yml"); this.blocksMenuFile = new File(plugin.getDataFolder(), "blocks-menu.yml"); reload(); } public void reload() { + ensureResource(mainConfigFile, "config.yml"); ensureResource(blocksMenuFile, "blocks-menu.yml"); blocksMenuConfig = loadWithDefaults(blocksMenuFile, "blocks-menu.yml"); - migrateLegacyMainConfig(); - migrateLegacyDebugSettings(); - updateMainConfigVersion(); + YamlConfiguration loadedMainConfig = loadWithDefaults(mainConfigFile, "config.yml"); + YamlConfiguration mainConfig = migrateMainConfigVersion(loadedMainConfig); + + boolean mainChanged = mainConfig != loadedMainConfig; + mainChanged |= migrateLegacyMainConfig(mainConfig); + mainChanged |= migrateLegacyDebugSettings(mainConfig); + if (mainConfig.getInt("config-version", 0) != MAIN_CONFIG_VERSION) { + mainConfig.set("config-version", MAIN_CONFIG_VERSION); + mainChanged = true; + } + + if (mainChanged) { + save(mainConfig, mainConfigFile); + } + plugin.reloadConfig(); } public FileConfiguration getBlocksMenuConfig() { @@ -45,31 +63,66 @@ public final class FeatureConfigManager { save(blocksMenuConfig, blocksMenuFile); } - private void migrateLegacyMainConfig() { - FileConfiguration mainConfig = plugin.getConfig(); - boolean migrated = false; - - if (mainConfig.contains("blocks-menu", true)) { - copySection(mainConfig.getConfigurationSection("blocks-menu"), blocksMenuConfig); - mainConfig.set("blocks-menu", null); - migrated = true; + public boolean updateMainConfigValue(String path, Object value) { + YamlConfiguration mainConfig = loadWithDefaults(mainConfigFile, "config.yml"); + mainConfig.set(path, value); + if (!save(mainConfig, mainConfigFile)) { + return false; } - - if (!migrated) { - return; - } - - mainConfig.set("config-version", MAIN_CONFIG_VERSION); - saveMainConfig(); - plugin.getLogger().info("已将便捷菜单配置迁移到 blocks-menu.yml。"); + plugin.getConfig().set(path, value); + return true; } - private void migrateLegacyDebugSettings() { - FileConfiguration mainConfig = plugin.getConfig(); + private YamlConfiguration migrateMainConfigVersion(YamlConfiguration existingConfig) { + int existingVersion = existingConfig.getInt("config-version", 0); + if (existingVersion >= MAIN_CONFIG_VERSION) { + return existingConfig; + } + + File backupFile = new File(plugin.getDataFolder(), + "config.v" + existingVersion + ".bak-" + System.currentTimeMillis() + ".yml"); + try { + Files.copy(mainConfigFile.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + YamlConfiguration migratedConfig = loadResource("config.yml"); + if (migratedConfig == null) { + existingConfig.set("config-version", MAIN_CONFIG_VERSION); + return existingConfig; + } + + for (String path : existingConfig.getKeys(true)) { + boolean legacyFeaturePath = path.startsWith("skin-bridge.") || path.startsWith("blocks-menu."); + if (!existingConfig.isConfigurationSection(path) + && (migratedConfig.contains(path) || legacyFeaturePath)) { + migratedConfig.set(path, existingConfig.get(path)); + } + } + migratedConfig.set("config-version", MAIN_CONFIG_VERSION); + plugin.getLogger().info("已将 config.yml 从版本 " + existingVersion + + " 迁移到 " + MAIN_CONFIG_VERSION + ",备份文件: " + backupFile.getName()); + return migratedConfig; + } catch (IOException exception) { + plugin.getLogger().severe("迁移 config.yml 失败: " + exception.getMessage()); + return existingConfig; + } + } + + private boolean migrateLegacyMainConfig(FileConfiguration mainConfig) { + if (!mainConfig.contains("blocks-menu", true)) { + return false; + } + + copySection(mainConfig.getConfigurationSection("blocks-menu"), blocksMenuConfig); + mainConfig.set("blocks-menu", null); + saveBlocksMenuConfig(); + plugin.getLogger().info("已将便捷菜单配置迁移到 blocks-menu.yml。"); + return true; + } + + private boolean migrateLegacyDebugSettings(FileConfiguration mainConfig) { boolean hasJeiDebug = mainConfig.contains("jei-sync.debug", true); boolean hasSkinBridgeDebug = mainConfig.contains("skin-bridge.debug", true); if (!hasJeiDebug && !hasSkinBridgeDebug) { - return; + return false; } boolean debugEnabled = mainConfig.getBoolean("debug", false) @@ -78,16 +131,8 @@ public final class FeatureConfigManager { mainConfig.set("debug", debugEnabled); mainConfig.set("jei-sync.debug", null); mainConfig.set("skin-bridge.debug", null); - saveMainConfig(); plugin.getLogger().info("已将独立功能调试开关合并到 config.yml 的全局 debug。"); - } - - private void updateMainConfigVersion() { - if (plugin.getConfig().getInt("config-version", 0) >= MAIN_CONFIG_VERSION) { - return; - } - plugin.getConfig().set("config-version", MAIN_CONFIG_VERSION); - saveMainConfig(); + return true; } private void copySection(ConfigurationSection source, FileConfiguration target) { @@ -101,34 +146,59 @@ public final class FeatureConfigManager { } } - private FileConfiguration loadWithDefaults(File file, String resourcePath) { - YamlConfiguration config = YamlConfiguration.loadConfiguration(file); - InputStream defaultsStream = plugin.getResource(resourcePath); - if (defaultsStream != null) { - YamlConfiguration defaults = YamlConfiguration.loadConfiguration( - new InputStreamReader(defaultsStream, StandardCharsets.UTF_8) - ); + private YamlConfiguration loadWithDefaults(File file, String resourcePath) { + YamlConfiguration config = loadFile(file); + YamlConfiguration defaults = loadResource(resourcePath); + if (defaults != null) { config.setDefaults(defaults); config.options().copyDefaults(true); } return config; } + private YamlConfiguration loadFile(File file) { + YamlConfiguration config = new YamlConfiguration(); + config.options().parseComments(true); + try { + config.load(file); + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().severe("加载 " + file.getName() + " 失败: " + exception.getMessage()); + throw new IllegalStateException("无法加载 " + file.getName() + ",请修复配置格式后重试。", exception); + } + return config; + } + + private YamlConfiguration loadResource(String resourcePath) { + InputStream resource = plugin.getResource(resourcePath); + if (resource == null) { + return null; + } + + YamlConfiguration config = new YamlConfiguration(); + config.options().parseComments(true); + try (InputStream input = resource; + InputStreamReader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) { + config.load(reader); + return config; + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().severe("加载内置资源 " + resourcePath + " 失败: " + exception.getMessage()); + return null; + } + } + private void ensureResource(File file, String resourcePath) { if (!file.exists()) { plugin.saveResource(resourcePath, false); } } - private void save(FileConfiguration config, File file) { + private boolean save(FileConfiguration config, File file) { try { AtomicYamlWriter.save(config, file); + return true; } catch (Exception exception) { plugin.getLogger().warning("保存 " + file.getName() + " 失败: " + exception.getMessage()); + return false; } } - - private void saveMainConfig() { - save(plugin.getConfig(), new File(plugin.getDataFolder(), "config.yml")); - } } diff --git a/src/main/java/cn/infstar/essentialsC/LangManager.java b/src/main/java/cn/infstar/essentialsC/LangManager.java index 293cfa2..6e39a4d 100644 --- a/src/main/java/cn/infstar/essentialsC/LangManager.java +++ b/src/main/java/cn/infstar/essentialsC/LangManager.java @@ -1,10 +1,8 @@ package cn.infstar.essentialsC; -import cn.infstar.essentialsC.util.AtomicYamlWriter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.bukkit.ChatColor; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; @@ -14,8 +12,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -24,9 +20,10 @@ import java.util.regex.Pattern; public class LangManager { - private static final int CURRENT_CONFIG_VERSION = 2; private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("(?i)&#([0-9a-f]{6})"); private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); + private static final LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); private static final Map THEME_COLORS = Map.of( "&a", "�fb9a", "&b", "ᒦff", @@ -37,13 +34,11 @@ public class LangManager { ); private final JavaPlugin plugin; - private FileConfiguration config; private FileConfiguration langFile; private String currentLanguage; public LangManager(JavaPlugin plugin) { this.plugin = plugin; - loadConfig(); loadLanguage(); } @@ -74,8 +69,7 @@ public class LangManager { public Component getComponent(String path, Map placeholders) { String value = langFile.getString(path); if (value == null) { - return LegacyComponentSerializer.legacySection() - .deserialize(translateColorCodes("&c缺少语言文本: " + path)); + return LEGACY_SECTION.deserialize(translateColorCodes("&c缺少语言文本: " + path)); } return renderToComponent(applyPlaceholders(value, placeholders)); @@ -88,8 +82,7 @@ public class LangManager { public Component getPrefixedComponent(String path, Map placeholders) { String value = langFile.getString(path); if (value == null) { - return LegacyComponentSerializer.legacySection() - .deserialize(getPrefix() + translateColorCodes("&c缺少语言文本: " + path)); + return LEGACY_SECTION.deserialize(getPrefix() + translateColorCodes("&c缺少语言文本: " + path)); } return renderToComponent(langFile.getString("prefix", "") + applyPlaceholders(value, placeholders)); } @@ -116,7 +109,6 @@ public class LangManager { } public void reload() { - loadConfig(); loadLanguage(); } @@ -124,66 +116,8 @@ public class LangManager { return currentLanguage; } - private void loadConfig() { - File configFile = new File(plugin.getDataFolder(), "config.yml"); - if (!configFile.exists()) { - plugin.saveResource("config.yml", false); - } - - migrateConfigIfNeeded(configFile); - config = YamlConfiguration.loadConfiguration(configFile); - config.addDefault("config-version", CURRENT_CONFIG_VERSION); - config.addDefault("language", "zh_CN"); - config.addDefault("debug", false); - config.options().copyDefaults(true); - - } - - private void migrateConfigIfNeeded(File configFile) { - FileConfiguration existingConfig = YamlConfiguration.loadConfiguration(configFile); - int existingVersion = existingConfig.getInt("config-version", 0); - if (existingVersion >= CURRENT_CONFIG_VERSION) { - return; - } - - File backupFile = new File(plugin.getDataFolder(), - "config.v" + existingVersion + ".bak-" + System.currentTimeMillis() + ".yml"); - - try { - Files.copy(configFile.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - InputStream defaultConfigStream = plugin.getResource("config.yml"); - if (defaultConfigStream != null) { - YamlConfiguration migratedConfig = YamlConfiguration.loadConfiguration( - new InputStreamReader(defaultConfigStream, StandardCharsets.UTF_8) - ); - for (String path : existingConfig.getKeys(true)) { - boolean migratedFeaturePath = path.startsWith("skin-bridge.") || path.startsWith("blocks-menu."); - if (!existingConfig.isConfigurationSection(path) - && (migratedConfig.contains(path) || migratedFeaturePath)) { - migratedConfig.set(path, existingConfig.get(path)); - } - } - boolean debugEnabled = existingConfig.getBoolean("debug", false) - || existingConfig.getBoolean("jei-sync.debug", false) - || existingConfig.getBoolean("skin-bridge.debug", false); - migratedConfig.set("debug", debugEnabled); - migratedConfig.set("jei-sync.debug", null); - migratedConfig.set("config-version", CURRENT_CONFIG_VERSION); - AtomicYamlWriter.save(migratedConfig, configFile); - } else { - existingConfig.set("config-version", CURRENT_CONFIG_VERSION); - AtomicYamlWriter.save(existingConfig, configFile); - } - - plugin.getLogger().info("已将 config.yml 从版本 " + existingVersion - + " 迁移到 " + CURRENT_CONFIG_VERSION + ",备份文件: " + backupFile.getName()); - } catch (IOException e) { - plugin.getLogger().severe("迁移 config.yml 失败: " + e.getMessage()); - } - } - private void loadLanguage() { - currentLanguage = config.getString("language", "zh_CN"); + currentLanguage = plugin.getConfig().getString("language", "zh_CN"); File langFolder = new File(plugin.getDataFolder(), "lang"); if (!langFolder.exists() && !langFolder.mkdirs()) { @@ -251,7 +185,7 @@ public class LangManager { for (Map.Entry color : THEME_COLORS.entrySet()) { themedText = themedText.replace(color.getKey(), color.getValue()); } - return ChatColor.translateAlternateColorCodes('&', expandHexColors(themedText)); + return LEGACY_SECTION.serialize(LEGACY_AMPERSAND.deserialize(expandHexColors(themedText))); } private String expandHexColors(String text) { @@ -270,7 +204,7 @@ public class LangManager { } private String renderToLegacy(String text) { - return LegacyComponentSerializer.legacySection().serialize(renderToComponent(text)); + return LEGACY_SECTION.serialize(renderToComponent(text)); } private Component renderToComponent(String text) { @@ -284,7 +218,7 @@ public class LangManager { // 配置中 MiniMessage 语法错误时回退到旧颜色码解析,避免消息完全不可用。 } } - return LegacyComponentSerializer.legacySection().deserialize(translateColorCodes(text)); + return LEGACY_SECTION.deserialize(translateColorCodes(text)); } private boolean looksLikeMiniMessage(String text) { diff --git a/src/main/java/cn/infstar/essentialsC/ModuleManager.java b/src/main/java/cn/infstar/essentialsC/ModuleManager.java index 9297021..1772d4c 100644 --- a/src/main/java/cn/infstar/essentialsC/ModuleManager.java +++ b/src/main/java/cn/infstar/essentialsC/ModuleManager.java @@ -1,10 +1,12 @@ package cn.infstar.essentialsC; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; +import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; @@ -47,7 +49,15 @@ public final class ModuleManager { plugin.saveResource("modules.yml", false); } - modulesConfig = YamlConfiguration.loadConfiguration(modulesFile); + YamlConfiguration loaded = new YamlConfiguration(); + loaded.options().parseComments(true); + try { + loaded.load(modulesFile); + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().severe("加载 modules.yml 失败: " + exception.getMessage()); + throw new IllegalStateException("无法加载 modules.yml,请修复配置格式后重试。", exception); + } + modulesConfig = loaded; modulesConfig.addDefault("config-version", CURRENT_CONFIG_VERSION); for (Map.Entry module : DEFAULT_MODULES.entrySet()) { modulesConfig.addDefault(path(module.getKey()), module.getValue()); diff --git a/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java b/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java index edf90fa..bf9eb67 100644 --- a/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java +++ b/src/main/java/cn/infstar/essentialsC/admin/AdminModeManager.java @@ -1,7 +1,6 @@ package cn.infstar.essentialsC.admin; import cn.infstar.essentialsC.EssentialsC; -import cn.infstar.essentialsC.util.AtomicYamlWriter; import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.attribute.Attribute; @@ -15,7 +14,6 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.scheduler.BukkitTask; -import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -28,8 +26,7 @@ public final class AdminModeManager implements Listener { private static final float VANILLA_FLY_SPEED = 0.1F; private final EssentialsC plugin; - private final File dataFile; - private final YamlConfiguration data; + private final AdminModeStore store; private final Set activePlayers = new HashSet<>(); private BukkitTask actionBarTask; @@ -37,8 +34,7 @@ public final class AdminModeManager implements Listener { public AdminModeManager(EssentialsC plugin) { this.plugin = plugin; addConfigDefaults(); - this.dataFile = new File(plugin.getDataFolder(), "admin-mode.yml"); - this.data = YamlConfiguration.loadConfiguration(dataFile); + this.store = new AdminModeStore(plugin); } public void reload() { @@ -82,29 +78,32 @@ public final class AdminModeManager implements Listener { actionBarTask.cancel(); actionBarTask = null; } - saveData(); } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - String playerPath = getPlayerPath(player); - if (!data.getBoolean(playerPath + ".active", false)) { + YamlConfiguration data = store.load(player.getUniqueId()); + if (data == null) { + sendLangMessage(player, "admin-mode.messages.save-failed"); + return; + } + if (!data.getBoolean("active", false)) { return; } - saveProfile(player, playerPath + ".admin"); - if (!saveData()) { + saveProfile(player, data, "admin"); + if (!saveData(player, data)) { activePlayers.add(player.getUniqueId()); startActionBarTask(); sendLangMessage(player, "admin-mode.messages.save-failed"); return; } - restoreNormalProfile(player); - data.set(playerPath + ".active", false); - if (!saveData()) { - data.set(playerPath + ".active", true); - loadProfile(player, playerPath + ".admin"); + restoreNormalProfile(player, data); + data.set("active", false); + if (!saveData(player, data)) { + data.set("active", true); + loadProfile(player, data, "admin"); activePlayers.add(player.getUniqueId()); startActionBarTask(); sendLangMessage(player, "admin-mode.messages.save-failed"); @@ -122,17 +121,21 @@ public final class AdminModeManager implements Listener { } private void enable(Player player) { - String playerPath = getPlayerPath(player); - player.closeInventory(); - saveProfile(player, playerPath + ".normal"); - data.set(playerPath + ".active", true); - if (!saveData()) { + YamlConfiguration data = store.load(player.getUniqueId()); + if (data == null) { sendLangMessage(player, "admin-mode.messages.save-failed"); - data.set(playerPath + ".active", false); + return; + } + player.closeInventory(); + saveProfile(player, data, "normal"); + data.set("active", true); + if (!saveData(player, data)) { + sendLangMessage(player, "admin-mode.messages.save-failed"); + data.set("active", false); return; } - if (!loadProfile(player, playerPath + ".admin")) { + if (!loadProfile(player, data, "admin")) { clearInventory(player); } @@ -149,22 +152,28 @@ public final class AdminModeManager implements Listener { } private boolean disable(Player player, boolean notify) { - String playerPath = getPlayerPath(player); - player.closeInventory(); - saveProfile(player, playerPath + ".admin"); - data.set(playerPath + ".active", true); - if (!saveData()) { + YamlConfiguration data = store.load(player.getUniqueId()); + if (data == null) { if (notify) { sendLangMessage(player, "admin-mode.messages.save-failed"); } return false; } - restoreNormalProfile(player); + player.closeInventory(); + saveProfile(player, data, "admin"); + data.set("active", true); + if (!saveData(player, data)) { + if (notify) { + sendLangMessage(player, "admin-mode.messages.save-failed"); + } + return false; + } + restoreNormalProfile(player, data); - data.set(playerPath + ".active", false); - if (!saveData()) { - data.set(playerPath + ".active", true); - loadProfile(player, playerPath + ".admin"); + data.set("active", false); + if (!saveData(player, data)) { + data.set("active", true); + loadProfile(player, data, "admin"); activePlayers.add(player.getUniqueId()); if (notify) { sendLangMessage(player, "admin-mode.messages.save-failed"); @@ -181,9 +190,8 @@ public final class AdminModeManager implements Listener { return true; } - private void restoreNormalProfile(Player player) { - String playerPath = getPlayerPath(player); - if (!loadProfile(player, playerPath + ".normal")) { + private void restoreNormalProfile(Player player, YamlConfiguration data) { + if (!loadProfile(player, data, "normal")) { clearInventory(player); player.setGameMode(GameMode.SURVIVAL); player.setAllowFlight(false); @@ -192,7 +200,7 @@ public final class AdminModeManager implements Listener { } } - private void saveProfile(Player player, String path) { + private void saveProfile(Player player, YamlConfiguration data, String path) { PlayerInventory inventory = player.getInventory(); data.set(path + ".storage", Arrays.asList(inventory.getStorageContents())); data.set(path + ".armor", Arrays.asList(inventory.getArmorContents())); @@ -213,24 +221,24 @@ public final class AdminModeManager implements Listener { data.set(path + ".fire-ticks", player.getFireTicks()); } - private boolean loadProfile(Player player, String path) { + private boolean loadProfile(Player player, YamlConfiguration data, String path) { if (!data.contains(path)) { return false; } PlayerInventory inventory = player.getInventory(); clearInventory(player); - inventory.setStorageContents(readItemArray(path + ".storage", inventory.getStorageContents().length)); - inventory.setArmorContents(readItemArray(path + ".armor", inventory.getArmorContents().length)); - inventory.setExtraContents(readItemArray(path + ".extra", inventory.getExtraContents().length)); + inventory.setStorageContents(readItemArray(data, path + ".storage", inventory.getStorageContents().length)); + inventory.setArmorContents(readItemArray(data, path + ".armor", inventory.getArmorContents().length)); + inventory.setExtraContents(readItemArray(data, path + ".extra", inventory.getExtraContents().length)); inventory.setHeldItemSlot(clampHeldSlot(data.getInt(path + ".held-slot", inventory.getHeldItemSlot()))); - player.setItemOnCursor(readItem(path + ".cursor")); + player.setItemOnCursor(readItem(data, path + ".cursor")); - player.setGameMode(readGameMode(path + ".game-mode", player.getGameMode())); + player.setGameMode(readGameMode(data, path + ".game-mode", player.getGameMode())); player.setAllowFlight(data.getBoolean(path + ".allow-flight", player.getAllowFlight())); player.setFlying(data.getBoolean(path + ".flying", false) && player.getAllowFlight()); player.setFlySpeed(clampFlySpeed(data.getDouble(path + ".fly-speed", VANILLA_FLY_SPEED))); - player.setHealth(readHealth(player, path + ".health")); + player.setHealth(readHealth(player, data, path + ".health")); player.setFoodLevel(clampFoodLevel(data.getInt(path + ".food-level", player.getFoodLevel()))); player.setSaturation(clampSaturation(data.getDouble(path + ".saturation", player.getSaturation()))); player.setExhaustion(clampExhaustion(data.getDouble(path + ".exhaustion", player.getExhaustion()))); @@ -241,7 +249,7 @@ public final class AdminModeManager implements Listener { return true; } - private ItemStack[] readItemArray(String path, int size) { + private ItemStack[] readItemArray(YamlConfiguration data, String path, int size) { ItemStack[] items = new ItemStack[size]; List list = data.getList(path); if (list == null) { @@ -257,7 +265,7 @@ public final class AdminModeManager implements Listener { return items; } - private GameMode readGameMode(String path, GameMode fallback) { + private GameMode readGameMode(YamlConfiguration data, String path, GameMode fallback) { try { return GameMode.valueOf(data.getString(path, fallback.name())); } catch (IllegalArgumentException ignored) { @@ -265,7 +273,7 @@ public final class AdminModeManager implements Listener { } } - private double readHealth(Player player, String path) { + private double readHealth(Player player, YamlConfiguration data, String path) { double maxHealth = player.getAttribute(Attribute.MAX_HEALTH) != null ? player.getAttribute(Attribute.MAX_HEALTH).getValue() : player.getHealth(); @@ -276,7 +284,7 @@ public final class AdminModeManager implements Listener { return Math.min(Math.max(1.0D, health), maxHealth); } - private ItemStack readItem(String path) { + private ItemStack readItem(YamlConfiguration data, String path) { Object value = data.get(path); if (value instanceof ItemStack itemStack) { return itemStack; @@ -363,22 +371,12 @@ public final class AdminModeManager implements Listener { return (float) Math.max(0.0D, Math.min(1.0D, exp)); } - private String getPlayerPath(Player player) { - return "players." + player.getUniqueId(); - } - private void sendLangMessage(Player player, String path) { player.sendMessage(EssentialsC.getLangManager().getPrefixedString(path)); } - private boolean saveData() { - try { - AtomicYamlWriter.save(data, dataFile); - return true; - } catch (Exception e) { - plugin.getLogger().warning("保存 admin-mode.yml 失败: " + e.getMessage()); - return false; - } + private boolean saveData(Player player, YamlConfiguration data) { + return store.save(player.getUniqueId(), data); } private void addConfigDefaults() { diff --git a/src/main/java/cn/infstar/essentialsC/admin/AdminModeStore.java b/src/main/java/cn/infstar/essentialsC/admin/AdminModeStore.java new file mode 100644 index 0000000..3f2b2dc --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/admin/AdminModeStore.java @@ -0,0 +1,119 @@ +package cn.infstar.essentialsC.admin; + +import cn.infstar.essentialsC.EssentialsC; +import cn.infstar.essentialsC.util.AtomicYamlWriter; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.UUID; +import java.util.logging.Logger; + +final class AdminModeStore { + + private final File directory; + private final File legacyFile; + private final Logger logger; + + AdminModeStore(EssentialsC plugin) { + this(new File(plugin.getDataFolder(), "admin-mode"), + new File(plugin.getDataFolder(), "admin-mode.yml"), plugin.getLogger()); + } + + AdminModeStore(File directory, File legacyFile, Logger logger) { + this.directory = directory; + this.legacyFile = legacyFile; + this.logger = logger; + migrateLegacyFile(); + } + + YamlConfiguration load(UUID playerId) { + File playerFile = getPlayerFile(playerId); + YamlConfiguration data = new YamlConfiguration(); + if (!playerFile.exists()) { + return data; + } + try { + data.load(playerFile); + } catch (IOException | InvalidConfigurationException exception) { + logger.warning("加载管理模式玩家存档失败 (" + playerId + "): " + exception.getMessage()); + return null; + } + return data; + } + + boolean save(UUID playerId, YamlConfiguration data) { + try { + AtomicYamlWriter.save(data, getPlayerFile(playerId)); + return true; + } catch (IOException exception) { + logger.warning("保存管理模式玩家存档失败 (" + playerId + "): " + exception.getMessage()); + return false; + } + } + + private void migrateLegacyFile() { + if (!legacyFile.exists()) { + return; + } + + YamlConfiguration legacy = new YamlConfiguration(); + try { + legacy.load(legacyFile); + } catch (IOException | InvalidConfigurationException exception) { + logger.warning("加载旧 admin-mode.yml 失败,已保留原文件: " + exception.getMessage()); + return; + } + + ConfigurationSection players = legacy.getConfigurationSection("players"); + if (players != null) { + for (String playerIdText : players.getKeys(false)) { + UUID playerId; + try { + playerId = UUID.fromString(playerIdText); + } catch (IllegalArgumentException exception) { + logger.warning("忽略 admin-mode.yml 中无效的玩家 UUID: " + playerIdText); + continue; + } + + if (getPlayerFile(playerId).exists()) { + continue; + } + YamlConfiguration migrated = new YamlConfiguration(); + copySection(players.getConfigurationSection(playerIdText), migrated); + if (!save(playerId, migrated)) { + logger.warning("admin-mode.yml 迁移未完成,已保留原文件供下次重试。"); + return; + } + } + } + + File backup = new File(legacyFile.getParentFile(), + "admin-mode.legacy-" + System.currentTimeMillis() + ".yml"); + try { + Files.move(legacyFile.toPath(), backup.toPath(), StandardCopyOption.REPLACE_EXISTING); + logger.info("已将 admin-mode.yml 拆分为每玩家存档,旧文件已备份为 " + backup.getName()); + } catch (IOException exception) { + logger.warning("备份旧 admin-mode.yml 失败: " + exception.getMessage()); + } + } + + private void copySection(ConfigurationSection source, YamlConfiguration target) { + if (source == null) { + return; + } + for (String path : source.getKeys(true)) { + if (!source.isConfigurationSection(path)) { + target.set(path, source.get(path)); + } + } + } + + private File getPlayerFile(UUID playerId) { + return new File(directory, playerId + ".yml"); + } +} diff --git a/src/main/java/cn/infstar/essentialsC/commands/AnvilCommand.java b/src/main/java/cn/infstar/essentialsC/commands/AnvilCommand.java index 11b361b..6b625b3 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/AnvilCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/AnvilCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class AnvilCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class AnvilCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openAnvil(null, true); + player.openInventory(MenuType.ANVIL.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.ANVIL, Sound.BLOCK_ANVIL_USE); return true; } diff --git a/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java b/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java index 4c4a6ba..58e4886 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/BlocksMenuCommand.java @@ -31,7 +31,7 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { private static final class BlocksMenuHolder implements InventoryHolder { private final Inventory inventory; - private BlocksMenuHolder(String title) { + private BlocksMenuHolder(Component title) { this.inventory = Bukkit.createInventory(this, MENU_SIZE, title); } @@ -44,7 +44,6 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { public BlocksMenuCommand() { super("essentialsc.command.blocks"); addConfigDefaults(); - plugin.getServer().getPluginManager().registerEvents(this, plugin); this.blockKey = new NamespacedKey(plugin, "block_key"); } @@ -55,7 +54,7 @@ public class BlocksMenuCommand extends BaseCommand implements Listener { } private void openMenu(Player player) { - Inventory menu = new BlocksMenuHolder(getLang().getString("blocks-menu.title")).getInventory(); + Inventory menu = new BlocksMenuHolder(getLang().getComponent("blocks-menu.title")).getInventory(); FileConfiguration menuConfig = plugin.getFeatureConfigManager().getBlocksMenuConfig(); var sectionsConfig = menuConfig.getConfigurationSection("sections"); diff --git a/src/main/java/cn/infstar/essentialsC/commands/CartographyTableCommand.java b/src/main/java/cn/infstar/essentialsC/commands/CartographyTableCommand.java index afa5063..2e2d039 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/CartographyTableCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/CartographyTableCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class CartographyTableCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class CartographyTableCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openCartographyTable(null, true); + player.openInventory(MenuType.CARTOGRAPHY_TABLE.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.CARTOGRAPHY_TABLE, Sound.ENTITY_VILLAGER_WORK_CARTOGRAPHER); return true; } diff --git a/src/main/java/cn/infstar/essentialsC/commands/FeedCommand.java b/src/main/java/cn/infstar/essentialsC/commands/FeedCommand.java index 6f4ad3e..1198f1c 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/FeedCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/FeedCommand.java @@ -24,7 +24,7 @@ public class FeedCommand extends BaseCommand { return true; } - Player target = Bukkit.getPlayer(args[0]); + Player target = Bukkit.getPlayerExact(args[0]); if (target == null) { player.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0]))); return true; diff --git a/src/main/java/cn/infstar/essentialsC/commands/FlyCommand.java b/src/main/java/cn/infstar/essentialsC/commands/FlyCommand.java index 68b00c1..c7afaf4 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/FlyCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/FlyCommand.java @@ -10,15 +10,13 @@ public class FlyCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - boolean currentFlyState = player.getAllowFlight(); + boolean currentFlyState = plugin.getPlayerStateManager().isFlyEnabled(player); if (currentFlyState) { - player.setAllowFlight(false); - player.setFlying(false); + plugin.getPlayerStateManager().disableFly(player); player.sendMessage(getLang().getPrefixedString("messages.fly-disabled")); } else { - player.setAllowFlight(true); - player.setFlying(true); + plugin.getPlayerStateManager().enableFly(player); player.sendMessage(getLang().getPrefixedString("messages.fly-enabled")); } diff --git a/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java b/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java index 895fe9c..8d83c7e 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/GlowCommand.java @@ -1,17 +1,12 @@ package cn.infstar.essentialsC.commands; -import org.bukkit.NamespacedKey; import org.bukkit.Sound; import org.bukkit.entity.Player; -import org.bukkit.persistence.PersistentDataType; public class GlowCommand extends BaseCommand { - private final NamespacedKey enabledKey; - public GlowCommand() { super("essentialsc.command.glow"); - this.enabledKey = new NamespacedKey(plugin, "glow_enabled"); } @Override @@ -24,15 +19,11 @@ public class GlowCommand extends BaseCommand { } if (targetState) { - player.setGlowing(true); - player.getPersistentDataContainer().set(enabledKey, PersistentDataType.BYTE, (byte) 1); + plugin.getPlayerStateManager().enableGlow(player); playShortcutSound(player, Sound.BLOCK_AMETHYST_BLOCK_CHIME); player.sendMessage(getLang().getPrefixedString("messages.glow-enabled")); } else { - if (currentState) { - player.setGlowing(false); - player.getPersistentDataContainer().remove(enabledKey); - } + plugin.getPlayerStateManager().disableGlow(player); playShortcutSound(player, Sound.BLOCK_AMETHYST_CLUSTER_FALL); player.sendMessage(getLang().getPrefixedString("messages.glow-disabled")); } @@ -53,12 +44,6 @@ public class GlowCommand extends BaseCommand { } private boolean isPluginGlowEnabled(Player player) { - Byte value = player.getPersistentDataContainer().get(enabledKey, PersistentDataType.BYTE); - boolean enabled = value != null && value == (byte) 1; - if (enabled && !player.isGlowing()) { - player.getPersistentDataContainer().remove(enabledKey); - return false; - } - return enabled; + return plugin.getPlayerStateManager().isGlowEnabled(player); } } diff --git a/src/main/java/cn/infstar/essentialsC/commands/GrindstoneCommand.java b/src/main/java/cn/infstar/essentialsC/commands/GrindstoneCommand.java index 58cca76..045babf 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/GrindstoneCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/GrindstoneCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class GrindstoneCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class GrindstoneCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openGrindstone(null, true); + player.openInventory(MenuType.GRINDSTONE.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.GRINDSTONE, Sound.BLOCK_GRINDSTONE_USE); return true; } diff --git a/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java b/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java index 2a3a11b..f143451 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/HealCommand.java @@ -26,7 +26,7 @@ public class HealCommand extends BaseCommand { return true; } - Player target = Bukkit.getPlayer(args[0]); + Player target = Bukkit.getPlayerExact(args[0]); if (target == null) { player.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0]))); return true; diff --git a/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java b/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java index d62424f..4abd187 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/HelpCommand.java @@ -43,9 +43,8 @@ public class HelpCommand extends BaseCommand implements TabCompleter { sendNoPermission(sender, "essentialsc.command.reload"); return true; } - plugin.reloadConfig(); - EssentialsC.getLangManager().reload(); plugin.getFeatureConfigManager().reload(); + EssentialsC.getLangManager().reload(); plugin.getModuleManager().reload(); CommandRegistry.clearInitializationFailures(); plugin.reloadRuntimeModules(); @@ -65,9 +64,8 @@ public class HelpCommand extends BaseCommand implements TabCompleter { sendNoPermission(sender, "essentialsc.command.reload"); return true; } - plugin.reloadConfig(); - EssentialsC.getLangManager().reload(); plugin.getFeatureConfigManager().reload(); + EssentialsC.getLangManager().reload(); plugin.getModuleManager().reload(); CommandRegistry.clearInitializationFailures(); plugin.reloadRuntimeModules(); @@ -95,7 +93,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter { if (subCommand.equals("version") || subCommand.equals("v")) { player.sendMessage(getLang().getPrefixedString("messages.version", - Map.of("version", plugin.getDescription().getVersion()))); + Map.of("version", plugin.getPluginMeta().getVersion()))); player.sendMessage(getLang().getPrefixedString("messages.paper-version", Map.of("version", Bukkit.getVersion()))); return true; @@ -108,7 +106,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter { } LangManager lang = getLang(); - String version = plugin.getDescription().getVersion(); + String version = plugin.getPluginMeta().getVersion(); sendPrefixed(player, lang.getString("help.title")); sendPrefixed(player, lang.getString("help.version", Map.of("version", version))); diff --git a/src/main/java/cn/infstar/essentialsC/commands/LoomCommand.java b/src/main/java/cn/infstar/essentialsC/commands/LoomCommand.java index 17fb14e..c22d2d2 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/LoomCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/LoomCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class LoomCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class LoomCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openLoom(null, true); + player.openInventory(MenuType.LOOM.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.LOOM, Sound.UI_LOOM_SELECT_PATTERN); return true; } diff --git a/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java b/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java index a920630..d53f17f 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/MobDropCommand.java @@ -22,7 +22,7 @@ public class MobDropCommand extends BaseCommand { public static final class MobDropMenuHolder implements InventoryHolder { private final Inventory inventory; - public MobDropMenuHolder(String title) { + public MobDropMenuHolder(Component title) { this.inventory = Bukkit.createInventory(this, MENU_SIZE, title); } @@ -49,7 +49,7 @@ public class MobDropCommand extends BaseCommand { ? "mobdrops-menu.status.enabled" : "mobdrops-menu.status.disabled"); - Inventory menu = new MobDropMenuHolder(lang.getString("mobdrops-menu.title")).getInventory(); + Inventory menu = new MobDropMenuHolder(lang.getComponent("mobdrops-menu.title")).getInventory(); ItemStack endermanItem = new ItemStack(Material.ENDER_PEARL); ItemMeta endermanMeta = endermanItem.getItemMeta(); diff --git a/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java b/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java index 59593e4..e77ae4f 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/NightVisionCommand.java @@ -1,19 +1,12 @@ package cn.infstar.essentialsC.commands; -import org.bukkit.NamespacedKey; import org.bukkit.Sound; import org.bukkit.entity.Player; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.persistence.PersistentDataType; public class NightVisionCommand extends BaseCommand { - private final NamespacedKey enabledKey; - public NightVisionCommand() { super("essentialsc.command.nightvision"); - this.enabledKey = new NamespacedKey(plugin, "nightvision_enabled"); } @Override @@ -26,15 +19,11 @@ public class NightVisionCommand extends BaseCommand { } if (targetState) { - player.addPotionEffect(new PotionEffect(PotionEffectType.NIGHT_VISION, Integer.MAX_VALUE, 0, false, false, false)); - player.getPersistentDataContainer().set(enabledKey, PersistentDataType.BYTE, (byte) 1); + plugin.getPlayerStateManager().enableNightVision(player); playShortcutSound(player, Sound.BLOCK_BEACON_POWER_SELECT); player.sendMessage(getLang().getPrefixedString("messages.nightvision-enabled")); } else { - if (currentState) { - player.removePotionEffect(PotionEffectType.NIGHT_VISION); - player.getPersistentDataContainer().remove(enabledKey); - } + plugin.getPlayerStateManager().disableNightVision(player); playShortcutSound(player, Sound.BLOCK_BEACON_DEACTIVATE); player.sendMessage(getLang().getPrefixedString("messages.nightvision-disabled")); } @@ -55,12 +44,6 @@ public class NightVisionCommand extends BaseCommand { } private boolean isPluginNightVisionEnabled(Player player) { - Byte value = player.getPersistentDataContainer().get(enabledKey, PersistentDataType.BYTE); - boolean enabled = value != null && value == (byte) 1; - if (enabled && !player.hasPotionEffect(PotionEffectType.NIGHT_VISION)) { - player.getPersistentDataContainer().remove(enabledKey); - return false; - } - return enabled; + return plugin.getPlayerStateManager().isNightVisionEnabled(player); } } diff --git a/src/main/java/cn/infstar/essentialsC/commands/SmithingTableCommand.java b/src/main/java/cn/infstar/essentialsC/commands/SmithingTableCommand.java index 125fed7..f4a4585 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/SmithingTableCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/SmithingTableCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class SmithingTableCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class SmithingTableCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openSmithingTable(null, true); + player.openInventory(MenuType.SMITHING.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.SMITHING_TABLE, Sound.BLOCK_SMITHING_TABLE_USE); return true; } diff --git a/src/main/java/cn/infstar/essentialsC/commands/StonecutterCommand.java b/src/main/java/cn/infstar/essentialsC/commands/StonecutterCommand.java index 40e7ba6..85466ed 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/StonecutterCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/StonecutterCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class StonecutterCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class StonecutterCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openStonecutter(null, true); + player.openInventory(MenuType.STONECUTTER.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.STONECUTTER, Sound.UI_STONECUTTER_SELECT_RECIPE); 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 1aa63d8..d5b840f 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/VanishCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/VanishCommand.java @@ -3,10 +3,12 @@ package cn.infstar.essentialsC.commands; import cn.infstar.essentialsC.EssentialsC; import cn.infstar.essentialsC.util.AtomicYamlWriter; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import java.io.File; +import java.io.IOException; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -53,7 +55,13 @@ public class VanishCommand extends BaseCommand { if (!stateFile.exists()) { return; } - FileConfiguration state = YamlConfiguration.loadConfiguration(stateFile); + YamlConfiguration state = new YamlConfiguration(); + try { + state.load(stateFile); + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().severe("加载 vanished-players.yml 失败: " + exception.getMessage()); + throw new IllegalStateException("无法加载 vanished-players.yml,请修复文件格式后重试。", exception); + } for (String value : state.getStringList("players")) { try { vanishedPlayers.add(UUID.fromString(value)); @@ -74,15 +82,22 @@ public class VanishCommand extends BaseCommand { } } - public static void restoreVisibility(EssentialsC plugin, Player player, boolean notify) { + public static boolean restoreVisibility(EssentialsC plugin, Player player, boolean notify) { if (!vanishedPlayers.remove(player.getUniqueId())) { - return; + return true; + } + if (!saveState(plugin)) { + vanishedPlayers.add(player.getUniqueId()); + if (notify) { + player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.vanish-save-failed")); + } + return false; } showPlayerToAll(plugin, player); - saveState(plugin); if (notify) { player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.vanish-permission-removed")); } + return true; } public static void applyHiddenState(EssentialsC plugin, Player player) { diff --git a/src/main/java/cn/infstar/essentialsC/commands/WorkbenchCommand.java b/src/main/java/cn/infstar/essentialsC/commands/WorkbenchCommand.java index 911bd33..0cbb978 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/WorkbenchCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/WorkbenchCommand.java @@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.MenuType; public class WorkbenchCommand extends BaseCommand { @@ -12,7 +13,7 @@ public class WorkbenchCommand extends BaseCommand { @Override protected boolean execute(Player player, String[] args) { - player.openWorkbench(null, true); + player.openInventory(MenuType.CRAFTING.builder().checkReachable(false).build(player)); playBlockShortcutSound(player, Material.CRAFTING_TABLE, Sound.UI_BUTTON_CLICK); return true; } diff --git a/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java b/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java index f5e531f..499f089 100644 --- a/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java +++ b/src/main/java/cn/infstar/essentialsC/listeners/MobDropMenuListener.java @@ -2,7 +2,6 @@ package cn.infstar.essentialsC.listeners; import cn.infstar.essentialsC.EssentialsC; import cn.infstar.essentialsC.commands.MobDropCommand; -import cn.infstar.essentialsC.util.AtomicYamlWriter; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; @@ -19,7 +18,6 @@ public class MobDropMenuListener implements Listener { public MobDropMenuListener(EssentialsC plugin) { this.plugin = plugin; - plugin.getServer().getPluginManager().registerEvents(this, plugin); } @EventHandler @@ -40,6 +38,12 @@ public class MobDropMenuListener implements Listener { } if (event.getRawSlot() == MobDropCommand.getEndermanSlot()) { + if (!player.hasPermission("essentialsc.mobdrops.enderman")) { + player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.no-permission", + Map.of("permission", "essentialsc.mobdrops.enderman"))); + player.closeInventory(); + return; + } toggleEndermanDrops(player); Bukkit.getScheduler().runTaskLater(plugin, () -> MobDropCommand.openMobDropMenu(plugin, player), 2L); } @@ -48,14 +52,10 @@ public class MobDropMenuListener implements Listener { private void toggleEndermanDrops(Player player) { FileConfiguration config = plugin.getConfig(); boolean newValue = !config.getBoolean("mob-drops.enderman.allow-drops", true); - config.set("mob-drops.enderman.allow-drops", newValue); - - try { - AtomicYamlWriter.save(config, plugin.getDataFolder().toPath().resolve("config.yml").toFile()); - } catch (Exception e) { - config.set("mob-drops.enderman.allow-drops", !newValue); + if (!plugin.getFeatureConfigManager().updateMainConfigValue( + "mob-drops.enderman.allow-drops", newValue)) { player.sendMessage(EssentialsC.getLangManager().getPrefixedString("messages.mobdrop-save-failed", - Map.of("error", e.getMessage()))); + Map.of("error", "无法写入 config.yml"))); return; } diff --git a/src/main/java/cn/infstar/essentialsC/maintenance/LuckPermsMaintenancePermissionResolver.java b/src/main/java/cn/infstar/essentialsC/maintenance/LuckPermsMaintenancePermissionResolver.java new file mode 100644 index 0000000..9064140 --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/maintenance/LuckPermsMaintenancePermissionResolver.java @@ -0,0 +1,56 @@ +package cn.infstar.essentialsC.maintenance; + +import cn.infstar.essentialsC.EssentialsC; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.model.user.User; +import net.luckperms.api.model.user.UserManager; +import net.luckperms.api.query.QueryOptions; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class LuckPermsMaintenancePermissionResolver implements MaintenancePermissionResolver { + + private final EssentialsC plugin; + private final LuckPerms luckPerms; + private final UserManager userManager; + private final AtomicBoolean failureLogged = new AtomicBoolean(); + + public LuckPermsMaintenancePermissionResolver(EssentialsC plugin) { + this.plugin = plugin; + this.luckPerms = plugin.getServer().getServicesManager().load(LuckPerms.class); + if (luckPerms == null) { + throw new IllegalStateException("LuckPerms 服务尚未注册"); + } + this.userManager = luckPerms.getUserManager(); + } + + @Override + public PermissionResult checkPermission(UUID uniqueId, String permission) { + if (uniqueId == null || permission == null || permission.isBlank()) { + return PermissionResult.DENIED; + } + + try { + User user = userManager.getUser(uniqueId); + if (user == null) { + user = userManager.loadUser(uniqueId).join(); + } + + QueryOptions queryOptions = luckPerms.getContextManager().getQueryOptions(user) + .orElseGet(() -> luckPerms.getContextManager().getStaticQueryOptions()); + boolean granted = user.getCachedData() + .getPermissionData(queryOptions) + .checkPermission(permission) + .asBoolean(); + failureLogged.set(false); + return granted ? PermissionResult.GRANTED : PermissionResult.DENIED; + } catch (RuntimeException exception) { + if (failureLogged.compareAndSet(false, true)) { + plugin.getLogger().warning("LuckPerms 维护权限查询失败,将在玩家加入后复核权限: " + + exception.getMessage()); + } + return PermissionResult.UNAVAILABLE; + } + } +} diff --git a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceListener.java b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceListener.java index 989e83b..a69edb8 100644 --- a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceListener.java +++ b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceListener.java @@ -4,95 +4,108 @@ import cn.infstar.essentialsC.EssentialsC; import cn.infstar.essentialsC.ModuleManager; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.ServerListPingEvent; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.util.Map; public final class MaintenanceListener implements Listener { private final EssentialsC plugin; + private final MaintenanceManager maintenanceManager; + private final MaintenancePermissionResolver permissionResolver; - public MaintenanceListener(EssentialsC plugin) { + public MaintenanceListener(EssentialsC plugin, MaintenanceManager maintenanceManager) { this.plugin = plugin; + this.maintenanceManager = maintenanceManager; + this.permissionResolver = MaintenancePermissionResolverFactory.create(plugin); } @EventHandler public void onServerListPing(ServerListPingEvent event) { - MaintenanceManager maintenanceManager = plugin.getMaintenanceManager(); if (!plugin.getModuleManager().isEnabled(ModuleManager.MAINTENANCE) - || maintenanceManager == null || !maintenanceManager.isEnabled() || !maintenanceManager.isMotdEnabled()) { return; } - event.setMotd(maintenanceManager.getMotd()); + event.motd(maintenanceManager.getMotd()); } - @EventHandler - public void onPlayerLogin(PlayerLoginEvent event) { - MaintenanceManager maintenanceManager = plugin.getMaintenanceManager(); - if (!plugin.getModuleManager().isEnabled(ModuleManager.MAINTENANCE) - || maintenanceManager == null - || !maintenanceManager.isEnabled()) { + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerPreLogin(AsyncPlayerPreLoginEvent event) { + if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { return; } - if (maintenanceManager.canJoin(event.getPlayer())) { + MaintenanceManager.AccessSnapshot snapshot = maintenanceManager.getAccessSnapshot(); + if (!snapshot.enabled() || snapshot.isWhitelisted(event.getUniqueId(), event.getName())) { return; } - event.disallow(PlayerLoginEvent.Result.KICK_OTHER, maintenanceManager.getKickMessage()); - notifyBlockedLogin(event, maintenanceManager); + if (permissionResolver == null) { + return; + } + + MaintenancePermissionResolver.PermissionResult permissionResult = permissionResolver + .checkPermission(event.getUniqueId(), snapshot.bypassPermission()); + if (permissionResult != MaintenancePermissionResolver.PermissionResult.DENIED) { + return; + } + + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, snapshot.kickMessage()); + notifyBlockedLogin(event.getName(), event.getUniqueId().toString(), event.getAddress(), snapshot); } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { - MaintenanceManager maintenanceManager = plugin.getMaintenanceManager(); - if (!plugin.getModuleManager().isEnabled(ModuleManager.MAINTENANCE) || maintenanceManager == null) { + if (!plugin.getModuleManager().isEnabled(ModuleManager.MAINTENANCE)) { return; } - maintenanceManager.addBossBarPlayer(event.getPlayer()); + Player player = event.getPlayer(); + if (maintenanceManager.isEnabled() && !maintenanceManager.canJoin(player)) { + MaintenanceManager.AccessSnapshot snapshot = maintenanceManager.getAccessSnapshot(); + player.kick(snapshot.kickMessage()); + InetSocketAddress socketAddress = player.getAddress(); + notifyBlockedLogin(player.getName(), player.getUniqueId().toString(), + socketAddress == null ? null : socketAddress.getAddress(), snapshot); + return; + } + + maintenanceManager.addBossBarPlayer(player); } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - MaintenanceManager maintenanceManager = plugin.getMaintenanceManager(); - if (maintenanceManager != null) { - maintenanceManager.removeBossBarPlayer(event.getPlayer()); - } + maintenanceManager.removeBossBarPlayer(event.getPlayer()); } - private void notifyBlockedLogin(PlayerLoginEvent event, MaintenanceManager maintenanceManager) { - if (!maintenanceManager.isNotifyEnabled()) { + private void notifyBlockedLogin(String playerName, String uniqueId, InetAddress inetAddress, + MaintenanceManager.AccessSnapshot snapshot) { + if (!snapshot.notifyEnabled()) { return; } - String address = EssentialsC.getLangManager().getString("maintenance.status.address-hidden"); - if (maintenanceManager.shouldIncludeAddressInNotification()) { - InetAddress inetAddress = event.getAddress(); - if (inetAddress != null) { - address = inetAddress.getHostAddress(); - } - } + String visibleAddress = snapshot.includeAddress() && inetAddress != null ? inetAddress.getHostAddress() : null; + plugin.getServer().getScheduler().runTask(plugin, () -> { + String address = visibleAddress == null + ? EssentialsC.getLangManager().getString("maintenance.status.address-hidden") + : visibleAddress; + var message = EssentialsC.getLangManager().getPrefixedComponent("maintenance.messages.login-blocked", + Map.of("player", playerName, "uuid", uniqueId, "address", address)); - String message = EssentialsC.getLangManager().getPrefixedString("maintenance.messages.login-blocked", - Map.of( - "player", event.getPlayer().getName(), - "uuid", event.getPlayer().getUniqueId().toString(), - "address", address - )); - - for (Player player : plugin.getServer().getOnlinePlayers()) { - if (player.hasPermission(maintenanceManager.getNotifyPermission())) { - player.sendMessage(message); + for (Player onlinePlayer : plugin.getServer().getOnlinePlayers()) { + if (onlinePlayer.hasPermission(snapshot.notifyPermission())) { + onlinePlayer.sendMessage(message); + } } - } + }); } } diff --git a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java index c8417b5..cca20e4 100644 --- a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java +++ b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenanceManager.java @@ -2,17 +2,19 @@ package cn.infstar.essentialsC.maintenance; import cn.infstar.essentialsC.EssentialsC; import cn.infstar.essentialsC.util.AtomicYamlWriter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; import org.bukkit.boss.BossBar; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.InvalidConfigurationException; 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.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -23,11 +25,14 @@ import java.util.UUID; public final class MaintenanceManager { private static final int CURRENT_CONFIG_VERSION = 1; + private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); + private static final LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); private final EssentialsC plugin; private final File configFile; private FileConfiguration config; private BossBar bossBar; + private volatile AccessSnapshot accessSnapshot = AccessSnapshot.disabled(); public MaintenanceManager(EssentialsC plugin) { this.plugin = plugin; @@ -40,7 +45,7 @@ public final class MaintenanceManager { plugin.saveResource("maintenance.yml", false); } - config = YamlConfiguration.loadConfiguration(configFile); + config = loadConfiguration(); config.addDefault("config-version", CURRENT_CONFIG_VERSION); config.addDefault("enabled", false); config.addDefault("bypass-permission", "essentialsc.maintenance.bypass"); @@ -58,6 +63,7 @@ public final class MaintenanceManager { config.addDefault("bossbar.style", "SOLID"); config.addDefault("bossbar.progress", 1.0D); config.options().copyDefaults(true); + publishAccessSnapshot(); refreshBossBar(); if (isEnabled()) { kickUnauthorizedPlayers(); @@ -78,6 +84,7 @@ public final class MaintenanceManager { config.set("enabled", previous); return OperationResult.SAVE_FAILED; } + publishAccessSnapshot(); refreshBossBar(); if (enabled) { kickUnauthorizedPlayers(); @@ -105,6 +112,10 @@ public final class MaintenanceManager { return player != null && (player.hasPermission(getBypassPermission()) || isWhitelisted(player)); } + public AccessSnapshot getAccessSnapshot() { + return accessSnapshot; + } + public boolean isWhitelisted(Player player) { if (player == null) { return false; @@ -170,11 +181,11 @@ public final class MaintenanceManager { return config.getBoolean("motd.enabled", true); } - public String getMotd() { + public Component getMotd() { return colorizeLines(config.getStringList("motd.lines")); } - public String getKickMessage() { + public Component getKickMessage() { return colorizeLines(config.getStringList("kick-message")); } @@ -185,7 +196,7 @@ public final class MaintenanceManager { } bossBar = plugin.getServer().createBossBar( - colorize(config.getString("bossbar.title", "&c服务器正在维护")), + LEGACY_SECTION.serialize(colorize(config.getString("bossbar.title", "&c服务器正在维护"))), readBossBarColor(), readBossBarStyle() ); @@ -211,15 +222,16 @@ public final class MaintenanceManager { } private void kickUnauthorizedPlayers() { - String message = getKickMessage(); + Component message = getKickMessage(); for (Player player : List.copyOf(plugin.getServer().getOnlinePlayers())) { if (!canJoin(player)) { - player.kick(LegacyComponentSerializer.legacySection().deserialize(message)); + player.kick(message); } } } public void shutdown() { + accessSnapshot = AccessSnapshot.disabled(); clearBossBar(); } @@ -299,6 +311,7 @@ public final class MaintenanceManager { config.set(path, previousEntries); return OperationResult.SAVE_FAILED; } + publishAccessSnapshot(); refreshBossBar(); if (isEnabled()) { kickUnauthorizedPlayers(); @@ -344,18 +357,36 @@ public final class MaintenanceManager { return normalizeEntry(input).toLowerCase(Locale.ROOT); } - private String colorizeLines(List lines) { + private Component colorizeLines(List lines) { if (lines == null || lines.isEmpty()) { - return ""; + return Component.empty(); } - return String.join("\n", lines.stream() - .map(this::colorize) - .toList()); + return colorize(String.join("\n", lines)); } - private String colorize(String text) { - return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text); + private Component colorize(String text) { + return LEGACY_AMPERSAND.deserialize(text == null ? "" : text); + } + + private void publishAccessSnapshot() { + Set whitelistUuids = new LinkedHashSet<>(); + for (String value : getWhitelistUuids()) { + UUID uuid = parseUuid(value); + if (uuid != null) { + whitelistUuids.add(uuid); + } + } + accessSnapshot = new AccessSnapshot( + isEnabled(), + getBypassPermission(), + whitelistUuids, + getWhitelistNames(), + getKickMessage(), + isNotifyEnabled(), + getNotifyPermission(), + shouldIncludeAddressInNotification() + ); } private boolean save() { @@ -368,6 +399,40 @@ public final class MaintenanceManager { } } + private YamlConfiguration loadConfiguration() { + YamlConfiguration loaded = new YamlConfiguration(); + loaded.options().parseComments(true); + try { + loaded.load(configFile); + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().severe("加载 maintenance.yml 失败: " + exception.getMessage()); + throw new IllegalStateException("无法加载 maintenance.yml,请修复配置格式后重试。", exception); + } + return loaded; + } + + public record AccessSnapshot(boolean enabled, String bypassPermission, Set whitelistUuids, + Set whitelistNames, Component kickMessage, boolean notifyEnabled, + String notifyPermission, boolean includeAddress) { + + public AccessSnapshot { + bypassPermission = bypassPermission == null ? "" : bypassPermission; + whitelistUuids = Set.copyOf(whitelistUuids); + whitelistNames = Set.copyOf(whitelistNames); + kickMessage = kickMessage == null ? Component.empty() : kickMessage; + notifyPermission = notifyPermission == null ? "" : notifyPermission; + } + + public boolean isWhitelisted(UUID uniqueId, String playerName) { + return whitelistUuids.contains(uniqueId) + || whitelistNames.contains(playerName == null ? "" : playerName.trim().toLowerCase(Locale.ROOT)); + } + + private static AccessSnapshot disabled() { + return new AccessSnapshot(false, "", Set.of(), Set.of(), Component.empty(), false, "", false); + } + } + public enum OperationResult { SUCCESS, UNCHANGED, diff --git a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenancePermissionResolver.java b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenancePermissionResolver.java new file mode 100644 index 0000000..7140a71 --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenancePermissionResolver.java @@ -0,0 +1,14 @@ +package cn.infstar.essentialsC.maintenance; + +import java.util.UUID; + +public interface MaintenancePermissionResolver { + + PermissionResult checkPermission(UUID uniqueId, String permission); + + enum PermissionResult { + GRANTED, + DENIED, + UNAVAILABLE + } +} diff --git a/src/main/java/cn/infstar/essentialsC/maintenance/MaintenancePermissionResolverFactory.java b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenancePermissionResolverFactory.java new file mode 100644 index 0000000..e96d00b --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/maintenance/MaintenancePermissionResolverFactory.java @@ -0,0 +1,35 @@ +package cn.infstar.essentialsC.maintenance; + +import cn.infstar.essentialsC.EssentialsC; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.InvocationTargetException; + +final class MaintenancePermissionResolverFactory { + + private static final String LUCKPERMS_RESOLVER_CLASS = + "cn.infstar.essentialsC.maintenance.LuckPermsMaintenancePermissionResolver"; + + private MaintenancePermissionResolverFactory() { + } + + static MaintenancePermissionResolver create(EssentialsC plugin) { + Plugin luckPerms = plugin.getServer().getPluginManager().getPlugin("LuckPerms"); + if (luckPerms == null || !luckPerms.isEnabled()) { + plugin.getLogger().info("未检测到 LuckPerms,维护模式将在玩家加入后复核绕过权限。"); + return null; + } + + try { + Class resolverClass = Class.forName(LUCKPERMS_RESOLVER_CLASS, true, plugin.getClass().getClassLoader()); + return (MaintenancePermissionResolver) resolverClass + .getConstructor(EssentialsC.class) + .newInstance(plugin); + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException + | IllegalAccessException | InvocationTargetException | LinkageError exception) { + plugin.getLogger().warning("LuckPerms 维护权限解析器初始化失败,将在玩家加入后复核权限: " + + exception.getMessage()); + return null; + } + } +} diff --git a/src/main/java/cn/infstar/essentialsC/player/PlayerStateManager.java b/src/main/java/cn/infstar/essentialsC/player/PlayerStateManager.java new file mode 100644 index 0000000..bf4f705 --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/player/PlayerStateManager.java @@ -0,0 +1,209 @@ +package cn.infstar.essentialsC.player; + +import cn.infstar.essentialsC.EssentialsC; +import org.bukkit.GameMode; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitTask; + +public final class PlayerStateManager implements Listener { + + public static final String FLY_PERMISSION = "essentialsc.command.fly"; + public static final String NIGHT_VISION_PERMISSION = "essentialsc.command.nightvision"; + public static final String GLOW_PERMISSION = "essentialsc.command.glow"; + + private final EssentialsC plugin; + private final NamespacedKey flyEnabledKey; + private final NamespacedKey flyPreviousAllowKey; + private final NamespacedKey flyPreviousFlyingKey; + private final NamespacedKey nightVisionEnabledKey; + private final NamespacedKey glowEnabledKey; + private final BukkitTask permissionTask; + + public PlayerStateManager(EssentialsC plugin) { + this.plugin = plugin; + this.flyEnabledKey = new NamespacedKey(plugin, "fly_enabled"); + this.flyPreviousAllowKey = new NamespacedKey(plugin, "fly_previous_allow"); + this.flyPreviousFlyingKey = new NamespacedKey(plugin, "fly_previous_flying"); + this.nightVisionEnabledKey = new NamespacedKey(plugin, "nightvision_enabled"); + this.glowEnabledKey = new NamespacedKey(plugin, "glow_enabled"); + this.permissionTask = plugin.getServer().getScheduler() + .runTaskTimer(plugin, this::reconcileOnlinePlayers, 40L, 40L); + } + + public boolean isFlyEnabled(Player player) { + return isMarked(player, flyEnabledKey); + } + + public void enableFly(Player player) { + if (isFlyEnabled(player)) { + player.setAllowFlight(true); + player.setFlying(true); + return; + } + + PersistentDataContainer data = player.getPersistentDataContainer(); + setBoolean(data, flyPreviousAllowKey, player.getAllowFlight()); + setBoolean(data, flyPreviousFlyingKey, player.isFlying()); + setBoolean(data, flyEnabledKey, true); + player.setAllowFlight(true); + player.setFlying(true); + } + + public void disableFly(Player player) { + PersistentDataContainer data = player.getPersistentDataContainer(); + if (!isMarked(data, flyEnabledKey)) { + return; + } + + boolean previousAllow = getBoolean(data, flyPreviousAllowKey); + boolean previousFlying = getBoolean(data, flyPreviousFlyingKey); + clearFlyMarkers(data); + + boolean allowFlight = previousAllow || hasGameModeFlight(player); + player.setFlying(previousFlying && allowFlight); + player.setAllowFlight(allowFlight); + } + + public boolean isNightVisionEnabled(Player player) { + return isMarked(player, nightVisionEnabledKey); + } + + public void enableNightVision(Player player) { + if (!player.hasPotionEffect(PotionEffectType.NIGHT_VISION)) { + player.addPotionEffect(new PotionEffect( + PotionEffectType.NIGHT_VISION, Integer.MAX_VALUE, 0, false, false, false)); + setBoolean(player.getPersistentDataContainer(), nightVisionEnabledKey, true); + } + } + + public void disableNightVision(Player player) { + if (!isNightVisionEnabled(player)) { + return; + } + player.removePotionEffect(PotionEffectType.NIGHT_VISION); + player.getPersistentDataContainer().remove(nightVisionEnabledKey); + } + + public boolean isGlowEnabled(Player player) { + return isMarked(player, glowEnabledKey); + } + + public void enableGlow(Player player) { + if (!player.isGlowing()) { + player.setGlowing(true); + setBoolean(player.getPersistentDataContainer(), glowEnabledKey, true); + } + } + + public void disableGlow(Player player) { + if (!isGlowEnabled(player)) { + return; + } + player.setGlowing(false); + player.getPersistentDataContainer().remove(glowEnabledKey); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + plugin.getServer().getScheduler().runTask(plugin, () -> reconcilePlayer(event.getPlayer(), false)); + } + + public void shutdown() { + permissionTask.cancel(); + for (Player player : plugin.getServer().getOnlinePlayers()) { + releaseOwnedStates(player, false); + } + } + + private void reconcileOnlinePlayers() { + for (Player player : plugin.getServer().getOnlinePlayers()) { + reconcilePlayer(player, true); + } + } + + private void reconcilePlayer(Player player, boolean notify) { + if (isFlyEnabled(player)) { + if (!player.hasPermission(FLY_PERMISSION)) { + disableFly(player); + notifyPermissionRemoval(player, "messages.fly-permission-removed", notify); + } else if (!player.getAllowFlight()) { + player.setAllowFlight(true); + } + } + + if (isNightVisionEnabled(player)) { + if (!player.hasPermission(NIGHT_VISION_PERMISSION)) { + disableNightVision(player); + notifyPermissionRemoval(player, "messages.nightvision-permission-removed", notify); + } else if (!player.hasPotionEffect(PotionEffectType.NIGHT_VISION)) { + player.addPotionEffect(new PotionEffect( + PotionEffectType.NIGHT_VISION, Integer.MAX_VALUE, 0, false, false, false)); + } + } + + if (isGlowEnabled(player)) { + if (!player.hasPermission(GLOW_PERMISSION)) { + disableGlow(player); + notifyPermissionRemoval(player, "messages.glow-permission-removed", notify); + } else if (!player.isGlowing()) { + player.setGlowing(true); + } + } + } + + private void releaseOwnedStates(Player player, boolean notify) { + if (isFlyEnabled(player)) { + disableFly(player); + notifyPermissionRemoval(player, "messages.fly-permission-removed", notify); + } + if (isNightVisionEnabled(player)) { + disableNightVision(player); + notifyPermissionRemoval(player, "messages.nightvision-permission-removed", notify); + } + if (isGlowEnabled(player)) { + disableGlow(player); + notifyPermissionRemoval(player, "messages.glow-permission-removed", notify); + } + } + + private void notifyPermissionRemoval(Player player, String path, boolean notify) { + if (notify) { + player.sendMessage(EssentialsC.getLangManager().getPrefixedString(path)); + } + } + + private boolean isMarked(Player player, NamespacedKey key) { + return isMarked(player.getPersistentDataContainer(), key); + } + + private boolean isMarked(PersistentDataContainer data, NamespacedKey key) { + Byte value = data.get(key, PersistentDataType.BYTE); + return value != null && value == (byte) 1; + } + + private boolean getBoolean(PersistentDataContainer data, NamespacedKey key) { + return isMarked(data, key); + } + + private void setBoolean(PersistentDataContainer data, NamespacedKey key, boolean value) { + data.set(key, PersistentDataType.BYTE, value ? (byte) 1 : (byte) 0); + } + + private void clearFlyMarkers(PersistentDataContainer data) { + data.remove(flyEnabledKey); + data.remove(flyPreviousAllowKey); + data.remove(flyPreviousFlyingKey); + } + + private boolean hasGameModeFlight(Player player) { + return player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR; + } +} diff --git a/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java b/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java index cac32c3..86f7721 100644 --- a/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java +++ b/src/main/java/cn/infstar/essentialsC/skinbridge/SkinBridgeManager.java @@ -1,7 +1,6 @@ package cn.infstar.essentialsC.skinbridge; import cn.infstar.essentialsC.EssentialsC; -import cn.infstar.essentialsC.util.AtomicYamlWriter; import com.destroystokyo.paper.profile.ProfileProperty; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -10,22 +9,19 @@ import com.google.gson.JsonParser; import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; 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 org.bukkit.scheduler.BukkitTask; -import java.io.File; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.ArrayList; import java.util.Base64; @@ -55,16 +51,19 @@ public final class SkinBridgeManager implements Listener { private final EssentialsC plugin; private final HttpClient httpClient; private final ThreadPoolExecutor executor; - private final File generatedCacheFile; + private final SkinCacheStore cacheStore; private final ConcurrentMap cache = new ConcurrentHashMap<>(); - private final ConcurrentMap generatedSkinCache = 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 ConcurrentMap> pendingGenerations = new ConcurrentHashMap<>(); private final AtomicLong configurationGeneration = new AtomicLong(); + private BukkitTask generatedCacheSaveTask; + private boolean generatedCacheDirty; + private volatile boolean shuttingDown; private volatile List providers = List.of(); private volatile SkinBridgeGateway gateway; @@ -82,7 +81,7 @@ public final class SkinBridgeManager implements Listener { public SkinBridgeManager(EssentialsC plugin) { this.plugin = plugin; - this.generatedCacheFile = new File(plugin.getDataFolder(), "skin-cache.yml"); + this.cacheStore = new SkinCacheStore(plugin); this.httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) @@ -136,10 +135,12 @@ public final class SkinBridgeManager implements Listener { } public void shutdown() { + shuttingDown = true; configurationGeneration.incrementAndGet(); cancelScheduledLookups(); cancelRunningLookups(); executor.shutdownNow(); + cancelGeneratedCacheSave(); saveGeneratedSkinCache(); cache.clear(); generatedSkinCache.clear(); @@ -367,7 +368,7 @@ public final class SkinBridgeManager implements Listener { if (logDetectionResults) { plugin.getLogger().info("SkinBridge 已识别玩家 " + playerName + " 的皮肤来源: " + provider.name()); } - SkinCacheKey cacheKey = new SkinCacheKey(matchedProfile.skinUrl(), matchedProfile.model()); + SkinCacheStore.Key cacheKey = new SkinCacheStore.Key(matchedProfile.skinUrl(), matchedProfile.model()); GeneratedSkin generatedSkin = getOrGenerateSkin(cacheKey, currentGateway); return cached(provider.name(), generatedSkin, State.EXTERNAL); } @@ -383,7 +384,7 @@ public final class SkinBridgeManager implements Listener { HttpRequest request = HttpRequest.newBuilder(requestUri) .timeout(Duration.ofSeconds(requestTimeoutSeconds)) .header("Accept", "application/json") - .header("User-Agent", "EssentialsC/" + plugin.getDescription().getVersion() + " SkinBridge") + .header("User-Agent", "EssentialsC/" + plugin.getPluginMeta().getVersion() + " SkinBridge") .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); @@ -480,66 +481,26 @@ public final class SkinBridgeManager implements Listener { } private void loadGeneratedSkinCache() { - if (!generatedCacheFile.exists()) { - return; - } - FileConfiguration config = YamlConfiguration.loadConfiguration(generatedCacheFile); - ConfigurationSection entries = config.getConfigurationSection("entries"); - if (entries == null) { - return; - } - long now = System.currentTimeMillis(); - for (String id : entries.getKeys(false)) { - String path = "entries." + id; - try { - String skinUrl = config.getString(path + ".skin-url", ""); - SkinModel model = SkinModel.valueOf(config.getString(path + ".model", "CLASSIC")); - long expiresAt = config.getLong(path + ".expires-at", 0L); - String value = config.getString(path + ".value", ""); - String signature = config.getString(path + ".signature", ""); - if (expiresAt > now && !skinUrl.isBlank() && !value.isBlank() && !signature.isBlank()) { - generatedSkinCache.put(new SkinCacheKey(skinUrl, model), - new GeneratedCacheEntry(new GeneratedSkin(value, signature), expiresAt)); - } - } catch (IllegalArgumentException ignored) { - plugin.getLogger().warning("忽略无效的 SkinBridge 缓存记录: " + id); - } - } + generatedSkinCache.putAll(cacheStore.load()); trimGeneratedSkinCache(); } private synchronized void saveGeneratedSkinCache() { - FileConfiguration config = new YamlConfiguration(); + if (!generatedCacheDirty) { + return; + } 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()); - config.set(path + ".model", key.model().name()); - config.set(path + ".expires-at", entry.expiresAtMillis()); - config.set(path + ".value", entry.skin().value()); - config.set(path + ".signature", entry.skin().signature()); - }); - try { - AtomicYamlWriter.save(config, generatedCacheFile); - } catch (Exception exception) { - plugin.getLogger().warning("保存 skin-cache.yml 失败: " + exception.getMessage()); + if (cacheStore.save(generatedSkinCache)) { + generatedCacheDirty = false; + } else if (!shuttingDown) { + scheduleGeneratedCacheSave(); } } - private String cacheId(SkinCacheKey key) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest((key.skinUrl() + "\n" + key.model().name()).getBytes(StandardCharsets.UTF_8)); - return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); - } catch (NoSuchAlgorithmException exception) { - throw new IllegalStateException("当前 Java 环境不支持 SHA-256。", exception); - } - } - - private GeneratedSkin getOrGenerateSkin(SkinCacheKey cacheKey, SkinBridgeGateway currentGateway) throws Exception { - GeneratedCacheEntry cachedEntry = generatedSkinCache.get(cacheKey); + private GeneratedSkin getOrGenerateSkin(SkinCacheStore.Key cacheKey, SkinBridgeGateway currentGateway) throws Exception { + SkinCacheStore.Entry cachedEntry = generatedSkinCache.get(cacheKey); if (cachedEntry != null && !cachedEntry.hasExpired()) { return cachedEntry.skin(); } @@ -587,11 +548,34 @@ public final class SkinBridgeManager implements Listener { } } - private synchronized void cacheGeneratedSkin(SkinCacheKey cacheKey, GeneratedSkin generatedSkin) { - generatedSkinCache.put(cacheKey, new GeneratedCacheEntry(generatedSkin, + private synchronized void cacheGeneratedSkin(SkinCacheStore.Key cacheKey, GeneratedSkin generatedSkin) { + if (shuttingDown) { + return; + } + generatedSkinCache.put(cacheKey, new SkinCacheStore.Entry(generatedSkin, System.currentTimeMillis() + Duration.ofMinutes(cacheMinutes).toMillis())); trimGeneratedSkinCache(); - saveGeneratedSkinCache(); + generatedCacheDirty = true; + scheduleGeneratedCacheSave(); + } + + private synchronized void scheduleGeneratedCacheSave() { + if (shuttingDown || generatedCacheSaveTask != null) { + return; + } + generatedCacheSaveTask = Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> { + synchronized (this) { + generatedCacheSaveTask = null; + } + saveGeneratedSkinCache(); + }, 100L); + } + + private synchronized void cancelGeneratedCacheSave() { + if (generatedCacheSaveTask != null) { + generatedCacheSaveTask.cancel(); + generatedCacheSaveTask = null; + } } private void trimGeneratedSkinCache() { @@ -690,7 +674,7 @@ public final class SkinBridgeManager implements Listener { try { return new MineSkinGateway(httpClient, endpoint, apiKey, visibility, timeoutSeconds, minimumSubmitIntervalMillis, - "EssentialsC/" + plugin.getDescription().getVersion() + " SkinBridge"); + "EssentialsC/" + plugin.getPluginMeta().getVersion() + " SkinBridge"); } catch (Exception | LinkageError exception) { plugin.getLogger().warning("加载 MineSkin SkinBridge 适配器失败: " + exception.getMessage()); return null; @@ -792,18 +776,9 @@ public final class SkinBridgeManager implements Listener { private record ProviderProfile(String skinUrl, SkinModel model) { } - 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; - } - } - private record CachedLookup(String providerId, GeneratedSkin skin, State state, long expiresAtMillis) { private boolean hasExpired() { return System.currentTimeMillis() >= expiresAtMillis; diff --git a/src/main/java/cn/infstar/essentialsC/skinbridge/SkinCacheStore.java b/src/main/java/cn/infstar/essentialsC/skinbridge/SkinCacheStore.java new file mode 100644 index 0000000..4cbe57d --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/skinbridge/SkinCacheStore.java @@ -0,0 +1,95 @@ +package cn.infstar.essentialsC.skinbridge; + +import cn.infstar.essentialsC.EssentialsC; +import cn.infstar.essentialsC.util.AtomicYamlWriter; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +final class SkinCacheStore { + + private final EssentialsC plugin; + private final File cacheFile; + + SkinCacheStore(EssentialsC plugin) { + this.plugin = plugin; + this.cacheFile = new File(plugin.getDataFolder(), "skin-cache.yml"); + } + + Map load() { + Map loaded = new HashMap<>(); + if (!cacheFile.exists()) { + return loaded; + } + + YamlConfiguration config = YamlConfiguration.loadConfiguration(cacheFile); + ConfigurationSection entries = config.getConfigurationSection("entries"); + if (entries == null) { + return loaded; + } + long now = System.currentTimeMillis(); + for (String id : entries.getKeys(false)) { + String path = "entries." + id; + try { + String skinUrl = config.getString(path + ".skin-url", ""); + SkinModel model = SkinModel.valueOf(config.getString(path + ".model", "CLASSIC")); + long expiresAt = config.getLong(path + ".expires-at", 0L); + String value = config.getString(path + ".value", ""); + String signature = config.getString(path + ".signature", ""); + if (expiresAt > now && !skinUrl.isBlank() && !value.isBlank() && !signature.isBlank()) { + loaded.put(new Key(skinUrl, model), + new Entry(new GeneratedSkin(value, signature), expiresAt)); + } + } catch (IllegalArgumentException exception) { + plugin.getLogger().warning("忽略无效的 SkinBridge 缓存记录: " + id); + } + } + return loaded; + } + + boolean save(Map entries) { + YamlConfiguration config = new YamlConfiguration(); + entries.forEach((key, entry) -> { + String path = "entries." + cacheId(key); + config.set(path + ".skin-url", key.skinUrl()); + config.set(path + ".model", key.model().name()); + config.set(path + ".expires-at", entry.expiresAtMillis()); + config.set(path + ".value", entry.skin().value()); + config.set(path + ".signature", entry.skin().signature()); + }); + try { + AtomicYamlWriter.save(config, cacheFile); + return true; + } catch (Exception exception) { + plugin.getLogger().warning("保存 skin-cache.yml 失败: " + exception.getMessage()); + return false; + } + } + + private String cacheId(Key key) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest((key.skinUrl() + "\n" + key.model().name()) + .getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("当前 Java 环境不支持 SHA-256。", exception); + } + } + + record Key(String skinUrl, SkinModel model) { + } + + record Entry(GeneratedSkin skin, long expiresAtMillis) { + 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 f95103b..c3f8028 100644 --- a/src/main/java/cn/infstar/essentialsC/teleport/TeleportRequestManager.java +++ b/src/main/java/cn/infstar/essentialsC/teleport/TeleportRequestManager.java @@ -8,12 +8,9 @@ import cn.infstar.essentialsC.api.event.TeleportRequestSendEvent; import cn.infstar.essentialsC.api.event.TeleportWarmupCancelledEvent; import cn.infstar.essentialsC.api.event.TeleportWarmupEvent; import cn.infstar.essentialsC.commands.VanishCommand; -import cn.infstar.essentialsC.util.AtomicYamlWriter; import net.kyori.adventure.title.Title; import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -25,7 +22,6 @@ import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.scheduler.BukkitTask; -import java.io.File; import java.time.Duration; import java.time.Instant; import java.util.ArrayDeque; @@ -57,8 +53,7 @@ public final class TeleportRequestManager implements Listener { private final Set warmupDamagedPlayers = new HashSet<>(); private final Set invulnerablePlayers = new HashSet<>(); private final Set ignoringRequests = new HashSet<>(); - private final File ignoreFile; - private final File cooldownFile; + private final TeleportStateStore stateStore; private BukkitTask cleanupTask; private int timeoutSeconds; @@ -82,10 +77,11 @@ public final class TeleportRequestManager implements Listener { public TeleportRequestManager(EssentialsC plugin) { this.plugin = plugin; - this.ignoreFile = new File(plugin.getDataFolder(), "teleport-ignore.yml"); - this.cooldownFile = new File(plugin.getDataFolder(), "teleport-cooldowns.yml"); + this.stateStore = new TeleportStateStore(plugin); reload(); - loadCooldowns(); + TeleportStateStore.CooldownState cooldownState = stateStore.loadCooldowns(); + sendCooldowns.putAll(cooldownState.send()); + acceptCooldowns.putAll(cooldownState.accept()); cleanupTask = Bukkit.getScheduler().runTaskTimer(plugin, this::cleanupExpiredState, 1200L, 1200L); } @@ -131,7 +127,8 @@ public final class TeleportRequestManager implements Listener { warmupSound = plugin.getConfig().getString("tpa.sounds.warmup", "block.note_block.banjo"); cancelSound = plugin.getConfig().getString("tpa.sounds.cancelled", "entity.item.break"); completeSound = plugin.getConfig().getString("tpa.sounds.complete", "entity.enderman.teleport"); - loadIgnoringRequests(); + ignoringRequests.clear(); + ignoringRequests.addAll(stateStore.loadIgnoringRequests()); } public void shutdown() { @@ -139,13 +136,13 @@ public final class TeleportRequestManager implements Listener { cleanupTask.cancel(); cleanupTask = null; } - saveCooldowns(); + stateStore.saveCooldowns(sendCooldowns, acceptCooldowns); requests.clear(); sendCooldowns.clear(); acceptCooldowns.clear(); cancelAllWarmups(); clearAllInvulnerability(); - saveIgnoringRequests(); + stateStore.saveIgnoringRequests(ignoringRequests); } public CreateRequestResult createRequest(Player requester, Player target, TeleportRequest.Type type) { @@ -335,13 +332,7 @@ public final class TeleportRequestManager implements Listener { } public boolean isVanished(Player player) { - if (VanishCommand.isVanished(player)) { - return true; - } - return player.hasMetadata("vanished") && player.getMetadata("vanished").stream() - .map(metadata -> metadata.asBoolean()) - .findFirst() - .orElse(false); + return VanishCommand.isVanished(player); } public ToggleIgnoreResult toggleIgnoringRequests(Player player) { @@ -354,7 +345,7 @@ public final class TeleportRequestManager implements Listener { ignoringRequests.add(uuid); nowIgnoring = true; } - if (!saveIgnoringRequests()) { + if (!stateStore.saveIgnoringRequests(ignoringRequests)) { if (nowIgnoring) { ignoringRequests.remove(uuid); } else { @@ -822,91 +813,7 @@ public final class TeleportRequestManager implements Listener { long now = System.currentTimeMillis(); sendCooldowns.values().removeIf(expiresAt -> expiresAt <= now); acceptCooldowns.values().removeIf(expiresAt -> expiresAt <= now); - saveCooldowns(); - } - - private void loadCooldowns() { - if (!cooldownFile.exists()) { - return; - } - FileConfiguration cooldownConfig = YamlConfiguration.loadConfiguration(cooldownFile); - loadCooldownMap(cooldownConfig, "send", sendCooldowns); - loadCooldownMap(cooldownConfig, "accept", acceptCooldowns); - } - - private void loadCooldownMap(FileConfiguration config, String path, Map destination) { - if (!config.isConfigurationSection(path)) { - return; - } - long now = System.currentTimeMillis(); - for (String key : config.getConfigurationSection(path).getKeys(false)) { - try { - UUID uuid = UUID.fromString(key); - long expiresAt = config.getLong(path + "." + key, 0L); - if (expiresAt > now) { - destination.put(uuid, expiresAt); - } - } catch (IllegalArgumentException ignored) { - plugin.getLogger().warning("忽略无效的 TPA 冷却记录 UUID: " + key); - } - } - } - - private void saveCooldowns() { - FileConfiguration cooldownConfig = new YamlConfiguration(); - long now = System.currentTimeMillis(); - saveCooldownMap(cooldownConfig, "send", sendCooldowns, now); - saveCooldownMap(cooldownConfig, "accept", acceptCooldowns, now); - try { - AtomicYamlWriter.save(cooldownConfig, cooldownFile); - } catch (Exception exception) { - plugin.getLogger().warning("保存 teleport-cooldowns.yml 失败: " + exception.getMessage()); - } - } - - private void saveCooldownMap(FileConfiguration config, String path, Map source, long now) { - source.forEach((uuid, expiresAt) -> { - if (expiresAt > now) { - config.set(path + "." + uuid, expiresAt); - } - }); - } - - private void loadIgnoringRequests() { - ignoringRequests.clear(); - if (!ignoreFile.exists()) { - return; - } - - FileConfiguration ignoreConfig = YamlConfiguration.loadConfiguration(ignoreFile); - if (!ignoreConfig.isConfigurationSection("ignored")) { - return; - } - - for (String key : ignoreConfig.getConfigurationSection("ignored").getKeys(false)) { - if (!ignoreConfig.getBoolean("ignored." + key, false)) { - continue; - } - try { - ignoringRequests.add(UUID.fromString(key)); - } catch (IllegalArgumentException ignored) { - plugin.getLogger().warning("忽略无效的 TPA 忽略记录 UUID: " + key); - } - } - } - - 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; - } + stateStore.saveCooldowns(sendCooldowns, acceptCooldowns); } public enum ToggleIgnoreResult { diff --git a/src/main/java/cn/infstar/essentialsC/teleport/TeleportStateStore.java b/src/main/java/cn/infstar/essentialsC/teleport/TeleportStateStore.java new file mode 100644 index 0000000..e548625 --- /dev/null +++ b/src/main/java/cn/infstar/essentialsC/teleport/TeleportStateStore.java @@ -0,0 +1,145 @@ +package cn.infstar.essentialsC.teleport; + +import cn.infstar.essentialsC.EssentialsC; +import cn.infstar.essentialsC.util.AtomicYamlWriter; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +final class TeleportStateStore { + + private final EssentialsC plugin; + private final File cooldownFile; + private final File ignoreFile; + private boolean cooldownWritable = true; + private boolean ignoreWritable = true; + + TeleportStateStore(EssentialsC plugin) { + this.plugin = plugin; + this.cooldownFile = new File(plugin.getDataFolder(), "teleport-cooldowns.yml"); + this.ignoreFile = new File(plugin.getDataFolder(), "teleport-ignore.yml"); + } + + CooldownState loadCooldowns() { + Map send = new HashMap<>(); + Map accept = new HashMap<>(); + if (!cooldownFile.exists()) { + return new CooldownState(send, accept); + } + + YamlConfiguration config = new YamlConfiguration(); + try { + config.load(cooldownFile); + } catch (IOException | InvalidConfigurationException exception) { + cooldownWritable = false; + plugin.getLogger().severe("加载 teleport-cooldowns.yml 失败,已禁止覆盖原文件: " + + exception.getMessage()); + return new CooldownState(send, accept); + } + loadCooldownMap(config, "send", send); + loadCooldownMap(config, "accept", accept); + return new CooldownState(send, accept); + } + + void saveCooldowns(Map send, Map accept) { + if (!cooldownWritable) { + return; + } + YamlConfiguration config = new YamlConfiguration(); + long now = System.currentTimeMillis(); + saveCooldownMap(config, "send", send, now); + saveCooldownMap(config, "accept", accept, now); + try { + AtomicYamlWriter.save(config, cooldownFile); + } catch (Exception exception) { + plugin.getLogger().warning("保存 teleport-cooldowns.yml 失败: " + exception.getMessage()); + } + } + + Set loadIgnoringRequests() { + Set ignoredPlayers = new HashSet<>(); + if (!ignoreFile.exists()) { + return ignoredPlayers; + } + + YamlConfiguration config = new YamlConfiguration(); + try { + config.load(ignoreFile); + } catch (IOException | InvalidConfigurationException exception) { + ignoreWritable = false; + plugin.getLogger().severe("加载 teleport-ignore.yml 失败,已禁止覆盖原文件: " + + exception.getMessage()); + return ignoredPlayers; + } + ConfigurationSection ignored = config.getConfigurationSection("ignored"); + if (ignored == null) { + return ignoredPlayers; + } + for (String key : ignored.getKeys(false)) { + if (!ignored.getBoolean(key, false)) { + continue; + } + try { + ignoredPlayers.add(UUID.fromString(key)); + } catch (IllegalArgumentException exception) { + plugin.getLogger().warning("忽略无效的 TPA 忽略记录 UUID: " + key); + } + } + return ignoredPlayers; + } + + boolean saveIgnoringRequests(Set ignoredPlayers) { + if (!ignoreWritable) { + return false; + } + YamlConfiguration config = new YamlConfiguration(); + for (UUID playerId : ignoredPlayers) { + config.set("ignored." + playerId, true); + } + try { + AtomicYamlWriter.save(config, ignoreFile); + return true; + } catch (Exception exception) { + plugin.getLogger().warning("保存 teleport-ignore.yml 失败: " + exception.getMessage()); + return false; + } + } + + private void loadCooldownMap(YamlConfiguration config, String path, Map destination) { + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) { + return; + } + long now = System.currentTimeMillis(); + for (String key : section.getKeys(false)) { + try { + UUID playerId = UUID.fromString(key); + long expiresAt = section.getLong(key, 0L); + if (expiresAt > now) { + destination.put(playerId, expiresAt); + } + } catch (IllegalArgumentException exception) { + plugin.getLogger().warning("忽略无效的 TPA 冷却记录 UUID: " + key); + } + } + } + + private void saveCooldownMap(YamlConfiguration config, String path, Map source, long now) { + source.forEach((playerId, expiresAt) -> { + if (expiresAt > now) { + config.set(path + "." + playerId, expiresAt); + } + }); + } + + record CooldownState(Map send, Map accept) { + } +} diff --git a/src/main/java/cn/infstar/essentialsC/tpsbar/TpsBarManager.java b/src/main/java/cn/infstar/essentialsC/tpsbar/TpsBarManager.java index 7e60520..69409ca 100644 --- a/src/main/java/cn/infstar/essentialsC/tpsbar/TpsBarManager.java +++ b/src/main/java/cn/infstar/essentialsC/tpsbar/TpsBarManager.java @@ -1,8 +1,9 @@ package cn.infstar.essentialsC.tpsbar; import cn.infstar.essentialsC.EssentialsC; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; import org.bukkit.boss.BossBar; @@ -26,6 +27,8 @@ import java.util.UUID; public final class TpsBarManager implements Listener, TpsBarService { + private static final LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); + private static final PlainTextComponentSerializer PLAIN_TEXT = PlainTextComponentSerializer.plainText(); private static final double MAX_TPS = 20.0D; private static final int UPDATE_INTERVAL_TICKS = 20; private static final BarStyle BAR_STYLE = BarStyle.SEGMENTED_20; @@ -282,11 +285,11 @@ public final class TpsBarManager implements Listener, TpsBarService { } private String buildTitle(double tps, double mspt, int ping) { - return ChatColor.translateAlternateColorCodes('&', applyPlaceholders(titleFormat, Map.of( + return applyPlaceholders(titleFormat, Map.of( "tps_1m", formatDouble(tps), "mspt", formatDouble(mspt), "ping", Integer.toString(ping) - ))); + )); } private BarColor resolveBarColor(double tps, double mspt, int ping) { @@ -317,7 +320,7 @@ public final class TpsBarManager implements Listener, TpsBarService { } private String prefixed(String message) { - return EssentialsC.getLangManager().getPrefix() + ChatColor.translateAlternateColorCodes('&', message); + return EssentialsC.getLangManager().getPrefix() + message; } private String applyPlaceholders(String text, Map placeholders) { @@ -329,7 +332,7 @@ public final class TpsBarManager implements Listener, TpsBarService { } private String stripColor(String message) { - return ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', message)); + return PLAIN_TEXT.serialize(LEGACY_SECTION.deserialize(message == null ? "" : message)); } private String formatDouble(double value) { diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml index 09355b4..79d6181 100644 --- a/src/main/resources/lang/en_US.yml +++ b/src/main/resources/lang/en_US.yml @@ -20,11 +20,14 @@ messages: suicide-message: '<#FF7E5E>{player} died by suicide.' fly-enabled: '<#00FB9A>Flight mode enabled.' fly-disabled: '<#FF7E5E>Flight mode disabled.' + fly-permission-removed: '<#FF7E5E>Your flight permission was removed, so flight mode was disabled.' nightvision-enabled: '<#00FB9A>Night vision enabled.' nightvision-disabled: '<#FF7E5E>Night vision disabled.' + nightvision-permission-removed: '<#FF7E5E>Your night vision permission was removed, so night vision was disabled.' nightvision-usage: '<#FF3300>Error: <#FF7E5E>Usage: /nightvision [on|off|toggle]' glow-enabled: '<#00FB9A>Glowing enabled.' glow-disabled: '<#FF7E5E>Glowing disabled.' + glow-permission-removed: '<#FF7E5E>Your glowing permission was removed, so glowing was disabled.' 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.' diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml index 02bd0f9..b75823a 100644 --- a/src/main/resources/lang/zh_CN.yml +++ b/src/main/resources/lang/zh_CN.yml @@ -20,11 +20,14 @@ messages: suicide-message: '<#FF7E5E>{player} 自杀了.' fly-enabled: '<#00FB9A>飞行模式已开启.' fly-disabled: '<#FF7E5E>飞行模式已关闭.' + fly-permission-removed: '<#FF7E5E>你的飞行权限已被移除,飞行模式已关闭.' nightvision-enabled: '<#00FB9A>夜视已开启.' nightvision-disabled: '<#FF7E5E>夜视已关闭.' + nightvision-permission-removed: '<#FF7E5E>你的夜视权限已被移除,夜视已关闭.' nightvision-usage: '<#FF3300>错误: <#FF7E5E>用法: /nightvision [on|off|toggle]' glow-enabled: '<#00FB9A>发光已开启.' glow-disabled: '<#FF7E5E>发光已关闭.' + glow-permission-removed: '<#FF7E5E>你的发光权限已被移除,发光已关闭.' glow-usage: '<#FF3300>错误: <#FF7E5E>用法: /glow [on|off|toggle]' vanish-enabled: '<#00FB9A>你已进入隐身模式.' vanish-disabled: '<#FF7E5E>你已退出隐身模式.' diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index be62473..6e6a2f8 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -7,6 +7,13 @@ api-version: '1.21.11' load: POSTWORLD folia-supported: false +dependencies: + server: + LuckPerms: + load: BEFORE + required: false + join-classpath: true + authors: [ Coldsmiles_7 ] website: www.infstar.cn diff --git a/src/test/java/cn/infstar/essentialsC/admin/AdminModeStoreTest.java b/src/test/java/cn/infstar/essentialsC/admin/AdminModeStoreTest.java new file mode 100644 index 0000000..e98e200 --- /dev/null +++ b/src/test/java/cn/infstar/essentialsC/admin/AdminModeStoreTest.java @@ -0,0 +1,45 @@ +package cn.infstar.essentialsC.admin; + +import cn.infstar.essentialsC.util.AtomicYamlWriter; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AdminModeStoreTest { + + @TempDir + Path temporaryDirectory; + + @Test + void migratesLegacyPlayersIntoIndependentFiles() throws Exception { + UUID playerId = UUID.randomUUID(); + Path legacyFile = temporaryDirectory.resolve("admin-mode.yml"); + YamlConfiguration legacy = new YamlConfiguration(); + legacy.set("players." + playerId + ".active", true); + legacy.set("players." + playerId + ".normal.level", 27); + AtomicYamlWriter.save(legacy, legacyFile.toFile()); + + AdminModeStore store = new AdminModeStore( + temporaryDirectory.resolve("admin-mode").toFile(), legacyFile.toFile(), Logger.getAnonymousLogger()); + + YamlConfiguration migrated = store.load(playerId); + assertNotNull(migrated); + assertTrue(migrated.getBoolean("active")); + assertEquals(27, migrated.getInt("normal.level")); + assertTrue(Files.exists(temporaryDirectory.resolve("admin-mode").resolve(playerId + ".yml"))); + assertFalse(Files.exists(legacyFile)); + try (var backups = Files.list(temporaryDirectory)) { + assertTrue(backups.anyMatch(path -> path.getFileName().toString().startsWith("admin-mode.legacy-"))); + } + } +} diff --git a/src/test/java/cn/infstar/essentialsC/compat/jei/RecipeGroupingTest.java b/src/test/java/cn/infstar/essentialsC/compat/jei/RecipeGroupingTest.java new file mode 100644 index 0000000..5338122 --- /dev/null +++ b/src/test/java/cn/infstar/essentialsC/compat/jei/RecipeGroupingTest.java @@ -0,0 +1,23 @@ +package cn.infstar.essentialsC.compat.jei; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RecipeGroupingTest { + + @Test + void groupsValuesInOnePassAndPreservesOrder() { + List recipes = List.of("crafting:first", "smelting:second", "crafting:third"); + + Map> grouped = RecipeGrouping.groupBy( + recipes, value -> value.substring(0, value.indexOf(':'))); + + assertEquals(List.of("crafting", "smelting"), grouped.keySet().stream().toList()); + assertEquals(List.of("crafting:first", "crafting:third"), grouped.get("crafting")); + assertEquals(List.of("smelting:second"), grouped.get("smelting")); + } +} diff --git a/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java b/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java index 8c822fb..8e6874a 100644 --- a/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java +++ b/src/test/java/cn/infstar/essentialsC/util/AtomicYamlWriterTest.java @@ -58,4 +58,19 @@ class AtomicYamlWriterTest { assertTrue(YamlConfiguration.loadConfiguration(target.toFile()).getBoolean("written")); } + + @Test + void preservesParsedChineseComments() throws Exception { + Path target = temporaryDirectory.resolve("commented.yml"); + YamlConfiguration configuration = new YamlConfiguration(); + configuration.options().parseComments(true); + configuration.loadFromString("# 中文配置注释\nenabled: false\n"); + configuration.set("enabled", true); + + AtomicYamlWriter.save(configuration, target.toFile()); + + String saved = Files.readString(target, StandardCharsets.UTF_8); + assertTrue(saved.contains("# 中文配置注释")); + assertTrue(saved.contains("enabled: true")); + } } 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 e23042e..b2ef5d5 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 @@ -1,6 +1,7 @@ package cn.infstar.essentialsC.v1_21_11.jei; import cn.infstar.essentialsC.compat.jei.JeiRecipeSyncAdapter; +import cn.infstar.essentialsC.compat.jei.RecipeGrouping; import io.netty.buffer.Unpooled; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -48,20 +49,17 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { var list = new ArrayList(); var seen = new HashSet>(); + var groupedRecipes = RecipeGrouping.groupBy( + recipeMap.values(), holder -> holder.value().getSerializer()); for (RecipeSerializer serializer : BuiltInRegistries.RECIPE_SERIALIZER) { if (!seen.add(serializer)) { continue; } - List> recipes = new ArrayList<>(); - for (RecipeHolder holder : recipeMap.values()) { - if (holder.value().getSerializer() == serializer) { - recipes.add(holder); - } - } + List> recipes = groupedRecipes.get(serializer); - if (!recipes.isEmpty()) { + if (recipes != null && !recipes.isEmpty()) { RecipeSerializer entrySerializer = recipes.get(0).value().getSerializer(); list.add(new FabricRecipeEntry(entrySerializer, recipes)); } diff --git a/versions/v26_1_2/build.gradle b/versions/v26_1_2/build.gradle index 6706de6..502ab86 100644 --- a/versions/v26_1_2/build.gradle +++ b/versions/v26_1_2/build.gradle @@ -1,3 +1,5 @@ +import org.gradle.api.attributes.java.TargetJvmVersion + plugins { id 'java-library' id 'io.papermc.paperweight.userdev' @@ -13,15 +15,20 @@ repositories { dependencies { api project(':compat-api') - paperweight.paperDevBundle('26.1.2.build.72-stable') + paperweight.paperDevBundle('26.1.2.build.74-stable') } java { toolchain.languageVersion.set(JavaLanguageVersion.of(25)) } +configurations.configureEach { + attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25) +} + paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.getMOJANG_PRODUCTION() tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' + options.release.set(21) } 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 f40b85f..5c8f4f9 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 @@ -1,6 +1,7 @@ package cn.infstar.essentialsC.v26_1_2.jei; import cn.infstar.essentialsC.compat.jei.JeiRecipeSyncAdapter; +import cn.infstar.essentialsC.compat.jei.RecipeGrouping; import io.netty.buffer.Unpooled; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -48,20 +49,17 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { var list = new ArrayList(); var seen = new HashSet>(); + var groupedRecipes = RecipeGrouping.groupBy( + recipeMap.values(), holder -> holder.value().getSerializer()); for (RecipeSerializer serializer : BuiltInRegistries.RECIPE_SERIALIZER) { if (!seen.add(serializer)) { continue; } - List> recipes = new ArrayList<>(); - for (RecipeHolder holder : recipeMap.values()) { - if (holder.value().getSerializer() == serializer) { - recipes.add(holder); - } - } + List> recipes = groupedRecipes.get(serializer); - if (!recipes.isEmpty()) { + if (recipes != null && !recipes.isEmpty()) { RecipeSerializer entrySerializer = recipes.get(0).value().getSerializer(); list.add(new FabricRecipeEntry(entrySerializer, recipes)); } diff --git a/versions/v26_2/build.gradle b/versions/v26_2/build.gradle index 3072c2a..84968a3 100644 --- a/versions/v26_2/build.gradle +++ b/versions/v26_2/build.gradle @@ -1,3 +1,5 @@ +import org.gradle.api.attributes.java.TargetJvmVersion + plugins { id 'java-library' id 'io.papermc.paperweight.userdev' @@ -13,15 +15,20 @@ repositories { dependencies { api project(':compat-api') - paperweight.paperDevBundle('26.2.build.31-alpha') + paperweight.paperDevBundle('26.2.build.87-stable') } java { toolchain.languageVersion.set(JavaLanguageVersion.of(25)) } +configurations.configureEach { + attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 25) +} + paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.getMOJANG_PRODUCTION() tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' + options.release.set(21) } 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 5524fb0..a2cee3e 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 @@ -1,6 +1,7 @@ package cn.infstar.essentialsC.v26_2.jei; import cn.infstar.essentialsC.compat.jei.JeiRecipeSyncAdapter; +import cn.infstar.essentialsC.compat.jei.RecipeGrouping; import io.netty.buffer.Unpooled; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -48,20 +49,17 @@ public final class JeiRecipeSyncAdapterImpl implements JeiRecipeSyncAdapter { var list = new ArrayList(); var seen = new HashSet>(); + var groupedRecipes = RecipeGrouping.groupBy( + recipeMap.values(), holder -> holder.value().getSerializer()); for (RecipeSerializer serializer : BuiltInRegistries.RECIPE_SERIALIZER) { if (!seen.add(serializer)) { continue; } - List> recipes = new ArrayList<>(); - for (RecipeHolder holder : recipeMap.values()) { - if (holder.value().getSerializer() == serializer) { - recipes.add(holder); - } - } + List> recipes = groupedRecipes.get(serializer); - if (!recipes.isEmpty()) { + if (recipes != null && !recipes.isEmpty()) { RecipeSerializer entrySerializer = recipes.get(0).value().getSerializer(); list.add(new FabricRecipeEntry(entrySerializer, recipes)); }