diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2b035..9028438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,69 +1,120 @@ -# Changelog +# Changelog / 更新日志 -本文件记录 Iridium 的重要变更。 +本文件记录 Iridium 的重要变更。 +This file records notable changes to Iridium. + +--- + +## [26.9.14] - 2026-09-14 + +### 修复 / Fixed +- 删除 `config/iridium.json` 后重载可正确重新生成(多路径探测 + 写入失败日志) +- Deleting `config/iridium.json` then reloading correctly recreates the file (multi-path probe + write-failure logs) + +### 变更 / Changed +- `motd_start_day` 默认为配置生成当天(YYYY-MM-DD),不再为空 +- `motd_start_day` defaults to the config creation date (`YYYY-MM-DD`), no longer empty +- 去掉 MOTD 标题「服务器信息」/「Server Info」 +- Removed the MOTD header title (`服务器信息` / `Server Info`) +- 默认 QQ 群增加可点击链接 `https://skymirror.top/qq` +- Default QQ group line is now a clickable link to `https://skymirror.top/qq` +- 进服命令提示文案去掉「已启用 Iridium」 +- Join tip header no longer says “Iridium is enabled” + +### 新增 / Added +- 英文文档 `README_en.md` +- English documentation `README_en.md` + +--- ## [26.9.13] - 2026-09-13 -### Added +### 新增 / Added - 首次启动自动生成带中文注释的 `config/iridium.json` +- On first load, generate a commented `config/iridium.json` - 配置文件支持 `//` 行注释 +- Config file supports `//` line comments -### Changed +### 变更 / Changed - 提交信息改为中文 +- Commit messages rewritten in Chinese - 清理历史 Release,仅保留当前版本 +- Cleaned old Releases; keep only the current version + +--- ## [26.9.8] - 2026-09-13 -### Changed -- `!!share`:去掉「点击复制完整数据」,仅保留物品名 + 悬停预览(与 ShowIt 风格一致) +### 变更 / Changed +- `!!share`:去掉「点击复制完整数据」,仅保留物品名 + 悬停预览 +- `!!share`: removed the copy-to-clipboard button; name + hover only + +--- ## [26.9.7] - 2026-09-13 -### Fixed -- `!!share`:无 NBT 改名时,正文 fallback 改为 ID 美化名(如 `Time Twister Wireless`),不再直接显示 `kubejs:...`;语言键命中则显示正式本地化名 +### 修复 / Fixed +- `!!share`:无 NBT 改名时 fallback 为 ID 美化名(如 `Time Twister Wireless`) +- `!!share`: humanized ID fallback when the item has no NBT custom name + +--- ## [26.9.6] - 2026-09-13 -### Fixed -- `!!share`:默认物品名改为客户端 `translate` 组件(如 `block.ae2.fluix_covered_cable`),本地化显示正式名;仅有 NBT 自定义名时仍显示改名 -- `!!share`:提示音在玩家位置播放,避免 "sound is too far away" +### 修复 / Fixed +- `!!share`:使用客户端 `translate` 组件显示本地化名 +- `!!share`: use client `translate` component for localized names +- `!!share`:提示音在玩家位置播放 +- `!!share`: play sound at the player to avoid “too far away” + +--- ## [26.9.5] - 2026-09-13 -### Fixed -- `!!hat`(1.17+):改用副手缓冲做真正交换,避免复杂 NBT 重建失败导致原头盔消失;副手非空时拒绝并提示 -- `!!share`:展示名优先取 `custom_name` / `display.Name` / translate,不再只显示物品 ID +### 修复 / Fixed +- `!!hat`(1.17+):副手缓冲交换,避免复杂 NBT 重建失败导致物品消失 +- `!!hat` (1.17+): offhand-buffer swap so complex NBT cannot wipe items +- `!!share`:优先显示自定义名 / translate +- `!!share`: prefer custom name / translate over raw item id + +--- ## [26.9.4] - 2026-09-13 -### Added -- 操作日志:`!!share` / `!!hat` / `!!head` / `!!c` 成功时写入 MCDR 控制台(`[Iridium] ...`) +### 新增 / Added +- 操作日志写入 MCDR 控制台 +- Command operations logged to the MCDR console + +--- ## [26.9.3] - 2026-09-13 -### Fixed -- `!!share` / `!!hat` / `!!head`:`info.version` 若为字符串时比较崩溃(`Cannot compare Version and str`) -- `!!c`:命令回调少绑定 `server` 导致 `do_calc() missing ... context` +### 修复 / Fixed +- 版本比较在 `str` / `Version` 之间崩溃 +- Version comparison crash between `str` and `Version` +- `!!c` 回调缺少 `server` 绑定 +- `!!c` callback missing `server` binding + +--- ## [26.9.2] - 2026-09-13 -### Fixed -- 语言文件 YAML 解析失败(值中含 `:` 未加引号导致 `mapping values are not allowed`) -- 进服事件名错误:`PLAYER_JOIN` → `PLAYER_JOINED`(否则插件 on_load 失败) +### 修复 / Fixed +- 语言文件 YAML 因值中冒号未加引号而解析失败 +- Language YAML failed when values contained unquoted colons +- 进服事件名:`PLAYER_JOIN` → `PLAYER_JOINED` +- Join event name: `PLAYER_JOIN` → `PLAYER_JOINED` + +--- ## [26.9.1] - 2026-09-13 -### Added -- `!!share`:展示主手物品(悬停完整数据,1.16+ 点击复制,可选提示音) -- `!!hat`:主手物品与头部装备互换(需 RCON) -- `!!head [player]`:获取一个玩家头颅,支持冷却与权限 -- `!!c `:游戏内四则运算计算器(AST 白名单,零依赖) -- 进服 MOTD:开服天数、自定义文案、可点击链接、`§` 颜色 -- 进服命令提示(可点击填入聊天框) -- 按 Minecraft 版本分支;不支持的功能会明确提示 +### 新增 / Added +- `!!share` / `!!hat` / `!!head` / `!!c`、进服 MOTD 与命令提示 +- `!!share` / `!!hat` / `!!head` / `!!c`, join MOTD and command tips +- 按 Minecraft 版本分支;不支持的功能会提示 +- Minecraft-version branching with clear prompts when unsupported -### Security -- 玩家名统一清洗为 `[A-Za-z0-9_]` 并限长后再拼进 `/give`、`data get`、`item replace` -- 控制台命令去除换行,防止拆成多行二次执行 -- tellraw 对物品 SNBT 做 JSON 转义与长度截断 -- MOTD 链接仅允许 `http://` / `https://` +### 安全 / Security +- 玩家名清洗、命令去换行、tellraw 转义、链接仅 http(s) +- Name sanitization, newline stripping, tellraw escaping, http(s)-only links diff --git a/README.md b/README.md index 1bea987..d6bffba 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MCDReforged 插件:物品展示 / 戴头 / 获取头颅 / 游戏内计算器 / | | | |---|---| -| 版本 | **26.9.13** | +| 版本 | **26.9.14** | | 作者 | 云镜之端/SKYMirror | | 许可 | 保留所有权益(All Rights Reserved) | | 前置 | MCDReforged `>= 2.6.0` | @@ -91,12 +91,12 @@ rcon.password=your_password "join_tip": true, "join_tip_delay_seconds": 1.5, "motd_enabled": true, - "motd_start_day": "", + "motd_start_day": "2026-09-14", "motd_lines": [ "§6欢迎 §e{player}§6 加入服务器!", "§7服务器已开服 §b{days}§7 天", "§7官网: {link:§b点击打开官网|https://www.skymirror.top}", - "§7QQ群: §f985402607" + "§7QQ群: {link:§f985402607|https://skymirror.top/qq}" ] } ``` @@ -111,7 +111,7 @@ rcon.password=your_password | `join_tip` | `true` | 进服是否提示可用命令 | | `join_tip_delay_seconds` | `1.5` | 进服消息延迟;`0` 立即 | | `motd_enabled` | `true` | 是否发送 MOTD | -| `motd_start_day` | `""` | 开服日期 `YYYY-MM-DD`(当天算第 1 天);留空不显示天数 | +| `motd_start_day` | 生成配置当天 | 开服日期 `YYYY-MM-DD`(当天算第 1 天);可改成真实开服日 | | `motd_lines` | 见上 | MOTD 文本行 | 修改配置后执行 `!!MCDR reload plugin` 生效。 diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..af2115f --- /dev/null +++ b/README_en.md @@ -0,0 +1,95 @@ +# Iridium + +MCDReforged plugin: item share / wear on head / player head / calculator / join MOTD. + +| | | +|---|---| +| Version | **26.9.14** | +| Author | 云镜之端/SKYMirror | +| License | All Rights Reserved | +| Requires | MCDReforged `>= 2.6.0` | +| Minecraft | **1.7.10+** (feature-gated by version) | +| Python deps | None | + +## Features + +| Command | Description | +|---------|-------------| +| `!!share` | Share held item: name in chat, hover for full data | +| `!!hat` | Swap main-hand item with head slot | +| `!!head [player]` | Get **one** player head (default: yourself); cooldown | +| `!!c ` | In-game calculator: `+ - * / // % ** ^` | +| Join MOTD | Uptime days, custom lines, clickable links, `§` colors | +| Join tips | Command list; click fills chat | + +### Version support + +| Feature | 1.7.10 | 1.8–1.12 | 1.13–1.16 | 1.17–1.20.4 | 1.20.5+ | +|---------|:------:|:--------:|:---------:|:-----------:|:-------:| +| `!!c` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `!!head` | ✅ | ✅ | ✅ | ✅ | ✅ profile | +| `!!share` | ❌ | ✅ entitydata | ✅ data get | ✅ | ✅ | +| `!!hat` | ❌ | ✅ RCON | ✅ RCON | ✅ RCON | ✅ RCON | +| MOTD / tips | ✅ | ✅ | ✅ | ✅ | ✅ | + +## Install + +1. Download `Iridium.mcdr` +2. Put it in MCDR `plugins/` +3. `!!MCDR reload plugin` or restart MCDR + +### RCON (required for `!!hat`) + +```properties +enable-rcon=true +rcon.port=25575 +rcon.password=your_password +``` + +## Config + +Path: `config/iridium.json` (auto-created with comments on first load) + +| Field | Default | Description | +|------|---------|-------------| +| `permission` | `1` | Min permission for commands | +| `head_cooldown_seconds` | `60` | `!!head` cooldown; `0` = none | +| `hat_cooldown_seconds` | `3` | `!!hat` cooldown; `0` = none | +| `share_sound` | `true` | Sound on `!!share` | +| `force_version` | `""` | Force MC version; empty = auto | +| `join_tip` | `true` | Command tips on join | +| `join_tip_delay_seconds` | `1.5` | Join message delay | +| `motd_enabled` | `true` | Send join MOTD | +| `motd_start_day` | today | Server start date `YYYY-MM-DD` (day 1 = that date) | +| `motd_lines` | see file | MOTD lines with placeholders | + +Default `motd_lines` (QQ group is clickable): + +```text +§6Welcome §e{player}§6 ! +§7Server has been open for §b{days}§7 days +§7Website: {link:§bOpen website|https://www.skymirror.top} +§7QQ: {link:§f985402607|https://skymirror.top/qq} +``` + +### MOTD placeholders + +| Token | Meaning | +|-------|---------| +| `{player}` | Joining player | +| `{days}` / `{day}` | Days since `motd_start_day` | +| `{online}` | Online count or `?` | +| `{link:text\|https://url}` | Clickable link | +| `{link:https://url}` | Clickable URL | +| `§` | Vanilla color/style codes | + +## Security + +- Calculator: AST whitelist, no arbitrary Python +- Player names sanitized to `[A-Za-z0-9_]` before game commands +- Newlines stripped from console commands +- MOTD links limited to `http(s)` only + +## Copyright + +Copyright (c) 2026 云镜之端/SKYMirror. All Rights Reserved. See `LICENSE`. diff --git a/catalogue/plugin_info.json b/catalogue/plugin_info.json index 9db7584..69c0525 100644 --- a/catalogue/plugin_info.json +++ b/catalogue/plugin_info.json @@ -12,6 +12,6 @@ "labels": ["tool"], "introduction": { "zh_cn": "README.md", - "en_us": "README.md" + "en_us": "README_en.md" } } diff --git a/iridium/config.py b/iridium/config.py index 60b05c7..6eaada9 100644 --- a/iridium/config.py +++ b/iridium/config.py @@ -1,11 +1,15 @@ import json -import re +from datetime import date from pathlib import Path from typing import List, Optional from mcdreforged.api.all import * +def _today() -> str: + return date.today().strftime("%Y-%m-%d") + + class Config(Serializable): # 使用 !!share / !!hat / !!head / !!c 所需的最低权限等级 permission: int = 1 @@ -23,8 +27,8 @@ class Config(Serializable): join_tip_delay_seconds: float = 1.5 # 是否发送进服 MOTD(开服天数 + 自定义行) motd_enabled: bool = True - # 开服日期,格式 YYYY-MM-DD,用于计算开服天数;留空则不显示天数 - motd_start_day: str = "" + # 开服日期,格式 YYYY-MM-DD;默认为配置生成当天(当天算第 1 天) + motd_start_day: str = _today() # MOTD 文本行。支持: # {player} 玩家名 # {days} 开服天数 @@ -36,14 +40,17 @@ class Config(Serializable): "§6欢迎 §e{player}§6 加入服务器!", "§7服务器已开服 §b{days}§7 天", "§7官网: {link:§b点击打开官网|https://www.skymirror.top}", - "§7QQ群: §f985402607", + "§7QQ群: {link:§f985402607|https://skymirror.top/qq}", ] CONFIG_FILE = "config/iridium.json" -# 首次生成时写入带注释的 JSON(MCDR 本身不写注释) -DEFAULT_CONFIG_TEXT = """{ + +def build_default_config_text() -> str: + """带注释的默认配置;开服日期为生成当天。""" + today = _today() + return f"""{{ // 使用 !!share / !!hat / !!head / !!c 所需的最低权限等级 // 0=控制台 1=普通玩家 2=OP/管理员 "permission": 1, @@ -71,22 +78,26 @@ DEFAULT_CONFIG_TEXT = """{ "motd_enabled": true, // 开服日期,格式 YYYY-MM-DD(当天算第 1 天) - // 留空则不显示开服天数 - "motd_start_day": "", + // 默认为本配置文件生成当天,可自行修改为真实开服日 + "motd_start_day": "{today}", // MOTD 文本行 - // 占位符: {player} 玩家名 {days} 开服天数 {online} 在线人数 - // 链接: {link:显示文字|https://url} 或 {link:https://url} + // 占位符: {{player}} 玩家名 {{days}} 开服天数 {{online}} 在线人数 + // 链接: {{link:显示文字|https://url}} 或 {{link:https://url}} // 颜色: 支持原版 § 颜色代码 "motd_lines": [ - "§6欢迎 §e{player}§6 加入服务器!", - "§7服务器已开服 §b{days}§7 天", - "§7官网: {link:§b点击打开官网|https://www.skymirror.top}", - "§7QQ群: §f985402607" + "§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 时原地更新字段,避免各模块持有过期引用 config = Config() @@ -136,22 +147,80 @@ def _strip_json_comments(text: str) -> str: return "".join(out) -def _config_path(server: PluginServerInterface) -> Path: - wd = Path(server.get_mcdr_config()["working_directory"]) - return wd / CONFIG_FILE +def _candidate_paths(server: PluginServerInterface) -> List[Path]: + """Possible config file locations, best first.""" + paths: List[Path] = [] + 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) + + try: + wd = server.get_mcdr_config().get("working_directory") + if wd: + add(Path(wd) / CONFIG_FILE) + except Exception: + pass + 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: + """Prefer an existing file; else the first writable candidate.""" + candidates = _candidate_paths(server) + for p in candidates: + if p.is_file(): + return p + return candidates[0] + + +def ensure_config_file(server: PluginServerInterface) -> Path: + """Create commented default config if missing. Always returns absolute path.""" + path = config_path(server) + try: + path.parent.mkdir(parents=True, exist_ok=True) + except Exception as e: + server.logger.error(f"无法创建配置目录 {path.parent}: {e}") + return path + if not path.is_file(): + try: + path.write_text(build_default_config_text(), encoding="utf-8") + server.logger.info(f"已生成配置文件: {path}") + except Exception as e: + server.logger.error(f"写入配置文件失败 {path}: {e}") + return path def load_config(server: PluginServerInterface) -> Config: - """Load config/iridium.json; create a commented default on first run.""" - path = _config_path(server) - path.parent.mkdir(parents=True, exist_ok=True) - if not path.exists(): - path.write_text(DEFAULT_CONFIG_TEXT, encoding="utf-8") - server.logger.info(f"已生成配置文件: {CONFIG_FILE}") - raw = path.read_text(encoding="utf-8") + """Load config; recreate a commented default whenever the file is missing.""" + path = ensure_config_file(server) + if not path.is_file(): + server.logger.warning(f"配置文件不存在且无法创建,使用内存默认值: {path}") + return Config() + try: + raw = path.read_text(encoding="utf-8") + except Exception as e: + server.logger.error(f"读取配置失败 {path}: {e},使用默认值") + return Config() try: data = json.loads(_strip_json_comments(raw)) except json.JSONDecodeError as e: - server.logger.error(f"配置文件 JSON 解析失败,使用默认值: {e}") + server.logger.error(f"配置文件 JSON 解析失败 {path}: {e},使用默认值") + return Config() + try: + return Config.deserialize(data) + except Exception as e: + server.logger.error(f"配置反序列化失败 {path}: {e},使用默认值") return Config() - return Config.deserialize(data) diff --git a/iridium/join_tip.py b/iridium/join_tip.py index a305ba5..40a7539 100644 --- a/iridium/join_tip.py +++ b/iridium/join_tip.py @@ -175,11 +175,10 @@ def build_motd(player: str, server: ServerInterface) -> Optional[RTextList]: lines = list(config.motd_lines or []) if not lines: return None - parts: List[RText] = [ - RText(tr("motd_header"), RColor.gold, styles=[RStyle.bold]) - ] - for line in lines: - parts.append(RText("\n")) + parts: List[RText] = [] + for i, line in enumerate(lines): + if i: + parts.append(RText("\n")) parts.extend(render_motd_line(line, player, server)) # Only auto-append the day footer when no line already uses {days}/{day} days = server_days() diff --git a/lang/en_us.yml b/lang/en_us.yml index 0077a77..8a4eff4 100644 --- a/lang/en_us.yml +++ b/lang/en_us.yml @@ -27,7 +27,6 @@ iridium.calc_help: 'Evaluate an expression, e.g. !!c 1+2*(3-1)' iridium.calc_usage: 'Usage: !!c , e.g. !!c 1+2*(3-1)' iridium.calc_error: 'Cannot calculate {0}: {1}' iridium.calc_div_zero: 'Division by zero' -iridium.join_tip_header: 'Iridium is enabled. Available commands: (click to fill chat)' +iridium.join_tip_header: 'Available commands: (click to fill chat)' iridium.join_tip_click: 'click to fill chat' -iridium.motd_header: 'Server Info' iridium.motd_days: 'Day {0} since server start' diff --git a/lang/zh_cn.yml b/lang/zh_cn.yml index fa863e2..61fe595 100644 --- a/lang/zh_cn.yml +++ b/lang/zh_cn.yml @@ -27,7 +27,6 @@ iridium.calc_help: '计算表达式,例如 !!c 1+2*(3-1)' iridium.calc_usage: '用法: !!c <表达式>,例如 !!c 1+2*(3-1)' iridium.calc_error: '无法计算 {0}: {1}' iridium.calc_div_zero: '除数不能为 0' -iridium.join_tip_header: '本服已启用 Iridium,可用命令:(点击命令可填入聊天框)' +iridium.join_tip_header: '可用命令:(点击命令可填入聊天框)' iridium.join_tip_click: '点击填入聊天栏' -iridium.motd_header: '服务器信息' iridium.motd_days: '开服第 {0} 天' diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index c0703ed..847b6cf 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "iridium", - "version": "26.9.13", + "version": "26.9.14", "name": "Iridium", "description": { "zh_cn": "物品展示 / 戴头 / 获取头颅 / 游戏内计算器 / 进服 MOTD", diff --git a/tests/test_pure.py b/tests/test_pure.py index 78c1353..8933b60 100644 --- a/tests/test_pure.py +++ b/tests/test_pure.py @@ -269,12 +269,15 @@ def test_extract_display_name(): def test_strip_json_comments(): import json + from datetime import date - from iridium.config import DEFAULT_CONFIG_TEXT, _strip_json_comments + from iridium.config import build_default_config_text, _strip_json_comments - parsed = json.loads(_strip_json_comments(DEFAULT_CONFIG_TEXT)) + text = build_default_config_text() + parsed = json.loads(_strip_json_comments(text)) 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"