2 Commits
12 changed files with 198 additions and 115 deletions

No files matched your search

+18
View File
@@ -5,6 +5,24 @@ This file records notable changes to Iridium.
--- ---
## [26.9.17] - 2026-09-14
### 清理 / Cleanup
- 移除未使用的常量、翻译键与导入
- Removed unused constants, translation keys, and imports
---
## [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 ## [26.9.15] - 2026-09-14
### 修复 / Fixed ### 修复 / Fixed
+17 -20
View File
@@ -4,7 +4,7 @@ MCDReforged 插件:物品展示 / 戴头 / 获取头颅 / 游戏内计算器 /
| | | | | |
|---|---| |---|---|
| 版本 | **26.9.15** | | 版本 | **26.9.17** |
| 作者 | 云镜之端/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/iridium.json`(首次加载自动生成;若存在旧文件 `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.15** | | Version | **26.9.17** |
| 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/iridium.json` (auto-created on first load; migrates legacy `config/iridium.json` if present) Path: `config/iridium/iridium.yml` (YAML with `#` comments; auto-created on first load; migrates legacy JSON configs)
| Field | Default | Description | | Field | Default | Description |
|------|---------|-------------| |------|---------|-------------|
+1 -1
View File
@@ -3,7 +3,7 @@
- ID: `iridium` - ID: `iridium`
- Name: Iridium - Name: Iridium
- Repository: https://github.com/HzSKYMirror/Iridium - Repository: https://github.com/HzSKYMirror/Iridium
- Release: https://github.com/HzSKYMirror/Iridium/releases/tag/v26.9.13 - Release: https://github.com/HzSKYMirror/Iridium/releases/tag/v26.9.17
- Labels: `tool` - Labels: `tool`
## Summary ## Summary
+1 -1
View File
@@ -10,7 +10,7 @@ import iridium.hat as hat
import iridium.head as head import iridium.head as head
import iridium.join_tip as join_tip import iridium.join_tip as join_tip
import iridium.share as share import iridium.share as share
from iridium.config import Config, apply_config, load_config from iridium.config import apply_config, load_config
from iridium.version_util import tr from iridium.version_util import tr
+133 -74
View File
@@ -1,11 +1,17 @@
import json import json
import shutil 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")
@@ -39,62 +45,59 @@ class Config(Serializable):
] ]
# MCDR data folder: config/<plugin_id>/iridium.json # 当前配置:MCDR 数据目录 → config/iridium/iridium.yml
CONFIG_FILE_NAME = "iridium.json" CONFIG_FILE_NAME = "iridium.yml"
# 旧路径(仅用于迁移)
LEGACY_CONFIG_FILE = "config/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()
# 模块级单例;on_load 时原地更新字段,避免各模块持有过期引用 # 模块级单例;on_load 时原地更新字段,避免各模块持有过期引用
config = Config() config = Config()
@@ -112,8 +115,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)
@@ -145,19 +166,28 @@ def _strip_json_comments(text: str) -> str:
def config_file_path(server: PluginServerInterface) -> Path: def config_file_path(server: PluginServerInterface) -> Path:
"""Canonical path: MCDR data folder → config/iridium/iridium.json""" """Canonical path: MCDR data folder → config/iridium/iridium.yml"""
return Path(server.get_data_folder()) / CONFIG_FILE_NAME return Path(server.get_data_folder()) / CONFIG_FILE_NAME
def _legacy_path(server: PluginServerInterface) -> Path: def _working_dir(server: PluginServerInterface) -> Path:
try: try:
wd = server.get_mcdr_config().get("working_directory") or "." wd = server.get_mcdr_config().get("working_directory") or "."
return (Path(wd) / LEGACY_CONFIG_FILE).expanduser().resolve() return Path(wd).expanduser()
except Exception: except Exception:
return Path.cwd() / LEGACY_CONFIG_FILE return Path.cwd()
def _write_commented_default(path: Path, server: PluginServerInterface) -> bool: def _legacy_json_paths(server: PluginServerInterface) -> List[Path]:
data_dir = Path(server.get_data_folder())
wd = _working_dir(server)
return [
data_dir / "iridium.json",
wd / "config" / "iridium.json",
]
def _write_default(path: Path, server: PluginServerInterface) -> bool:
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") path.write_text(build_default_config_text(), encoding="utf-8")
@@ -168,49 +198,78 @@ def _write_commented_default(path: Path, server: PluginServerInterface) -> bool:
return False return False
def _migrate_legacy(server: PluginServerInterface, dest: Path) -> None: def _try_load_dict_from_text(text: str) -> Optional[dict]:
"""If old config/iridium.json exists, copy it to the data folder path.""" text = text.strip()
src = _legacy_path(server) 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:
if src.is_file() and src.resolve() != dest.resolve() and not dest.is_file(): if not src.is_file():
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) dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest) # keep comments in default template + overlay known keys via dump
server.logger.info(f"已从旧路径迁移配置: {src} -> {dest}") 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.warning(f"迁移旧配置失败 {src}: {e}") server.logger.warning(f"迁移旧配置失败 {src}: {e}")
def load_config(server: PluginServerInterface) -> Config: def load_config(server: PluginServerInterface) -> Config:
""" """
Load config using MCDR's official data folder (config/iridium/iridium.json). Load config/iridium/iridium.yml (MCDR data folder).
1. Prefer that path; create commented default if missing Create commented default if missing; migrate legacy JSON if needed.
2. Migrate legacy config/iridium.json if present
3. Fallback: load_config_simple
""" """
path = config_file_path(server) path = config_file_path(server)
_migrate_legacy(server, path) _migrate_legacy_json(server, path)
if not path.is_file(): if not path.is_file():
_write_commented_default(path, server) _write_default(path, server)
if not path.is_file():
server.logger.warning(f"配置文件不存在且无法创建,使用内存默认值: {path}")
return Config()
if path.is_file():
try: try:
raw = path.read_text(encoding="utf-8") raw = path.read_text(encoding="utf-8")
data = json.loads(_strip_json_comments(raw)) data = _load_yaml_text(raw)
if not isinstance(data, dict):
raise ValueError("YAML root must be a mapping")
server.logger.info(f"配置已加载: {path}") server.logger.info(f"配置已加载: {path}")
return Config.deserialize(data) return Config.deserialize(data)
except Exception as e: except Exception as e:
server.logger.error(f"读取/解析配置失败 {path}: {e},尝试 load_config_simple") server.logger.error(f"读取/解析 YAML 配置失败 {path}: {e},使用默认值")
# Official MCDR loader as last resort
try:
cfg = server.load_config_simple(
CONFIG_FILE_NAME,
target_class=Config,
in_data_folder=True,
)
server.logger.info(f"配置已通过 load_config_simple 加载: {path}")
return cfg
except Exception as e:
server.logger.error(f"load_config_simple 失败: {e},使用内存默认值")
return Config() return Config()
-1
View File
@@ -260,7 +260,6 @@ def _extract_item_payload(payload: str, version: Version) -> Optional[str]:
def _do_swap(server: PluginServerInterface, src: CommandSource, player: str, version: Version) -> bool: def _do_swap(server: PluginServerInterface, src: CommandSource, player: str, version: Version) -> bool:
name = sanitize_name(player) name = sanitize_name(player)
head_slot = _head_slot(version) head_slot = _head_slot(version)
hand_slot = _hand_slot(version, 0)
# 1.17+: true swap via offhand buffer — no need to rebuild item NBT # 1.17+: true swap via offhand buffer — no need to rebuild item NBT
if is_at_least(version, "1.17"): if is_at_least(version, "1.17"):
-2
View File
@@ -6,8 +6,6 @@ from iridium.config import config
# Fallback strings when ServerInterface is unavailable (unit tests / early import) # Fallback strings when ServerInterface is unavailable (unit tests / early import)
_FALLBACK = { _FALLBACK = {
"click_copy": "click to copy data",
"click_suggest": "click for data command",
"calc_usage": "Usage: !!c <expression>", "calc_usage": "Usage: !!c <expression>",
"share_showing": "is showing", "share_showing": "is showing",
} }
-2
View File
@@ -11,8 +11,6 @@ iridium.share_help: 'Show the item in your main hand (hover to view)'
iridium.share_done: 'Item shared to chat' iridium.share_done: 'Item shared to chat'
iridium.share_unsupported_version: 'Version {0} cannot read held item (needs Minecraft 1.8+)' iridium.share_unsupported_version: 'Version {0} cannot read held item (needs Minecraft 1.8+)'
iridium.share_timeout: 'Timed out reading held item, try again later' iridium.share_timeout: 'Timed out reading held item, try again later'
iridium.click_copy: 'click to copy data'
iridium.click_suggest: 'click for data command'
iridium.share_showing: 'is showing' iridium.share_showing: 'is showing'
iridium.hat_help: 'Swap the item in your hand with your head slot' iridium.hat_help: 'Swap the item in your hand with your head slot'
iridium.hat_success: 'Swapped hand item with head equipment' iridium.hat_success: 'Swapped hand item with head equipment'
-2
View File
@@ -11,8 +11,6 @@ iridium.share_help: '展示手上物品的完整信息(悬停查看)'
iridium.share_done: '已在聊天栏展示物品' iridium.share_done: '已在聊天栏展示物品'
iridium.share_unsupported_version: '当前版本 {0} 无法读取手持物品(需要 Minecraft 1.8+)' iridium.share_unsupported_version: '当前版本 {0} 无法读取手持物品(需要 Minecraft 1.8+)'
iridium.share_timeout: '读取手持物品超时,请稍后再试' iridium.share_timeout: '读取手持物品超时,请稍后再试'
iridium.click_copy: '点击复制完整数据'
iridium.click_suggest: '点击获取完整数据命令'
iridium.share_showing: '正在展示' iridium.share_showing: '正在展示'
iridium.hat_help: '将手上物品与头部装备互换' iridium.hat_help: '将手上物品与头部装备互换'
iridium.hat_success: '已交换手上物品与头部装备' iridium.hat_success: '已交换手上物品与头部装备'
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "iridium", "id": "iridium",
"version": "26.9.15", "version": "26.9.17",
"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