forked from Fallen-Breath/MinecraftDataAPI
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5bb98c6
commit 21334fc
Showing
8 changed files
with
532 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.idea | ||
debug.py | ||
debug_plugin.py | ||
PlayerInfoAPI_v.py | ||
temp.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
import collections | ||
import re | ||
from queue import Queue, Empty | ||
from threading import RLock | ||
from typing import Dict, Optional, Union | ||
|
||
import json5 | ||
from mcdreforged.api.all import * | ||
|
||
PLUGIN_METADATA = { | ||
'id': 'minecraft_data_api', | ||
'version': '1.0.0', | ||
'name': 'Minecraft Data API', | ||
'description': 'A MCDReforged api plugin to get player data information and more', | ||
'author': [ | ||
'Fallen_Breath' | ||
], | ||
'link': 'https://github.com/MCDReforged/MinecraftDataAPI' | ||
} | ||
|
||
DEFAULT_TIME_OUT = 5 # seconds | ||
|
||
|
||
class PlayerDataGetter: | ||
class QueueTask: | ||
def __init__(self): | ||
self.queue = Queue() | ||
self.count = 0 | ||
|
||
def __init__(self): | ||
self.queue_lock = RLock() | ||
self.work_queue = {} # type: Dict[str, PlayerDataGetter.QueueTask] | ||
self.server = None # type: Optional[ServerInterface] | ||
self.json_parser = MinecraftJsonParser() | ||
|
||
def attach(self, server: ServerInterface): | ||
self.server = server | ||
|
||
def get_queue_task(self, player) -> QueueTask: | ||
with self.queue_lock: | ||
if player not in self.work_queue: | ||
self.work_queue[player] = self.QueueTask() | ||
return self.work_queue[player] | ||
|
||
def get_player_info(self, player: str, path: str, timeout: Optional[float]): | ||
if self.server.is_on_executor_thread(): | ||
raise RuntimeError('Cannot invoke get_player_info on the task executor thread') | ||
if len(path) >= 1 and not path.startswith(' '): | ||
path = ' ' + path | ||
if timeout is None: | ||
timeout = DEFAULT_TIME_OUT | ||
command = 'data get entity {}{}'.format(player, path) | ||
if self.server.is_rcon_running(): | ||
return self.server.rcon_query(command) | ||
else: | ||
task = self.get_queue_task(player) | ||
task.count += 1 | ||
try: | ||
self.server.execute(command) | ||
content = task.queue.get(timeout=timeout) | ||
except Empty: | ||
return None | ||
finally: | ||
task.count -= 1 | ||
try: | ||
return self.json_parser.convert_minecraft_json(content) | ||
except Exception as e: | ||
self.server.logger.error('[{}] Fail to Convert data "{}": {}'.format( | ||
PLUGIN_METADATA['name'], | ||
content if len(content) < 64 else '{}...'.format(content[:64]), | ||
e | ||
)) | ||
|
||
def on_info(self, info: Info): | ||
if not info.is_user: | ||
if re.match(r'^\w+ has the following entity data: .*$', info.content): | ||
player = info.content.split(' ')[0] | ||
task = self.get_queue_task(player) | ||
if task.count > 0: | ||
task.queue.put(info.content) | ||
|
||
|
||
class MinecraftJsonParser: | ||
@classmethod | ||
def convert_minecraft_json(cls, text: str): | ||
r""" | ||
Convert Minecraft json string into standard json string and json.loads() it | ||
Also if the input has a prefix of "xxx has the following entity data: " it will automatically remove it, more ease! | ||
Example available inputs: | ||
- Alex has the following entity data: {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} | ||
- {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} | ||
- [0.0d, 10, 1.7E9] | ||
- {Air: 300s, Text: "\\o/..\""} | ||
- "hello" | ||
- 0b | ||
:param str text: The Minecraft style json string | ||
:return: Parsed result | ||
""" | ||
|
||
# Alex has the following entity data: {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} | ||
# yeet the prefix | ||
text = re.sub(r'^.* has the following entity data: ', '', text) # yeet prefix | ||
|
||
# {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} | ||
# remove letter after number outside string | ||
text = cls.remove_letter_after_number(text) | ||
|
||
# {a: 0, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} | ||
return json5.loads(text) | ||
|
||
@classmethod | ||
def remove_letter_after_number(cls, text: str) -> str: | ||
result = '' | ||
while text: | ||
pos = min(text.find('"'), text.find("'")) | ||
quote = None | ||
if pos == -1: | ||
pos = len(text) | ||
part_str = text[pos:] | ||
result += re.sub(r'(?<=\d)[a-zA-Z](?=(\D|$))', '', text[:pos]) # remove letter after number outside string | ||
if part_str: | ||
quote = part_str[0] | ||
result += quote | ||
part_str = part_str[1:] # remove the beginning quote | ||
while part_str: | ||
slash_pos = part_str.find('\\') | ||
if slash_pos == -1: | ||
slash_pos = len(part_str) | ||
quote_pos = part_str[:slash_pos].find(quote) | ||
if quote_pos == -1: # cannot find a quote in front of the first slash | ||
if slash_pos == len(part_str): | ||
raise ValueError('Cannot find a string ending quote') | ||
result += part_str[:slash_pos + 2] | ||
part_str = part_str[slash_pos + 2:] | ||
else: | ||
result += part_str[:quote_pos + 1] | ||
part_str = part_str[quote_pos + 1:] # found an un-escaped quote | ||
break | ||
text = part_str | ||
return result | ||
|
||
|
||
data_getter = None # type: Optional[PlayerDataGetter] | ||
|
||
|
||
def on_load(server, prev): | ||
global data_getter | ||
if hasattr(prev, 'data_getter'): | ||
data_getter = prev.data_getter | ||
else: | ||
data_getter = PlayerDataGetter() | ||
data_getter.attach(server) | ||
|
||
|
||
def on_info(server: ServerInterface, info): | ||
data_getter.on_info(info) | ||
|
||
|
||
# ------------------ | ||
# API Interfaces | ||
# ------------------ | ||
|
||
|
||
def convert_minecraft_json(text: str): | ||
""" | ||
Convert a mojang style "json" str to a json like object | ||
:param text: The name of the player | ||
""" | ||
return data_getter.json_parser.convert_minecraft_json(text) | ||
|
||
|
||
def get_player_info(player: str, data_path: str = '', *, timeout: Optional[float] = None): | ||
""" | ||
Get information from a player | ||
It's required to be executed in a separated thread. It can not be invoked on the task executor thread of MCDR | ||
:param player: The name of the player | ||
:param data_path: Optional, the data nbt path you want to query | ||
:param timeout: The timeout limit for querying | ||
:return: A parsed json like object contains the information. e.g. a dict | ||
""" | ||
return data_getter.get_player_info(player, data_path, timeout) | ||
|
||
|
||
Coordinate = collections.namedtuple('Coordinate', 'x y z') | ||
|
||
|
||
def get_player_coordinate(player: str, *, timeout: Optional[float] = None) -> Union[int or str]: | ||
""" | ||
Return the coordinate of a player | ||
The return value is a tuple with 3 elements (x, y, z). Each element is a float | ||
The return value is also a namedtuple, you can use coord.x, coord.y, coord.z to access the value | ||
""" | ||
pos = get_player_info(player, 'Pos', timeout=timeout) | ||
if pos is None: | ||
raise ValueError('Fail to query the coordinate of player {}'.format(player)) | ||
return Coordinate(x=float(pos[0]), y=float(pos[1]), z=float(pos[2])) | ||
|
||
|
||
def get_player_dimension(player: str, *, timeout: Optional[float] = None) -> Union[int or str]: | ||
""" | ||
Return the dimension of a player and return an int representing the dimension. Compatible with MC 1.16 | ||
If the dim result is a str, the server should be in 1.16, and it will convert the dimension name into the old integer | ||
format if the dimension is overworld, nether or the end. Otherwise the origin dimension name str is returned | ||
""" | ||
dim_convert = { | ||
'minecraft:overworld': 0, | ||
'minecraft:the_nether': -1, | ||
'minecraft:the_end': 1 | ||
} | ||
dim = get_player_info(player, 'Dimension', timeout=timeout) | ||
if dim is None: | ||
raise ValueError('Fail to query the dimension of player {}'.format(player)) | ||
if type(dim) is str: # 1.16+ | ||
dim = dim_convert.get(dim, dim) | ||
return dim | ||
|
||
|
||
def get_dimension_translation_text(dim_id: int) -> RText: | ||
""" | ||
Return a RTextTranslation object indicating the dimension name which can be recognized by Minecraft | ||
If the dimension id is not supported, it will just return a RText object wrapping the dimension id | ||
:param dim_id: a int representing the dimension. Should be 0, -1 or 1 | ||
""" | ||
dimension_translation = { | ||
0: 'createWorld.customize.preset.overworld', | ||
-1: 'advancements.nether.root.title', | ||
1: 'advancements.end.root.title' | ||
} | ||
if dim_id in dimension_translation: | ||
return RTextTranslation(dimension_translation[dim_id]) | ||
else: | ||
return RText(dim_id) | ||
|
||
# ----------------------- | ||
# API Interfaces ends | ||
# ----------------------- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# MinecraftDataAPI | ||
------------- | ||
|
||
[中文](https://github.com/MCDReforged/MinecraftDataAPI/blob/master/README_cn.md) | ||
|
||
A MCDReforged api plugin to get player data information and more | ||
|
||
## Dependency | ||
|
||
- `json5` module | ||
|
||
## Usage | ||
|
||
Use `server.get_plugin_instance()` to get the MinecraftDataAPI instance | ||
|
||
```python | ||
api = server.get_plugin_instance('minecraft_data_api') | ||
``` | ||
|
||
You can declare the dependency of this plugin in PLUGIN_METADATA: | ||
|
||
```python | ||
PLUGIN_METADATA = { | ||
'dependencies': { | ||
'minecraft_data_api': '*', | ||
} | ||
} | ||
``` | ||
|
||
## Function list | ||
|
||
### convert_minecraft_json | ||
|
||
```python | ||
def convert_minecraft_json(text: str) | ||
``` | ||
|
||
Convert Minecraft style json format into a python object | ||
|
||
Minecraft style json format is something like these: | ||
|
||
- `Steve has the following entity data: [-227.5d, 64.0d, 12.3E4d]` | ||
- `[-227.5d, 64.0d, 231.5d]` | ||
- `Alex has the following entity data: {HurtByTimestamp: 0, SleepTimer: 0s, ..., Inventory: [], foodTickTimer: 0}` | ||
|
||
It will automatically detect if there is a `<name> has the following entity data: `. If there is, it will erase it before converting | ||
|
||
Args: | ||
- text: A data get entity or other command result that use Minecraft style json format | ||
|
||
Return: | ||
- A parsed json result. It can be a `dict`, a `list`, a `int` or a `None` | ||
|
||
Samples: | ||
|
||
- Input `Steve has the following entity data: [-227.5d, 64.0d, 231.5d]`, output `[-227.5, 64.0, 123000.0]` | ||
|
||
- Input `{HurtByTimestamp: 0, SleepTimer: 0s, Inventory: [], foodTickTimer: 0}`, output `{'HurtByTimestamp': 0, 'SleepTimer': 0, 'Inventory': [], 'foodTickTimer': 0}` | ||
|
||
### get_player_info | ||
|
||
```python | ||
def get_player_info(player: str, data_path: str = '', *, timeout: Optional[float] = None) | ||
``` | ||
|
||
Execute `data get entity <name> [<path>]` and parse the result | ||
|
||
If it's in MCDReforged and rcon is enabled it will use rcon to query | ||
|
||
Args: | ||
- name: Name of the player who you want to get his/her info | ||
- path: An optional `path` parameter in `data get entity` command | ||
- timeout: The maximum time to wait the data result if rcon is off. Return `None` if time out | ||
|
||
Return: | ||
- A parsed json result. It can be a `dict`, a `list`, a `int` or a `None` | ||
|
||
Please refer to the Player.dat page on minecraft wiki | ||
|
||
[player.dat format](https://minecraft.gamepedia.com/Player.dat_format) | ||
|
||
### get_player_coordinate | ||
|
||
```python | ||
def get_player_coordinate(player: str, *, timeout: Optional[float] = None) -> Union[int or str] | ||
``` | ||
|
||
Use `get_player_info` to query the `Pos` data of the player to get the player's coordinate. A `ValueError` will be risen if query failed | ||
|
||
It will convert the return value into a named tuple `collections.namedtuple('Coordinate', 'x y z')` for easier use of the return value | ||
|
||
### get_player_dimension | ||
|
||
``` | ||
def get_player_dimension(player: str, *, timeout: Optional[float] = None) -> Union[int or str] | ||
``` | ||
|
||
Use `get_player_info` to query the `Dimension` data of the player to get the player's dimension. A `ValueError` will be risen if query failed | ||
|
||
It contains a dimension data convert. It will convert the dimension name in MC 1.16+ (e.g. `minecraft:overworld`) into the related integer | ||
|
||
Dimension name mapping: | ||
|
||
```python | ||
dim_convert = { | ||
'minecraft:overworld': 0, | ||
'minecraft:the_nether': -1, | ||
'minecraft:the_end': 1 | ||
} | ||
``` | ||
|
||
If the dimension it gets is not in the mapping, for example it's a custom dimension, then it will return the name of the dimension directly | ||
|
||
### get_dimension_translation_text | ||
|
||
``` | ||
def get_dimension_translation_text(dim_id: int) -> RText | ||
``` | ||
|
||
Convert the dimension id into a RTextTranslation object that can be translated by Minecraft. The mapping of the translation key is as follows | ||
|
||
```python | ||
dimension_translation = { | ||
0: 'createWorld.customize.preset.overworld', | ||
-1: 'advancements.nether.root.title', | ||
1: 'advancements.end.root.title' | ||
} | ||
``` | ||
|
||
If the dimension id is not in the mapping, it will return a RText object contains the input id directly | ||
|
||
You can safely use `api.get_dimension_translation_text(api.get_player_dimension('Steve'))` to get text component storing the dimension the player is in |
Oops, something went wrong.