diff --git a/CHANGELOG.md b/CHANGELOG.md index d61e3cc..d956048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,8 @@ - PlaceholderAPI 的 Maven repository 改為 `https://repo.helpch.at/releases/`。舊網址 `repo.extendedclip.com` 現在只會轉址,在有網路白名單的環境會導致建置失敗。 - Maven 插件(都維持 3.x):compiler 3.11.0 → 3.16.0、enforcer 3.4.1 → 3.6.3、surefire 3.2.5 → 3.6.0。Java 版本改由單一的 `maven.compiler.release` 屬性設定。 - 測試依賴:JUnit 5.10.2 → 5.14.4、Mockito 5.11.0 → 5.24.0。surefire 3.6.0 之後 Mockito 無法在測試 JVM 中 self-attach,因此改用 `-javaagent` 載入,由 maven-dependency-plugin 的 `properties` goal 提供 jar 路徑。 -- 文件:README 新增依賴說明、ActionBar 設定與 VeinMiner 整合的說明,並修正兩處跟實際行為不符的描述: - - `/autopickup reload` 不會存檔,而是重新讀取檔案。 - - 聊天訊息實際從 `lang.yml` 讀取,`config.yml` 的 `messages:` 目前沒有作用。 +- **`config.yml` 的 `messages:` 現在會生效。** 以前所有訊息都只從 `lang.yml` 讀取,`config.yml` 裡的訊息改了也沒反應。現在的規則是:`config.yml` 中被改過的訊息(跟內建預設不同,或是內建沒有的 key)優先;其餘從 `lang.yml` 讀取;兩邊都沒有就用內建預設。ActionBar 模板(`messages.actionbar`、`messages.actionbar-entry`)也套用同一套規則。原本在 `lang.yml` 改過的訊息不受影響。 +- 文件:README 新增依賴說明、ActionBar 設定與 VeinMiner 整合的說明,並修正 `/autopickup reload` 的描述:它不會存檔,而是重新讀取檔案。 ### 新增 @@ -36,6 +35,7 @@ 2. 如果有用 PlaceholderAPI,建議升到 **2.12.3 以上**。根據 PlaceholderAPI 的版本說明,該版修正了 Paper 26.2 新版本號格式的解析問題。 3. 如果有用 VeinMiner,請確認你的 VeinMiner 版本支援 Paper 26.2。 4. `plugins/AutoPickup/` 底下的設定檔與資料檔(`players.yml`、`filters.yml`)格式沒有變,可以直接沿用。 +5. 如果你曾經在 `config.yml` 的 `messages:` 改過訊息(以前不會生效),升級後這些修改會開始生效,並優先於 `lang.yml`。 ## [1.0.0] diff --git a/CLAUDE.md b/CLAUDE.md index 0bc2ed8..fbcc023 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Note: directory is `com/mrpippi/`; Java package declarations use `com.autopickup - Calls `saveDefaultConfig()`, `loadGuiConfig()`, then reads `settings.default-enabled` - Instantiates `PlayerStateManager` and `FilterManager`, registers listeners and command - PlaceholderAPI integration is **optional** and loaded via reflection so the plugin runs without PAPI on the classpath -- `getMessage(String path)` reads from `lang.yml` (always loaded in `onEnable`, so `config.yml` `messages:` is effectively unused), converts `&` codes and MiniMessage tags via `legacyToMiniMessage()` + `MiniMessage.deserialize()` +- `getMessage(String path)` resolves the template via `resolveMessage(path, getConfig(), langConfig)`: a `config.yml` value that differs from the bundled default (or has no bundled default) wins, then `lang.yml`, then the bundled default; then converts `&` codes and MiniMessage tags via `legacyToMiniMessage()` + `MiniMessage.deserialize()` - `getGuiMessage(String path)` does the same but reads from `gui.yml` - `reload()` reloads all config/data files; called by `/autopickup reload` @@ -129,7 +129,7 @@ messages: reloaded: "&aConfiguration reloaded." ``` -All message values support `&` color codes and MiniMessage tags. Note: `getMessage()` actually reads `lang.yml`, so these `messages:` entries are not used at runtime. `settings.actionbar.enabled` / `display-ticks` control the ActionBar pickup notification. +All message values support `&` color codes and MiniMessage tags. An edited `config.yml` message overrides the same key in `lang.yml`; unedited defaults fall through to `lang.yml` (see `AutoPickupPlugin.resolveMessage`). `ActionBarManager` resolves `messages.actionbar*` the same way. `settings.actionbar.enabled` / `display-ticks` control the ActionBar pickup notification. ### `gui.yml` Controls all GUI text: title format, button names/lore, mode display names, item slot prefixes. @@ -137,7 +137,7 @@ Supports the same color code syntax plus runtime placeholders: `{mode}`, `{page} Accessed via `AutoPickupPlugin.getGuiMessage(String path)` or `getGuiConfig()`. ### `lang.yml` -Source of all chat messages (`messages.*`) plus the ActionBar templates `messages.actionbar` / `messages.actionbar-entry`. Same keys and syntax as the `config.yml` `messages:` block. +Fallback source for chat messages (`messages.*`) and the ActionBar templates `messages.actionbar` / `messages.actionbar-entry`, used when `config.yml` doesn't override the key. ### `plugin.yml` - Command: `autopickup`, alias: `ap` diff --git a/README.md b/README.md index 910a01e..cb3d41b 100644 --- a/README.md +++ b/README.md @@ -119,10 +119,17 @@ settings: display-ticks: 40 messages: - # …(見下方說明) + toggled-on: "&aAuto-pickup has been &fenabled&a." + # …其餘訊息與 lang.yml 相同(見下方) ``` -> **注意:** 目前所有聊天訊息都從 `lang.yml` 讀取,`config.yml` 裡的 `messages:` 區塊**不會被使用**。要修改訊息請編輯 `lang.yml`。 +訊息可以寫在 `config.yml` 或 `lang.yml`,規則如下: + +1. `config.yml` 裡**被改過**的訊息(跟內建預設值不同,或是新增了內建沒有的 key,例如 `actionbar`)優先使用。 +2. 其餘訊息從 `lang.yml` 讀取。 +3. 兩邊都沒有時,使用內建預設值。 + +因此只想改幾句訊息時,直接改 `config.yml` 就會生效;沒改的訊息不會蓋掉你在 `lang.yml` 的設定。改完執行 `/autopickup reload` 即可套用。 ### `gui.yml` @@ -137,7 +144,7 @@ messages: ### `lang.yml` -所有聊天訊息與 ActionBar 文字的來源。支援 `&` 色碼(如 `&a` 綠色)與 MiniMessage 標籤(如 ``、`<#RRGGBB>`),兩種可以混用。 +聊天訊息與 ActionBar 文字的來源(`config.yml` 中被改過的同名訊息會優先,見上方)。支援 `&` 色碼(如 `&a` 綠色)與 MiniMessage 標籤(如 ``、`<#RRGGBB>`),兩種可以混用。 ```yaml messages: diff --git a/src/main/java/com/mrpippi/AutoPickupPlugin.java b/src/main/java/com/mrpippi/AutoPickupPlugin.java index edaf346..63de56c 100644 --- a/src/main/java/com/mrpippi/AutoPickupPlugin.java +++ b/src/main/java/com/mrpippi/AutoPickupPlugin.java @@ -9,12 +9,14 @@ import com.autopickup.manager.PlayerStateManager; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.configuration.Configuration; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.util.Map; +import java.util.Objects; /** * Main plugin class for AutoPickup. @@ -183,10 +185,45 @@ public Component getGuiMessage(String path) { * */ public Component getMessage(String path) { - String msg = langConfig != null ? langConfig.getString(path, "") : getConfig().getString(path, ""); + String msg = Objects.requireNonNullElse(getMessageTemplate(path), ""); return MINI_MESSAGE.deserialize(legacyToMiniMessage(msg)); } + /** + * Returns the raw (unparsed) message template for {@code path}, or {@code null} if neither + * config.yml nor lang.yml defines it. See {@link #resolveMessage} for precedence. + */ + public String getMessageTemplate(String path) { + return resolveMessage(path, getConfig(), langConfig); + } + + /** + * Resolves a message template from config.yml and lang.yml. + * + *
    + *
  1. config.yml, if the server's file sets {@code path} to something other than the + * bundled default (an edited message, or a key the bundled config.yml doesn't have); + *
  2. otherwise lang.yml; + *
  3. otherwise config.yml including its bundled defaults; + *
  4. otherwise {@code null}. + *
