Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 的描述:它不會存檔,而是重新讀取檔案。

### 新增

Expand All @@ -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]

Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -129,15 +129,15 @@ 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.
Supports the same color code syntax plus runtime placeholders: `{mode}`, `{page}`, `{total_pages}`, `{search}`, `{search_display}`, `{count}`.
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`
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -137,7 +144,7 @@ messages:

### `lang.yml`

所有聊天訊息與 ActionBar 文字的來源。支援 `&` 色碼(如 `&a` 綠色)與 MiniMessage 標籤(如 `<green>`、`<#RRGGBB>`),兩種可以混用。
聊天訊息與 ActionBar 文字的來源(`config.yml` 中被改過的同名訊息會優先,見上方)。支援 `&` 色碼(如 `&a` 綠色)與 MiniMessage 標籤(如 `<green>`、`<#RRGGBB>`),兩種可以混用。

```yaml
messages:
Expand Down
39 changes: 38 additions & 1 deletion src/main/java/com/mrpippi/AutoPickupPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -183,10 +185,45 @@ public Component getGuiMessage(String path) {
* </ul>
*/
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.
*
* <ol>
* <li>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);
* <li>otherwise lang.yml;
* <li>otherwise config.yml including its bundled defaults;
* <li>otherwise {@code null}.
* </ol>
*
* <p>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.
Expand Down
16 changes: 7 additions & 9 deletions src/main/java/com/mrpippi/manager/ActionBarManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,14 @@ private void sendNow(Player player) {
/**
* Builds the full ActionBar component from the accumulated pickup map.
*
* <p>Template: {@code settings.actionbar-format} in lang.yml with placeholder
* <p>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<Material, Integer> pickups) {
String entryTemplate = getLangString("messages.actionbar-entry", "<translate:{item}> &7x{count}");
String barTemplate = getLangString("messages.actionbar", "&a+ &f{entries}");
String entryTemplate = getTemplate("messages.actionbar-entry", "<translate:{item}> &7x{count}");
String barTemplate = getTemplate("messages.actionbar", "&a+ &f{entries}");

List<String> entryStrings = new ArrayList<>();
int total = 0;
Expand All @@ -150,13 +151,10 @@ private Component buildActionBar(Map<Material, Integer> 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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/lang.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# AutoPickup language messages
# Supports & color codes (e.g. &a green, &c red) and MiniMessage tags (e.g. <green>, <#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."
Expand Down
78 changes: 77 additions & 1 deletion src/test/java/com/mrpippi/AutoPickupPluginTest.java
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -52,4 +57,75 @@ void unknownCodesAndTrailingAmpersandAreLeftAsIs() {
void miniMessageTagsPassThrough() {
assertEquals("<yellow>Hi</yellow> <green>x", convert("<yellow>Hi</yellow> &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));
}
}
Loading