diff --git a/README.md b/README.md index 1d5d397..f255900 100644 --- a/README.md +++ b/README.md @@ -9,25 +9,46 @@ A [MCDReforged](https://github.com/Fallen-Breath/MCDReforged) plugin, inspired b [MinecraftDataAPI](https://github.com/MCDReforged/MinecraftDataAPI/) -[MCDReforged](https://github.com/Fallen-Breath/MCDReforged) >= 2.1.3 +[MCDReforged](https://github.com/Fallen-Breath/MCDReforged) >= 2.12.0 + +> Changed in v2.2: +> +> No longer supports MCDR v2.11 or earlier ## Commands 1. `!!whereis` or `!!vris`(can be modified in config):Show coordinate of other player -Command format: `!!whereis [args(optional)]` +Command format: `!!whereis [args(optional)]` (multiple players allowed) + +​ Optional arguments could be following values (multiple arguments should be divided by space): -​ `-a` or `-s` arguments are allowed (can be called in one argument as `-as` or `-sa`) +​ `-a`, `--all` means broadcasting coordinate to all the players and highlight target player -​ `-a` means broadcasting coordinate to **a**ll the players and highlight target player +​ `-s`, `--sudo` allows querying coordinate of protected players -​ `-s` means **s**udo, allows querying coordinate of protected players +Both 2 arguments requires `admin` permission level in the config file of this plugin -​ Both 2 arguments requires `admin` permission level in the config file of this plugin +> Changed in v2.2: +> +> No longer supports arguments like `-as` and `-sa`, multiple players allowed 2. `!!here` (can be modified in config): Broadcast your current coordinate -​ Any `!!here` divided with space in chat message can be responded if inline here is enabled in config. **New in version 2.1** +> New in v2.1: +> +> Inline `!!here` command divided by space can be responded if enabled + +Optional argument can be used with both commands: (unavailable for inline `!!here`) + +​ `-h`, `--highlight` Set highlight target player time, overrides config settings + +> Players won't be highlighted if their location is only queried, not broadcast + +> New in v2.2: +> +> New optional argument `-h ` introduced + ## Config File @@ -37,13 +58,14 @@ Calling `!!MCDR plg reload where_is` to reload is required to make it loaded aft Here is the config items in the file -**WARNING: These 2 items which are marked with asterisk below should be configured manually before you update to 2.x or you'll lose all the configuration during loading the new version!!! You can ignore this message if you haven't install this plugin before** +> [!WARNING] +> These 2 items which are marked with asterisk below should be configured manually before you update to 2.x or you'll lose all the configuration during loading the new version!!! You can ignore this message if you haven't install this plugin before | Keys | Value type | Default value | Introduction | |----------------------------------|---------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `enable_where_is` | `bool` | `true` | Set it to `true` to enable query other player location | | `enable_here` | `bool` | `true` | Set it to `true` to enable broadcast your location | -| `enable_inline_here` | `bool` | `false` | Set it to `true` to make inline here enabled. **New in version 2.1** | +| `enable_inline_here` | `bool` | `false` | Set it to `true` to make inline here enabled. | | *`command_prefix` | `dict`(which includes 2 items) | In the following sheets | Command prefix of this plugin | | `broadcast_to_console` | `bool` | `true` | `!!here` or `!!vris -a` will also send message to server console | | `permission_requirements` | `dict`(which includes 3 items) | In the following sheets | Minimum permission of commands | @@ -54,7 +76,11 @@ Here is the config items in the file | `location_protection` | `dict`(which includes 5 items) | In the following sheets | Player coordinate protection configuration | | `dimension_translation_mode` | `Literal['mcdr', 'minecraft']` | `'mcdr'` | `mcdr`: MCDReforged translates dimension names; `minecraft`: Minecraft translates dimension names. In 1.19 and later versions, using `mcdr` will avoid translation failure due to these dimension key change. | | `custom_dimension_name` | `Dict[str, Dict[str, str]]` | Too long to show | A mapping of dimension name translation for `mcdr` translation mode. This fist layer keys are the languages. The second layer keys are the dimension IDs (Non-vanilla dimensions are allowed. vanilla dimensions are required to fill and should omit the namespace). | -| `custom_vanilla_translation_key` | `Dict[str, str]` | Too long to show | A mapping of dimension translation keys for `minecraft` translation mode. (Non-vanilla dimensions are allowed. vanilla dimensions are required to fill and should omit the namespace). **New in version 2.1** | +| `custom_vanilla_translation_key` | `Dict[str, str]` | Too long to show | A mapping of dimension translation keys for `minecraft` translation mode. (Non-vanilla dimensions are allowed. vanilla dimensions are required to fill and should omit the namespace). | + +> New in v2.1: +> +> New configuration items: `inline_here` and `custom_vanilla_translation_key` In the sheet above, the items which have stable items is showing below: diff --git a/README_zh.md b/README_zh.md index 0be6409..6609f8d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,25 +9,45 @@ Where Is [MinecraftDataAPI](https://github.com/MCDReforged/MinecraftDataAPI/) -[MCDReforged](https://github.com/Fallen-Breath/MCDReforged) >= 2.1.3 +[MCDReforged](https://github.com/Fallen-Breath/MCDReforged) >= 2.12.0 + +> 于 2.2 版本变更: +> +> 不再支持 MCDR 2.11 及更早版本 ## 指令 1. `!!whereis` 或者 `!!vris`(可在配置文件中修改):显示一个其他玩家的坐标。 -指令格式: `!!whereis <玩家> [参数(可选)]` +指令格式: `!!whereis <玩家> [参数(可选)]` (可以写多个玩家) + +​ 可选参数可以为以下值(多个参数使用空格隔开): -​ 可以加`-a` 或者 `-s`(可以合成一个参数写作 `-as` 或者 `-sa`)。 +​ `-a`, `--all` 意为向所有玩家发送坐标并高亮该玩家 -​ `-a` 意为向所有(**a**ll)玩家发送坐标并高亮该玩家; +​ `-s`, `--sudo` 意为提权,允许查看受保护的玩家的坐标 -​ `-s` 意为提权(**s**udo),允许查看受保护的玩家的坐标。 +​ 两个参数均需要插件配置中设置的 `admin` 等级来执行 -​ 两个参数均需要插件配置中设置的 `admin` 等级来执行。 +> 于 2.2 版本变更: +> +> 不再支持 `-as` 或 `-sa` 形式的参数,允许添加多个玩家 2. `!!here` (可在配置文件中修改): 广播自己当前的坐标 -​ 在配置文件中启用聊天中任意 here 指令解析时,聊天信息中用空格隔开的任意 `!!here` 字段均会被响应 **2.1新版功能** +> 2.1 新版功能: +> +> 在启用了配置项的情况下, 聊天行中空格隔开的 `!!here` 指令可以被响应 + +两条指令均可使用的可选参数: (对行中 `!!here` 无效) + +​ `-h`, `--highlight` <时长> 设定目标玩家的高亮时长 + +> 其位置仅被查询而非被广播的情况下,不能高亮玩家 + +> 2.2 新版功能: +> +> 引入了新可选参数 `-h ` ## 配置文件 @@ -37,24 +57,30 @@ Where Is 以下为配置文件内容 -**警告:下方被星号标记的两项的值须在更新到2.x版本前手动配置,否则您将在加载新版本时丢失本插件的全部配置,若您此前从未安装过本插件可无视该条信息** - -| 键 | 值的类型 | 默认值 | 说明 | -|----------------------------------|--------------------------------|-------------------------|------------------------------------------------------------------------------------------| -| `enable_where_is` | `bool` | `true` | 设置为`true`以启用查询玩家坐标的功能 | -| `enable_here` | `bool` | `true` | 设置为`true`以启用广播自己坐标的功能 | -| `enable_here` | `bool` | `false` | 设置为 `true` 以允许聊天中任意 here 指令解析功能 **2.1 新版功能** | -| *`command_prefix` | `dict`(含固定的2个项目) | `'!!vris', '!!whereis'` | 插件指令前缀 | -| `broadcast_to_console` | `bool` | `true` | `!!here` 或 `!!vris -a` 会将坐标信息同时显示在服务端控制台 | -| `permission_requirements` | `dict`(含固定的3个项目) | 见下表 | 指令要求的最小权限等级 | -| *`hightlight_time` | `dict`(含固定的2个项目) | 见下表 | 当包含 `-a` 参数时高亮玩家的时间 | -| `display_waypoints` | `dict`(含固定的2个项目) | 见下表 | 是否显示小地图坐标点 | -| `query_timeout` | `int` | `3` | Minecraft Data API的超时时间 | -| `click_to_teleport` | `bool` | `true` | 允许玩家点击补全传送指令 (仍需OP以执行) | -| `location_protection` | `dict`(含固定的5个项目) | 见下表 | 玩家坐标保护相关设定 | -| `dimension_translation_mode` | `Literal['mcdr', 'minecraft']` | `'mcdr'` | `mcdr`: 维度名称由MCDR翻译; `minecraft`: 维度名称由Minecraft翻译。在1.19及以上版本,用`mcdr`项可以避免因翻译键名改动造成的翻译失败 | -| `custom_dimension_name` | `Dict[str, Dict[str, str]` | 内容过长不便展示 | 由 MCDR 翻译的维度名称的翻译键值映射。首层键名为语言,二级键名为维度ID(支持非原版维度,原版维度必须填写且应去掉命名空间) | -| `custom_vanilla_translation_key` | `Dict[str, str]` | 内容过长不便展示 | 由 Minecraft 翻译的维度键名映射 (支持非原版维度,原版维度必须填写且应去掉命名空间). **2.1 新版功能** | +> [!WARNING] +> 下方被星号标记的两项的值须在更新到2.x版本前手动配置,否则您将在加载新版本时丢失本插件的全部配置,若您此前从未安装过本插件可无视该条信息 + +| 键 | 值的类型 | 默认值 | 说明 | +|----------------------------------|--------------------------------|----------|------------------------------------------------------------------------------------------| +| `enable_where_is` | `bool` | `true` | 设置为`true`以启用查询玩家坐标的功能 | +| `enable_here` | `bool` | `true` | 设置为`true`以启用广播自己坐标的功能 | +| `enable_here` | `bool` | `false` | 设置为 `true` 以允许聊天中任意 here 指令解析功能 | +| *`command_prefix` | `dict`(含固定的2个项目) | 见下表 | 插件指令前缀 | +| `broadcast_to_console` | `bool` | `true` | `!!here` 或 `!!vris -a` 会将坐标信息同时显示在服务端控制台 | +| `permission_requirements` | `dict`(含固定的3个项目) | 见下表 | 指令要求的最小权限等级 | +| *`hightlight_time` | `dict`(含固定的2个项目) | 见下表 | 当包含 `-a` 参数时高亮玩家的时间 | +| `display_waypoints` | `dict`(含固定的2个项目) | 见下表 | 是否显示小地图坐标点 | +| `query_timeout` | `int` | `3` | Minecraft Data API的超时时间 | +| `click_to_teleport` | `bool` | `true` | 允许玩家点击补全传送指令 (仍需OP以执行) | +| `location_protection` | `dict`(含固定的5个项目) | 见下表 | 玩家坐标保护相关设定 | +| `dimension_translation_mode` | `Literal['mcdr', 'minecraft']` | `'mcdr'` | `mcdr`: 维度名称由MCDR翻译; `minecraft`: 维度名称由Minecraft翻译。在1.19及以上版本,用`mcdr`项可以避免因翻译键名改动造成的翻译失败 | +| `custom_dimension_name` | `Dict[str, Dict[str, str]` | 内容过长不便展示 | 由 MCDR 翻译的维度名称的翻译键值映射。首层键名为语言,二级键名为维度ID(支持非原版维度,原版维度必须填写且应去掉命名空间) | +| `custom_vanilla_translation_key` | `Dict[str, str]` | 内容过长不便展示 | 由 Minecraft 翻译的维度键名映射 (支持非原版维度,原版维度必须填写且应去掉命名空间). | + + +> 2.1 新版功能: +> +> 新的配置项目: `inline_here` 和 `custom_vanilla_translation_key` 上述提到的含固定键值对的的配置项如下: diff --git a/img.png b/img.png index 7bee358..beca7e5 100644 Binary files a/img.png and b/img.png differ diff --git a/lang/en_us.yml b/lang/en_us.yml index 97611d8..4d7dfec 100644 --- a/lang/en_us.yml +++ b/lang/en_us.yml @@ -1,7 +1,24 @@ where_is: + help: + detailed: | + §7--------§9 {name} §rv{ver} §7--------§r + Query other player's location + §d[Basic usage]§r + §7{vris} §5§3 [optional]§r Query player's location + §7{here} §3[optional]§r §rBroadcast your own location + §d[Advanced usage]§r + Argument §5§r allows multiple players + Argument §3[optional]§r for command §7{vris}§r: + §8> §b-a§7, §b--all§r Broadcast player's location + §8> §b-s§7, §b--sudo§r Allow to query protected player + Argument §3[optional]§r for both commands: + §8> §b-h§7, §b--highlight§6 §r Set highlight time + vris: Query location of other player + here: Broadcast your own location + suggest: Click to fill command §7{} err: generic: "Error occurred: {}" - not_online: "Required player is not online" + not_online: "Required player {} is not online" perm_denied: "Permission denied" invalid_args: "Invalid argument" warn: diff --git a/lang/zh_cn.yml b/lang/zh_cn.yml index 2be555c..89dd72d 100644 --- a/lang/zh_cn.yml +++ b/lang/zh_cn.yml @@ -1,7 +1,24 @@ where_is: + help: + detailed: | + §7--------§9 {name} §rv{ver} §7--------§r + 查询其他玩家的位置 + §d【基础用法】§r + §7{vris} §5<玩家>§3 [可选参数]§r 查询玩家的位置 + §7{here} §r广播你自己的位置 + §d【进阶用法】§r + 参数 §5<玩家>§r 允许添加多个玩家 + 指令 §7{vris}§r 的 §3[可选参数]§r 可为: + §8> §b-a§7, §b--all§r 广播查询到的玩家位置 + §8> §b-s§7, §b--sudo§r 允许查询受保护的玩家 + 适用于所有指令的参数 §3[可选参数]§r: + §8> §b-h§7, §b--highlight§6 <时长>§r 设置高亮时长 + vris: 查询其他玩家的位置 + here: 广播你自己的位置 + suggest: 点击填入指令 §7{} err: - generic: "出现错误: {}" - not_online: "这玩家不在线啊" + generic: "出现错误: §4{}" + not_online: "玩家 §4{}§c 不在线" perm_denied: "权限不足" invalid_args: "无效的参数" warn: diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 268b18b..a00d674 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "where_is", - "version": "2.1.1+build.9", + "version": "2.2.0+build.12", "name": "Where Is", "description": { "en_us": "Query players' coordinates", @@ -12,7 +12,7 @@ "link": "https://github.com/Lazy-Bing-Server/WhereIs-MCDR", "dependencies": { "minecraft_data_api": "*", - "mcdreforged": ">=2.1.3" + "mcdreforged": ">=2.12.0" }, "entrypoint": "where_is.entry", "resources": [ diff --git a/where_is/constants.py b/where_is/constants.py index 84cd3cc..d6a039a 100644 --- a/where_is/constants.py +++ b/where_is/constants.py @@ -14,4 +14,4 @@ ID_TO_REG = dict([(v, k) for k, v in REG_TO_ID.items()]) psi = ServerInterface.get_instance().as_plugin_server_interface() -DEBUG = False +DEBUG = True diff --git a/where_is/dimensions.py b/where_is/dimensions.py index eacea40..4c3d59e 100644 --- a/where_is/dimensions.py +++ b/where_is/dimensions.py @@ -4,7 +4,7 @@ from mcdreforged.api.rtext import RColor, RTextBase, RTextTranslation from where_is.config import config from where_is.constants import OVERWORLD, NETHER, END, REG_TO_ID, ID_TO_REG, OVERWORLD_SHORT, NETHER_SHORT, END_SHORT -from where_is.utils import rtr, dim_tr +from where_is.utils import rtr, dim_tr, debug from where_is.position import Position """ @@ -113,6 +113,7 @@ def get_dimension(text: str) -> Dimension: return LegacyDimension(int(text)) except: pass - if text in REG_TO_ID: + debug(text) + if text in REG_TO_ID.keys(): return LegacyDimension(REG_TO_ID[text]) return CustomDimension(text) diff --git a/where_is/entry.py b/where_is/entry.py index c360d76..207480c 100644 --- a/where_is/entry.py +++ b/where_is/entry.py @@ -1,29 +1,120 @@ -from typing import Optional, Union +import re +from typing import Optional, Union, List -from mcdreforged.api.command import Literal, QuotableText -from mcdreforged.api.rtext import RColor, RTextBase, RText, RAction, RTextList -from mcdreforged.api.types import CommandSource, PlayerCommandSource, Info, PluginServerInterface -from minecraft_data_api import get_player_info, get_player_dimension, get_server_player_list +from mcdreforged.api.all import * +from minecraft_data_api import get_player_info, get_player_dimension from where_is.config import config from where_is.constants import psi from where_is.dimensions import get_dimension, Dimension, LegacyDimension from where_is.position import Position -from where_is.utils import rtr, debug, ntr, named_thread +from where_is.online_players import online_players +from where_is.utils import rtr, debug, ntr, named_thread, MessageText +from where_is.node import QuotableTextList + + +PLAYER_LIST = 'player_list' +SUDO_COUNT = 'sudo_count' +ALL_COUNT = 'all_count' +HIGHLIGHT_TIME = 'highlight_time' + + +def htr( + translation_key: str, + *args, + prefixes: Optional[List[str]] = None, + suggest_prefix: Optional[str] = None, + **kwargs, +) -> RTextMCDRTranslation: + prefixes = prefixes or [""] + + def __get_regex_result(line: str): + pattern = r"(?<=§7){}[\S ]*?(?=§)" + for prefix in prefixes: + result = re.search(pattern.format(prefix), line) + if result is not None: + return result + return None + + def __htr(key: str, *inner_args, **inner_kwargs) -> MessageText: + nonlocal suggest_prefix + original = ntr(key, *inner_args, **inner_kwargs) + processed: List[MessageText] = [] + if not isinstance(original, str): + return key + for line in original.splitlines(): + result = __get_regex_result(line) + if result is not None: + command = result.group().strip() + " " + if suggest_prefix is not None: + command = suggest_prefix.strip() + " " + command + processed.append( + RText(line) + .c(RAction.suggest_command, command) + .h(rtr("help.suggest", command)) + ) + + debug(f'Rich help line: "{line}"') + debug( + "Suggest prefix: {}".format( + f'"{suggest_prefix}"' + if isinstance(suggest_prefix, str) + else suggest_prefix + ) + ) + debug(f'Suggest command: "{command}"') + else: + processed.append(line) + return RTextBase.join("\n", processed) + + return rtr(translation_key, *args, **kwargs).set_translator(__htr) @named_thread -def where_is(source: CommandSource, target_player: str, args: str = '-'): - para_list = list(args[1:]) - if 's' not in para_list and not config.location_protection.is_allowed(source, target_player): +def where_is(source: CommandSource, context: CommandContext): + player_list = context.get(PLAYER_LIST, []) + sudo = context.get(SUDO_COUNT, 0) > 0 + to_all = context.get(ALL_COUNT, 0) > 0 + highlight_time = context.get(HIGHLIGHT_TIME) + if len(player_list) == 0: + where_is_help(source, context) + for player in player_list: + _where_is(source, player, sudo=sudo, to_all=to_all, highlight_time=highlight_time) + + +def where_is_help(source: CommandSource, context: CommandContext): + current_prefix = context.command.split(' ')[0] + meta = psi.get_self_metadata() + version = meta.version + version_str = ".".join([str(n) for n in version.component]) + if version.pre is not None: + version_str += "-" + str(version.pre) + source.reply(htr( + 'help.detailed', + vris=current_prefix, + here=config.command_prefix.here_prefixes[0], + name=meta.name, + ver=version_str, + prefixes=[current_prefix, config.command_prefix.here_prefixes[0]] + )) + + +def _where_is( + source: CommandSource, + target_player: str, + sudo: bool = False, + to_all: bool = False, + highlight_time: Optional[int] = None +): + highlight_time = highlight_time or config.highlight_time.where_is + debug("Highlight time: {} s".format(highlight_time)) + if not sudo and not config.location_protection.is_allowed(source, target_player): source.reply(rtr('err.player_protected').set_color(RColor.red)) return + if target_player not in online_players.get_player_list(): + source.reply(rtr('err.not_online', target_player).set_color(RColor.red)) + return try: - player_list = get_server_player_list(timeout=config.query_timeout)[2] - debug(str(player_list)) - if target_player not in tuple([] if player_list is None else player_list): - source.reply(rtr('err.not_online').set_color(RColor.red)) - return coordinate = get_player_pos(target_player, timeout=config.query_timeout) dimension = get_dimension(get_player_dimension(target_player, timeout=config.query_timeout)) rtext = where_is_text(target_player, coordinate, dimension) @@ -32,17 +123,21 @@ def where_is(source: CommandSource, target_player: str, args: str = '-'): psi.logger.exception('Unexpected exception occurred while querying player location') return - if 'a' in para_list: + if to_all: say(rtext) - if config.highlight_time.where_is > 0: - psi.execute('effect give {} minecraft:glowing {} 0 true'.format( - target_player, config.highlight_time.where_is)) + if highlight_time > 0: + psi.execute( + f'effect give {target_player} minecraft:glowing {highlight_time} 0 true' + ) else: source.reply(rtext) @named_thread -def here(source: PlayerCommandSource): +def here(source: PlayerCommandSource, context: Optional[CommandContext] = None): + highlight_time = config.highlight_time.here + if context is not None: + highlight_time = context.get(HIGHLIGHT_TIME, highlight_time) if psi.get_plugin_metadata('here') is not None: psi.logger.warning(ntr('warn.duplicated_here')) return @@ -56,21 +151,13 @@ def here(source: PlayerCommandSource): return say(rtext) - if config.highlight_time.here > 0: - psi.execute('effect give {} minecraft:glowing {} 0 true'.format( - source.player, config.highlight_time.here)) + if highlight_time > 0: + psi.execute( + f'effect give {source.player} minecraft:glowing {config.highlight_time.here} 0 true' + ) def coordinate_text(x: float, y: float, z: float, dimension: Dimension): - """ - Coordinate text converter from TISUnion/Here(http://github.com/TISUnion/Here) - Licensed under GNU General Public License v3.0 - :param x: Coordinate on X axis - :param y: Coordinate on Y axis - :param z: Coordinate on Z axis - :param dimension: Converted dimension objects - :return: RText object of this coordinates - """ coord = RText('[{}, {}, {}]'.format(int(x), int(y), int(z)), dimension.get_coordinate_color()) if config.click_to_teleport: return ( @@ -83,14 +170,6 @@ def coordinate_text(x: float, y: float, z: float, dimension: Dimension): def where_is_text(target_player: str, pos: Position, dim: Dimension) -> RTextBase: - """ - Main text converter from TISUnion/Here(http://github.com/TISUnion/Here) - Licensed under GNU General Public License v3.0 - :param target_player: Target player name string - :param pos: Coordinate object - :param dim: Dimension object - :return: Main RText - """ x, y, z = pos.x, pos.y, pos.z # basic text: someone @ dimension [x, y, z] @@ -133,7 +212,6 @@ def get_player_pos(player: str, *, timeout: Optional[float] = None) -> Position: return Position(x=float(pos[0]), y=float(pos[1]), z=float(pos[2])) -# Should be run in new thread def say(text: Union[str, RTextBase]): if config.ocd: if config.broadcast_to_console: @@ -144,8 +222,9 @@ def say(text: Union[str, RTextBase]): if config.broadcast_to_console: for line in RTextBase.from_any(text).to_colored_text().splitlines(): psi.logger.info(line) - current_amount, max_amount, player_list = get_server_player_list(timeout=config.query_timeout) - if current_amount >= 1: + # Tell each player separately to apply language preference + player_list = online_players.get_player_list() + if len(player_list) >= 1: for player in player_list: psi.tell(player, text) @@ -153,7 +232,10 @@ def say(text: Union[str, RTextBase]): def on_user_info(server: PluginServerInterface, info: Info): if config.enable_here and config.enable_inline_here: source = info.get_command_source() - if source.has_permission(config.permission_requirements.here) and isinstance(source, PlayerCommandSource): + if ( + source.has_permission(config.permission_requirements.here) + and isinstance(source, PlayerCommandSource) + ): args = info.content.split(' ') for prefix in config.command_prefix.here_prefixes: if prefix in args: @@ -161,44 +243,44 @@ def on_user_info(server: PluginServerInterface, info: Info): break -def is_available_para(string: str): - arg_list = list(string) - if arg_list.pop(0) != '-' or len(arg_list) == 0: - return False - if 'a' in arg_list: - arg_list.remove('a') - if 's' in arg_list: - arg_list.remove('s') - if len(arg_list) != 0: - return False - return True - - def register_commands(server: PluginServerInterface): if config.enable_where_is: - server.register_command( - Literal(config.command_prefix.where_is_prefixes).then( - QuotableText("player").requires( - config.permission_requirements.query_is_allowed, lambda: rtr('err.perm_denied') - ).runs( - lambda src, ctx: where_is(src, ctx['player'])).then( - QuotableText('args').requires( - config.permission_requirements.is_admin, lambda: rtr('err.perm_denied') - ).requires( - lambda src, ctx: is_available_para(ctx['args']), lambda: rtr('err.invalid_args') - ).runs( - lambda src, ctx: where_is(src, ctx['player'], ctx['args']) - ) - ) + where_is_root = Literal(config.command_prefix.where_is_prefixes).requires( + config.permission_requirements.query_is_allowed + ).runs(where_is) + where_is_root.then( + QuotableTextList('player', PLAYER_LIST).suggests( + lambda: online_players.get_player_list() + ).redirects(where_is_root) + ) + where_is_root.then( + CountingLiteral({"-s", "--sudo"}, SUDO_COUNT).requires( + config.permission_requirements.is_admin + ).redirects(where_is_root) + ) + where_is_root.then( + CountingLiteral({"-a", "--all"}, ALL_COUNT).requires( + config.permission_requirements.is_admin + ).redirects(where_is_root) + ) + where_is_root.then( + Literal({"-h", "--highlight"}).then( + Integer(HIGHLIGHT_TIME).redirects(where_is_root) ) ) + server.register_command(where_is_root) if config.enable_here and not config.enable_inline_here: - server.register_command( - Literal(config.command_prefix.here_prefixes).requires( - config.permission_requirements.broadcast_is_allowed, lambda: rtr('err.perm_denied') - ).runs(lambda src: here(src)) + here_root = Literal(config.command_prefix.here_prefixes).requires( + config.permission_requirements.broadcast_is_allowed, + lambda: rtr('err.perm_denied') + ).runs(here) + here_root.then( + Literal('highlight').then( + Integer(HIGHLIGHT_TIME).redirects(here_root) + ) ) + server.register_command(here_root) def register_help_messages(server: PluginServerInterface): @@ -219,6 +301,11 @@ def register_customized_translations(server: PluginServerInterface): def on_load(server: PluginServerInterface, prev_modules): + online_players.register_event_listeners() register_help_messages(server) register_customized_translations(server) register_commands(server) + for pre in config.command_prefix.here_prefixes: + server.register_help_message(pre, rtr('help.here')) + for pre in config.command_prefix.where_is_prefixes: + server.register_help_message(pre, rtr('help.vris')) diff --git a/where_is/node.py b/where_is/node.py new file mode 100644 index 0000000..5a1a4e3 --- /dev/null +++ b/where_is/node.py @@ -0,0 +1,15 @@ +from mcdreforged.api.all import QuotableText, CommandContext, ParseResult + +from typing import Iterable + + +class QuotableTextList(QuotableText): + def __init__(self, name: str or Iterable[str], list_key: str): + super().__init__(name) + self.__list_key = list_key + + def get_list_key(self): + return self.__list_key + + def _on_visited(self, context: CommandContext, parse_result: "ParseResult"): + context[self.__list_key] = context.get(self.__list_key, []) + [parse_result.value] diff --git a/where_is/online_players.py b/where_is/online_players.py new file mode 100644 index 0000000..f2dbbba --- /dev/null +++ b/where_is/online_players.py @@ -0,0 +1,88 @@ +from mcdreforged.api.all import MCDRPluginEvents + +from threading import RLock + +from typing import List + +from where_is.utils import named_thread, psi, debug +from where_is.config import config + + +class OnlinePlayers: + def __init__(self): + self.__lock = RLock() + self.__players: List[str] = [] + self.__limit = 0 + + def get_player_list(self, refresh: bool = False): + with self.__lock: + if refresh: + self.__refresh_online_players() + return self.__players.copy() + + def get_player_limit(self, refresh: bool = False): + with self.__lock: + if refresh: + self.__refresh_online_players() + return self.__limit + + def __add_player(self, player: str): + with self.__lock: + if psi.is_server_startup() and player not in self.__players: + self.__players.append(player) + + def __remove_player(self, player: str): + with self.__lock: + if player in self.__players: + self.__players.remove(player) + + @named_thread + def __refresh_online_players(self): + with self.__lock: + if not psi.is_server_startup(): + return + api = psi.get_plugin_instance("minecraft_data_api") + player_tuple = api.get_server_player_list( + timeout=config.query_timeout + ) + if player_tuple is not None: + count, self.__limit, self.__players = player_tuple + debug( + "Player list refreshed: " + + ", ".join(self.__players) + + f" (max {self.__limit})" + ) + if count != len(self.__players): + psi.logger.warning( + "Incorrect player count found while refreshing player list" + ) + + @named_thread + def __clear_online_players(self): + with self.__lock: + self.__limit, self.__players = None, None + debug("Cleared online player cache") + + def register_event_listeners(self): + psi.register_event_listener( + MCDRPluginEvents.PLUGIN_LOADED, + lambda *args, **kwargs: self.__refresh_online_players(), + ) + psi.register_event_listener( + MCDRPluginEvents.SERVER_STARTUP, + lambda *args, **kwargs: self.__refresh_online_players(), + ) + psi.register_event_listener( + MCDRPluginEvents.PLAYER_JOINED, + lambda _, player, __: self.__add_player(player), + ) + psi.register_event_listener( + MCDRPluginEvents.PLAYER_LEFT, lambda _, player: self.__remove_player(player) + ) + psi.register_event_listener( + MCDRPluginEvents.SERVER_STOP, + lambda *args, **kwargs: self.__clear_online_players(), + ) + + +online_players = OnlinePlayers() diff --git a/where_is/utils.py b/where_is/utils.py index efaf1fa..d7e54a1 100644 --- a/where_is/utils.py +++ b/where_is/utils.py @@ -13,8 +13,13 @@ # Utilities -def rtr(translation_key: str, *args, with_prefix: bool = True, **kwargs) -> RTextMCDRTranslation: - if with_prefix and not translation_key.startswith(TRANSLATION_KEY_PREFIX): +def rtr( + translation_key: str, + *args, + _vris_rtr_with_prefix: bool = True, + **kwargs +) -> RTextMCDRTranslation: + if _vris_rtr_with_prefix and not translation_key.startswith(TRANSLATION_KEY_PREFIX): translation_key = f"{TRANSLATION_KEY_PREFIX}{translation_key}" return RTextMCDRTranslation(translation_key, *args, **kwargs).set_translator(ntr) @@ -24,15 +29,20 @@ def debug(msg: Union[str, RTextBase]): def ntr( - translation_key: str, *args, language: Optional[str] = None, _mcdr_tr_language: Optional[str] = None, - allow_failure: bool = True, _default_fallback: Optional[MessageText] = None, log_error_message: bool = True, **kwargs + translation_key: str, + *args, + _mcdr_tr_language: Optional[str] = None, + _mcdr_tr_allow_failure: bool = True, + _vris_ntr_default_fallback: Optional[MessageText] = None, + _vris_ntr_log_error_message: bool = True, + **kwargs ) -> MessageText: - if language is not None and _mcdr_tr_language is None: - _mcdr_tr_language = language try: return psi.tr( - translation_key, *args, language=_mcdr_tr_language, - _mcdr_tr_language=_mcdr_tr_language, allow_failure=False, **kwargs + translation_key, *args, + _mcdr_tr_language=_mcdr_tr_language, + _mcdr_tr_allow_failure=False, + **kwargs ) except (KeyError, ValueError): fallback_language = psi.get_mcdr_language() @@ -49,19 +59,19 @@ def ntr( if item not in languages: languages.append(item) languages = ', '.join(languages) - if allow_failure: - if log_error_message: + if _mcdr_tr_allow_failure: + if _vris_ntr_log_error_message: psi.logger.error(f'Error translate text "{translation_key}" to language {languages}') - if _default_fallback is None: + if _vris_ntr_default_fallback is None: return translation_key - return _default_fallback + return _vris_ntr_default_fallback else: raise KeyError(f'Translation key "{translation_key}" not found with language {languages}') def dim_tr(key: str, *args, lang: Optional[str] = None, allow_failure: bool = True, **kwargs): try: - return ntr(key, *args, lang=lang, allow_failure=False, **kwargs) + return ntr(key, *args, lang=lang, _mcdr_tr_allow_failure=False, **kwargs) except Exception as exc: if not allow_failure: raise exc