+ * + *

Unedited bundled defaults in config.yml therefore never shadow a customised lang.yml. + */ + public static String resolveMessage(String path, Configuration config, FileConfiguration lang) { + if (config != null && config.contains(path, true)) { + String value = config.getString(path); + Configuration defaults = config.getDefaults(); + String bundled = defaults != null ? defaults.getString(path) : null; + if (value != null && !value.equals(bundled)) return value; + } + if (lang != null) { + String value = lang.getString(path); + if (value != null) return value; + } + return config != null ? config.getString(path) : null; + } + /** * Pre-processes a string by converting legacy {@code &} / {@code §} color codes * into equivalent MiniMessage tags so that both syntaxes can coexist. diff --git a/src/main/java/com/mrpippi/manager/ActionBarManager.java b/src/main/java/com/mrpippi/manager/ActionBarManager.java index 94de9b9..1b83792 100644 --- a/src/main/java/com/mrpippi/manager/ActionBarManager.java +++ b/src/main/java/com/mrpippi/manager/ActionBarManager.java @@ -120,13 +120,14 @@ private void sendNow(Player player) { /** * Builds the full ActionBar component from the accumulated pickup map. * - *

Template: {@code settings.actionbar-format} in lang.yml with placeholder + *

Template: {@code messages.actionbar} (config.yml or lang.yml, see + * {@link AutoPickupPlugin#resolveMessage}) with placeholder * {@code {entries}} being a comma-joined list of per-entry strings and * {@code {total}} being the sum of all counts. */ private Component buildActionBar(Map pickups) { - String entryTemplate = getLangString("messages.actionbar-entry", " &7x{count}"); - String barTemplate = getLangString("messages.actionbar", "&a+ &f{entries}"); + String entryTemplate = getTemplate("messages.actionbar-entry", " &7x{count}"); + String barTemplate = getTemplate("messages.actionbar", "&a+ &f{entries}"); List entryStrings = new ArrayList<>(); int total = 0; @@ -150,13 +151,10 @@ private Component buildActionBar(Map pickups) { return miniMessage.deserialize(AutoPickupPlugin.legacyToMiniMessage(barRaw)); } - private String getLangString(String path, String fallback) { + private String getTemplate(String path, String fallback) { try { - var langConfig = plugin.getLangConfig(); - if (langConfig != null) { - String val = langConfig.getString(path); - if (val != null) return val; - } + String val = plugin.getMessageTemplate(path); + if (val != null) return val; } catch (Exception ignored) {} return fallback; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 02d2f26..4facd0b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -12,6 +12,9 @@ settings: # How many ticks the actionbar remains visible after the last pickup (20 ticks = 1 second) display-ticks: 40 +# Chat messages. Any message you change here (or add, e.g. actionbar / actionbar-entry) +# takes priority over lang.yml; messages left at their default values fall back to lang.yml. +# Supports & color codes and MiniMessage tags. messages: toggled-on: "&aAuto-pickup has been &fenabled&a." toggled-off: "&cAuto-pickup has been &fdisabled&c." diff --git a/src/main/resources/lang.yml b/src/main/resources/lang.yml index ab99b22..42a5dbd 100644 --- a/src/main/resources/lang.yml +++ b/src/main/resources/lang.yml @@ -1,6 +1,8 @@ # AutoPickup language messages # Supports & color codes (e.g. &a green, &c red) and MiniMessage tags (e.g. , <#RRGGBB>) # https://docs.adventure.kyori.net/minimessage/format.html +# +# A message that is changed in config.yml (messages:) takes priority over the same key here. messages: toggled-on: "&aAuto-pickup has been &fenabled&a." diff --git a/src/test/java/com/mrpippi/AutoPickupPluginTest.java b/src/test/java/com/mrpippi/AutoPickupPluginTest.java index 98f78a3..f07b551 100644 --- a/src/test/java/com/mrpippi/AutoPickupPluginTest.java +++ b/src/test/java/com/mrpippi/AutoPickupPluginTest.java @@ -1,10 +1,15 @@ package com.autopickup; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + import static org.junit.jupiter.api.Assertions.*; -/** Characterization tests: records the current behaviour of AutoPickupPlugin.legacyToMiniMessage. */ +/** Tests for AutoPickupPlugin's static helpers: legacyToMiniMessage (characterization) and resolveMessage. */ class AutoPickupPluginTest { private static String convert(String s) { @@ -52,4 +57,75 @@ void unknownCodesAndTrailingAmpersandAreLeftAsIs() { void miniMessageTagsPassThrough() { assertEquals("Hi x", convert("Hi &ax")); } + + // --- resolveMessage: config.yml vs lang.yml precedence --- + + private static final String KEY = "messages.toggled-on"; + private static final String BUNDLED = "&aAuto-pickup has been &fenabled&a."; + + /** A server config.yml backed by the bundled config.yml as defaults, like JavaPlugin.getConfig(). */ + private static YamlConfiguration serverConfig(String yaml) throws InvalidConfigurationException { + YamlConfiguration config = new YamlConfiguration(); + config.loadFromString(yaml); + var in = AutoPickupPluginTest.class.getClassLoader().getResourceAsStream("config.yml"); + config.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(in, StandardCharsets.UTF_8))); + return config; + } + + private static YamlConfiguration yaml(String yaml) throws InvalidConfigurationException { + YamlConfiguration config = new YamlConfiguration(); + config.loadFromString(yaml); + return config; + } + + @Test + void bundledDefaultsAreWhatTheTestsAssume() throws Exception { + assertEquals(BUNDLED, serverConfig("").getString(KEY)); + } + + @Test + void editedConfigMessageWinsOverLang() throws Exception { + var config = serverConfig("messages:\n toggled-on: \"from config\"\n"); + var lang = yaml("messages:\n toggled-on: \"from lang\"\n"); + assertEquals("from config", AutoPickupPlugin.resolveMessage(KEY, config, lang)); + } + + @Test + void uneditedConfigMessageDoesNotShadowCustomisedLang() throws Exception { + var config = serverConfig("messages:\n toggled-on: \"" + BUNDLED + "\"\n"); + var lang = yaml("messages:\n toggled-on: \"from lang\"\n"); + assertEquals("from lang", AutoPickupPlugin.resolveMessage(KEY, config, lang)); + } + + @Test + void keyMissingFromConfigFileFallsBackToLang() throws Exception { + var config = serverConfig("settings:\n default-enabled: true\n"); + var lang = yaml("messages:\n toggled-on: \"from lang\"\n"); + assertEquals("from lang", AutoPickupPlugin.resolveMessage(KEY, config, lang)); + } + + @Test + void keyMissingFromBothFilesFallsBackToBundledDefault() throws Exception { + assertEquals(BUNDLED, AutoPickupPlugin.resolveMessage(KEY, serverConfig(""), yaml(""))); + assertEquals(BUNDLED, AutoPickupPlugin.resolveMessage(KEY, serverConfig(""), null)); + } + + @Test + void configKeyWithNoBundledDefaultWins() throws Exception { + var config = serverConfig("messages:\n actionbar: \"from config\"\n"); + var lang = yaml("messages:\n actionbar: \"from lang\"\n"); + assertEquals("from config", AutoPickupPlugin.resolveMessage("messages.actionbar", config, lang)); + } + + @Test + void langOnlyKeyIsUsed() throws Exception { + var lang = yaml("messages:\n actionbar: \"from lang\"\n"); + assertEquals("from lang", AutoPickupPlugin.resolveMessage("messages.actionbar", serverConfig(""), lang)); + } + + @Test + void unknownKeyResolvesToNull() throws Exception { + assertNull(AutoPickupPlugin.resolveMessage("messages.nope", serverConfig(""), yaml(""))); + assertNull(AutoPickupPlugin.resolveMessage("messages.nope", null, null)); + } }