2 Commits
6 changed files with 220 additions and 130 deletions

No files matched your search

+22
View File
@@ -5,6 +5,28 @@ This file records notable changes to Iridium.
--- ---
## [26.9.16] - 2026-09-14
### 变更 / Changed
- 配置格式改为 **YAML**:`config/iridium/iridium.yml`(`#` 原生注释)
- Config format is now **YAML**: `config/iridium/iridium.yml` (native `#` comments)
- 自动从旧 JSON(`config/iridium.json` 或 `config/iridium/iridium.json`)迁移
- Auto-migrates from legacy JSON configs
---
## [26.9.15] - 2026-09-14
### 修复 / Fixed
- 配置文件写到错误路径导致「删掉后重载不生成」:改用 MCDR 数据目录 `config/iridium/iridium.json`(`get_data_folder`)
- Config written to the wrong path so delete+reload did not recreate it; now uses MCDR data folder `config/iridium/iridium.json`
### 变更 / Changed
- 文档中的配置路径更新为 `config/iridium/iridium.json`
- Docs updated to the correct config path
---
## [26.9.14] - 2026-09-14 ## [26.9.14] - 2026-09-14
### 修复 / Fixed ### 修复 / Fixed
+17 -20
View File
@@ -4,7 +4,7 @@ MCDReforged 插件:物品展示 / 戴头 / 获取头颅 / 游戏内计算器 /
| | | | | |
|---|---| |---|---|
| 版本 | **26.9.14** | | 版本 | **26.9.16** |
| 作者 | 云镜之端/SKYMirror | | 作者 | 云镜之端/SKYMirror |
| 许可 | 保留所有权益(All Rights Reserved) | | 许可 | 保留所有权益(All Rights Reserved) |
| 前置 | MCDReforged `>= 2.6.0` | | 前置 | MCDReforged `>= 2.6.0` |
@@ -79,26 +79,23 @@ rcon.password=your_password
## 配置 ## 配置
路径:`config/iridium.json`(首次加载自动生成) 路径:`config/iridium/iridium.yml`(YAML,`#` 注释;首次加载自动生成;旧的 `config/iridium.json` / `config/iridium/iridium.json` 会自动迁移)
```json ```yaml
{ permission: 1
"permission": 1, head_cooldown_seconds: 60
"head_cooldown_seconds": 60, hat_cooldown_seconds: 3
"hat_cooldown_seconds": 3, share_sound: true
"share_sound": true, force_version: ""
"force_version": "", join_tip: true
"join_tip": true, join_tip_delay_seconds: 1.5
"join_tip_delay_seconds": 1.5, motd_enabled: true
"motd_enabled": true, motd_start_day: "2026-09-14"
"motd_start_day": "2026-09-14", motd_lines:
"motd_lines": [ - "§6欢迎 §e{player}§6 加入服务器!"
"§6欢迎 §e{player}§6 加入服务器!", - "§7服务器已开服 §b{days}§7 天"
"§7服务器已开服 §b{days}§7 天", - "§7官网: {link:§b点击打开官网|https://www.skymirror.top}"
"§7官网: {link:§b点击打开官网|https://www.skymirror.top}", - "§7QQ群: {link:§f985402607|https://skymirror.top/qq}"
"§7QQ群: {link:§f985402607|https://skymirror.top/qq}"
]
}
``` ```
| 字段 | 默认 | 说明 | | 字段 | 默认 | 说明 |
+2 -2
View File
@@ -4,7 +4,7 @@ MCDReforged plugin: item share / wear on head / player head / calculator / join
| | | | | |
|---|---| |---|---|
| Version | **26.9.14** | | Version | **26.9.16** |
| Author | 云镜之端/SKYMirror | | Author | 云镜之端/SKYMirror |
| License | All Rights Reserved | | License | All Rights Reserved |
| Requires | MCDReforged `>= 2.6.0` | | Requires | MCDReforged `>= 2.6.0` |
@@ -48,7 +48,7 @@ rcon.password=your_password
## Config ## Config
Path: `config/iridium.json` (auto-created with comments on first load) Path: `config/iridium/iridium.yml` (YAML with `#` comments; auto-created on first load; migrates legacy JSON configs)
| Field | Default | Description | | Field | Default | Description |
|------|---------|-------------| |------|---------|-------------|
+153 -98
View File
@@ -1,10 +1,17 @@
import json import json
import shutil
from datetime import date from datetime import date
from io import StringIO
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import Any, List, Optional
from mcdreforged.api.all import * from mcdreforged.api.all import *
try:
from ruamel.yaml import YAML
except ImportError: # pragma: no cover
YAML = None # type: ignore
def _today() -> str: def _today() -> str:
return date.today().strftime("%Y-%m-%d") return date.today().strftime("%Y-%m-%d")
@@ -29,13 +36,7 @@ class Config(Serializable):
motd_enabled: bool = True motd_enabled: bool = True
# 开服日期,格式 YYYY-MM-DD;默认为配置生成当天(当天算第 1 天) # 开服日期,格式 YYYY-MM-DD;默认为配置生成当天(当天算第 1 天)
motd_start_day: str = _today() motd_start_day: str = _today()
# MOTD 文本行。支持: # MOTD 文本行
# {player} 玩家名
# {days} 开服天数
# {online} 在线人数(拿不到时显示 ?)
# {link:显示文字|https://url} 可点击链接
# {link:https://url} 链接,显示为完整 URL
# § 颜色代码
motd_lines: List[str] = [ motd_lines: List[str] = [
"§6欢迎 §e{player}§6 加入服务器!", "§6欢迎 §e{player}§6 加入服务器!",
"§7服务器已开服 §b{days}§7 天", "§7服务器已开服 §b{days}§7 天",
@@ -44,58 +45,63 @@ class Config(Serializable):
] ]
CONFIG_FILE = "config/iridium.json" # 当前配置:MCDR 数据目录 → config/iridium/iridium.yml
CONFIG_FILE_NAME = "iridium.yml"
# 历史路径(仅用于迁移)
LEGACY_JSON_NAMES = (
"iridium.json", # config/iridium/iridium.json
)
def build_default_config_text() -> str: def build_default_config_text() -> str:
"""带注释的默认配置;开服日期为生成当天。""" """带注释的默认 YAML 配置;开服日期为生成当天。"""
today = _today() today = _today()
return f"""{{ return f"""# Iridium 配置文件 (YAML)
// 使用 !!share / !!hat / !!head / !!c 所需的最低权限等级 # 修改后执行: !!MCDR reload plugin
// 0=控制台 1=普通玩家 2=OP/管理员 # This file uses standard YAML. '#' starts a comment.
"permission": 1,
// !!head 冷却时间(秒),0 表示不限制 # 使用 !!share / !!hat / !!head / !!c 所需的最低权限等级
"head_cooldown_seconds": 60, # 0=控制台 1=普通玩家 2=OP/管理员
permission: 1
// !!hat 冷却时间(秒),0 表示不限制 # !!head 冷却时间(秒),0 表示不限制
"hat_cooldown_seconds": 3, head_cooldown_seconds: 60
// !!share 是否播放提示音 # !!hat 冷却时间(秒),0 表示不限制
"share_sound": true, hat_cooldown_seconds: 3
// 强制指定 Minecraft 版本(如 "1.12.2"、"1.20.1") # !!share 是否播放提示音
// 留空则自动检测;检测失败时可手动填写 share_sound: true
"force_version": "",
// 玩家进服时是否提示可用命令 # 强制指定 Minecraft 版本(如 "1.12.2"、"1.20.1")
"join_tip": true, # 留空则自动检测;检测失败时可手动填写
force_version: ""
// 进服消息延迟(秒),0 为立即发送 # 玩家进服时是否提示可用命令
"join_tip_delay_seconds": 1.5, join_tip: true
// 是否发送进服 MOTD(开服天数 + 自定义文案) # 进服消息延迟(秒),0 为立即发送
"motd_enabled": true, join_tip_delay_seconds: 1.5
// 开服日期,格式 YYYY-MM-DD(当天算第 1 天) # 是否发送进服 MOTD(开服天数 + 自定义文案)
// 默认为本配置文件生成当天,可自行修改为真实开服日 motd_enabled: true
"motd_start_day": "{today}",
// MOTD 文本行 # 开服日期,格式 YYYY-MM-DD(当天算第 1 天)
// 占位符: {{player}} 玩家名 {{days}} 开服天数 {{online}} 在线人数 # 默认为本配置文件生成当天,可自行修改为真实开服日
// 链接: {{link:显示文字|https://url}} 或 {{link:https://url}} motd_start_day: "{today}"
// 颜色: 支持原版 § 颜色代码
"motd_lines": [ # MOTD 文本行
"§6欢迎 §e{{player}}§6 加入服务器!", # 占位符: {{player}} 玩家名 {{days}} 开服天数 {{online}} 在线人数
"§7服务器已开服 §b{{days}}§7 天", # 链接: {{link:显示文字|https://url}} 或 {{link:https://url}}
"§7官网: {{link:§b点击打开官网|https://www.skymirror.top}}", # 颜色: 支持原版 § 颜色代码
"§7QQ群: {{link:§f985402607|https://skymirror.top/qq}}" motd_lines:
] - "§6欢迎 §e{{player}}§6 加入服务器!"
}} - "§7服务器已开服 §b{{days}}§7 天"
- "§7官网: {{link:§b点击打开官网|https://www.skymirror.top}}"
- "§7QQ群: {{link:§f985402607|https://skymirror.top/qq}}"
""" """
# 兼容旧引用
DEFAULT_CONFIG_TEXT = build_default_config_text() DEFAULT_CONFIG_TEXT = build_default_config_text()
# 模块级单例;on_load 时原地更新字段,避免各模块持有过期引用 # 模块级单例;on_load 时原地更新字段,避免各模块持有过期引用
@@ -115,8 +121,26 @@ def apply_config(new_cfg: Config) -> None:
config.motd_lines = list(new_cfg.motd_lines) config.motd_lines = list(new_cfg.motd_lines)
def _yaml():
if YAML is None:
raise RuntimeError("ruamel.yaml is required")
return YAML(typ="safe")
def _load_yaml_text(text: str) -> Any:
return _yaml().load(StringIO(text))
def _dump_yaml(data: dict) -> str:
buf = StringIO()
y = _yaml()
y.default_flow_style = False
y.dump(data, buf)
return buf.getvalue()
def _strip_json_comments(text: str) -> str: def _strip_json_comments(text: str) -> str:
"""Remove // line comments outside of strings.""" """Remove // line comments outside of strings (legacy JSON only)."""
out = [] out = []
i = 0 i = 0
n = len(text) n = len(text)
@@ -147,80 +171,111 @@ def _strip_json_comments(text: str) -> str:
return "".join(out) return "".join(out)
def _candidate_paths(server: PluginServerInterface) -> List[Path]: def config_file_path(server: PluginServerInterface) -> Path:
"""Possible config file locations, best first.""" """Canonical path: MCDR data folder → config/iridium/iridium.yml"""
paths: List[Path] = [] return Path(server.get_data_folder()) / CONFIG_FILE_NAME
seen = set()
def add(p: Path) -> None:
try:
rp = p.expanduser().resolve()
except Exception:
rp = p
key = str(rp)
if key not in seen:
seen.add(key)
paths.append(rp)
def _working_dir(server: PluginServerInterface) -> Path:
try: try:
wd = server.get_mcdr_config().get("working_directory") wd = server.get_mcdr_config().get("working_directory") or "."
if wd: return Path(wd).expanduser()
add(Path(wd) / CONFIG_FILE)
except Exception: except Exception:
pass return Path.cwd()
try:
add(Path.cwd() / CONFIG_FILE)
except Exception:
pass
# Fallback: next to CWD if MCDR launched from elsewhere
add(Path.cwd() / "config" / "iridium.json")
return paths
def config_path(server: PluginServerInterface) -> Path: def _legacy_json_paths(server: PluginServerInterface) -> List[Path]:
"""Prefer an existing file; else the first writable candidate.""" data_dir = Path(server.get_data_folder())
candidates = _candidate_paths(server) wd = _working_dir(server)
for p in candidates: return [
if p.is_file(): data_dir / "iridium.json",
return p wd / "config" / "iridium.json",
return candidates[0] ]
def ensure_config_file(server: PluginServerInterface) -> Path: def _write_default(path: Path, server: PluginServerInterface) -> bool:
"""Create commented default config if missing. Always returns absolute path."""
path = config_path(server)
try: try:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(build_default_config_text(), encoding="utf-8")
server.logger.info(f"已生成配置文件: {path}")
return True
except Exception as e: except Exception as e:
server.logger.error(f"无法创建配置目录 {path.parent}: {e}") server.logger.error(f"写入配置文件失败 {path}: {e}")
return path return False
if not path.is_file():
def _try_load_dict_from_text(text: str) -> Optional[dict]:
text = text.strip()
if not text:
return None
if text.startswith("{"):
return json.loads(_strip_json_comments(text))
data = _load_yaml_text(text)
return data if isinstance(data, dict) else None
def _migrate_legacy_json(server: PluginServerInterface, dest: Path) -> None:
"""Convert old JSON config (with optional // comments) to YAML if dest is missing."""
if dest.is_file():
return
for src in _legacy_json_paths(server):
try: try:
path.write_text(build_default_config_text(), encoding="utf-8") if not src.is_file():
server.logger.info(f"已生成配置文件: {path}") continue
raw = src.read_text(encoding="utf-8")
data = _try_load_dict_from_text(raw)
if not data:
continue
dest.parent.mkdir(parents=True, exist_ok=True)
# keep comments in default template + overlay known keys via dump
cfg = Config.deserialize(data)
overlay = {
"permission": cfg.permission,
"head_cooldown_seconds": cfg.head_cooldown_seconds,
"hat_cooldown_seconds": cfg.hat_cooldown_seconds,
"share_sound": cfg.share_sound,
"force_version": cfg.force_version,
"join_tip": cfg.join_tip,
"join_tip_delay_seconds": cfg.join_tip_delay_seconds,
"motd_enabled": cfg.motd_enabled,
"motd_start_day": cfg.motd_start_day,
"motd_lines": list(cfg.motd_lines),
}
# Prefer commented default template when values match defaults is complex;
# dump parsed values with a short header for clarity.
header = (
"# Iridium 配置 (从旧 JSON 迁移)\n"
"# Migrated from legacy JSON config\n\n"
)
dest.write_text(header + _dump_yaml(overlay), encoding="utf-8")
server.logger.info(f"已从旧 JSON 迁移配置: {src} -> {dest}")
return
except Exception as e: except Exception as e:
server.logger.error(f"写入配置文件失败 {path}: {e}") server.logger.warning(f"迁移旧配置失败 {src}: {e}")
return path
def load_config(server: PluginServerInterface) -> Config: def load_config(server: PluginServerInterface) -> Config:
"""Load config; recreate a commented default whenever the file is missing.""" """
path = ensure_config_file(server) Load config/iridium/iridium.yml (MCDR data folder).
Create commented default if missing; migrate legacy JSON if needed.
"""
path = config_file_path(server)
_migrate_legacy_json(server, path)
if not path.is_file():
_write_default(path, server)
if not path.is_file(): if not path.is_file():
server.logger.warning(f"配置文件不存在且无法创建,使用内存默认值: {path}") server.logger.warning(f"配置文件不存在且无法创建,使用内存默认值: {path}")
return Config() return Config()
try: try:
raw = path.read_text(encoding="utf-8") raw = path.read_text(encoding="utf-8")
except Exception as e: data = _load_yaml_text(raw)
server.logger.error(f"读取配置失败 {path}: {e},使用默认值") if not isinstance(data, dict):
return Config() raise ValueError("YAML root must be a mapping")
try: server.logger.info(f"配置已加载: {path}")
data = json.loads(_strip_json_comments(raw))
except json.JSONDecodeError as e:
server.logger.error(f"配置文件 JSON 解析失败 {path}: {e},使用默认值")
return Config()
try:
return Config.deserialize(data) return Config.deserialize(data)
except Exception as e: except Exception as e:
server.logger.error(f"配置反序列化失败 {path}: {e},使用默认值") server.logger.error(f"读取/解析 YAML 配置失败 {path}: {e},使用默认值")
return Config() return Config()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "iridium", "id": "iridium",
"version": "26.9.14", "version": "26.9.16",
"name": "Iridium", "name": "Iridium",
"description": { "description": {
"zh_cn": "物品展示 / 戴头 / 获取头颅 / 游戏内计算器 / 进服 MOTD", "zh_cn": "物品展示 / 戴头 / 获取头颅 / 游戏内计算器 / 进服 MOTD",
+25 -9
View File
@@ -1,6 +1,7 @@
"""Pure-function unit tests (no live MCDR server required).""" """Pure-function unit tests (no live MCDR server required)."""
import sys import json
import os import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -268,20 +269,35 @@ def test_extract_display_name():
def test_strip_json_comments(): def test_strip_json_comments():
import json
from datetime import date from datetime import date
from iridium.config import build_default_config_text, _strip_json_comments from iridium.config import _strip_json_comments, build_default_config_text
text = build_default_config_text() # legacy JSON comment stripper still works
parsed = json.loads(_strip_json_comments(text)) assert json.loads(_strip_json_comments('{"a": 1 // x\n}'))["a"] == 1
assert parsed["permission"] == 1
assert "skymirror.top" in parsed["motd_lines"][2]
assert parsed["motd_start_day"] == date.today().strftime("%Y-%m-%d")
# comment-looking text inside string is kept
assert json.loads(_strip_json_comments('{"a": "http://x"}'))["a"] == "http://x" assert json.loads(_strip_json_comments('{"a": "http://x"}'))["a"] == "http://x"
def test_default_yaml_config():
from datetime import date
from iridium.config import _load_yaml_text, build_default_config_text
data = _load_yaml_text(build_default_config_text())
assert data["permission"] == 1
assert data["motd_start_day"] == date.today().strftime("%Y-%m-%d")
assert any("skymirror.top" in line for line in data["motd_lines"])
assert any("skymirror.top/qq" in line for line in data["motd_lines"])
def test_legacy_json_to_yaml_parse():
from iridium.config import _try_load_dict_from_text
assert _try_load_dict_from_text('{"permission": 2}')["permission"] == 2
assert _try_load_dict_from_text('{\n // c\n "permission": 3\n} ')["permission"] == 3
assert _try_load_dict_from_text("permission: 4\nmotd_start_day: '2020-01-01'\n")["permission"] == 4
if __name__ == "__main__": if __name__ == "__main__":
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
failed = 0 failed = 0