├── .gitignore ├── PlayerInfoAPI.py ├── README.md └── README_cn.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | debug.py 3 | debug_plugin.py 4 | PlayerInfoAPI_v.py 5 | temp.txt -------------------------------------------------------------------------------- /PlayerInfoAPI.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import re 4 | import ast 5 | import json 6 | from threading import Lock 7 | 8 | try: 9 | import Queue 10 | except ImportError: 11 | import queue as Queue 12 | 13 | 14 | work_queue = {} 15 | queue_lock = Lock() 16 | query_count = 0 17 | 18 | 19 | def convertMinecraftJson(text): 20 | r""" 21 | Convert Minecraft json string into standard json string and json.loads() it 22 | Also if the input has a prefix of "xxx has the following entity data: " it will automatically remove it, more ease! 23 | Example available inputs: 24 | - Alex has the following entity data: {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} 25 | - {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} 26 | - [0.0d, 10, 1.7E9] 27 | - {Air: 300s, Text: "\\o/..\""} 28 | - "hello" 29 | - 0b 30 | 31 | :param str text: The Minecraft style json string 32 | :return: Parsed result 33 | """ 34 | 35 | # Alex has the following entity data: {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} 36 | # yeet the prefix 37 | text = re.sub(r'^.* has the following entity data: ', '', text) # yeet prefix 38 | 39 | # {a: 0b, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} 40 | # remove letter after number 41 | text = re.sub(r'(?<=\d)[a-zA-Z](?=(\D|$))', '', text) 42 | 43 | # {a: 0, big: 2.99E7, c: "minecraft:white_wool", d: '{"text":"rua"}'} 44 | # add quotation marks to all 45 | text = re.sub(r'([a-zA-Z.]+)(?=:)', '"\g<1>"', text) 46 | 47 | # {"a": 0, "big": 2.99E7, "c": ""minecraft":white_wool", "d": '{"text":"rua"}'} 48 | # remove unnecessary quotation created by namespaces 49 | list_a = re.split(r'""[a-zA-Z.]+":', text) # split good texts 50 | list_b = re.findall(r'""[a-zA-Z.]+":', text) # split bad texts 51 | result = list_a[0] 52 | for i in range(len(list_b)): 53 | result += list_b[i].replace('""', '"').replace('":', ':') + list_a[i + 1] 54 | 55 | # {"a": 0, "big": 2.99E7, "c": "minecraft.white_wool", "d": '{"text":"rua"}'} 56 | # process apostrophe string 57 | text = ''.join([i for i in mcSingleQuotationJsonReader(result)]) 58 | 59 | # {"a": 0, "big": 2.99E7, "c": "minecraft.white_wool", "d": "{\"text\": \"rua\"}"} 60 | # finish 61 | return json.loads(text) 62 | 63 | 64 | def mcSingleQuotationJsonReader(data): 65 | part = data # type: str 66 | count = 1 67 | while True: 68 | spliter = part.split(r"'{", 1) # Match first opening braces 69 | yield spliter[0] 70 | if len(spliter) == 1: 71 | return # No more 72 | else: 73 | part_2 = spliter[1].split(r"}'") # Match all closing braces 74 | index = 0 75 | res = jsonCheck("".join(part_2[:index + 1])) 76 | while not res: 77 | index += 1 78 | if index == len(part_2): 79 | raise RuntimeError("Out of index") # Looks like illegal json string 80 | res = jsonCheck("".join(part_2[:index + 1])) 81 | j_dict = "" 82 | while res: 83 | # Is real need? 84 | j_dict = res 85 | index += 1 86 | if index == len(part_2): 87 | break # Yep, is real 88 | res = jsonCheck("".join(part_2[:index + 1])) 89 | 90 | yield j_dict # Match a json string 91 | 92 | # Restore split string 93 | part_2 = part_2[index:] 94 | part = part_2[0] 95 | if len(part_2) > 1: 96 | for i in part_2[1:]: 97 | part += "}'" 98 | part += i 99 | count += 1 100 | 101 | 102 | def jsonCheck(j): 103 | checking = "".join(["{", j, "}"]) 104 | try: 105 | # Plan A 106 | # checking = checking.replace("\"", "\\\"") 107 | # checking = checking.replace("\'","\\\'") 108 | # checking = checking.replace("\\n", "\\\\n") 109 | checking = checking.replace(r'\\', "\\") 110 | res = json.loads(checking) 111 | except ValueError: 112 | try: 113 | # Plan B 114 | res = ast.literal_eval(checking) 115 | except: 116 | return False 117 | 118 | data = json.dumps({"data": json.dumps(res)}) 119 | return data[9:-1] 120 | 121 | 122 | def get_queue(player): 123 | with queue_lock: 124 | if player not in work_queue: 125 | work_queue[player] = Queue.Queue() 126 | return work_queue[player] 127 | 128 | 129 | def clean_queue(): 130 | global work_queue, queue_lock 131 | with queue_lock: 132 | for q in work_queue.values(): 133 | q.queue.clear() 134 | 135 | 136 | def getPlayerInfo(server, player, path='', timeout=5): 137 | """ 138 | Use command "data get entity []" to query the entity data of a player 139 | And then parse the Minecraft style json result into standard json string and decode it into python object 140 | If it runs in MCDR and rcon is enabled it will use rcon to query, otherwise it will send to command to the stdin of the 141 | server and waiting for result. 142 | 143 | :param server: The server object from MCD or MCDR 144 | :param str player: The player you want to query data 145 | :param str path: The precise data path you want to query 146 | :param int or float timeout: Maximum waiting time before server respond if rcon is disabled 147 | :return: The result object, or None if query fails 148 | """ 149 | if len(path) >= 1 and not path.startswith(' '): 150 | path = ' ' + path 151 | command = 'data get entity {}{}'.format(player, path) 152 | if hasattr(server, 'MCDR') and server.is_rcon_running(): 153 | result = server.rcon_query(command) 154 | else: 155 | global query_count 156 | query_count += 1 157 | try: 158 | server.execute(command) 159 | result = get_queue(player).get(timeout=timeout) 160 | except Queue.Empty: 161 | return None 162 | finally: 163 | query_count -= 1 164 | try: 165 | return convertMinecraftJson(result) 166 | except json.JSONDecodeError: 167 | server.logger.error('Fail to parse Minecraft json {}'.format(result)) 168 | return None 169 | 170 | 171 | def onServerInfo(server, info): 172 | global work_queue 173 | if info.isPlayer == 0 and re.match(r'^\w+ has the following entity data: .*$', info.content): 174 | player = info.content.split(' ')[0] 175 | if query_count > 0: 176 | get_queue(player).put(info.content) 177 | else: 178 | clean_queue() 179 | 180 | 181 | def on_info(server, info): 182 | info2 = copy.deepcopy(info) 183 | info2.isPlayer = info2.is_player 184 | onServerInfo(server, info2) 185 | 186 | 187 | if __name__ == '__main__': 188 | for line in convertMinecraftJson.__doc__.splitlines(): 189 | if re.match(r'^\s*- .*$', line): 190 | s = line.split('-')[-1] 191 | print('{} -> {}'.format(s, convertMinecraftJson(s))) 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayerInfoAPI 2 | ------------- 3 | 4 | [中文](https://github.com/TISUnion/PlayerInfoAPI/blob/master/README_cn.md) 5 | 6 | **Check [MinecraftDataAPI](https://github.com/MCDReforged/MinecraftDataAPI) for MCDR 1.0+** 7 | 8 | a MCDReforged plugin to provide API for getting player information 9 | 10 | Now support 1.14+ new JSON string format 11 | 12 | (Thank Pandaria for providing the regular expression) 13 | 14 | ## Usage 15 | 16 | Use imp.load_source() to load PlayerInfoAPI in your plugin first 17 | 18 | ``` 19 | from imp import load_source 20 | PlayerInfoAPI = load_source('PlayerInfoAPI','./plugins/PlayerInfoAPI.py') 21 | ``` 22 | 23 | for MCDReforged use `server.get_plugin_instance()` to get PlayerInfoAPI instance 24 | 25 | ``` 26 | PlayerInfoAPI = server.get_plugin_instance('PlayerInfoAPI') 27 | ``` 28 | 29 | ### PlayerInfoAPI.convertMinecraftJson(text) 30 | 31 | Convert Minecraft style json format into a dict 32 | 33 | Minecraft style json format is something like these: 34 | 35 | - `Steve has the following entity data: [-227.5d, 64.0d, 12.3E4d]` 36 | - `[-227.5d, 64.0d, 231.5d]` 37 | - `Alex has the following entity data: {HurtByTimestamp: 0, SleepTimer: 0s, ..., Inventory: [], foodTickTimer: 0}` 38 | 39 | It will automatically detect if there is a ` has the following entity data: `. If there is, it will erase it before converting 40 | 41 | Args: 42 | - text: A data get entity or other command result that use Minecraft style json format 43 | 44 | Return: 45 | - A parsed json result. It can be a `dict`, a `list`, a `int` or a `None` 46 | 47 | Samples: 48 | 49 | - Input `Steve has the following entity data: [-227.5d, 64.0d, 231.5d]`, output `[-227.5, 64.0, 123000.0]` 50 | 51 | - Input `{HurtByTimestamp: 0, SleepTimer: 0s, Inventory: [], foodTickTimer: 0}`, output `{'HurtByTimestamp': 0, 'SleepTimer': 0, 'Inventory': [], 'foodTickTimer': 0}` 52 | 53 | ### PlayerInfoAPI.getPlayerInfo(server, name, path='', timeout=5) 54 | 55 | Call `data get entity []` and parse the result 56 | 57 | If it's in MCDReforged and rcon is enabled it will use rcon to query 58 | 59 | Args: 60 | - server: The Server Object 61 | - name: Name of the player who you want to get his/her info 62 | - path: An optional `path` parameter in `data get entity` command 63 | - timeout: The maximum time to wait the data result if rcon is off. Return `None` if time out 64 | 65 | Return: 66 | - A parsed json result. It can be a `dict`, a `list`, a `int` or a `None` 67 | 68 | Please refer to the Player.dat page on minecraft wiki 69 | [player.dat format](https://minecraft.gamepedia.com/Player.dat_format) 70 | 71 | ## Example 72 | 73 | ``` 74 | def onServerInfo(server, info): 75 | if info.content.startswith('!!test'): 76 | result = PlayerInfoAPI.getPlayerInfo(server,info.player) 77 | server.say("Dim:"+str(result["Dimension"])+"Pos:"+str(result["Pos"][0])+","+str(result["Pos"][1])+","+str(result["Pos"][2])) 78 | ``` 79 | 80 | you can also refer to the demo of Here plugin with this API(in newapi branch) 81 | 82 | [Here(Demo)](https://github.com/TISUnion/Here/tree/newapi) 83 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # PlayerInfoAPI 2 | ------------- 3 | 4 | **正在使用 MCDR 1.0+?去看看 [MinecraftDataAPI](https://github.com/MCDReforged/MinecraftDataAPI) 吧** 5 | 6 | 一个提供获取玩家信息的 MCDReforged 插件 API 7 | 8 | 现在支持 1.14.4 以上的新 JSON 字符串格式 9 | 10 | (感谢 Pandaria 提供的有关正则表达式的支持) 11 | 12 | ## 使用方法 13 | 14 | 你必须先使用 imp.load_source() 来在你的插件中载入PlayerInfoAPI 15 | 16 | ``` 17 | from imp import load_source 18 | PlayerInfoAPI = load_source('PlayerInfoAPI','./plugins/PlayerInfoAPI.py') 19 | ``` 20 | 21 | 如果使用 MCDReforged 必须使用 `server.get_plugin_instance()` 来获得 PlayerInfoAPI 插件实例 22 | 23 | ``` 24 | PlayerInfoAPI = server.get_plugin_instance('PlayerInfoAPI') 25 | ``` 26 | 27 | ### 方法:PlayerInfoAPI.convertMinecraftJson(text) 28 | 29 | 将麻将牌JSON转换成Python可读取的字典数据类型 30 | 31 | 麻将牌JSON的形式如下: 32 | 33 | - `Steve has the following entity data: [-227.5d, 64.0d, 12.3E4d]` 34 | - `[-227.5d, 64.0d, 231.5d]` 35 | - `Alex has the following entity data: {HurtByTimestamp: 0, SleepTimer: 0s, ..., Inventory: [], foodTickTimer: 0}` 36 | 37 | 会自动检查消息是否包含 ` has the following entity data: `前缀,并且会在转换前自动去除 38 | 39 | 参数: 40 | - text: 从`/data get entity`指令或者其他命令获得的麻将牌JSON数据 41 | 42 | 返回: 43 | - json 解析后的结果。它可以是一个 `dict`, 一个 `list`, 一个 `int` 或者一个 `None` 44 | 45 | 示例: 46 | 47 | - 输入: `Steve has the following entity data: [-227.5d, 64.0d, 231.5d]`, 输出: `[-227.5, 64.0, 123000.0]` 48 | 49 | - 输入: `{HurtByTimestamp: 0, SleepTimer: 0s, Inventory: [], foodTickTimer: 0}`, 输出: `{'HurtByTimestamp': 0, 'SleepTimer': 0, 'Inventory': [], 'foodTickTimer': 0}` 50 | 51 | ### 方法:PlayerInfoAPI.getPlayerInfo(server, name, path='') 52 | 53 | 自动执行 `/data get entity []` 并解析返回数据 54 | 55 | 如果在 MCDReforged 中使用并且开启了rcon,插件会自动从rcon执行获得结果 56 | 57 | 参数: 58 | - server: Server对象 59 | - name: 要获得TA数据的目标玩家 60 | - path: 在`/data get entity` 指令中的可选参数`path` 61 | - timeout: 当 rcon 关闭时等待结果的最长时间。如果超时了则返回 `None` 62 | 63 | 64 | 返回: 65 | - 解析后的结果 66 | 67 | 你可以在Minecraft Wiki参考关于Player.det的格式信息 68 | [Player.dat格式](https://minecraft-zh.gamepedia.com/Player.dat%E6%A0%BC%E5%BC%8F) 69 | 70 | ## 示例 71 | 72 | ``` 73 | def onServerInfo(server, info): 74 | if info.content.startswith('!!test'): 75 | result = PlayerInfoAPI.getPlayerInfo(server,info.player) 76 | server.say("Dim:"+str(result["Dimension"])+"Pos:"+str(result["Pos"][0])+","+str(result["Pos"][1])+","+str(result["Pos"][2])) 77 | ``` 78 | 79 | 你也可以参考使用了这个API的插件例子(位于newapi分支) 80 | 81 | [Here(Demo)](https://github.com/TISUnion/Here/tree/newapi) --------------------------------------------------------------------------------