发布 Iridium 26.9.1
MCDR plugin: item share, hat, player head, calculator, join MOTD. All rights reserved. Author: 云镜之端/SKYMirror
This commit is contained in:
commit
09c2392b99
18 files changed
+1796
No files matched your search
@@ -0,0 +1,242 @@
|
||||
import re
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from mcdreforged.api.all import *
|
||||
|
||||
from iridium.config import config
|
||||
from iridium.utils import sanitize_name
|
||||
from iridium.version_util import tr
|
||||
|
||||
LINK_WITH_TEXT_RE = re.compile(r"\{link:(?P<text>[^|}]+)\|(?P<url>[^}]+)\}")
|
||||
LINK_URL_ONLY_RE = re.compile(r"\{link:(?P<url>[^}|]+)\}")
|
||||
|
||||
_COLOR_MAP = {
|
||||
"0": RColor.black,
|
||||
"1": RColor.dark_blue,
|
||||
"2": RColor.dark_green,
|
||||
"3": RColor.dark_aqua,
|
||||
"4": RColor.dark_red,
|
||||
"5": RColor.dark_purple,
|
||||
"6": RColor.gold,
|
||||
"7": RColor.gray,
|
||||
"8": RColor.dark_gray,
|
||||
"9": RColor.blue,
|
||||
"a": RColor.green,
|
||||
"b": RColor.aqua,
|
||||
"c": RColor.red,
|
||||
"d": RColor.light_purple,
|
||||
"e": RColor.yellow,
|
||||
"f": RColor.white,
|
||||
}
|
||||
|
||||
_STYLE_MAP = {
|
||||
"k": RStyle.obfuscated,
|
||||
"l": RStyle.bold,
|
||||
"m": RStyle.strikethrough,
|
||||
"n": RStyle.underlined,
|
||||
"o": RStyle.italic,
|
||||
}
|
||||
|
||||
|
||||
def server_days() -> Optional[int]:
|
||||
raw = (config.motd_start_day or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y.%m.%d"):
|
||||
try:
|
||||
start = datetime.strptime(raw, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
return max(0, (datetime.now() - start).days) + 1
|
||||
return None
|
||||
|
||||
|
||||
def online_hint(server: ServerInterface) -> str:
|
||||
try:
|
||||
info = server.get_server_information()
|
||||
if info is not None and getattr(info, "player_count", None) is not None:
|
||||
return str(info.player_count)
|
||||
except Exception:
|
||||
pass
|
||||
return "?"
|
||||
|
||||
|
||||
def replace_placeholders(line: str, player: str, server: ServerInterface) -> str:
|
||||
days = server_days()
|
||||
days_text = str(days) if days is not None else "?"
|
||||
# Sanitize player name before it can re-enter the {link:...} parser or § codes
|
||||
safe_player = sanitize_name(player) or "Player"
|
||||
return (
|
||||
line.replace("{player}", safe_player)
|
||||
.replace("{days}", days_text)
|
||||
.replace("{day}", days_text)
|
||||
.replace("{online}", online_hint(server))
|
||||
)
|
||||
|
||||
|
||||
def parse_colored_pieces(segment: str) -> List[RText]:
|
||||
"""Split a string with § color codes into RText pieces."""
|
||||
pieces: List[RText] = []
|
||||
color: Optional[RColor] = None
|
||||
styles: List[RStyle] = []
|
||||
buf = ""
|
||||
|
||||
def flush() -> None:
|
||||
nonlocal buf
|
||||
if not buf:
|
||||
return
|
||||
pieces.append(RText(buf, color=color, styles=list(styles) if styles else None))
|
||||
buf = ""
|
||||
|
||||
i = 0
|
||||
n = len(segment)
|
||||
while i < n:
|
||||
if segment[i] == "§" and i + 1 < n:
|
||||
code = segment[i + 1].lower()
|
||||
i += 2
|
||||
if code == "r":
|
||||
flush()
|
||||
color = None
|
||||
styles = []
|
||||
continue
|
||||
if code in _COLOR_MAP:
|
||||
flush()
|
||||
color = _COLOR_MAP[code]
|
||||
styles = []
|
||||
continue
|
||||
if code in _STYLE_MAP:
|
||||
flush()
|
||||
style = _STYLE_MAP[code]
|
||||
if style not in styles:
|
||||
styles.append(style)
|
||||
continue
|
||||
buf += f"§{code}"
|
||||
continue
|
||||
buf += segment[i]
|
||||
i += 1
|
||||
flush()
|
||||
return pieces or [RText(segment or "")]
|
||||
|
||||
|
||||
def _apply_click(piece: RText, url: str) -> RText:
|
||||
return piece.c(RAction.open_url, url).h(RText(url, RColor.gray))
|
||||
|
||||
|
||||
def render_motd_line(line: str, player: str, server: ServerInterface) -> List[RText]:
|
||||
raw = replace_placeholders(line, player, server)
|
||||
|
||||
# Tokenize: plain text | (text, url)
|
||||
tokens: List[Tuple[str, str, Optional[str]]] = []
|
||||
last = 0
|
||||
|
||||
def push_text(chunk: str) -> None:
|
||||
if chunk:
|
||||
tokens.append(("text", chunk, None))
|
||||
|
||||
for match in LINK_WITH_TEXT_RE.finditer(raw):
|
||||
push_text(raw[last : match.start()])
|
||||
tokens.append(("link", match.group("text"), match.group("url").strip()))
|
||||
last = match.end()
|
||||
|
||||
# Remaining part may still contain {link:url}
|
||||
rest = raw[last:]
|
||||
pos = 0
|
||||
for match in LINK_URL_ONLY_RE.finditer(rest):
|
||||
push_text(rest[pos : match.start()])
|
||||
url = match.group("url").strip()
|
||||
tokens.append(("link", url, url))
|
||||
pos = match.end()
|
||||
push_text(rest[pos:])
|
||||
if not tokens:
|
||||
tokens.append(("text", raw, None))
|
||||
|
||||
out: List[RText] = []
|
||||
for kind, text, url in tokens:
|
||||
if kind == "text":
|
||||
out.extend(parse_colored_pieces(text))
|
||||
continue
|
||||
assert url is not None
|
||||
if not url.lower().startswith(("http://", "https://")):
|
||||
out.extend(parse_colored_pieces(text))
|
||||
continue
|
||||
colored = parse_colored_pieces(text)
|
||||
if not colored:
|
||||
colored = [RText(text, RColor.aqua)]
|
||||
for piece in colored:
|
||||
out.append(_apply_click(piece, url))
|
||||
return out
|
||||
|
||||
|
||||
def build_motd(player: str, server: ServerInterface) -> Optional[RTextList]:
|
||||
if not config.motd_enabled:
|
||||
return None
|
||||
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.extend(render_motd_line(line, player, server))
|
||||
# Only auto-append the day footer when no line already uses {days}/{day}
|
||||
days = server_days()
|
||||
if days is not None and not any(("{days}" in ln or "{day}" in ln) for ln in lines):
|
||||
parts.append(RText("\n"))
|
||||
parts.append(RText(tr("motd_days", days), RColor.gray))
|
||||
return RTextList(*parts)
|
||||
|
||||
|
||||
def command_line(cmd: str, desc: str) -> List[RText]:
|
||||
return [
|
||||
RText(cmd, RColor.aqua, styles=[RStyle.underlined])
|
||||
.c(RAction.suggest_command, cmd)
|
||||
.h(RText(tr("join_tip_click"), RColor.gray)),
|
||||
RText(" - ", RColor.dark_gray),
|
||||
RText(desc, RColor.white),
|
||||
]
|
||||
|
||||
|
||||
def build_command_tip() -> RTextList:
|
||||
parts: List[RText] = [
|
||||
RText("[Iridium] ", RColor.gray),
|
||||
RText(tr("join_tip_header"), RColor.gold),
|
||||
]
|
||||
for cmd, desc in (
|
||||
("!!share", tr("share_help")),
|
||||
("!!hat", tr("hat_help")),
|
||||
("!!head", tr("head_help_short")),
|
||||
("!!c 1+1", tr("calc_help")),
|
||||
):
|
||||
parts.append(RText("\n"))
|
||||
parts.extend(command_line(cmd, desc))
|
||||
return RTextList(*parts)
|
||||
|
||||
|
||||
def send_join_message(server: ServerInterface, player: str) -> None:
|
||||
try:
|
||||
motd = build_motd(player, server)
|
||||
if motd is not None:
|
||||
server.tell(player, motd)
|
||||
if config.join_tip:
|
||||
server.tell(player, build_command_tip())
|
||||
except Exception as e:
|
||||
server.logger.debug(f"join message failed for {player}: {e}")
|
||||
|
||||
|
||||
def on_player_joined(server: PluginServerInterface, player: str, info: Info) -> None:
|
||||
if not config.motd_enabled and not config.join_tip:
|
||||
return
|
||||
delay = max(0.0, float(config.join_tip_delay_seconds))
|
||||
if delay <= 0:
|
||||
send_join_message(server, player)
|
||||
return
|
||||
timer = threading.Timer(delay, lambda: send_join_message(server, player))
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
|
||||
|
||||
def register(server: PluginServerInterface) -> None:
|
||||
server.register_event_listener(MCDRPluginEvents.PLAYER_JOIN, on_player_joined)
|
||||
Reference in new issue
Block a user