diff --git a/.gitignore b/.gitignore index bef59e2..f3738cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc *.log login.py -run.py \ No newline at end of file +run.py +/build \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index cbf5c90..de5182f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,30 +1,45 @@ Inspired by remyroy's COPS, a Minecraft client in Python. Protocol implementation based on barneymc -Currently Supports Minecraft 1.7.4 + +Spock currently supports Minecraft 1.8.1 spock ===== Bot framework, currently under heavy development -Protocol stuff lives in spock/mcp -Map stuff lives in spock/mcmap -Important client stuff lives in spock/plugins/core -Less important stuff lives in spock/plugins/helpers - -Client could loosely be called "event-driven", by default plugins register handler functions to handle events that may or may not be emitted by other plugins. Everything is a plugin in spock, including the event loop/framework itself, so you can feel free to rip out the entire back end and replace it with your framework or paradigm of choice. As long as you provide the same simple APIs, other plugins won't know the difference. - -Currently writing Spock plugins requires a fairly intricate understanding of the MC protocol, since there aren't many plugins that provide higher abstractions than MC packets. That said the API is starting to shape up quite nicely, we've got threading and the start of a World API. Once Python 3.4 comes out we'll probably try to stablize the event loop API around asyncio and provide some sort of ghetto fallback for older Python 3.x +Protocol stuff lives in spock/mcp +Map stuff lives in spock/mcmap +Important client stuff lives in spock/plugins/core +Less important stuff lives in spock/plugins/helpers + +Client could loosely be called "event-driven", by default plugins register +handler functions to handle events that may or may not be emitted by other +plugins. Everything is a plugin in spock, including the event loop/framework +itself, so you can feel free to rip out the entire back end and replace it with +your framework or paradigm of choice. As long as you provide the same simple +APIs, other plugins won't know the difference. + +Currently writing Spock plugins requires a fairly intricate understanding of the +MC protocol, since there aren't many plugins that provide higher abstractions +than MC packets. That said the API is starting to shape up quite nicely, we've +got timers and the start of a World API. + +Speaking of compatibility, Spock runs on Python 3.x on *Nix operating systems, +and requires PyCrypto. Theoretically it runs on Windows but no one has ever +tested it and I'm fairly sure there are a couple (easy to fix) errors that will +pop up. Python 2.x would be nice, but it becomes an ever more distant goal as +Spock continues to grow dependencies on new Python features. Not impossible, +just not a priority -Speaking of compatibility, Spock runs on Python 3.x on *Nix operating systems, and requires PyCrypto. Theoretically it runs on Windows but no one has ever tested it and I'm fairly sure there are a couple (easy to fix) errors that will - pop up. Python 2.x would be nice, but it becomes an ever more distant goal as Spock continues to grow dependencies on new Python features. Not impossible, just not a priority - I'll write a real ReadMe and API docs when everything is done and stable-ish. -For now you can check out the plugins folder to get a vague idea of what plugins should look like, find me on #mcdevs or email me at nickelpro@gmail.com if you have questions +For now you can check out the plugins folder to get a vague idea of what plugins +should look like, find me on #mcdevs or email me at nickelpro@gmail.com if you +have questions ###Legal License is MIT and can be found in license.md -The NBT parser and the original protocol implementation came from other projects, +The NBT parser and the original protocol implementation came from other projects, relevant legal information and attribution can be found in legal.md diff --git a/examples/demo.py b/examples/demo.py index 27871c4..2ec7d73 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,27 +1,14 @@ +""" +Basic demo example +""" + from spock.client import Client from spock.plugins import DefaultPlugins from demoplugin import DemoPlugin #Open login.py and put in your username and password from login import username, password -from spock.plugins.helpers.clientinfo import ClientInfoPlugin -from spock.plugins.helpers.move import MovementPlugin - -settings = { - 'username': "Bot", - 'authenticated': False, #Authenticate with authserver.mojang.com - 'bufsize': 4096, #Size of socket buffer - 'sock_quit': True, #Stop bot on socket error or hangup - 'sess_quit': True, #Stop bot on failed session login - 'thread_workers': 5, #Number of workers in the thread pool - 'packet_trace': True, - 'plugins': DefaultPlugins, #Plugins - 'plugin_settings': {}, #Extra settings for plugins -} plugins = DefaultPlugins -plugins.append(ClientInfoPlugin) -plugins.append(MovementPlugin) -# plugins.append(DemoPlugin) - -client = Client(plugins = plugins, settings = settings) -client.start(host="192.168.2.19", port=25565) \ No newline at end of file +plugins.append(DemoPlugin) +client = Client(plugins = plugins, username = username, password = password) +client.start() \ No newline at end of file diff --git a/examples/demoplugin.py b/examples/demoplugin.py index 96c7e13..239befc 100644 --- a/examples/demoplugin.py +++ b/examples/demoplugin.py @@ -2,22 +2,22 @@ #TODO: Make this cooler class DemoPlugin: - def __init__(self, ploader, settings): - #Login Success - ploader.reg_event_handler( - (mcdata.LOGIN_STATE, mcdata.SERVER_TO_CLIENT, 0x02), - self.print_packets - ) - #Chat Message - ploader.reg_event_handler( - (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x02), - self.print_packets - ) - #Player List Item - ploader.reg_event_handler( - (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x38), - self.print_packets - ) + def __init__(self, ploader, settings): + #Login Success + ploader.reg_event_handler( + (mcdata.LOGIN_STATE, mcdata.SERVER_TO_CLIENT, 0x02), + self.print_packets + ) + #Chat Message + ploader.reg_event_handler( + (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x02), + self.print_packets + ) + #Player List Item + ploader.reg_event_handler( + (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x38), + self.print_packets + ) - def print_packets(self, name, packet): - print(packet) \ No newline at end of file + def print_packets(self, name, packet): + print(packet) \ No newline at end of file diff --git a/examples/login.py b/examples/login.py index 137f2ec..9d81fd9 100644 --- a/examples/login.py +++ b/examples/login.py @@ -1,2 +1,2 @@ username = 'Put Your Username Here' -password = 'Put Your Password Here' \ No newline at end of file +password = 'Put Your Password Here' diff --git a/examples/settings.py b/examples/settings.py index 07f038b..73804fb 100644 --- a/examples/settings.py +++ b/examples/settings.py @@ -1,13 +1,12 @@ from spock.plugins.defaults import DefaultPlugins default_settings = { - 'username': 'Bot', #minecraft.net username or name for unauthenticated servers - 'password': '', #Password for account, ignored if not authenticated - 'authenticated': True, #Authenticate with authserver.mojang.com - 'bufsize': 4096, #Size of socket buffer - 'sock_quit': True, #Stop bot on socket error or hangup - 'sess_quit': True, #Stop bot on failed session login - 'thread_workers': 5, #Number of workers in the thread pool - 'plugins': DefaultPlugins, #Plugins - 'plugin_settings': {}, #Extra settings for plugins + 'username': 'Bot', #minecraft.net username or name for unauthenticated servers + 'password': '', #Password for account, ignored if not authenticated + 'authenticated': True, #Authenticate with authserver.mojang.com + 'bufsize': 4096, #Size of socket buffer + 'sock_quit': True, #Stop bot on socket error or hangup + 'sess_quit': True, #Stop bot on failed session login + 'plugins': DefaultPlugins, #Plugins + 'plugin_settings': {}, #Extra settings for plugins } \ No newline at end of file diff --git a/license.md b/license.md index 65b8f0f..6b614ca 100644 --- a/license.md +++ b/license.md @@ -1,4 +1,4 @@ -Copyright (C) 2013 Nick Gamberini +Copyright (C) 2015 Nick Gamberini Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,4 +16,4 @@ Copyright (C) 2013 Nick Gamberini AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. \ No newline at end of file + THE SOFTWARE. diff --git a/plugins/AntiAFK.py b/plugins/AntiAFK.py index 6c595a4..0020260 100644 --- a/plugins/AntiAFK.py +++ b/plugins/AntiAFK.py @@ -8,19 +8,19 @@ #Very bad and naive Anti-AFK plugin class AntiAFKPlugin: - def __init__(self, client, settings): - self.client = client - client.register_dispatch(self.avoid_afk, 0x03) - client.register_dispatch(self.revive, 0x08) + def __init__(self, client, settings): + self.client = client + client.register_dispatch(self.avoid_afk, 0x03) + client.register_dispatch(self.revive, 0x08) - def avoid_afk(self, packet): - msg = packet.data['text'].lower() - if ('afk plugin' in msg): - self.client.push(Packet('PLAY>Chat Message', data={ - "text": "Hello, I am Spock_Bot, this message is to avoid AFKGC" - }) - ) + def avoid_afk(self, packet): + msg = packet.data['text'].lower() + if ('afk plugin' in msg): + self.client.push(Packet(ident=0x03, data={ + "text": "Hello, I am Spock_Bot, this message is to avoid AFKGC" + }) + ) - def revive(self, packet): - if self.client.health['health']<=0: - self.client.push(Packet(ident='PLAY>Client Status', data={'action': 0})) + def revive(self, packet): + if self.client.health['health']<=0: + self.client.push(Packet(ident=0xCD, data={'payload': 1})) diff --git a/plugins/DebugPlugin.py b/plugins/DebugPlugin.py index 1a32865..ae10c44 100644 --- a/plugins/DebugPlugin.py +++ b/plugins/DebugPlugin.py @@ -4,33 +4,31 @@ from spock.mcmap import mapdata from spock.mcp import mcdata from spock.utils import pl_announce +import time class DebugPlugin: - def __init__(self, ploader, settings): - for packet in mcdata.hashed_structs: - ploader.reg_event_handler(packet, self.debug) - #ploader.reg_event_handler('tick', self.tick) - #ploader.reg_event_handler('w_map_chunk', self.map) - #ploader.reg_event_handler('w_block_update', self.block_update) + def __init__(self, ploader, settings): + #for packet in mcdata.hashed_structs: + # ploader.reg_event_handler(packet, self.debug) + #ploader.reg_event_handler('w_block_update', self.block_test) + #ploader.reg_event_handler('client_tick', self.timer_test) + ploader.reg_event_handler('cl_health_update', self.clinfo_test) + self.old_time = 0 - def debug(self, name, packet): - if packet.ident() == (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x26): - packet.data['data'] = b'' - #print(packet) - #print(packet) + def debug(self, name, packet): + if packet.ident() == (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x26): + packet.data['data'] = b'' + #print(packet) + #print(packet) - def block_update(self, name, data): - print('Block Updated!') - print( - 'Block is:', mapdata.blocks[data.id]['display_name']+',', - 'Biome:', mapdata.biomes[data.biome]['display_name'] - ) - print('Block Light:', str(data.block_light)+',', 'Sky Light:', data.sky_light) + def clinfo_test(self, event, data): + print('Health Update', data) + def block_test(self, event, block): + print('Block update at:', block['location']) + print('New block data:', block['block_data']) - def map(self, name, data): - print(data) - - def tick(self, name, data): - print('tick!') - print('Current threads:', threading.active_count()) \ No newline at end of file + def timer_test(self, _, __): + new_time = int(round(time.time() * 1000)) + print(new_time - self.old_time) + self.old_time = new_time diff --git a/plugins/EchoPacket.py b/plugins/EchoPacket.py index 7481e05..2710910 100644 --- a/plugins/EchoPacket.py +++ b/plugins/EchoPacket.py @@ -1,8 +1,12 @@ -from spock.mcp.mcdata import structs +from spock.mcp.mcdata import hashed_structs +from spock.mcp import mcdata class EchoPacketPlugin: - def __init__(self, ploader, settings): - ploader.reg_event_handler(list(structs.keys()), self.echopacket) + def __init__(self, ploader, settings): + for i in list(hashed_structs.keys()): + ploader.reg_event_handler(i, self.echopacket) - def echopacket(self, name, packet): - print(packet) \ No newline at end of file + def echopacket(self, name, packet): + #Dont print Chunk Data and Map Chunk Bulk + if packet.ident != (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x26) and packet.ident != (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x21) and packet.ident != (mcdata.PLAY_STATE, mcdata.CLIENT_TO_SERVER, 0x04): + print(packet) diff --git a/plugins/ReConnect.py b/plugins/ReConnect.py index abb0026..32c7a34 100644 --- a/plugins/ReConnect.py +++ b/plugins/ReConnect.py @@ -3,45 +3,44 @@ In the meantime, go look at plugins in spock.net.plugins for more up-to-date plugin examples """ -import threading from spock.mcp.mcpacket import Packet from spock.net.cflags import cflags from spock.net.timer import EventTimer #Will relentlessly try to reconnect to a server class ReConnectPlugin: - def __init__(self, client, settings): - self.client = client - self.lock = False - self.kill = False - self.delay = 0 - - client.register_handler(self.start_timer, cflags['SOCKET_ERR'], cflags['SOCKET_HUP']) - client.register_handler(self.stop, cflags['KILL_EVENT']) - client.register_dispatch(self.start_timer, 0xFF) - client.register_dispatch(self.grab_host, 0x02) - client.register_dispatch(self.reset_reconnect_time, 0x01) - - def start_timer(self, *args): - if not self.lock: - self.client.register_timer(EventTimer(self.delay, self.reconnect)) - self.lock = True - - def stop(self, *args): - self.kill = True - - def reconnect(self, *args): - if not self.kill: - if self.delay < 300: - self.delay += 30 - self.client.start_session(self.client.mc_username, self.client.mc_password) - self.client.login(self.host, self.port) - self.lock = False - - def reset_reconnect_time(self, *args): - self.delay = 0 - - #Grabs host and port on handshake - def grab_host(self, packet): - self.host = packet.data['host'] - self.port = packet.data['port'] + def __init__(self, client, settings): + self.client = client + self.lock = False + self.kill = False + self.delay = 0 + + client.register_handler(self.start_timer, cflags['SOCKET_ERR'], cflags['SOCKET_HUP']) + client.register_handler(self.stop, cflags['KILL_EVENT']) + client.register_dispatch(self.start_timer, 0xFF) + client.register_dispatch(self.grab_host, 0x02) + client.register_dispatch(self.reset_reconnect_time, 0x01) + + def start_timer(self, *args): + if not self.lock: + self.client.register_timer(EventTimer(self.delay, self.reconnect)) + self.lock = True + + def stop(self, *args): + self.kill = True + + def reconnect(self, *args): + if not self.kill: + if self.delay < 300: + self.delay += 30 + self.client.start_session(self.client.mc_username, self.client.mc_password) + self.client.login(self.host, self.port) + self.lock = False + + def reset_reconnect_time(self, *args): + self.delay = 0 + + #Grabs host and port on handshake + def grab_host(self, packet): + self.host = packet.data['host'] + self.port = packet.data['port'] diff --git a/setup.py b/setup.py index 9fc290b..28ba294 100644 --- a/setup.py +++ b/setup.py @@ -4,11 +4,11 @@ name='spock', version='1.2', packages=[ - 'spock', - 'spock.mcp', - 'spock.mcmap', - 'spock.plugins', - 'spock.plugins.core', - 'spock.plugins.helpers', + 'spock', + 'spock.mcp', + 'spock.mcmap', + 'spock.plugins', + 'spock.plugins.core', + 'spock.plugins.helpers', ], ) diff --git a/spock/client.py b/spock/client.py index 5e916df..95a6f57 100644 --- a/spock/client.py +++ b/spock/client.py @@ -1,68 +1,68 @@ from spock.plugins import DefaultPlugins class PluginLoader: - def __init__(self, client, settings): - self.plugins = settings['plugins'] - del settings['plugins'] - self.plugin_settings = settings['plugin_settings'] - del settings['plugin_settings'] - self.announce = {} - self.extensions = { - 'Client': client, - 'Settings': settings - } + def __init__(self, client, settings): + self.plugins = settings['plugins'] + del settings['plugins'] + self.plugin_settings = settings['plugin_settings'] + del settings['plugin_settings'] + self.announce = {} + self.extensions = { + 'Client': client, + 'Settings': settings + } - for plugin in self.plugins: - if hasattr(plugin, 'pl_announce'): - for ident in plugin.pl_announce: - self.announce[ident] = plugin - # Make an attempt at providing the reg_event_handler API - # But we can't guarantee it will be there (Ha!) - event = self.requires('Event') - self.reg_event_handler = event.reg_event_handler if event else None - while self.plugins: - plugin = self.plugins.pop() - plugin(self, self.plugin_settings.get(plugin, None)) + for plugin in self.plugins: + if hasattr(plugin, 'pl_announce'): + for ident in plugin.pl_announce: + self.announce[ident] = plugin + # Make an attempt at providing the reg_event_handler API + # But we can't guarantee it will be there (Ha!) + event = self.requires('Event') + self.reg_event_handler = event.reg_event_handler if event else None + while self.plugins: + plugin = self.plugins.pop() + if plugin.__name__ not in self.extensions: + obj = plugin(self, self.plugin_settings.get(plugin, None)) + self.extensions[obj.__class__.__name__] = obj - def requires(self, ident): - if ident not in self.extensions: - if ident in self.announce: - plugin = self.announce[ident] - self.plugins.remove(plugin) - plugin(self, self.plugin_settings.get(plugin, None)) - else: - return None - return self.extensions[ident] + def requires(self, ident): + if ident not in self.extensions: + if ident in self.announce: + plugin = self.announce[ident] + self.plugins.remove(plugin) + plugin(self, self.plugin_settings.get(plugin, None)) + else: + return None + return self.extensions[ident] - def provides(self, ident, obj): - self.extensions[ident] = obj + def provides(self, ident, obj): + self.extensions[ident] = obj #2 values = Attribute&Setting name, default value #3 values = Attribute name, setting name, default value default_settings = [ - ('plugins', DefaultPlugins), - ('plugin_settings', {}), - ('mc_username', 'username', 'Bot'), - ('mc_password', 'password', ''), - ('authenticated', True), - ('thread_workers', 5), - ('bufsize', 4096), - ('sock_quit', True), - ('sess_quit', True), - ('packet_trace', False) + ('plugins', DefaultPlugins), + ('plugin_settings', {}), + ('mc_username', 'username', 'Bot'), + ('mc_password', 'password', ''), + ('authenticated', True), + ('bufsize', 4096), + ('sock_quit', True), + ('sess_quit', True), ] for index, setting in enumerate(default_settings): - if len(setting) == 2: - default_settings[index] = (setting[0], setting[0], setting[1]) + if len(setting) == 2: + default_settings[index] = (setting[0], setting[0], setting[1]) class Client: - def __init__(self, **kwargs): - #Grab some settings - settings = kwargs.get('settings', {}) - final_settings = {} - for setting in default_settings: - val = kwargs.get(setting[1], settings.get(setting[1], setting[2])) - final_settings[setting[0]] = val + def __init__(self, **kwargs): + #Grab some settings + settings = kwargs.get('settings', {}) + final_settings = {} + for setting in default_settings: + val = kwargs.get(setting[1], settings.get(setting[1], setting[2])) + final_settings[setting[0]] = val - PluginLoader(self, final_settings) \ No newline at end of file + PluginLoader(self, final_settings) \ No newline at end of file diff --git a/spock/mcmap/mapdata.py b/spock/mcmap/mapdata.py index 455a2ec..abca538 100644 --- a/spock/mcmap/mapdata.py +++ b/spock/mcmap/mapdata.py @@ -1,1537 +1,1722 @@ #Shamelessly stolen/adapted from Mineflayer biomes = { - 0: { - 'display_name': 'Ocean', - 'temperature': 0.5, - }, - 1: { - 'display_name': 'Plains', - 'temperature': 0.8, - }, - 2: { - 'display_name': 'Desert', - 'temperature': 2, - }, - 3: { - 'display_name': 'Extreme Hills', - 'temperature': 0.2, - }, - 4: { - 'display_name': 'Forest', - 'temperature': 0.7, - }, - 5: { - 'display_name': 'Taiga', - 'temperature': 0.05, - }, - 6: { - 'display_name': 'Swampland', - 'temperature': 0.8, - }, - 7: { - 'temperature': 0.5, - }, - 8: { - 'display_name': 'Hell', - 'temperature': 2, - }, - 9: { - 'display_name': 'Sky', - 'temperature': 0.5, - }, - 10: { + 0: { + 'display_name': 'Ocean', + 'temperature': 0.5, + }, + 1: { + 'display_name': 'Plains', + 'temperature': 0.8, + }, + 2: { + 'display_name': 'Desert', + 'temperature': 2, + }, + 3: { + 'display_name': 'Extreme Hills', + 'temperature': 0.2, + }, + 4: { + 'display_name': 'Forest', + 'temperature': 0.7, + }, + 5: { + 'display_name': 'Taiga', + 'temperature': 0.05, + }, + 6: { + 'display_name': 'Swampland', + 'temperature': 0.8, + }, + 7: { + 'temperature': 0.5, + }, + 8: { + 'display_name': 'Hell', + 'temperature': 2, + }, + 9: { + 'display_name': 'Sky', + 'temperature': 0.5, + }, + 10: { - 'display_name': 'Frozen Ocean', - 'temperature': 0, - }, - 11: { + 'display_name': 'Frozen Ocean', + 'temperature': 0, + }, + 11: { - 'display_name': 'Frozen River', - 'temperature': 0, - }, - 12: { + 'display_name': 'Frozen River', + 'temperature': 0, + }, + 12: { - 'display_name': 'Ice Plains', - 'temperature': 0, - }, - 13: { + 'display_name': 'Ice Plains', + 'temperature': 0, + }, + 13: { - 'display_name': 'Ice Mountains', - 'temperature': 0, - }, - 14: { + 'display_name': 'Ice Mountains', + 'temperature': 0, + }, + 14: { - 'display_name': 'Mushroom Island', - 'temperature': 0.9, - }, - 15: { + 'display_name': 'Mushroom Island', + 'temperature': 0.9, + }, + 15: { - 'display_name': 'Mushroom Island Shore', - 'temperature': 0.9, - }, - 16: { + 'display_name': 'Mushroom Island Shore', + 'temperature': 0.9, + }, + 16: { - 'display_name': 'Beach', - 'temperature': 0.8, - }, - 17: { + 'display_name': 'Beach', + 'temperature': 0.8, + }, + 17: { - 'display_name': 'Desert Hills', - 'temperature': 2, - }, - 18: { + 'display_name': 'Desert Hills', + 'temperature': 2, + }, + 18: { - 'display_name': 'Forest Hills', - 'temperature': 0.7, - }, - 19: { + 'display_name': 'Forest Hills', + 'temperature': 0.7, + }, + 19: { - 'display_name': 'Taiga Hills', - 'temperature': 0.05, - }, - 20: { + 'display_name': 'Taiga Hills', + 'temperature': 0.05, + }, + 20: { - 'display_name': 'Extreme Hills Edge', - 'temperature': 0.2, - }, - 21: { + 'display_name': 'Extreme Hills Edge', + 'temperature': 0.2, + }, + 21: { - 'display_name': 'Jungle', - 'temperature': 1.2, - }, - 22: { + 'display_name': 'Jungle', + 'temperature': 1.2, + }, + 22: { - 'display_name': 'Jungle Hills', - 'temperature': 1.2, - }, - 129: { - 'display_name': 'Sunflower Plains', - 'temperature': 0.8, - }, + 'display_name': 'Jungle Hills', + 'temperature': 1.2, + }, + 23: { + + 'display_name': 'Jungle Edge', + 'temperature': 0.95, + }, + 24: { + + 'display_name': 'Deep Ocean', + 'temperature:': 0.5, + }, + 25: { + + 'display_name': 'Stone Beach', + 'temperature:': 0.2, + }, + 26: { + + 'display_name': 'Cold Beach', + 'temperature:': 0, + }, + 27: { + + 'display_name': 'Birch Forest', + 'temperature:': 0.6, + }, + 28: { + + 'display_name': 'Birch Forest Hills', + 'temperature:': 0.6, + }, + 29: { + + 'display_name': 'Roofed Forest', + 'temperature:': 0.7, + }, + 30: { + + 'display_name': 'Cold Taiga', + 'temperature:': 0, + }, + 31: { + + 'display_name': 'Cold Taiga Hills', + 'temperature:': 0, + }, + 32: { + + 'display_name': 'Mega Taiga', + 'temperature:': 0.3, + }, + 33: { + + 'display_name': 'Mega Taiga Hills', + 'temperature:': 0.3, + }, + 34: { + + 'display_name': 'Extreme Hills+', + 'temperature:': 0.2, + }, + 35: { + + 'display_name': 'Savanna', + 'temperature:': 1.0, + }, + 36: { + + 'display_name': 'Savanna Plateau', + 'temperature:': 1.0, + }, + 37: { + + 'display_name': 'Mesa', + 'temperature:': 1.0, + }, + 38: { + + 'display_name': 'Mesa Plateau F', + 'temperature:': 1.0, + }, + 39: { + + 'display_name': 'Mesa Plateau', + 'temperature:': 1.0, + }, + 129: { + 'display_name': 'Sunflower Plains', + 'temperature': 0.8, + }, + 130: { + + 'display_name': 'Desert M', + 'temperature:': 2, + }, + 131: { + + 'display_name': 'Extreme Hills M', + 'temperature:': 0.2, + }, + 132: { + + 'display_name': 'Flower Forest', + 'temperature:': 0.7, + }, + 133: { + + 'display_name': 'Taiga M', + 'temperature:': 0.25, + }, + 134: { + + 'display_name': 'Swampland M', + 'temperature:': 0.8, + }, + 140: { + + 'display_name': 'Ice Plains Spikes', + 'temperature:': 0, + }, + 149: { + + 'display_name': 'Jungle M', + 'temperature:': 0.95, + }, + 151: { + + 'display_name': 'Jungle Edge M', + 'temperature:': 0.95, + }, + 155: { + + 'display_name': 'Birch Forest M', + 'temperature:': 0.6, + }, + 156: { + + 'display_name': 'Birch Forest Hills M', + 'temperature:': 0.6, + }, + 157: { + + 'display_name': 'Roofed Forest M', + 'temperature:': 0.7, + }, + 158: { + + 'display_name': 'Cold Taiga M', + 'temperature:': 0, + }, + 160: { + + 'display_name': 'Mega Spruce Taiga', + 'temperature:': 0.25, + }, + 161: { + + 'display_name': 'Mega Spruce Taiga Hills', + 'temperature:': 0.25, + }, + 162: { + + 'display_name': 'Extreme Hills+ M', + 'temperature:': 0.2, + }, + 163: { + + 'display_name': 'Savanna M', + 'temperature:': 1.0, + }, + 164: { + + 'display_name': 'Savanna Plateau M', + 'temperature:': 1.0, + }, + 165: { + + 'display_name': 'Mesa (Bryce)', + 'temperature:': 1.0, + }, + 166: { + + 'display_name': 'Mesa Plateau F M', + 'temperature:': 1.0, + }, + 167: { + + 'display_name': 'Mesa Plateau M', + 'temperature:': 1.0, + }, } blocks = { - 0: { - 'display_name': 'Air', - 'name': 'air', - 'hardness': 0, - 'stack_size': None, - 'diggable': False, - 'bounding_box': 'empty' - }, - 1: { - 'display_name': 'Stone', - 'name': 'stone', - 'hardness': 1.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 2: { - 'display_name': 'Grass Block', - 'name': 'grass', - 'hardness': 0.6, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 3: { - 'display_name': 'Dirt', - 'name': 'dirt', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 4: { - 'display_name': 'Cobblestone', - 'name': 'stonebrick', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 5: { - 'display_name': 'Wooden Planks', - 'name': 'wood', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 6: { - 'display_name': 'Sapling', - 'name': 'sapling', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 7: { - 'display_name': 'Bedrock', - 'name': 'bedrock', - 'hardness': None, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'block' - }, - 8: { - 'display_name': 'Water', - 'name': 'water', - 'hardness': 100, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'empty' - }, - 9: { - 'display_name': 'Stationary Water', - 'name': 'waterStationary', - 'hardness': 100, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'empty' - }, - 10: { - 'display_name': 'Lava', - 'name': 'lava', - 'hardness': 0, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'empty' - }, - 11: { - 'display_name': 'Stationary Lava', - 'name': 'lavaStationary', - 'hardness': 100, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'empty' - }, - 12: { - 'display_name': 'Sand', - 'name': 'sand', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 13: { - 'display_name': 'Gravel', - 'name': 'gravel', - 'hardness': 0.6, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 14: { - 'display_name': 'Gold Ore', - 'name': 'oreGold', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 15: { - 'display_name': 'Iron Ore', - 'name': 'oreIron', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (274, 257, 278), - }, - 16: { - 'display_name': 'Coal Ore', - 'name': 'oreCoal', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 17: { - 'display_name': 'Wood', - 'name': 'log', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 18: { - 'display_name': 'Leaves', - 'name': 'leaves', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'leaves' - }, - 19: { - 'display_name': 'Sponge', - 'name': 'sponge', - 'hardness': 0.6, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 20: { - 'display_name': 'Glass', - 'name': 'glass', - 'hardness': 0.3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 21: { - 'display_name': 'Lapis Lazuli Ore', - 'name': 'oreLapis', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (274, 257, 278), - }, - 22: { - 'display_name': 'Lapis Lazuli Block', - 'name': 'blockLapis', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (274, 257, 278), - }, - 23: { - 'display_name': 'Dispenser', - 'name': 'dispenser', - 'hardness': 3.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 24: { - 'display_name': 'Sandstone', - 'name': 'sandStone', - 'hardness': 0.8, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 25: { - 'display_name': 'Note Block', - 'name': 'musicBlock', - 'hardness': 0.8, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 26: { - 'display_name': 'Bed', - 'name': 'bed', - 'hardness': 0.2, - 'stack_size': 1, - 'diggable': True, - 'bounding_box': 'block' - }, - 27: { - 'display_name': 'Powered Rail', - 'name': 'goldenRail', - 'hardness': 0.7, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock' - }, - 28: { - 'display_name': 'Detector Rail', - 'name': 'detectorRail', - 'hardness': 0.7, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock' - }, - 29: { - 'display_name': 'Sticky Piston', - 'name': 'pistonStickyBase', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 30: { - 'display_name': 'Cobweb', - 'name': 'web', - 'hardness': 4, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'web', - 'harvest_tools': (359, 267, 268, 272, 276, 283), - }, - 31: { - 'display_name': 'Grass', - 'name': 'tallgrass', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 32: { - 'display_name': 'Dead Bush', - 'name': 'deadbush', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 33: { - 'display_name': 'Piston', - 'name': 'pistonBase', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 34: { - 'name': 'pistonExtension', - 'display_name': 'Piston Extension', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 35: { - 'display_name': 'Wool', - 'name': 'cloth', - 'hardness': 0.8, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wool' - }, - 36: { - 'name': 'blockMovedByPiston', - 'display_name': 'Block Moved by Piston', - 'hardness': 0, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'block' - }, - 37: { - 'display_name': 'Flower', - 'name': 'flower', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 38: { - 'display_name': 'Rose', - 'name': 'rose', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 39: { - 'display_name': 'Brown Mushroom', - 'name': 'mushroomBrown', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 40: { - 'display_name': 'Red Mushroom', - 'name': 'mushroomRed', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 41: { - 'display_name': 'Block of Gold', - 'name': 'blockGold', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 42: { - 'display_name': 'Block of Iron', - 'name': 'blockIron', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (274, 257, 278), - }, - 43: { - 'display_name': 'Double Stone Slab', - 'name': 'stoneSlabDouble', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 44: { - 'display_name': 'Stone Slab', - 'name': 'stoneSlab', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 45: { - 'display_name': 'Bricks', - 'name': 'brick', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 46: { - 'display_name': 'TNT', - 'name': 'tnt', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 47: { - 'display_name': 'Bookshelf', - 'name': 'bookshelf', - 'hardness': 1.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 48: { - 'display_name': 'Moss Stone', - 'name': 'stoneMoss', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 49: { - 'display_name': 'Obsidian', - 'name': 'obsidian', - 'hardness': 50, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (278,), - }, - 50: { - 'display_name': 'Torch', - 'name': 'torch', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 51: { - 'display_name': 'Fire', - 'name': 'fire', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 52: { - 'display_name': 'Monster Spawner', - 'name': 'mobSpawner', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 53: { - 'display_name': 'Wooden Stairs', - 'name': 'stairsWood', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 54: { - 'display_name': 'Chest', - 'name': 'chest', - 'hardness': 2.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 55: { - 'display_name': 'Redstone Dust', - 'name': 'redstoneDust', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 56: { - 'display_name': 'Diamond Ore', - 'name': 'oreDiamond', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 57: { - 'display_name': 'Block of Diamond', - 'name': 'blockDiamond', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 58: { - 'display_name': 'Crafting Table', - 'name': 'workbench', - 'hardness': 2.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 59: { - 'display_name': 'Crops', - 'name': 'crops', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 60: { - 'display_name': 'Farmland', - 'name': 'farmland', - 'hardness': 0.6, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 61: { - 'display_name': 'Furnace', - 'name': 'furnace', - 'hardness': 3.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 62: { - 'display_name': 'Burning Furnace', - 'name': 'furnaceBurning', - 'hardness': 3.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 63: { - 'display_name': 'Sign Post', - 'name': 'signPost', - 'hardness': 1, - 'stack_size': 1, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'wood' - }, - 64: { - 'display_name': 'Wooden Door', - 'name': 'doorWood', - 'hardness': 3, - 'stack_size': 1, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 65: { - 'display_name': 'Ladder', - 'name': 'ladder', - 'hardness': 0.4, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 66: { - 'display_name': 'Rail', - 'name': 'rail', - 'hardness': 0.7, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock' - }, - 67: { - 'display_name': 'Cobblestone Stairs', - 'name': 'stairsStone', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 68: { - 'display_name': 'Wall Sign', - 'name': 'signWall', - 'hardness': 1, - 'stack_size': 1, - 'diggable': True, - 'bounding_box': 'empty' - }, - 69: { - 'display_name': 'Lever', - 'name': 'lever', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 70: { - 'display_name': 'Stone Pressure Plate', - 'name': 'stonePressurePlate', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 71: { - 'display_name': 'Iron Door', - 'name': 'doorIron', - 'hardness': 5, - 'stack_size': 1, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 72: { - 'display_name': 'Wooden Pressure Plate', - 'name': 'woodPressurePlate', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'wood' - }, - 73: { - 'display_name': 'Redstone Ore', - 'name': 'oreRedstone', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 74: { - 'display_name': 'Glowing Redstone Ore', - 'name': 'oreRedstoneGlowing', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 75: { - 'display_name': 'Redstone Torch (Inactive)', - 'name': 'notGateInactive', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 76: { - 'display_name': 'Redstone Torch (Active)', - 'name': 'notGateActive', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 77: { - 'display_name': 'Stone Button', - 'name': 'buttonStone', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 78: { - 'display_name': 'Snow', - 'name': 'snow', - 'hardness': 0.1, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'dirt', - 'harvest_tools': (269, 273, 256, 277, 284), - }, - 79: { - 'display_name': 'Ice', - 'name': 'ice', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock' - }, - 80: { - 'display_name': 'Snow Block', - 'name': 'snowBlock', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt', - 'harvest_tools': (269, 273, 256, 277, 284), - }, - 81: { - 'display_name': 'Cactus', - 'name': 'cactus', - 'hardness': 0.4, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 82: { - 'display_name': 'Clay', - 'name': 'clay', - 'hardness': 0.6, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 83: { - 'display_name': 'Sugar cane', - 'name': 'reeds', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 84: { - 'display_name': 'Jukebox', - 'name': 'jukebox', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 85: { - 'display_name': 'Fence', - 'name': 'fence', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 86: { - 'display_name': 'Pumpkin', - 'name': 'pumpkin', - 'hardness': 1, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'plant' - }, - 87: { - 'display_name': 'Netherrack', - 'name': 'hellrock', - 'hardness': 0.4, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 88: { - 'display_name': 'Soul Sand', - 'name': 'hellsand', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 89: { - 'display_name': 'Glowstone', - 'name': 'lightgem', - 'hardness': 0.3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 90: { - 'display_name': 'Portal', - 'name': 'portal', - 'hardness': None, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'empty' - }, - 91: { - 'display_name': 'Jack \'o\' Lantern', - 'name': 'litpumpkin', - 'hardness': 1, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'plant' - }, - 92: { - 'display_name': 'Cake', - 'name': 'cake', - 'hardness': 0.5, - 'stack_size': 1, - 'diggable': True, - 'bounding_box': 'block' - }, - 93: { - 'display_name': 'Redstone Repeater (Inactive)', - 'name': 'redstoneRepeaterInactive', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 94: { - 'display_name': 'Redstone Repeater (Active)', - 'name': 'redstoneRepeaterActive', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 95: { - 'display_name': 'Locked chest', - 'name': 'lockedchest', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 96: { - 'display_name': 'Trapdoor', - 'name': 'trapdoor', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 97: { - 'display_name': 'Monster Egg', - 'name': 'monsterStoneEgg', - 'hardness': 0.75, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 98: { - 'display_name': 'Stone Brick', - 'name': 'stonebricksmooth', - 'hardness': 1.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 99: { - 'display_name': 'Huge Brown Mushroom', - 'name': 'mushroomHugeBrown', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 100: { - 'display_name': 'Huge Red Mushroom', - 'name': 'mushroomHugeRed', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 101: { - 'display_name': 'Iron Bars', - 'name': 'fenceIron', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 102: { - 'display_name': 'Glass Pane', - 'name': 'thinGlass', - 'hardness': 0.3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 103: { - 'display_name': 'Melon', - 'name': 'melon', - 'hardness': 1, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'melon' - }, - 104: { - 'display_name': 'Pumpkin Stem', - 'name': 'pumpkinStem', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 105: { - 'display_name': 'Melon Stem', - 'name': 'melonStem', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 106: { - 'display_name': 'Vines', - 'name': 'vine', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'plant' - }, - 107: { - 'display_name': 'Fence Gate', - 'name': 'fenceGate', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 108: { - 'display_name': 'Brick Stairs', - 'name': 'stairsBrick', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 109: { - 'display_name': 'Stone Brick Stairs', - 'name': 'stairsStoneBrickSmooth', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 110: { - 'display_name': 'Mycelium', - 'name': 'mycel', - 'hardness': 0.6, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'dirt' - }, - 111: { - 'display_name': 'Lily Pad', - 'name': 'waterlily', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 112: { - 'display_name': 'Nether Brick', - 'name': 'netherBrick', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 113: { - 'display_name': 'Nether Brick Fence', - 'name': 'netherFence', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 114: { - 'display_name': 'Nether Brick Stairs', - 'name': 'stairsNetherBrick', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 115: { - 'display_name': 'Nether Wart', - 'name': 'netherStalk', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 116: { - 'display_name': 'Enchantment Table', - 'name': 'enchantmentTable', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 117: { - 'display_name': 'Brewing Stand', - 'name': 'brewingStand', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 118: { - 'display_name': 'Cauldron', - 'name': 'cauldron', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 119: { - 'name': 'endPortal', - 'display_name': 'End Portal', - 'hardness': None, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'empty' - }, - 120: { - 'display_name': 'End Portal Frame', - 'name': 'endPortalFrame', - 'hardness': None, - 'stack_size': 64, - 'diggable': False, - 'bounding_box': 'block' - }, - 121: { - 'display_name': 'End Stone', - 'name': 'whiteStone', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 122: { - 'display_name': 'Dragon Egg', - 'name': 'dragonEgg', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 123: { - 'display_name': 'Redstone Lamp (Inactive)', - 'name': 'redstoneLightInactive', - 'hardness': 0.3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 124: { - 'display_name': 'Redstone Lamp (Active)', - 'name': 'redstoneLightActive', - 'hardness': 0.3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 125: { - 'display_name': 'Wooden Double Slab', - 'name': 'woodSlabDouble', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 126: { - 'display_name': 'Wooden Slab', - 'name': 'woodSlab', - 'hardness': 2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 127: { - 'display_name': 'Cocoa Pod', - 'name': 'cocoa', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'plant' - }, - 128: { - 'display_name': 'Sandstone Stairs', - 'name': 'stairsSandStone', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 129: { - 'display_name': 'Emerald Ore', - 'name': 'oreEmerald', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 130: { - 'display_name': 'Ender Chest', - 'name': 'enderChest', - 'hardness': 22.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 131: { - 'display_name': 'Tripwire Hook', - 'name': 'tripWireSource', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 132: { - 'display_name': 'Tripwire', - 'name': 'tripWire', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 133: { - 'display_name': 'Block of Emerald', - 'name': 'blockEmerald', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (257, 278), - }, - 134: { - 'display_name': 'Spruce Wood Stairs', - 'name': 'stairsWoodSpruce', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 135: { - 'display_name': 'Birch Wood Stairs', - 'name': 'stairsWoodBirch', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 136: { - 'display_name': 'Jungle Wood Stairs', - 'name': 'stairsWoodJungle', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 137: { - 'display_name': 'Command Block', - 'name': 'commandBlock', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 138: { - 'display_name': 'Beacon', - 'name': 'beacon', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 139: { - 'display_name': 'Cobblestone Wall', - 'name': 'cobbleWall', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 140: { - 'display_name': 'Flower Pot', - 'name': 'flowerPot', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 141: { - 'display_name': 'Carrots', - 'name': 'carrots', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 142: { - 'display_name': 'Potatoes', - 'name': 'potatoes', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 143: { - 'display_name': 'Wooden Button', - 'name': 'buttonWood', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty' - }, - 144: { - 'display_name': 'Mob Head', - 'name': 'skull', - 'hardness': 1, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 145: { - 'display_name': 'Anvil', - 'name': 'anvil', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 146: { - 'display_name': 'Trapped Chest', - 'name': 'trappedChest', - 'hardness': 2.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 147: { - 'display_name': 'Weighted Pressure plate (Light)', - 'name': 'pressurePlateWeightedLight', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 148: { - 'display_name': 'Weighted Pressure plate (Heavy)', - 'name': 'pressurePlateWeightedHeavy', - 'hardness': 0.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 149: { - 'display_name': 'Redstone Comparator (Inactive)', - 'name': 'redstoneComparatorInactive', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 150: { - 'display_name': 'Redstone Comparator (Active)', - 'name': 'redstoneComparatorActive', - 'hardness': 0, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block' - }, - 151: { - 'display_name': 'Daylight Sensor', - 'name': 'daylightSensor', - 'hardness': 0.2, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'wood' - }, - 152: { - 'display_name': 'Block of Redstone', - 'name': 'redstoneBlock', - 'hardness': 5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 153: { - 'display_name': 'Nether Quartz Ore', - 'name': 'netherQuartzOre', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 154: { - 'display_name': 'Hopper', - 'name': 'hopper', - 'hardness': 3, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 155: { - 'display_name': 'Block of Quartz', - 'name': 'quartzBlock', - 'hardness': 0.8, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 156: { - 'display_name': 'Quartz Stairs', - 'name': 'quartzStairs', - 'hardness': 0.8, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, - 157: { - 'display_name': 'Activator Rail', - 'name': 'activatorRail', - 'hardness': 0.7, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'empty', - 'material': 'rock' - }, - 158: { - 'display_name': 'Dropper', - 'name': 'dropper', - 'hardness': 3.5, - 'stack_size': 64, - 'diggable': True, - 'bounding_box': 'block', - 'material': 'rock', - 'harvest_tools': (270, 274, 257, 278, 285), - }, + 0: { + 'display_name': 'Air', + 'name': 'air', + 'hardness': 0, + 'stack_size': None, + 'diggable': False, + 'bounding_box': 'empty' + }, + 1: { + 'display_name': 'Stone', + 'name': 'stone', + 'hardness': 1.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 2: { + 'display_name': 'Grass Block', + 'name': 'grass', + 'hardness': 0.6, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 3: { + 'display_name': 'Dirt', + 'name': 'dirt', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 4: { + 'display_name': 'Cobblestone', + 'name': 'stonebrick', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 5: { + 'display_name': 'Wooden Planks', + 'name': 'wood', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 6: { + 'display_name': 'Sapling', + 'name': 'sapling', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 7: { + 'display_name': 'Bedrock', + 'name': 'bedrock', + 'hardness': None, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'block' + }, + 8: { + 'display_name': 'Water', + 'name': 'water', + 'hardness': 100, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'empty' + }, + 9: { + 'display_name': 'Stationary Water', + 'name': 'waterStationary', + 'hardness': 100, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'empty' + }, + 10: { + 'display_name': 'Lava', + 'name': 'lava', + 'hardness': 0, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'empty' + }, + 11: { + 'display_name': 'Stationary Lava', + 'name': 'lavaStationary', + 'hardness': 100, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'empty' + }, + 12: { + 'display_name': 'Sand', + 'name': 'sand', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 13: { + 'display_name': 'Gravel', + 'name': 'gravel', + 'hardness': 0.6, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 14: { + 'display_name': 'Gold Ore', + 'name': 'oreGold', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 15: { + 'display_name': 'Iron Ore', + 'name': 'oreIron', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (274, 257, 278), + }, + 16: { + 'display_name': 'Coal Ore', + 'name': 'oreCoal', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 17: { + 'display_name': 'Wood', + 'name': 'log', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 18: { + 'display_name': 'Leaves', + 'name': 'leaves', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'leaves' + }, + 19: { + 'display_name': 'Sponge', + 'name': 'sponge', + 'hardness': 0.6, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 20: { + 'display_name': 'Glass', + 'name': 'glass', + 'hardness': 0.3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 21: { + 'display_name': 'Lapis Lazuli Ore', + 'name': 'oreLapis', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (274, 257, 278), + }, + 22: { + 'display_name': 'Lapis Lazuli Block', + 'name': 'blockLapis', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (274, 257, 278), + }, + 23: { + 'display_name': 'Dispenser', + 'name': 'dispenser', + 'hardness': 3.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 24: { + 'display_name': 'Sandstone', + 'name': 'sandStone', + 'hardness': 0.8, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 25: { + 'display_name': 'Note Block', + 'name': 'musicBlock', + 'hardness': 0.8, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 26: { + 'display_name': 'Bed', + 'name': 'bed', + 'hardness': 0.2, + 'stack_size': 1, + 'diggable': True, + 'bounding_box': 'block' + }, + 27: { + 'display_name': 'Powered Rail', + 'name': 'goldenRail', + 'hardness': 0.7, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock' + }, + 28: { + 'display_name': 'Detector Rail', + 'name': 'detectorRail', + 'hardness': 0.7, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock' + }, + 29: { + 'display_name': 'Sticky Piston', + 'name': 'pistonStickyBase', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 30: { + 'display_name': 'Cobweb', + 'name': 'web', + 'hardness': 4, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'web', + 'harvest_tools': (359, 267, 268, 272, 276, 283), + }, + 31: { + 'display_name': 'Grass', + 'name': 'tallgrass', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 32: { + 'display_name': 'Dead Bush', + 'name': 'deadbush', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 33: { + 'display_name': 'Piston', + 'name': 'pistonBase', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 34: { + 'name': 'pistonExtension', + 'display_name': 'Piston Extension', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 35: { + 'display_name': 'Wool', + 'name': 'cloth', + 'hardness': 0.8, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wool' + }, + 36: { + 'name': 'blockMovedByPiston', + 'display_name': 'Block Moved by Piston', + 'hardness': 0, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'block' + }, + 37: { + 'display_name': 'Flower', + 'name': 'flower', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 38: { + 'display_name': 'Rose', + 'name': 'rose', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 39: { + 'display_name': 'Brown Mushroom', + 'name': 'mushroomBrown', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 40: { + 'display_name': 'Red Mushroom', + 'name': 'mushroomRed', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 41: { + 'display_name': 'Block of Gold', + 'name': 'blockGold', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 42: { + 'display_name': 'Block of Iron', + 'name': 'blockIron', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (274, 257, 278), + }, + 43: { + 'display_name': 'Double Stone Slab', + 'name': 'stoneSlabDouble', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 44: { + 'display_name': 'Stone Slab', + 'name': 'stoneSlab', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 45: { + 'display_name': 'Bricks', + 'name': 'brick', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 46: { + 'display_name': 'TNT', + 'name': 'tnt', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 47: { + 'display_name': 'Bookshelf', + 'name': 'bookshelf', + 'hardness': 1.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 48: { + 'display_name': 'Moss Stone', + 'name': 'stoneMoss', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 49: { + 'display_name': 'Obsidian', + 'name': 'obsidian', + 'hardness': 50, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (278,), + }, + 50: { + 'display_name': 'Torch', + 'name': 'torch', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 51: { + 'display_name': 'Fire', + 'name': 'fire', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 52: { + 'display_name': 'Monster Spawner', + 'name': 'mobSpawner', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 53: { + 'display_name': 'Wooden Stairs', + 'name': 'stairsWood', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 54: { + 'display_name': 'Chest', + 'name': 'chest', + 'hardness': 2.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 55: { + 'display_name': 'Redstone Dust', + 'name': 'redstoneDust', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 56: { + 'display_name': 'Diamond Ore', + 'name': 'oreDiamond', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 57: { + 'display_name': 'Block of Diamond', + 'name': 'blockDiamond', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 58: { + 'display_name': 'Crafting Table', + 'name': 'workbench', + 'hardness': 2.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 59: { + 'display_name': 'Crops', + 'name': 'crops', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 60: { + 'display_name': 'Farmland', + 'name': 'farmland', + 'hardness': 0.6, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 61: { + 'display_name': 'Furnace', + 'name': 'furnace', + 'hardness': 3.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 62: { + 'display_name': 'Burning Furnace', + 'name': 'furnaceBurning', + 'hardness': 3.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 63: { + 'display_name': 'Sign Post', + 'name': 'signPost', + 'hardness': 1, + 'stack_size': 1, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'wood' + }, + 64: { + 'display_name': 'Wooden Door', + 'name': 'doorWood', + 'hardness': 3, + 'stack_size': 1, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 65: { + 'display_name': 'Ladder', + 'name': 'ladder', + 'hardness': 0.4, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 66: { + 'display_name': 'Rail', + 'name': 'rail', + 'hardness': 0.7, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock' + }, + 67: { + 'display_name': 'Cobblestone Stairs', + 'name': 'stairsStone', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 68: { + 'display_name': 'Wall Sign', + 'name': 'signWall', + 'hardness': 1, + 'stack_size': 1, + 'diggable': True, + 'bounding_box': 'empty' + }, + 69: { + 'display_name': 'Lever', + 'name': 'lever', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 70: { + 'display_name': 'Stone Pressure Plate', + 'name': 'stonePressurePlate', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 71: { + 'display_name': 'Iron Door', + 'name': 'doorIron', + 'hardness': 5, + 'stack_size': 1, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 72: { + 'display_name': 'Wooden Pressure Plate', + 'name': 'woodPressurePlate', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'wood' + }, + 73: { + 'display_name': 'Redstone Ore', + 'name': 'oreRedstone', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 74: { + 'display_name': 'Glowing Redstone Ore', + 'name': 'oreRedstoneGlowing', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 75: { + 'display_name': 'Redstone Torch (Inactive)', + 'name': 'notGateInactive', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 76: { + 'display_name': 'Redstone Torch (Active)', + 'name': 'notGateActive', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 77: { + 'display_name': 'Stone Button', + 'name': 'buttonStone', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 78: { + 'display_name': 'Snow', + 'name': 'snow', + 'hardness': 0.1, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'dirt', + 'harvest_tools': (269, 273, 256, 277, 284), + }, + 79: { + 'display_name': 'Ice', + 'name': 'ice', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock' + }, + 80: { + 'display_name': 'Snow Block', + 'name': 'snowBlock', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt', + 'harvest_tools': (269, 273, 256, 277, 284), + }, + 81: { + 'display_name': 'Cactus', + 'name': 'cactus', + 'hardness': 0.4, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 82: { + 'display_name': 'Clay', + 'name': 'clay', + 'hardness': 0.6, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 83: { + 'display_name': 'Sugar cane', + 'name': 'reeds', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 84: { + 'display_name': 'Jukebox', + 'name': 'jukebox', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 85: { + 'display_name': 'Fence', + 'name': 'fence', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 86: { + 'display_name': 'Pumpkin', + 'name': 'pumpkin', + 'hardness': 1, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'plant' + }, + 87: { + 'display_name': 'Netherrack', + 'name': 'hellrock', + 'hardness': 0.4, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 88: { + 'display_name': 'Soul Sand', + 'name': 'hellsand', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 89: { + 'display_name': 'Glowstone', + 'name': 'lightgem', + 'hardness': 0.3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 90: { + 'display_name': 'Portal', + 'name': 'portal', + 'hardness': None, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'empty' + }, + 91: { + 'display_name': 'Jack \'o\' Lantern', + 'name': 'litpumpkin', + 'hardness': 1, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'plant' + }, + 92: { + 'display_name': 'Cake', + 'name': 'cake', + 'hardness': 0.5, + 'stack_size': 1, + 'diggable': True, + 'bounding_box': 'block' + }, + 93: { + 'display_name': 'Redstone Repeater (Inactive)', + 'name': 'redstoneRepeaterInactive', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 94: { + 'display_name': 'Redstone Repeater (Active)', + 'name': 'redstoneRepeaterActive', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 95: { + 'display_name': 'Locked chest', + 'name': 'lockedchest', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 96: { + 'display_name': 'Trapdoor', + 'name': 'trapdoor', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 97: { + 'display_name': 'Monster Egg', + 'name': 'monsterStoneEgg', + 'hardness': 0.75, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 98: { + 'display_name': 'Stone Brick', + 'name': 'stonebricksmooth', + 'hardness': 1.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 99: { + 'display_name': 'Huge Brown Mushroom', + 'name': 'mushroomHugeBrown', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 100: { + 'display_name': 'Huge Red Mushroom', + 'name': 'mushroomHugeRed', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 101: { + 'display_name': 'Iron Bars', + 'name': 'fenceIron', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 102: { + 'display_name': 'Glass Pane', + 'name': 'thinGlass', + 'hardness': 0.3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 103: { + 'display_name': 'Melon', + 'name': 'melon', + 'hardness': 1, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'melon' + }, + 104: { + 'display_name': 'Pumpkin Stem', + 'name': 'pumpkinStem', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 105: { + 'display_name': 'Melon Stem', + 'name': 'melonStem', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 106: { + 'display_name': 'Vines', + 'name': 'vine', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'plant' + }, + 107: { + 'display_name': 'Fence Gate', + 'name': 'fenceGate', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 108: { + 'display_name': 'Brick Stairs', + 'name': 'stairsBrick', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 109: { + 'display_name': 'Stone Brick Stairs', + 'name': 'stairsStoneBrickSmooth', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 110: { + 'display_name': 'Mycelium', + 'name': 'mycel', + 'hardness': 0.6, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'dirt' + }, + 111: { + 'display_name': 'Lily Pad', + 'name': 'waterlily', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 112: { + 'display_name': 'Nether Brick', + 'name': 'netherBrick', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 113: { + 'display_name': 'Nether Brick Fence', + 'name': 'netherFence', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 114: { + 'display_name': 'Nether Brick Stairs', + 'name': 'stairsNetherBrick', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 115: { + 'display_name': 'Nether Wart', + 'name': 'netherStalk', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 116: { + 'display_name': 'Enchantment Table', + 'name': 'enchantmentTable', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 117: { + 'display_name': 'Brewing Stand', + 'name': 'brewingStand', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 118: { + 'display_name': 'Cauldron', + 'name': 'cauldron', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 119: { + 'name': 'endPortal', + 'display_name': 'End Portal', + 'hardness': None, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'empty' + }, + 120: { + 'display_name': 'End Portal Frame', + 'name': 'endPortalFrame', + 'hardness': None, + 'stack_size': 64, + 'diggable': False, + 'bounding_box': 'block' + }, + 121: { + 'display_name': 'End Stone', + 'name': 'whiteStone', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 122: { + 'display_name': 'Dragon Egg', + 'name': 'dragonEgg', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 123: { + 'display_name': 'Redstone Lamp (Inactive)', + 'name': 'redstoneLightInactive', + 'hardness': 0.3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 124: { + 'display_name': 'Redstone Lamp (Active)', + 'name': 'redstoneLightActive', + 'hardness': 0.3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 125: { + 'display_name': 'Wooden Double Slab', + 'name': 'woodSlabDouble', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 126: { + 'display_name': 'Wooden Slab', + 'name': 'woodSlab', + 'hardness': 2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 127: { + 'display_name': 'Cocoa Pod', + 'name': 'cocoa', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'plant' + }, + 128: { + 'display_name': 'Sandstone Stairs', + 'name': 'stairsSandStone', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 129: { + 'display_name': 'Emerald Ore', + 'name': 'oreEmerald', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 130: { + 'display_name': 'Ender Chest', + 'name': 'enderChest', + 'hardness': 22.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 131: { + 'display_name': 'Tripwire Hook', + 'name': 'tripWireSource', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 132: { + 'display_name': 'Tripwire', + 'name': 'tripWire', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 133: { + 'display_name': 'Block of Emerald', + 'name': 'blockEmerald', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (257, 278), + }, + 134: { + 'display_name': 'Spruce Wood Stairs', + 'name': 'stairsWoodSpruce', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 135: { + 'display_name': 'Birch Wood Stairs', + 'name': 'stairsWoodBirch', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 136: { + 'display_name': 'Jungle Wood Stairs', + 'name': 'stairsWoodJungle', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 137: { + 'display_name': 'Command Block', + 'name': 'commandBlock', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 138: { + 'display_name': 'Beacon', + 'name': 'beacon', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 139: { + 'display_name': 'Cobblestone Wall', + 'name': 'cobbleWall', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 140: { + 'display_name': 'Flower Pot', + 'name': 'flowerPot', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 141: { + 'display_name': 'Carrots', + 'name': 'carrots', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 142: { + 'display_name': 'Potatoes', + 'name': 'potatoes', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 143: { + 'display_name': 'Wooden Button', + 'name': 'buttonWood', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty' + }, + 144: { + 'display_name': 'Mob Head', + 'name': 'skull', + 'hardness': 1, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 145: { + 'display_name': 'Anvil', + 'name': 'anvil', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 146: { + 'display_name': 'Trapped Chest', + 'name': 'trappedChest', + 'hardness': 2.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 147: { + 'display_name': 'Weighted Pressure plate (Light)', + 'name': 'pressurePlateWeightedLight', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 148: { + 'display_name': 'Weighted Pressure plate (Heavy)', + 'name': 'pressurePlateWeightedHeavy', + 'hardness': 0.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 149: { + 'display_name': 'Redstone Comparator (Inactive)', + 'name': 'redstoneComparatorInactive', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 150: { + 'display_name': 'Redstone Comparator (Active)', + 'name': 'redstoneComparatorActive', + 'hardness': 0, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block' + }, + 151: { + 'display_name': 'Daylight Sensor', + 'name': 'daylightSensor', + 'hardness': 0.2, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'wood' + }, + 152: { + 'display_name': 'Block of Redstone', + 'name': 'redstoneBlock', + 'hardness': 5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 153: { + 'display_name': 'Nether Quartz Ore', + 'name': 'netherQuartzOre', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 154: { + 'display_name': 'Hopper', + 'name': 'hopper', + 'hardness': 3, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 155: { + 'display_name': 'Block of Quartz', + 'name': 'quartzBlock', + 'hardness': 0.8, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 156: { + 'display_name': 'Quartz Stairs', + 'name': 'quartzStairs', + 'hardness': 0.8, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, + 157: { + 'display_name': 'Activator Rail', + 'name': 'activatorRail', + 'hardness': 0.7, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'empty', + 'material': 'rock' + }, + 158: { + 'display_name': 'Dropper', + 'name': 'dropper', + 'hardness': 3.5, + 'stack_size': 64, + 'diggable': True, + 'bounding_box': 'block', + 'material': 'rock', + 'harvest_tools': (270, 274, 257, 278, 285), + }, } blocks = tuple(blocks[i] for i in range(len(blocks))) diff --git a/spock/mcmap/smpmap.py b/spock/mcmap/smpmap.py index 7fcbc94..eb79268 100644 --- a/spock/mcmap/smpmap.py +++ b/spock/mcmap/smpmap.py @@ -1,4 +1,5 @@ """ +Used for storing map data Chunks are packed in X, Z, Y order The array walks down X, every 16 elements you enter a new Z-level @@ -13,222 +14,250 @@ [256]-[511] are X = 0-15, Z = 0-15, Y = 1 and so on -Chunk Coords * 16 + Block Coords gives you the actual position of the block in the world - """ import array import struct from spock import utils -class BiomeData: - """ A 16x16 array stored in each ChunkColumn. """ - data = None - - def fill(self): - if not self.data: - self.data = array.array('B', [0]*256) - - def unpack(self, buff): - self.data = array.array('B', buff.read(256)) - - def pack(self): - return self.data.tostring() - - def get(self, x, z): - self.fill() - return self.data[x + z * 16] - - def put(self, x, z, d): - self.fill() - self.data[x + z * 16] = d - +DIMENSION_NETHER = -0x01 +DIMENSION_OVERWOLD = 0x00 +DIMENSION_END = 0x01 class ChunkData: - """ A 16x16x16 array for storing block IDs. """ - length = 16*16*16 - data = None + length = 16*16*16 + ty = 'B' + data = None - def fill(self): - if not self.data: - self.data = array.array('B', [0]*self.length) + def fill(self): + if not self.data: + self.data = array.array(self.ty, [0]*self.length) - def unpack(self, buff): - self.data = array.array('B', buff.read(self.length)) + def unpack(self, buff): + self.data = array.array(self.ty, buff.read(self.length)) - def pack(self): - self.fill() - return self.data.tostring() + def pack(self): + self.fill() + return self.data.tobytes() - def get(self, x, y, z): - self.fill() - return self.data[x + ((y * 16) + z) * 16] + def get(self, x, y, z): + self.fill() + return self.data[x + ((y * 16) + z) * 16] - def put(self, x, y, z, data): - self.fill() - self.data[x + ((y * 16) + z) * 16] = data + def set(self, x, y, z, data): + self.fill() + self.data[x + ((y * 16) + z) * 16] = data +class BiomeData(ChunkData): + """ A 16x16 array stored in each ChunkColumn. """ + length = 16*16 + data = None -class ChunkDataNibble(ChunkData): - """ A 16x16x8 array for storing metadata, light or add. Each array element - contains two 4-bit elements. """ - length = 16*16*8 - - def get(self, x, y, z): - self.fill() - x, r = divmod(x, 2) - i = x + ((y * 16) + z) * 16 - - if r == 0: - return self.data[i] >> 4 - else: - return self.data[i] & 0x0F - - def put(self, x, y, z, data): - self.fill() - x, r = divmod(x, 2) - i = x + ((y * 16) + z) * 16 + def get(self, x, z): + self.fill() + return self.data[x + z * 16] - if r == 0: - self.data[i] = (self.data[i] & 0x0F) | ((data & 0x0F) << 4) - else: - self.data[i] = (self.data[i] & 0xF0) | (data & 0x0F) + def set(self, x, z, d): + self.fill() + self.data[x + z * 16] = d +class ChunkDataShort(ChunkData): + """ A 16x16x16 array for storing block IDs/Metadata. """ + length = 16*16*16*2 + ty = 'H' -class Chunk(dict): - """ Collates the various data arrays """ - - def __init__(self): - self['block_data'] = ChunkData() - self['block_meta'] = ChunkDataNibble() - self['block_add'] = ChunkDataNibble() - self['light_block'] = ChunkDataNibble() - self['light_sky'] = ChunkDataNibble() +class ChunkDataNibble(ChunkData): + """ A 16x16x8 array for storing metadata, light or add. Each array element + contains two 4-bit elements. """ + length = 16*16*8 + + def get(self, x, y, z): + self.fill() + x, r = divmod(x, 2) + i = x + ((y * 16) + z) * 16 + if r: + return self.data[i] & 0x0F + else: + return self.data[i] >> 4 + + def set(self, x, y, z, data): + self.fill() + x, r = divmod(x, 2) + i = x + ((y * 16) + z) * 16 + if r: + self.data[i] = (self.data[i] & 0xF0) | (data & 0x0F) + else: + self.data[i] = (self.data[i] & 0x0F) | ((data & 0x0F) << 4) + +class Chunk: + def __init__(self): + self.block_data = ChunkDataShort() + self.light_block = ChunkDataNibble() + self.light_sky = ChunkDataNibble() class ChunkColumn: - """ Initialised chunks are a Chunk, otherwise None. """ - - def __init__(self): - self.chunks = [None]*16 - self.biome = BiomeData() - - def unpack(self, buff, mask1, mask2, skylight=True, ground_up=True): - #In the protocol, each section is packed sequentially (i.e. attributes - #pertaining to the same chunk are *not* grouped) - self.unpack_section(buff, 'block_data', mask1) - self.unpack_section(buff, 'block_meta', mask1) - self.unpack_section(buff, 'light_block', mask1) - if skylight: - self.unpack_section(buff, 'light_sky', mask1) - self.unpack_section(buff, 'block_add', mask2) - if ground_up: - self.biome.unpack(buff) - - def unpack_section(self, buff, section, mask): - #Iterate over the bitmask - for i in range(16): - if mask & (1 << i): - if self.chunks[i] == None: - self.chunks[i] = Chunk() - self.chunks[i][section].unpack(buff) - -class World: - """ A bunch of ChunkColumns. """ - - def __init__(self): - self.columns = {} #chunk columns are address by a tuple (x, z) - - def unpack_raw(self, buff, ty): - return struct.unpack('>'+ty, buff.read(struct.calcsize(ty))) - - def unpack_bulk(self, data): - skylight = data['sky_light'] - ground_up = True - - # Read compressed data - data = utils.BoundBuffer(data['data']) - - for bitmap in data['bitmaps']: - # Read chunk metadata - x_chunk = bitmap['x'] - z_chunk = bitmap['z'] - mask1 = bitmap['primary_bitmap'] - mask2 = bitmap['secondary_bitmap'] - - # Grab the relevant column - key = (x_chunk, z_chunk) - if key not in self.columns: - self.columns[key] = ChunkColumn() - - # Unpack the chunk column data - self.columns[key].unpack(data, mask1, mask2, skylight, ground_up) - - def unpack_column(self, data): - x_chunk = data['x_chunk'] - z_chunk = data['z_chunk'] - ground_up = data['ground_up_continuous'] - mask1 = data['primary_bitmap'] - mask2 = data['secondary_bitmap'] - data = BoundBuffer(data['data']) - skylight = True - - key = (x_chunk, z_chunk) - if key not in self.columns: - self.columns[key] = ChunkColumn() - - self.columns[key].unpack(data, mask1, mask2, skylight, ground_up) - - def get(self, x, y, z, key): - x, rx = divmod(x, 16) - y, ry = divmod(y, 16) - z, rz = divmod(z, 16) - - if not (x,z) in self.columns: - return 0 - column = self.columns[(x,z)] - - chunk = column.chunks[y] - if chunk == None: - return 0 - - return chunk[key].get(rx,ry,rz) - - def put(self, x, y, z, key, data): - x, rx = divmod(x, 16) - y, ry = divmod(y, 16) - z, rz = divmod(z, 16) - - if (x,z) in self.columns: - column = self.columns[(x,z)] - else: - column = ChunkColumn() - self.columns[(x,z)] = column - - chunk = column.chunks[y] - if chunk == None: - chunk = Chunk() - column.chunks[y] = chunk - - chunk[key].put(rx,ry,rz,data) - - def get_biome(self, x, z): - x, rx = divmod(x, 16) - z, rz = divmod(z, 16) - - if (x,z) not in self.columns: - return 0 - - return self.columns[(x,z)].biome.get(rx, rz) - - def set_biome(self, x, z, data): - x, rx = divmod(x, 16) - z, rz = divmod(z, 16) - - if (x,z) in self.columns: - column = self.columns[(x,z)] - else: - column = ChunkColumn() - self.columns[(x,z)] = column - - return column.biome.put(rx, rz, data) \ No newline at end of file + def __init__(self): + self.chunks = [None]*16 + self.biome = BiomeData() + + def unpack(self, buff, mask, skylight=True, continuous=True): + #In the protocol, each section is packed sequentially (i.e. attributes + #pertaining to the same chunk are *not* grouped) + self.unpack_block_data(buff, mask) + self.unpack_light_block(buff, mask) + if skylight: + self.unpack_light_sky(buff, mask) + if continuous: + self.biome.unpack(buff) + + def unpack_block_data(self, buff, mask): + for i in range(16): + if mask&(1<>8, data&0xFF + + def set_block(self, x, y, z, block_id = None, meta = None, data = None): + x, rx = divmod(x, 16) + y, ry = divmod(y, 16) + z, rz = divmod(z, 16) + + if (x,z) in self.columns: + column = self.columns[(x,z)] + else: + column = ChunkColumn() + self.columns[(x,z)] = column + chunk = column.chunks[y] + if chunk == None: + chunk = Chunk() + column.chunks[y] = chunk + + if data == None: + data = (block_id<<8)|(meta&0xFF) + chunk.block_data.set(rx, ry, rz, data) + + def get_light(self, x, y, z): + x, rx = divmod(x, 16) + y, ry = divmod(y, 16) + z, rz = divmod(z, 16) + + if not (x,z) in self.columns: + return 0, 0 + column = self.columns[(x,z)] + chunk = column.chunks[y] + if chunk == None: + return 0, 0 + + return chunk.light_block.get(rx,ry,rz), chunk.light_sky.get(rx,ry,rz) + + def set_light(self, x, y, z, light_block = None, light_sky = None): + x, rx = divmod(x, 16) + y, ry = divmod(y, 16) + z, rz = divmod(z, 16) + + if (x,z) in self.columns: + column = self.columns[(x, z)] + else: + column = ChunkColumn() + self.columns[(x, z)] = column + chunk = column.chunks[y] + if chunk == None: + chunk = Chunk() + column.chunks[y] = chunk + + if light_block != None: + chunk.light_block.set(rx, ry, rz, light_block&0xF) + if light_sky != None: + chunk.light_sky.set(rx, ry, rz, light_sky&0xF) + + def get_biome(self, x, z): + x, rx = divmod(x, 16) + z, rz = divmod(z, 16) + + if (x,z) not in self.columns: + return 0 + + return self.columns[(x,z)].biome.get(rx, rz) + + def set_biome(self, x, z, data): + x, rx = divmod(x, 16) + z, rz = divmod(z, 16) + + if (x,z) in self.columns: + column = self.columns[(x,z)] + else: + column = ChunkColumn() + self.columns[(x,z)] = column + + return column.biome.set(rx, rz, data) diff --git a/spock/mcmap/smpmap2.py b/spock/mcmap/smpmap2.py index 57d2860..145b619 100644 --- a/spock/mcmap/smpmap2.py +++ b/spock/mcmap/smpmap2.py @@ -1,191 +1,14 @@ -import struct -from spock import utils +#Eventually put advanced map functions here class MapBlock: - def __init__(self, base_id = 0, add_id = 0): - self.base_id = base_id - self.add_id = add_id - self.id = self.calc_id() - self.meta = 0 - self.light = 0 - self.sky_light = 0 - self.block_light = 0 - self.biome = 0 - - def calc_id(self): - self.id = (self.add_id<<8)+self.base_id - return self.id - - #Needs to do proper light calc based on time_of_day - def calc_light(self, time): - self.light = max(self.block_light, self.sky_light) - -class Chunk: - def __init__(self): - self.length = 16*16*16 - self.time = 0 - self.blocks = [MapBlock() for i in range(self.length)] - - def get(self, x, y, z): - return self.blocks[x+((y*16)+z)*16] - - def put(self, x, y, z, data): - block = self.blocks[x+((y*16)+z)*16] - block.id = data['block_id'] - block.base_id = data['block_id']&0xFF - block.add_id = data['block_id']>>8 - block.meta = data['metadata'] - return block - - def unpack_data(self, buff): - for idx, i in enumerate(buff.recv(self.length)): - self.blocks[idx].id = i - self.blocks[idx].base_id = i - self.blocks[idx].add_id = 0 - - def unpack_meta(self, buff): - for idx, i in enumerate(buff.recv(self.length>>1)): - self.blocks[idx*2].meta = i>>4 - self.blocks[idx*2+1].meta = i&0x0F - - def unpack_add(self, buff): - for idx, i in enumerate(buff.recv(self.length>>1)): - self.blocks[idx*2].add_id = i>>4 - self.blocks[idx*2].calc_id() - self.blocks[idx*2+1].add_id = i&0x0F - self.blocks[idx*2+1].calc_id() - - def unpack_blight(self, buff): - for idx, i in enumerate(buff.recv(self.length>>1)): - self.blocks[idx*2].block_light = i>>4 - self.blocks[idx*2+1].block_light = i&0x0F - self.update_light() - - def unpack_slight(self, buff): - for idx, i in enumerate(buff.recv(self.length>>1)): - self.blocks[idx*2].sky_light = i>>4 - self.blocks[idx*2+1].sky_light = i&0x0F - self.update_light() - - def unpack_biome(self, x, z, biome_id): - for y in range(16): - self.blocks[x+((y*16)+z)*16].biome = biome_id - - def update_light(self, time = None): - if time: self.time = time - for block in self.blocks: - block.calc_light(self.time) - -class ChunkColumn: - def __init__(self): - self.chunks = [None]*16 - self.biome = [None]*256 - - def unpack(self, buff, primary_bitmap, add_bitmap, skylight, continuous): - primary_mask = [] - add_mask = [] - for i in range(16): - if primary_bitmap&(1<= (1<<32): - return None - if total&(1<<31): - total = total - (1<<32) - return total + total = 0 + shift = 0 + val = 0x80 + while val&0x80: + val = struct.unpack('B', bbuff.read(1))[0] + total |= ((val&0x7F)<= (1<<32): + return None + if total&(1<<31): + total = total - (1<<32) + return total def pack_varint(val): - if val >= (1<<31) or val < -(1<<31): - return None - o = b'' - if val < 0: - val = (1<<32)+val - while val>=0x80: - bits = val&0x7F - val >>= 7 - o += struct.pack('B', (0x80|bits)) - bits = val&0x7F - o += struct.pack('B', bits) - return o + if val >= (1<<31) or val < -(1<<31): + return None + o = b'' + if val < 0: + val = (1<<32)+val + while val>=0x80: + bits = val&0x7F + val >>= 7 + o += struct.pack('B', (0x80|bits)) + bits = val&0x7F + o += struct.pack('B', bits) + return o + +#Like a varint, but a 64-bit signed value +def unpack_varlong(bbuff): + total = 0 + shift = 0 + val = 0x80 + while val&0x80: + val = struct.unpack('B', bbuff.read(1))[0] + total |= ((val&0x7F)<= (1<<64): + return None + if total&(1<<64): + total = total - (1<<64) + return total + +def pack_varlong(val): + if val >= (1<<63) or val < -(1<<63): + return None + o = b'' + if val < 0: + val = (1<<64)+val + while val>=0x80: + bits = val&0x7F + val >>= 7 + o += struct.pack('B', (0x80|bits)) + bits = val&0x7F + o += struct.pack('B', bits) + return o + +# Three values packed into one 64-bit long +# x: 26 MSBs, y: 12 bits, z: 26 LSBs +def unpack_position(bbuff): + position = {} + val = unpack(MC_LONG, bbuff) + position['x'] = val>>38; + position['y'] = (val>>26)&0xFFF + position['z'] = val&0x3FFFFFF + return position + +def pack_position(position): + val = (position['x']&0x3FFFFFF)<<38 + val |= (position['y']&0xFFF)<<26 + val |= (position['z']&0x3FFFFFF) + return pack(MC_ULONG, val) + # return val # Slots are dictionaries that hold info about # inventory items, they also have funky -# enchantment data stored in gziped NBT structs +# enchantment data + +#TODO: This is probably still wrong def unpack_slot(bbuff): - slot = {} - slot['id'] = unpack(MC_SHORT, bbuff) - if slot['id'] != -1: - slot['amount'] = unpack(MC_BYTE, bbuff) - slot['damage'] = unpack(MC_SHORT, bbuff) - length = unpack(MC_SHORT, bbuff) - if length > 0: - data = bbuff.recv(length) - try: - ench_bbuff = utils.BoundBuffer( - #Adding 16 to the window bits field tells zlib - #to take care of the gzip headers for us - zlib.decompress(data, 16+zlib.MAX_WBITS) - ) - assert(unpack(MC_BYTE, ench_bbuff) == nbt.TAG_COMPOUND) - name = nbt.TAG_String(buffer = ench_bbuff) - ench = nbt.TAG_Compound(buffer = ench_bbuff) - ench.name = name - slot['enchants'] = ench - except: - slot['enchant_data'] = data - return slot + slot = {} + slot['id'] = unpack(MC_SHORT, bbuff) + if slot['id'] != -1: + slot['amount'] = unpack(MC_BYTE, bbuff) + slot['damage'] = unpack(MC_SHORT, bbuff) + nbt_start = unpack(MC_BYTE, bbuff) + if nbt_start > 0: + assert(nbt_start == nbt.TAG_COMPOUND) + name = nbt.TAG_String(buffer = bbuff) + ench = nbt.TAG_Compound(buffer = bbuff) + ench.name = name + slot['enchants'] = ench + return slot def pack_slot(slot): - o = pack(MC_SHORT, data['id']) - if data['id'] != -1: - o += pack(MC_BYTE, data['amount']) - o += pack(MC_SHORT, data['damage']) - if 'enchantment_data' in data: - o += pack(MC_SHORT, len(data['enchant_data'])) - o += data['enchant_data'] - elif 'enchants' in data: - ench = data['enchants'] - bbuff = utils.BoundBuffer() - TAG_Byte(ench.id)._render_buffer(bbuff) - TAG_String(ench.name)._render_buffer(bbuff) - ench._render_buffer(bbuff) - #Python zlib.compress doesn't provide wbits for some reason - #So we'll use a compression object instead, no biggie - compress = zlib.compressobj(wbits = 16+zlib.MAX_WBITS) - ench = compress.compress(bbuff.flush()) - ench += compress.flush() - o += pack(MC_SHORT, len(ench)) - o += ench - else: - o += pack(MC_SHORT, -1) - return o - -# Metadata is a dictionary list thing that -# holds metadata about entities. Currently -# implemented as a list/tuple thing, might + o = pack(MC_SHORT, slot['id']) + if slot['id'] != -1: + o += pack(MC_BYTE, slot['amount']) + o += pack(MC_SHORT, slot['damage']) + if 'enchants' in slot: + ench = slot['enchants'] + bbuff = utils.BoundBuffer() + TAG_Byte(ench.id)._render_buffer(bbuff) + TAG_String(ench.name)._render_buffer(bbuff) + ench._render_buffer(bbuff) + o += bbuff.flush() + else: + o += pack(MC_BYTE, 0) + return o + +# Metadata is a dictionary list thing that +# holds metadata about entities. Currently +# implemented as a list/tuple thing, might # switch to dicts metadata_lookup = MC_BYTE, MC_SHORT, MC_INT, MC_FLOAT, MC_STRING, MC_SLOT def unpack_metadata(bbuff): - metadata = [] - head = unpack(MC_UBYTE, bbuff) - while head != 127: - key = head & 0x1F # Lower 5 bits - typ = head >> 5 # Upper 3 bits - if typ < len(metadata_lookup) and typ >= 0: - val = unpack(metadata_lookup[typ], bbuff) - elif typ == 6: - val = [unpack(MC_INT, bbuff) for i in range(3)] - else: - return None - metadata.append((key, (typ, val))) - head = unpack(MC_UBYTE, bbuff) - return metadata + metadata = [] + head = unpack(MC_UBYTE, bbuff) + while head != 0x7F: + key = head & 0x1F # Lower 5 bits + typ = head >> 5 # Upper 3 bits + if typ < len(metadata_lookup) and typ >= 0: + val = unpack(metadata_lookup[typ], bbuff) + elif typ == 6: + val = [unpack(MC_INT, bbuff) for i in range(3)] + else: + return None + metadata.append((key, (typ, val))) + head = unpack(MC_UBYTE, bbuff) + return metadata def pack_metadata(metadata): - o = b'' - for key, tmp in data: - typ, val = tmp - o += pack(MC_UBYTE, (typ << 5)|key) - if typ < len(metadata_lookup) and typ >= 0: - o += pack(metadata_lookup[typ], bbuff) - elif typ == 6: - for i in range(3): - o += pack(MC_INT, val[i]) - else: - return None - o += pack(MC_BYTE, 127) - return o + o = b'' + for key, tmp in data: + typ, val = tmp + o += pack(MC_UBYTE, (typ << 5)|key) + if typ < len(metadata_lookup) and typ >= 0: + o += pack(metadata_lookup[typ], bbuff) + elif typ == 6: + for i in range(3): + o += pack(MC_INT, val[i]) + else: + return None + o += pack(MC_BYTE, 0x7F) + return o endian = '>' def unpack(data_type, bbuff): - if data_type < len(mcdata.data_structs): - format = mcdata.data_structs[data_type] - return struct.unpack(endian+format[0], bbuff.recv(format[1]))[0] - elif data_type == MC_VARINT: - return unpack_varint(bbuff) - elif data_type == MC_STRING: - return bbuff.recv(unpack(MC_VARINT, bbuff)).decode('utf-8') - elif data_type == MC_SLOT: - return unpack_slot(bbuff) - elif data_type == MC_META: - return unpack_metadata(bbuff) - else: - return None + if data_type < len(mcdata.data_structs): + format = mcdata.data_structs[data_type] + return struct.unpack(endian+format[0], bbuff.recv(format[1]))[0] + elif data_type == MC_VARINT: + return unpack_varint(bbuff) + elif data_type == MC_VARLONG: + return unpack_varlong(bbuff) + elif data_type == MC_UUID: + a, b = struct.unpack('>QQ', bbuff.recv(16)) + return (a<<64)|b + elif data_type == MC_POSITION: + return unpack_position(bbuff) + elif data_type == MC_STRING: + return bbuff.recv(unpack(MC_VARINT, bbuff)).decode('utf-8') + elif data_type == MC_CHAT: + return json.loads(unpack(MC_STRING, bbuff)) + elif data_type == MC_SLOT: + return unpack_slot(bbuff) + elif data_type == MC_META: + return unpack_metadata(bbuff) + else: + return None def pack(data_type, data): - if data_type < len(mcdata.data_structs): - format = mcdata.data_structs[data_type] - return struct.pack(endian+format[0], data) - elif data_type == MC_VARINT: - return pack_varint(data) - elif data_type == MC_STRING: - data = data.encode('utf-8') - return pack(MC_VARINT, len(data)) + data - elif data_type == MC_SLOT: - return pack_slot(data) - elif data_type == MC_META: - return pack_metadata(data) - else: - return None \ No newline at end of file + if data_type < len(mcdata.data_structs): + format = mcdata.data_structs[data_type] + return struct.pack(endian+format[0], data) + elif data_type == MC_VARINT: + return pack_varint(data) + elif data_type == MC_VARLONG: + return pack_varlong(data) + elif data_type == MC_UUID: + return struct.pack('>QQ', (data>>64)&((1<<64)-1), data&((1<<64)-1)) + elif data_type == MC_POSITION: + return pack_position(data) + elif data_type == MC_STRING: + data = data.encode('utf-8') + return pack(MC_VARINT, len(data)) + data + elif data_type == MC_CHAT: + return pack(MC_STRING, json.dumps(data)) + elif data_type == MC_SLOT: + return pack_slot(data) + elif data_type == MC_META: + return pack_metadata(data) + else: + return None diff --git a/spock/mcp/mcdata.py b/spock/mcp/mcdata.py index 1a7b6ce..d7147bf 100644 --- a/spock/mcp/mcdata.py +++ b/spock/mcp/mcdata.py @@ -1,18 +1,12 @@ #Most of the data formats, structures, and magic values -MC_PROTOCOL_VERSION = 4 +MC_PROTOCOL_VERSION = 47 SERVER_TO_CLIENT = 0x00 CLIENT_TO_SERVER = 0x01 -direction_names = ["<", ">"] - -HANDSHAKE_STATE = 0x00 -STATUS_STATE = 0x01 -LOGIN_STATE = 0x02 -PLAY_STATE = 0x03 - -state_names = ["HANDSHAKE", "STATUS", "LOGIN", "PLAY"] +PROTO_COMP_ON = 0x00 +PROTO_COMP_OFF = 0x01 MC_BOOL = 0x00 MC_UBYTE = 0x01 @@ -21,893 +15,1125 @@ MC_SHORT = 0x04 MC_UINT = 0x05 MC_INT = 0x06 -MC_LONG = 0x07 -MC_FLOAT = 0x08 -MC_DOUBLE = 0x09 -MC_VARINT = 0x0A -MC_STRING = 0x0B -MC_SLOT = 0x0C -MC_META = 0x0D +MC_ULONG = 0x07 +MC_LONG = 0x08 +MC_FLOAT = 0x09 +MC_DOUBLE = 0x0A +MC_VARINT = 0x0B +MC_VARLONG = 0x0C +MC_UUID = 0x0D +MC_POSITION = 0x0E +MC_STRING = 0x0F +MC_CHAT = 0x10 +MC_SLOT = 0x11 +MC_META = 0x12 + +HANDSHAKE_STATE = 0x00 +STATUS_STATE = 0x01 +LOGIN_STATE = 0x02 +PLAY_STATE = 0x03 + +SMP_NETHER =-0x01 +SMP_OVERWORLD = 0x00 +SMP_END = 0x01 + +FLG_XPOS_REL = 0x01 +FLG_YPOS_REL = 0x02 +FLG_ZPOS_REL = 0x04 +FLG_YROT_REL = 0x08 +FLG_XROT_REL = 0x10 + +PL_ADD_PLAYER = 0x00 +PL_UPDATE_GAMEMODE = 0x01 +PL_UPDATE_LATENCY = 0x02 +PL_UPDATE_DISPLAY = 0x03 +PL_REMOVE_PLAYER = 0x04 + +CE_ENTER_COMBAT = 0x00 +CE_END_COMBAT = 0x01 +CE_ENTITY_DEAD = 0x02 + +WB_SET_SIZE = 0x00 +WB_LERP_SIZE = 0x01 +WB_SET_CENTER = 0x02 +WB_INITIALIZE = 0x03 +WB_SET_WARN_TIME = 0x04 +WB_SET_WARN_BLOCKS = 0x05 + +SO_CREATE_BOARD = 0x00 +SO_REMOVE_BOARD = 0x01 +SO_UPDATE_BOARD = 0x02 + +TL_TITLE = 0x00 +TL_SUBTITLE = 0x01 +TL_TIMES = 0x02 +TL_CLEAR = 0x03 +TL_RESET = 0x04 + +UE_INTERACT = 0x00 +UE_ATTACK = 0x01 +UE_INTERACT_AT = 0x02 + +CL_STATUS_RESPAWN = 0x00 +CL_STATUS_STATS = 0x01 +CL_STATUS_INV = 0x02 data_structs = ( - #(struct_suffix, size), #type - ('?', 1), #bool - ('B', 1), #ubyte - ('b', 1), #byte - ('H', 2), #ushort - ('h', 2), #short - ('I', 4), #uint - ('i', 4), #int - ('q', 8), #long - ('f', 4), #float - ('d', 8), #double + #(struct_suffix, size), #type + ('?', 1), #bool + ('B', 1), #ubyte + ('b', 1), #byte + ('H', 2), #ushort + ('h', 2), #short + ('I', 4), #uint + ('i', 4), #int + ('Q', 8), #ulong + ('q', 8), #long + ('f', 4), #float + ('d', 8), #double +) + +particles = ( + #(name, data_length) + ('explosion_normal' , 0), + ('explosion_large' , 0), + ('explosion_huge' , 0), + ('fireworks_spark' , 0), + ('water_bubble' , 0), + ('water_splash' , 0), + ('water_wake' , 0), + ('suspended' , 0), + ('suspended_depth' , 0), + ('crit' , 0), + ('crit_magic' , 0), + ('smoke_normal' , 0), + ('smoke_large' , 0), + ('spell' , 0), + ('spell_instant' , 0), + ('spell_mob' , 0), + ('spell_mob_ambient', 0), + ('spell_witch' , 0), + ('drip_water' , 0), + ('drip_lava' , 0), + ('villager_angry' , 0), + ('villager_happy' , 0), + ('town_aura' , 0), + ('note' , 0), + ('portal' , 0), + ('enchantment_table', 0), + ('flame' , 0), + ('lava' , 0), + ('footstep' , 0), + ('cloud' , 0), + ('redstone' , 0), + ('snowball' , 0), + ('snow_shovel' , 0), + ('slime' , 0), + ('heart' , 0), + ('barrier' , 0), + ('icon_crack' , 2), + ('block_crack' , 1), + ('block_dust' , 1), + ('water_drop' , 0), + ('item_take' , 0), + ('mob_appearance' , 0), ) #Structs formatted for readibility #Packed into tuples at end of file packet_names = { - HANDSHAKE_STATE: { - SERVER_TO_CLIENT: {}, - CLIENT_TO_SERVER: { - 0x00: 'Handshake', - }, - }, + HANDSHAKE_STATE: { + SERVER_TO_CLIENT: {}, + CLIENT_TO_SERVER: { + 0x00: 'Handshake', + }, + }, - STATUS_STATE: { - SERVER_TO_CLIENT: { - 0x00: 'Status Response', - 0x01: 'Status Ping', - }, - CLIENT_TO_SERVER: { - 0x00: 'Status Request', - 0x01: 'Status Ping', - }, - }, + STATUS_STATE: { + SERVER_TO_CLIENT: { + 0x00: 'Status Response', + 0x01: 'Status Ping', + }, + CLIENT_TO_SERVER: { + 0x00: 'Status Request', + 0x01: 'Status Ping', + }, + }, - LOGIN_STATE: { - SERVER_TO_CLIENT: { - 0x00: 'Disconnect', - 0x01: 'Encryption Request', - 0x02: 'Login Success', - }, - CLIENT_TO_SERVER: { - 0x00: 'Login Start', - 0x01: 'Encryption Response', - }, - }, + LOGIN_STATE: { + SERVER_TO_CLIENT: { + 0x00: 'Disconnect', + 0x01: 'Encryption Request', + 0x02: 'Login Success', + 0x03: 'Set Compression', + }, + CLIENT_TO_SERVER: { + 0x00: 'Login Start', + 0x01: 'Encryption Response', + }, + }, - PLAY_STATE: { - SERVER_TO_CLIENT: { - 0x00: 'Keep Alive', - 0x01: 'Join Game', - 0x02: 'Chat Message', - 0x03: 'Time Update', - 0x04: 'Entity Equipment', - 0x05: 'Spawn Position', - 0x06: 'Update Health', - 0x07: 'Respawn', - 0x08: 'Player Position and Look', - 0x09: 'Held Item Change', - 0x0A: 'Use Bed', - 0x0B: 'Animation', - 0x0C: 'Spawn Player', - 0x0D: 'Collect Item', - 0x0E: 'Spawn Object', - 0x0F: 'Spawn Mob', - 0x10: 'Spawn Painting', - 0x11: 'Spawn Experience Orb', - 0x12: 'Entity Velocity', - 0x13: 'Destroy Entities', - 0x14: 'Entity', - 0x15: 'Entity Relative Move', - 0x16: 'Entity Look', - 0x17: 'Entity Look and Relative Move', - 0x18: 'Entity Teleport', - 0x19: 'Entity Head Look', - 0x1A: 'Entity Status', - 0x1B: 'Attach Entity', - 0x1C: 'Entity Metadata', - 0x1D: 'Entity Effect', - 0x1E: 'Remove Entity Effect', - 0x1F: 'Set Experience', - 0x20: 'Entity Properties', - 0x21: 'Chunk Data', - 0x22: 'Multi Block Change', - 0x23: 'Block Change', - 0x24: 'Block Action', - 0x25: 'Block Break Animation', - 0x26: 'Map Chunk Bulk', - 0x27: 'Explosion', - 0x28: 'Effect', - 0x29: 'Sound Effect', - 0x2A: 'Particle', - 0x2B: 'Change Game State', - 0x2C: 'Spawn Global Entity', - 0x2D: 'Open Window', - 0x2E: 'Close Window', - 0x2F: 'Set Slot', - 0x30: 'Window Items', - 0x31: 'Window Property', - 0x32: 'Confirm Transaction', - 0x33: 'Update Sign', - 0x34: 'Maps', - 0x35: 'Update Block Entity', - 0x36: 'Sign Editor Open', - 0x37: 'Statistics', - 0x38: 'Player List Item', - 0x39: 'Player Abilities', - 0x3A: 'Tab-Complete', - 0x3B: 'Scoreboard Objective', - 0x3C: 'Update Score', - 0x3D: 'Display Scoreboard', - 0x3E: 'Teams', - 0x3F: 'Plugin Message', - 0x40: 'Disconnect', - }, + PLAY_STATE: { + SERVER_TO_CLIENT: { + 0x00: 'Keep Alive', + 0x01: 'Join Game', + 0x02: 'Chat Message', + 0x03: 'Time Update', + 0x04: 'Entity Equipment', + 0x05: 'Spawn Position', + 0x06: 'Update Health', + 0x07: 'Respawn', + 0x08: 'Player Position and Look', + 0x09: 'Held Item Change', + 0x0A: 'Use Bed', + 0x0B: 'Animation', + 0x0C: 'Spawn Player', + 0x0D: 'Collect Item', + 0x0E: 'Spawn Object', + 0x0F: 'Spawn Mob', + 0x10: 'Spawn Painting', + 0x11: 'Spawn Experience Orb', + 0x12: 'Entity Velocity', + 0x13: 'Destroy Entities', + 0x14: 'Entity', + 0x15: 'Entity Relative Move', + 0x16: 'Entity Look', + 0x17: 'Entity Look and Relative Move', + 0x18: 'Entity Teleport', + 0x19: 'Entity Head Look', + 0x1A: 'Entity Status', + 0x1B: 'Attach Entity', + 0x1C: 'Entity Metadata', + 0x1D: 'Entity Effect', + 0x1E: 'Remove Entity Effect', + 0x1F: 'Set Experience', + 0x20: 'Entity Properties', + 0x21: 'Chunk Data', + 0x22: 'Multi Block Change', + 0x23: 'Block Change', + 0x24: 'Block Action', + 0x25: 'Block Break Animation', + 0x26: 'Map Chunk Bulk', + 0x27: 'Explosion', + 0x28: 'Effect', + 0x29: 'Sound Effect', + 0x2A: 'Particle', + 0x2B: 'Change Game State', + 0x2C: 'Spawn Global Entity', + 0x2D: 'Open Window', + 0x2E: 'Close Window', + 0x2F: 'Set Slot', + 0x30: 'Window Items', + 0x31: 'Window Property', + 0x32: 'Confirm Transaction', + 0x33: 'Update Sign', + 0x34: 'Maps', + 0x35: 'Update Block Entity', + 0x36: 'Sign Editor Open', + 0x37: 'Statistics', + 0x38: 'Player List Item', + 0x39: 'Player Abilities', + 0x3A: 'Tab-Complete', + 0x3B: 'Scoreboard Objective', + 0x3C: 'Update Score', + 0x3D: 'Display Scoreboard', + 0x3E: 'Teams', + 0x3F: 'Plugin Message', + 0x40: 'Disconnect', + 0x41: 'Server Difficulty', + 0x42: 'Combat Event', + 0x43: 'Camera', + 0x44: 'World Border', + 0x45: 'Title', + 0x46: 'Set Compression', + 0x47: 'Player List Header/Footer', + 0x48: 'Resource Pack Send', + 0x49: 'Update Entity NBT', + }, - CLIENT_TO_SERVER: { - 0x00: 'Keep Alive', - 0x01: 'Chat Message', - 0x02: 'Use Entity', - 0x03: 'Player', - 0x04: 'Player Position', - 0x05: 'Player Look', - 0x06: 'Player Position and Look', - 0x07: 'Player Digging', - 0x08: 'Player Block Placement', - 0x09: 'Held Item Change', - 0x0A: 'Animation', - 0x0B: 'Entity Action', - 0x0C: 'Steer Vehicle', - 0x0D: 'Close Window', - 0x0E: 'Click Window', - 0x0F: 'Confirm Transaction', - 0x10: 'Creative Inventory Action', - 0x11: 'Enchant Item', - 0x12: 'Update Sign', - 0x13: 'Player Abilities', - 0x14: 'Tab-Complete', - 0x15: 'Client Settings', - 0x16: 'Client Status', - 0x17: 'Plugin Message', - }, - }, + CLIENT_TO_SERVER: { + 0x00: 'Keep Alive', + 0x01: 'Chat Message', + 0x02: 'Use Entity', + 0x03: 'Player', + 0x04: 'Player Position', + 0x05: 'Player Look', + 0x06: 'Player Position and Look', + 0x07: 'Player Digging', + 0x08: 'Player Block Placement', + 0x09: 'Held Item Change', + 0x0A: 'Animation', + 0x0B: 'Entity Action', + 0x0C: 'Steer Vehicle', + 0x0D: 'Close Window', + 0x0E: 'Click Window', + 0x0F: 'Confirm Transaction', + 0x10: 'Creative Inventory Action', + 0x11: 'Enchant Item', + 0x12: 'Update Sign', + 0x13: 'Player Abilities', + 0x14: 'Tab-Complete', + 0x15: 'Client Settings', + 0x16: 'Client Status', + 0x17: 'Plugin Message', + 0x18: 'Spectate', + 0x19: 'Resource Pack Status', + }, + }, } packet_structs = { - HANDSHAKE_STATE: { - SERVER_TO_CLIENT: { - #Empty, server doesn't handshake - }, - CLIENT_TO_SERVER: { - #Handshake - 0x00: ( - (MC_VARINT, 'protocol_version'), - (MC_STRING, 'host'), - (MC_USHORT, 'port'), - (MC_VARINT, 'next_state'), - ), - }, - }, + HANDSHAKE_STATE: { + SERVER_TO_CLIENT: { + #Empty, server doesn't handshake + }, + CLIENT_TO_SERVER: { + #Handshake + 0x00: ( + (MC_VARINT, 'protocol_version'), + (MC_STRING, 'host'), + (MC_USHORT, 'port'), + (MC_VARINT, 'next_state'), + ), + }, + }, - STATUS_STATE: { - SERVER_TO_CLIENT: { - #Status Response - 0x00: ( - (MC_STRING, 'json_response'), - ), - #Status Ping - 0x01: ( - (MC_LONG, 'time'), - ), - }, - CLIENT_TO_SERVER: { - #Status Request - 0x00: ( - #Empty Packet - ), - #Status Ping - 0x01: ( - (MC_LONG, 'time'), - ), - }, - }, + STATUS_STATE: { + SERVER_TO_CLIENT: { + #Status Response + 0x00: ( + (MC_STRING, 'response'), + ), + #Status Ping + 0x01: ( + (MC_LONG, 'time'), + ), + }, + CLIENT_TO_SERVER: { + #Status Request + 0x00: ( + #Empty Packet + ), + #Status Ping + 0x01: ( + (MC_LONG, 'time'), + ), + }, + }, - LOGIN_STATE: { - SERVER_TO_CLIENT: { - #Disconnect - 0x00: ( - (MC_STRING, 'json_data'), - ), - #Encryption Request - 0x01: ( - (MC_STRING, 'server_id'), - #Extension - #byte string 'public_key' - #byte string 'verify_token' - ), - #Login Success - 0x02: ( - (MC_STRING, 'uuid'), - (MC_STRING, 'username'), - ), - }, - CLIENT_TO_SERVER: { - #Login Start - 0x00: ( - (MC_STRING, 'name'), - ), - #Encryption Response - 0x01: ( - #Extension - #byte string 'shared_secret' - #byte string 'verify token' - ), - }, - }, + LOGIN_STATE: { + SERVER_TO_CLIENT: { + #Disconnect + 0x00: ( + (MC_CHAT, 'json_data'), + ), + #Encryption Request + 0x01: ( + (MC_STRING, 'server_id'), + #Extension + #byte string 'public_key' + #byte string 'verify_token' + ), + #Login Success + 0x02: ( + (MC_STRING, 'uuid'), + (MC_STRING, 'username'), + ), + #Set Compression + 0x03: ( + (MC_VARINT, 'threshold'), + ), + }, + CLIENT_TO_SERVER: { + #Login Start + 0x00: ( + (MC_STRING, 'name'), + ), + #Encryption Response + 0x01: ( + #Extension + #byte string 'shared_secret' + #byte string 'verify token' + ), + }, + }, - PLAY_STATE: { - SERVER_TO_CLIENT: { - #Keep Alive - 0x00: ( - (MC_INT, 'keep_alive'), - ), - #Join Game - 0x01: ( - (MC_INT , 'eid'), - (MC_UBYTE , 'gamemode'), - (MC_BYTE , 'dimension'), - (MC_UBYTE , 'difficulty'), - (MC_UBYTE , 'max_players'), - (MC_STRING, 'level_type'), - ), - #Chat Message - 0x02: ( - (MC_STRING, 'json_data'), - ), - #Time Update - 0x03: ( - (MC_LONG, 'world_age'), - (MC_LONG, 'time_of_day'), - ), - #Entity Equipment - 0x04: ( - (MC_INT , 'eid'), - (MC_SHORT, 'slot'), - (MC_SLOT , 'item'), - ), - #Spawn Position - 0x05: ( - (MC_INT, 'x'), - (MC_INT, 'y'), - (MC_INT, 'z'), - ), - #Update Health - 0x06: ( - (MC_FLOAT, 'health'), - (MC_SHORT, 'food'), - (MC_FLOAT, 'saturation'), - ), - #Respawn - 0x07: ( - (MC_INT , 'dimension'), - (MC_UBYTE , 'difficulty'), - (MC_UBYTE , 'gamemode'), - (MC_STRING, 'level_type'), - ), - #Player Position and Look - 0x08: ( - (MC_DOUBLE, 'x'), - (MC_DOUBLE, 'y'), - (MC_DOUBLE, 'z'), - (MC_FLOAT , 'yaw'), - (MC_FLOAT , 'pitch'), - (MC_BOOL , 'on_ground'), - ), - #Held Item Change - 0x09: ( - (MC_BYTE, 'slot'), - ), - #Use Bed - 0x0A: ( - (MC_INT , 'eid'), - (MC_INT , 'x'), - (MC_UBYTE, 'y'), - (MC_INT , 'z') - ), - #Animation - 0x0B: ( - (MC_VARINT, 'eid'), - (MC_UBYTE , 'animation'), - ), - #Spawn Player - 0x0C: ( - (MC_VARINT, 'eid'), - (MC_STRING, 'player_uuid'), - (MC_STRING, 'player_name'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_BYTE , 'yaw'), - (MC_BYTE , 'pitch'), - (MC_SHORT , 'current_item'), - (MC_META , 'metadata'), - ), - #Collect Item - 0x0D: ( - (MC_INT, 'collected_eid'), - (MC_INT, 'collector_eid'), - ), - #Spawn Object - 0x0E: ( - (MC_VARINT, 'eid'), - (MC_UBYTE , 'type'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_BYTE , 'pitch'), - (MC_BYTE , 'yaw'), - (MC_INT , 'obj_data') - #Extension - #If obj_data != 0 - #short 'speed_x' - #short 'speed_y' - #short 'speed_z' - ), - #Spawn Mob - 0x0F: ( - (MC_VARINT, 'eid'), - (MC_UBYTE , 'type'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_BYTE , 'pitch'), - (MC_BYTE , 'head_pitch'), - (MC_BYTE , 'yaw'), - (MC_SHORT , 'velocity_x'), - (MC_SHORT , 'velocity_y'), - (MC_SHORT , 'velocity_z'), - (MC_META , 'metadata'), - ), - #Spawn Painting - 0x10: ( - (MC_VARINT, 'eid'), - (MC_STRING, 'title'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_INT , 'direction'), - ), - #Spawn Experience Orb - 0x11: ( - (MC_VARINT, 'eid'), - (MC_UBYTE , 'type'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_SHORT , 'count'), - ), - #Entity Velocity - 0x12: ( - (MC_INT , 'eid'), - (MC_SHORT, 'velocity_x'), - (MC_SHORT, 'velocity_y'), - (MC_SHORT, 'velocity_z'), - ), - #Destroy Entities - 0x13: ( - #Extension - #List of ints 'eids' - ), - #Entity - 0x14: ( - (MC_INT, 'eid'), - ), - #Entity Relative Move - 0x15: ( - (MC_INT , 'eid'), - (MC_BYTE, 'dx'), - (MC_BYTE, 'dy'), - (MC_BYTE, 'dz'), - ), - #Entity Look - 0x16: ( - (MC_INT , 'eid'), - (MC_BYTE, 'yaw'), - (MC_BYTE, 'pitch'), - ), - #Entity Look and Relative Move - 0x17: ( - (MC_INT , 'eid'), - (MC_BYTE, 'dx'), - (MC_BYTE, 'dy'), - (MC_BYTE, 'dz'), - (MC_BYTE, 'yaw'), - (MC_BYTE, 'pitch'), - ), - #Entity Teleport - 0x18: ( - (MC_INT , 'eid'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_BYTE, 'yaw'), - (MC_BYTE, 'pitch'), - ), - #Entity Head Look - 0x19: ( - (MC_INT , 'eid'), - (MC_BYTE, 'head_yaw'), - ), - #Entity Status - 0x1A: ( - (MC_INT , 'eid'), - (MC_BYTE, 'status') - ), - #Attach Entity - 0x1B: ( - (MC_INT , 'eid'), - (MC_INT , 'v_eid'), - (MC_BOOL, 'leash'), - ), - #Entity Metadata - 0x1C: ( - (MC_INT, 'eid'), - (MC_META , 'metadata') - ), - #Entity Effect - 0x1D: ( - (MC_INT , 'eid'), - (MC_BYTE , 'effect'), - (MC_BYTE , 'amplifier'), - (MC_SHORT, 'duration'), - ), - #Remove Entity Effect - 0x1E: ( - (MC_INT , 'eid'), - (MC_BYTE, 'effect'), - ), - #Set Experience - 0x1F: ( - (MC_FLOAT, 'exp_bar'), - (MC_SHORT, 'level'), - (MC_SHORT, 'total_exp'), - ), - #Entity Properties - 0x20: ( - (MC_INT, 'eid'), - #Extension - #List of dicts 'properties' - #Entity properties are complex beasts - #Consult the decoder to get all of the keys - ), - #Chunk Data - 0x21: ( - (MC_INT , 'chunk_x'), - (MC_INT , 'chunk_z'), - (MC_BOOL , 'continuous'), - (MC_USHORT, 'primary_bitmap'), - (MC_USHORT, 'add_bitmap'), - #Extension - #byte string 'data' - ), - #Multi Block Change - 0x22: ( - (MC_INT , 'chunk_x'), - (MC_INT , 'chunk_z'), - #Extension - #List of dicts 'blocks' - ), - #Block Change - 0x23: ( - (MC_INT , 'x'), - (MC_UBYTE , 'y'), - (MC_INT , 'z'), - (MC_VARINT, 'block_id'), - (MC_UBYTE , 'metadata'), - ), - #Block Action - 0x24: ( - (MC_INT , 'x'), - (MC_SHORT , 'y'), - (MC_INT , 'z'), - (MC_UBYTE , 'byte_1'), - (MC_UBYTE , 'byte_2'), - (MC_VARINT, 'block_id'), - ), - #Block Break Animation - 0x25: ( - (MC_VARINT, 'eid'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - (MC_BYTE , 'stage'), - ), - #Map Chunk Bulk - 0x26: ( - # 'sky_light' is stuck in the middle of - # the packet, so it's easier to handle - # it in an extension - #Extension - #bool 'sky_light' - #byte string 'data' - #List of dicts 'metadata' - #Metadata is identical to 0x21 - #But the 'continuous' bool is assumed True - ), - #Explosion - 0x27: ( - (MC_FLOAT, 'x'), - (MC_FLOAT, 'y'), - (MC_FLOAT, 'z'), - (MC_FLOAT, 'radius'), - # 'player_%' fields at end of packet for - # some reason, easier to handle in extension - #Extension - #List of lists 'blocks' - #Each list is 3 ints x,y,z - #float 'player_x' - #float 'player_y' - #float 'player_z' - ), - #Effect - 0x28: ( - (MC_INT , 'effect'), - (MC_INT , 'x'), - (MC_BYTE, 'y'), - (MC_INT , 'z'), - (MC_INT , 'data'), - (MC_BOOL, 'no_rel_vol'), - ), - #Sound Effect - 0x29: ( - (MC_STRING, 'name'), - (MC_INT , 'ef_x'), - (MC_INT , 'ef_y'), - (MC_INT , 'ef_z'), - (MC_FLOAT , 'vol'), - (MC_UBYTE , 'pitch'), - ), - #Particle - 0x2A: ( - (MC_STRING, 'name'), - (MC_FLOAT , 'x'), - (MC_FLOAT , 'y'), - (MC_FLOAT , 'z'), - (MC_FLOAT , 'off_x'), - (MC_FLOAT , 'off_y'), - (MC_FLOAT , 'off_z'), - (MC_FLOAT , 'speed'), - (MC_FLOAT , 'num'), - ), - #Change Game State - 0x2B: ( - (MC_UBYTE, 'reason'), - (MC_FLOAT, 'value'), - ), - #Spawn Global Entity - 0x2C: ( - (MC_VARINT, 'eid'), - (MC_BYTE, 'type'), - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - ), - #Open Window - 0x2D: ( - (MC_UBYTE , 'window_id'), - (MC_UBYTE , 'inv_type'), - (MC_STRING, 'title'), - (MC_UBYTE , 'slot_count'), - (MC_BOOL , 'use_title'), - ), - #Close Window - 0x2E: ( - (MC_UBYTE, 'window_id'), - ), - #Set Slot - 0x2F: ( - (MC_UBYTE, 'window_id'), - (MC_SHORT, 'slot'), - (MC_SLOT , 'slot_data'), - ), - #Window Items - 0x30: ( - (MC_UBYTE, 'window_id'), - #Extension - #List of slots 'slots' - ), - #Window Property - 0x31: ( - (MC_UBYTE, 'window_id'), - (MC_SHORT, 'property'), - (MC_SHORT, 'value'), - ), - #Confirm Transaction - 0x32: ( - (MC_UBYTE, 'window_id'), - (MC_SHORT, 'action'), - (MC_BOOL , 'accepted'), - ), - #Update Sign - 0x33: ( - (MC_INT , 'x'), - (MC_SHORT , 'y'), - (MC_INT , 'z'), - (MC_STRING, 'line_1'), - (MC_STRING, 'line_2'), - (MC_STRING, 'line_3'), - (MC_STRING, 'line_4'), - ), - #Maps - 0x34: ( - (MC_VARINT, 'item_damage'), - #Extension - #byte string 'data' - ), - #Update Block Entity - 0x35: ( - (MC_INT , 'x'), - (MC_SHORT , 'y'), - (MC_INT , 'z'), - (MC_UBYTE , 'action'), - #Extension - #NBT Data 'nbt' - ), - #Sign Editor Open - 0x36: ( - (MC_INT , 'x'), - (MC_INT , 'y'), - (MC_INT , 'z'), - ), - #Statistics - 0x37: ( - #Extension - #List of lists 'entries' - #First value is a string, stat's name - #Second value is an int, stat's value - ), - #Player List Item - 0x38: ( - (MC_STRING, 'player_name'), - (MC_BOOL , 'online'), - (MC_SHORT , 'ping'), + PLAY_STATE: { + SERVER_TO_CLIENT: { + #Keep Alive + 0x00: ( + (MC_VARINT, 'keep_alive'), + ), + #Join Game + 0x01: ( + (MC_INT , 'eid'), + (MC_UBYTE , 'gamemode'), + (MC_BYTE , 'dimension'), + (MC_UBYTE , 'difficulty'), + (MC_UBYTE , 'max_players'), + (MC_STRING, 'level_type'), + (MC_BOOL , 'reduce_debug'), + ), + #Chat Message + 0x02: ( + (MC_CHAT, 'json_data'), + (MC_BYTE, 'position'), + ), + #Time Update + 0x03: ( + (MC_LONG, 'world_age'), + (MC_LONG, 'time_of_day'), + ), + #Entity Equipment + 0x04: ( + (MC_VARINT, 'eid'), + (MC_SHORT , 'slot'), + (MC_SLOT , 'item'), + ), + #Spawn Position + 0x05: ( + (MC_POSITION, 'location'), + ), + #Update Health + 0x06: ( + (MC_FLOAT , 'health'), + (MC_VARINT, 'food'), + (MC_FLOAT , 'saturation'), + ), + #Respawn + 0x07: ( + (MC_INT , 'dimension'), + (MC_UBYTE , 'difficulty'), + (MC_UBYTE , 'gamemode'), + (MC_STRING, 'level_type'), + ), + #Player Position and Look + 0x08: ( + (MC_DOUBLE, 'x'), + (MC_DOUBLE, 'y'), + (MC_DOUBLE, 'z'), + (MC_FLOAT , 'yaw'), + (MC_FLOAT , 'pitch'), + (MC_BYTE , 'flags'), + ), + #Held Item Change + 0x09: ( + (MC_BYTE, 'slot'), + ), + #Use Bed + 0x0A: ( + (MC_INT , 'eid'), + (MC_POSITION, 'location'), + ), + #Animation + 0x0B: ( + (MC_VARINT, 'eid'), + (MC_UBYTE , 'animation'), + ), + #Spawn Player + 0x0C: ( + (MC_VARINT, 'eid'), + (MC_UUID , 'player_uuid'), + (MC_INT , 'x'), + (MC_INT , 'y'), + (MC_INT , 'z'), + (MC_BYTE , 'yaw'), + (MC_BYTE , 'pitch'), + (MC_SHORT , 'current_item'), + (MC_META , 'metadata'), + ), + #Collect Item + 0x0D: ( + (MC_VARINT, 'collected_eid'), + (MC_VARINT, 'collector_eid'), + ), + #Spawn Object + 0x0E: ( + (MC_VARINT, 'eid'), + (MC_UBYTE , 'type'), + (MC_INT , 'x'), + (MC_INT , 'y'), + (MC_INT , 'z'), + (MC_BYTE , 'pitch'), + (MC_BYTE , 'yaw'), + (MC_INT , 'obj_data'), + #Extension + #If obj_data != 0 + #short 'speed_x' + #short 'speed_y' + #short 'speed_z' + ), + #Spawn Mob + 0x0F: ( + (MC_VARINT, 'eid'), + (MC_UBYTE , 'type'), + (MC_INT , 'x'), + (MC_INT , 'y'), + (MC_INT , 'z'), + (MC_BYTE , 'pitch'), + (MC_BYTE , 'head_pitch'), + (MC_BYTE , 'yaw'), + (MC_SHORT , 'velocity_x'), + (MC_SHORT , 'velocity_y'), + (MC_SHORT , 'velocity_z'), + (MC_META , 'metadata'), + ), + #Spawn Painting + 0x10: ( + (MC_VARINT , 'eid'), + (MC_STRING , 'title'), + (MC_POSITION, 'location'), + (MC_INT , 'direction'), + ), + #Spawn Experience Orb + 0x11: ( + (MC_VARINT, 'eid'), + (MC_INT , 'x'), + (MC_INT , 'y'), + (MC_INT , 'z'), + (MC_SHORT , 'count'), + ), + #Entity Velocity + 0x12: ( + (MC_VARINT, 'eid'), + (MC_SHORT , 'velocity_x'), + (MC_SHORT , 'velocity_y'), + (MC_SHORT , 'velocity_z'), + ), + #Destroy Entities + 0x13: ( + #Extension + #List of ints 'eids' + ), + #Entity + 0x14: ( + (MC_VARINT, 'eid'), + ), + #Entity Relative Move + 0x15: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'dx'), + (MC_BYTE , 'dy'), + (MC_BYTE , 'dz'), + (MC_BOOL , 'on_ground'), #Boats move slower when true + ), + #Entity Look + 0x16: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'yaw'), + (MC_BYTE , 'pitch'), + (MC_BOOL , 'on_ground'), + ), + #Entity Look and Relative Move + 0x17: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'dx'), + (MC_BYTE , 'dy'), + (MC_BYTE , 'dz'), + (MC_BYTE , 'yaw'), + (MC_BYTE , 'pitch'), + (MC_BOOL , 'on_ground'), + ), + #Entity Teleport + 0x18: ( + (MC_VARINT, 'eid'), + (MC_INT , 'x'), + (MC_INT , 'y'), + (MC_INT , 'z'), + (MC_BYTE , 'yaw'), + (MC_BYTE , 'pitch'), + (MC_BOOL , 'on_ground'), + ), + #Entity Head Look + 0x19: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'head_yaw'), + ), + #Entity Status + 0x1A: ( + (MC_INT , 'eid'), + (MC_BYTE, 'status') + ), + #Attach Entity + 0x1B: ( + (MC_INT , 'eid'), + (MC_INT , 'v_eid'), + (MC_BOOL, 'leash'), + ), + #Entity Metadata + 0x1C: ( + (MC_VARINT, 'eid'), + (MC_META , 'metadata'), + ), + #Entity Effect + 0x1D: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'effect'), + (MC_BYTE , 'amplifier'), + (MC_SHORT , 'duration'), + (MC_BOOL , 'no_particles'), + ), + #Remove Entity Effect + 0x1E: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'effect'), + ), + #Set Experience + 0x1F: ( + (MC_FLOAT , 'exp_bar'), + (MC_VARINT, 'level'), + (MC_VARINT, 'total_exp'), + ), + #Entity Properties + 0x20: ( + (MC_VARINT, 'eid'), + #Extension + #List of dicts 'properties' + #Entity properties are complex beasts + #Consult the decoder to get all of the keys + ), + #Chunk Data + 0x21: ( + (MC_INT , 'chunk_x'), + (MC_INT , 'chunk_z'), + (MC_BOOL , 'continuous'), + (MC_USHORT, 'primary_bitmap'), + #Extension + #byte string 'data' + ), + #Multi Block Change + 0x22: ( + (MC_INT , 'chunk_x'), + (MC_INT , 'chunk_z'), + #Extension + #List of dicts 'blocks' + ), + #Block Change + 0x23: ( + (MC_POSITION, 'location'), + (MC_VARINT , 'block_data'), + ), + #Block Action + 0x24: ( + (MC_POSITION, 'location'), + (MC_UBYTE , 'byte_1'), + (MC_UBYTE , 'byte_2'), + (MC_VARINT , 'block_id'), + ), + #Block Break Animation + 0x25: ( + (MC_VARINT , 'eid'), + (MC_POSITION, 'location'), + (MC_BYTE , 'stage'), + ), + #Map Chunk Bulk + 0x26: ( + #Extension + #bool 'sky_light' + #List of dicts 'metadata' + #byte string 'data' + #Metadata is identical to 0x21 + #But the 'continuous' bool is assumed True + ), + #Explosion + 0x27: ( + (MC_FLOAT, 'x'), + (MC_FLOAT, 'y'), + (MC_FLOAT, 'z'), + (MC_FLOAT, 'radius'), + #Extension + #List of lists 'blocks' + #Each list is 3 ints x,y,z + #float 'player_x' + #float 'player_y' + #float 'player_z' + ), + #Effect + 0x28: ( + (MC_INT , 'effect'), + (MC_POSITION, 'location'), + (MC_INT , 'data'), + (MC_BOOL , 'no_rel_vol'), + ), + #Sound Effect + 0x29: ( + (MC_STRING, 'name'), + (MC_INT , 'ef_x'), + (MC_INT , 'ef_y'), + (MC_INT , 'ef_z'), + (MC_FLOAT , 'vol'), + (MC_UBYTE , 'pitch'), + ), + #Particle + 0x2A: ( + (MC_INT , 'id'), + (MC_BOOL , 'long_dist'), + (MC_FLOAT , 'x'), + (MC_FLOAT , 'y'), + (MC_FLOAT , 'z'), + (MC_FLOAT , 'off_x'), + (MC_FLOAT , 'off_y'), + (MC_FLOAT , 'off_z'), + (MC_FLOAT , 'speed'), + (MC_INT , 'num'), + #Extension + #List of ints 'data' + #Possibly zero length list of + #particle-dependent data + ), + #Change Game State + 0x2B: ( + (MC_UBYTE, 'reason'), + (MC_FLOAT, 'value'), + ), + #Spawn Global Entity + 0x2C: ( + (MC_VARINT, 'eid'), + (MC_BYTE, 'type'), + (MC_INT , 'x'), + (MC_INT , 'y'), + (MC_INT , 'z'), + ), + #Open Window + 0x2D: ( + (MC_UBYTE , 'window_id'), + (MC_STRING, 'inv_type'), + (MC_CHAT , 'title'), + (MC_UBYTE , 'slot_count'), + (MC_BOOL , 'use_title'), + #Extension + #Only present if 'inv_type' == 'EntityHorse' + #MC_INT 'eid' + ), + #Close Window + 0x2E: ( + (MC_UBYTE, 'window_id'), + ), + #Set Slot + 0x2F: ( + (MC_BYTE , 'window_id'), + (MC_SHORT, 'slot'), + (MC_SLOT , 'slot_data'), + ), + #Window Items + 0x30: ( + (MC_UBYTE, 'window_id'), + #Extension + #List of slots 'slots' + ), + #Window Property + 0x31: ( + (MC_UBYTE, 'window_id'), + (MC_SHORT, 'property'), + (MC_SHORT, 'value'), + ), + #Confirm Transaction + 0x32: ( + (MC_UBYTE, 'window_id'), + (MC_SHORT, 'action'), + (MC_BOOL , 'accepted'), + ), + #Update Sign + 0x33: ( + (MC_POSITION, 'location'), + (MC_CHAT , 'line_1'), + (MC_CHAT , 'line_2'), + (MC_CHAT , 'line_3'), + (MC_CHAT , 'line_4'), + ), + #Maps + 0x34: ( + (MC_VARINT, 'item_damage'), + (MC_BYTE , 'scale'), + #Extension + #List of tuples 'icons', (Direction, Type, X, Y) + #MC_BYTE 'columns' + #If Columns > 0 + #MC_BYTE 'rows' + #MC_BYTE 'x' + #MC_BYTE 'y' + #byte string 'data' + ), + #Update Block Entity + 0x35: ( + (MC_POSITION, 'location'), + (MC_UBYTE , 'action'), + #Extension + #NBT Data 'nbt' + ), + #Sign Editor Open + 0x36: ( + (MC_POSITION, 'location'), + ), + #Statistics + 0x37: ( + #Extension + #List of lists 'entries' + #First value is a string, stat's name + #Second value is an int, stat's value + ), + #Player List Item + 0x38: ( + (MC_VARINT, 'action'), + #Extension + #List of dicts 'player_list' + #MC_UUID 'uuid' + #action == 0, ADD_PLAYER + #MC_STRING 'name' + #List of dicts, 'properties' + #MC_STRING 'name' + #MC_STRING 'value' + #MC_BOOL 'signed' + #signed == True + #MC_STRING 'signature' + #MC_VARINT 'gamemode' + #MC_VARINT 'ping' + #MC_BOOL 'has_display' + #has_display == True + #MC_CHAT 'display_name' + #action == 1 UPDATE_GAMEMODE + #MC_VARINT 'gamemode' + #action == 2 UPDATE_LATENCY + #MC_VARINT 'ping' + #action == 3 UPDATE_DISPLAY + #MC_BOOL 'has_display' + #has_display == True + #MC_CHAT 'dsiplay_name' + #action == 4 REMOVE_PLAYER + #No extra fields + ), + #Player Abilities + 0x39: ( + (MC_BYTE , 'flags'), + (MC_FLOAT, 'flying_speed'), + (MC_FLOAT, 'walking_speed'), + ), + #Tab-Complete + 0x3A: ( + #Extension + #List of strings 'matches' + ), + #Scoreboard Objective + 0x3B: ( + (MC_STRING, 'obj_name'), + (MC_BYTE , 'action'), + #Extension + #action == 0 or action == 2 + #MC_STRING 'obj_val' + #MC_STRING 'type' + ), + #Update Score + 0x3C: ( + (MC_STRING, 'item_name'), + (MC_BYTE , 'action'), + (MC_STRING, 'score_name'), + #Extension + #action == 0 + #MC_VARINT 'value' + ), + #Display Scoreboard + 0x3D: ( + (MC_BYTE , 'position'), + (MC_STRING, 'score_name'), + ), + #Teams + 0x3E: ( + (MC_STRING, 'team_name'), + (MC_BYTE , 'action'), + #Extension + #Depends on action + #0 gets all fields + #1 gets no fields + #For 2: + #MC_STRING 'display_name' + #MC_STRING 'team_prefix' + #MC_STRING 'team_suffix' + #MC_BYTE 'friendly_fire' + #MC_STRING 'name_visibility' + #For 3 or 4: + # List of strings 'players' + ), + #Plugin Message + 0x3F: ( + (MC_STRING, 'channel'), + #Extension + #byte string 'data' + ), + #Disconnect + 0x40: ( + (MC_STRING, 'reason'), + ), + #Server Difficulty + 0x41: ( + (MC_UBYTE, 'difficulty'), + ), + #Combat Event + 0x42: ( + (MC_VARINT, 'event'), + #Extension + #CE_END_COMBAT + #MC_VARINT 'duration' + #MC_INT 'eid' + #CE_ENTITY_DEAD + #MC_VARINT 'player_id' + #MC_INT 'eid' + #MC_STRING 'message' + ), + #Camera + 0x43: ( + (MC_VARINT, 'camera_id'), + ), + #World Border + 0x44: ( + (MC_VARINT, 'action'), + #Extension + #WB_SET_SIZE + #MC_DOUBLE 'radius' + #WB_LERP_SIZE + #MC_DOUBLE 'old_radius' + #MC_DOUBLE 'new_radius' + #MC_VARLONG 'speed' + #WB_SET_CENTER + #MC_DOUBLE 'x' + #MC_DOUBLE 'z' + #WB_INITIALIZE + #MC_DOUBLE 'x' + #MC_DOUBLE 'z' + #MC_DOUBLE 'old_radius' + #MC_DOUBLE 'new_radius' + #MC_VARLONG 'speed' + #MC_VARINT 'port_tele_bound' #Portal Teleport Boundary + #MC_VARINT 'warn_time' + #MC_VARINT 'warn_blocks' + #WB_SET_WARN_TIME + #MC_VARINT 'warn_time' + #WB_SET_WARN_BLOCKS + #MC_VARINT 'warn_blocks' + ), + #Title + 0x45: ( + (MC_VARINT, 'action'), + #Extension + #TL_TITLE + #TL_SUBTITLE + #MC_CHAT 'text' + #TL_TIMES + #MC_INT 'fade_in' + #MC_INT 'stay' + #MC_INT 'fade_out' + ), + #Set Compression + 0x46: ( + (MC_VARINT, 'threshold'), + ), + #Play List Header/Footer + 0x47: ( + (MC_CHAT, 'header'), + (MC_CHAT, 'footer'), + ), + #Resource Pack Send + 0x48: ( + (MC_STRING, 'url'), + (MC_STRING, 'hash'), + ), + #Update Entity NBT + 0x48: ( + (MC_VARINT, 'eid'), + #Extension + #NBT Data 'nbt' + ), + }, - ), - #Player Abilities - 0x39: ( - (MC_BYTE, 'flags'), - (MC_FLOAT, 'flying_speed'), - (MC_FLOAT, 'walking_speed'), - ), - #Tab-Complete - 0x3A: ( - #Extension - #List of strings 'matches' - ), - #Scoreboard Objective - 0x3B: ( - (MC_STRING, 'obj_name'), - (MC_STRING, 'obj_val'), - (MC_BYTE , 'action'), - ), - #Update Score - 0x3C: ( - (MC_STRING, 'item_name'), - (MC_BYTE , 'action'), - (MC_STRING, 'score_name'), - (MC_INT , 'value'), - ), - #Display Scoreboard - 0x3D: ( - (MC_BYTE , 'position'), - (MC_STRING, 'score_name'), - ), - #Teams - 0x3E: ( - (MC_STRING, 'team_name'), - (MC_BYTE , 'mode'), - #Extension - #Depends on mode - #0 gets all fields - #1 gets no fields - #For 2: - #string 'display_name' - #string 'team_prefix' - #string 'team_suffix' - #byte 'friendly_fire' - #For 3 or 4: - # List of strings 'players' - ), - #Plugin Message - 0x3F: ( - (MC_STRING, 'channel'), - #Extension - #byte string 'data' - ), - #Disconnect - 0x40: ( - (MC_STRING, 'reason'), - ), - }, - - CLIENT_TO_SERVER: { - #Keep Alive - 0x00: ( - (MC_INT, 'keep_alive'), - ), - #Chat Message - 0x01: ( - (MC_STRING, 'message'), - ), - #Use Entity - 0x02: ( - (MC_INT , 'target'), - (MC_BYTE, 'mouse'), - ), - #Player - 0x03: ( - (MC_BOOL, 'on_ground'), - ), - #Player Position - 0x04: ( - (MC_DOUBLE, 'x'), - (MC_DOUBLE, 'stance'), - (MC_DOUBLE, 'y'), - (MC_DOUBLE, 'z'), - (MC_BOOL , 'on_ground'), - ), - #Player Look - 0x05: ( - (MC_FLOAT, 'yaw'), - (MC_FLOAT, 'pitch'), - (MC_BOOL , 'on_ground'), - ), - #Player Position and Look - 0x06: ( - (MC_DOUBLE, 'x'), - (MC_DOUBLE, 'stance'), - (MC_DOUBLE, 'y'), - (MC_DOUBLE, 'z'), - (MC_FLOAT, 'yaw'), - (MC_FLOAT, 'pitch'), - (MC_BOOL , 'on_ground'), - ), - #Player Digging - 0x07: ( - (MC_BYTE , 'status'), - (MC_INT , 'x'), - (MC_UBYTE, 'y'), - (MC_INT , 'z'), - (MC_BYTE , 'face'), - ), - #Player Block Placement - 0x08: ( - (MC_INT , 'x'), - (MC_UBYTE, 'y'), - (MC_INT , 'z'), - (MC_BYTE , 'direction'), - (MC_SLOT , 'held_item'), - (MC_BYTE , 'cur_pos_x'), - (MC_BYTE , 'cur_pos_y'), - (MC_BYTE , 'cur_pos_z'), - ), - #Held Item Change - 0x09: ( - (MC_SHORT, 'slot'), - ), - #Animation - 0x0A: ( - (MC_INT, 'eid'), - (MC_BYTE, 'animation'), - ), - #Entity Action - 0x0B: ( - (MC_INT , 'eid'), - (MC_BYTE, 'action'), - (MC_INT , 'jump_boost'), - ), - #Steer Vehicle - 0x0C: ( - (MC_FLOAT, 'sideways'), - (MC_FLOAT, 'forward'), - (MC_BOOL , 'jump'), - (MC_BOOL , 'unmount'), - ), - #Close Window - 0x0D: ( - (MC_BYTE, 'window_id'), - ), - #Click Window - 0x0E: ( - (MC_BYTE , 'window_id'), - (MC_SHORT, 'slot'), - (MC_BYTE , 'button'), - (MC_SHORT, 'action'), - (MC_BYTE , 'mode'), - (MC_SLOT , 'clicked_item'), - ), - #Confirm Transaction - 0x0F: ( - (MC_BYTE , 'window_id'), - (MC_SHORT, 'action'), - (MC_BOOL , 'accepted'), - ), - #Creative Inventory Action - 0x10: ( - (MC_SHORT, 'slot'), - (MC_SLOT , 'clicked_item'), - ), - #Enchant Item - 0x11: ( - (MC_BYTE, 'window_id'), - (MC_BYTE, 'enchantment'), - ), - #Update Sign - 0x12: ( - (MC_INT , 'x'), - (MC_SHORT , 'y'), - (MC_INT , 'z'), - (MC_STRING, 'line_1'), - (MC_STRING, 'line_2'), - (MC_STRING, 'line_3'), - (MC_STRING, 'line_4'), - ), - #Player Abilities - 0x13: ( - (MC_BYTE, 'flags'), - (MC_FLOAT, 'flying_speed'), - (MC_FLOAT, 'walking_speed'), - ), - #Tab-Complete - 0x14: ( - (MC_STRING, 'text'), - ), - #Client Settings - 0x15: ( - (MC_STRING, 'locale'), - (MC_BYTE , 'view_distance'), - (MC_BYTE , 'chat_flags'), - (MC_BOOL , 'chat_colors'), - (MC_BYTE , 'difficulty'), - (MC_BOOL , 'show_cape'), - ), - #Client Status - 0x16: ( - (MC_BYTE, 'action'), - ), - #Plugin Message - 0x17: ( - (MC_STRING, 'channel'), - #Extension - #byte string 'data' - ), - }, - }, + CLIENT_TO_SERVER: { + #Keep Alive + 0x00: ( + (MC_VARINT, 'keep_alive'), + ), + #Chat Message + 0x01: ( + (MC_STRING, 'message'), + ), + #Use Entity + 0x02: ( + (MC_VARINT, 'target'), + (MC_VARINT, 'action'), + #Extension + #action == UE_INTERACT_AT + #MC_FLOAT 'target_x' + #MC_FLOAT 'target_y' + #MC_FLOAT 'target_z' + ), + #Player + 0x03: ( + (MC_BOOL, 'on_ground'), + ), + #Player Position + 0x04: ( + (MC_DOUBLE, 'x'), + (MC_DOUBLE, 'y'), + (MC_DOUBLE, 'z'), + (MC_BOOL , 'on_ground'), + ), + #Player Look + 0x05: ( + (MC_FLOAT, 'yaw'), + (MC_FLOAT, 'pitch'), + (MC_BOOL , 'on_ground'), + ), + #Player Position and Look + 0x06: ( + (MC_DOUBLE, 'x'), + (MC_DOUBLE, 'y'), + (MC_DOUBLE, 'z'), + (MC_FLOAT, 'yaw'), + (MC_FLOAT, 'pitch'), + (MC_BOOL , 'on_ground'), + ), + #Player Digging + 0x07: ( + (MC_BYTE , 'status'), + (MC_POSITION, 'location'), + (MC_BYTE , 'face'), + ), + #Player Block Placement + 0x08: ( + (MC_POSITION, 'location'), + (MC_BYTE , 'direction'), + (MC_SLOT , 'held_item'), + (MC_BYTE , 'cur_pos_x'), + (MC_BYTE , 'cur_pos_y'), + (MC_BYTE , 'cur_pos_z'), + ), + #Held Item Change + 0x09: ( + (MC_SHORT, 'slot'), + ), + #Animation + #Is This ever used? Is this supposed to be empty?? + 0x0A: ( + (MC_VARINT, 'eid'), + (MC_BYTE , 'animation'), + ), + #Entity Action + 0x0B: ( + (MC_VARINT, 'eid'), + (MC_VARINT, 'action'), + (MC_VARINT, 'jump_boost'), + ), + #Steer Vehicle + 0x0C: ( + (MC_FLOAT, 'sideways'), + (MC_FLOAT, 'forward'), + (MC_UBYTE, 'flags'), + ), + #Close Window + 0x0D: ( + (MC_BYTE, 'window_id'), + ), + #Click Window + 0x0E: ( + (MC_BYTE , 'window_id'), + (MC_SHORT, 'slot'), + (MC_BYTE , 'button'), + (MC_SHORT, 'action'), + (MC_BYTE , 'mode'), + (MC_SLOT , 'clicked_item'), + ), + #Confirm Transaction + 0x0F: ( + (MC_BYTE , 'window_id'), + (MC_SHORT, 'action'), + (MC_BOOL , 'accepted'), + ), + #Creative Inventory Action + 0x10: ( + (MC_SHORT, 'slot'), + (MC_SLOT , 'clicked_item'), + ), + #Enchant Item + 0x11: ( + (MC_BYTE, 'window_id'), + (MC_BYTE, 'enchantment'), + ), + #Update Sign + 0x12: ( + (MC_POSITION, 'location'), + (MC_CHAT , 'line_1'), + (MC_CHAT , 'line_2'), + (MC_CHAT , 'line_3'), + (MC_CHAT , 'line_4'), + ), + #Player Abilities + 0x13: ( + (MC_BYTE , 'flags'), + (MC_FLOAT, 'flying_speed'), + (MC_FLOAT, 'walking_speed'), + ), + #Tab-Complete + 0x14: ( + (MC_STRING, 'text'), + (MC_BOOL , 'has_position') + #Extension + #has_position == True + #MC_POSITION 'block_loc' + ), + #Client Settings + 0x15: ( + (MC_STRING, 'locale'), + (MC_BYTE , 'view_distance'), + (MC_BYTE , 'chat_flags'), + (MC_BOOL , 'chat_colours'), + (MC_UBYTE , 'skin_flags'), + ), + #Client Status + 0x16: ( + (MC_VARINT, 'action'), + ), + #Plugin Message + 0x17: ( + (MC_STRING, 'channel'), + #Extension + #byte string 'data' + ), + #Spectate + 0x18: ( + (MC_UUID, 'target_player'), + ), + #Resource Pack Status + 0x19: ( + (MC_STRING, 'hash'), + (MC_VARINT, 'result'), + ), + }, + }, } #Useful for some lookups hashed_names = { - (state, direction, packet_id): - packet_names[state][direction][packet_id] - for state in packet_names - for direction in packet_names[state] - for packet_id in packet_names[state][direction] + (state, direction, packet_id): + packet_names[state][direction][packet_id] + for state in packet_names + for direction in packet_names[state] + for packet_id in packet_names[state][direction] } hashed_structs = { - (state, direction, packet_id): - packet_structs[state][direction][packet_id] - for state in packet_structs - for direction in packet_structs[state] - for packet_id in packet_structs[state][direction] + (state, direction, packet_id): + packet_structs[state][direction][packet_id] + for state in packet_structs + for direction in packet_structs[state] + for packet_id in packet_structs[state][direction] } -#Lookup packets by name -packet_idents = {} -for state in packet_names: - for direction in packet_names[state]: - for packet_id in packet_names[state][direction]: - packet_idents[state_names[state] + direction_names[direction] + packet_names[state][direction][packet_id]] = (state, direction, packet_id) +state_lookup = "HANDSHAKE", "STATUS", "LOGIN", "PLAY" + +packet_ident2str = { + (state, direction, packet_id): + state_lookup[state] + ("<", ">")[direction] + packet_names[state][direction][packet_id] + for state in packet_structs + for direction in packet_structs[state] + for packet_id in packet_structs[state][direction] +} +packet_str2ident = {v: k for k, v in packet_ident2str.items()} #Pack the protocol more efficiently packet_names = tuple(tuple(packet_names[i][j] for j in (0,1)) for i in (0,1,2,3)) diff --git a/spock/mcp/mcpacket.py b/spock/mcp/mcpacket.py index e51d555..d68bd24 100644 --- a/spock/mcp/mcpacket.py +++ b/spock/mcp/mcpacket.py @@ -1,107 +1,99 @@ import copy +import zlib from time import gmtime, strftime from spock import utils from spock.mcp import datautils, mcdata from spock.mcp.mcpacket_extensions import hashed_extensions from spock.mcp.mcdata import ( - MC_BOOL, MC_UBYTE, MC_BYTE, MC_USHORT, MC_SHORT, MC_UINT, MC_INT, - MC_LONG, MC_FLOAT, MC_DOUBLE, MC_STRING, MC_VARINT, MC_SLOT, MC_META + MC_BOOL, MC_UBYTE, MC_BYTE, MC_USHORT, MC_SHORT, MC_UINT, MC_INT, + MC_LONG, MC_FLOAT, MC_DOUBLE, MC_VARINT, MC_VARLONG, MC_UUID, MC_POSITION, + MC_STRING, MC_CHAT, MC_SLOT, MC_META ) -#TODO: Wow this class ended up a bit of a mess, cleanup and refactor soon^TM -from spock.utils import BufferUnderflowException - - class Packet(object): - length = None - id = 0x00 - state = None - direction = None - - def __init__(self, ident=(mcdata.HANDSHAKE_STATE, mcdata.CLIENT_TO_SERVER, 0x00), data=None): - if isinstance(ident, str): - ident = mcdata.packet_idents[ident] - - if len(ident) == 3: - self.state, self.direction, self.id = ident - else: - self.state, self.direction = ident - - self.__hash_ident() - self.data = data if data else {} - - def __hash_ident(self): - self.__hashed_ident = (self.state, self.direction, self.id) - - def clone(self): - return Packet(ident=self.ident(), data=copy.deepcopy(self.data)) - - def ident(self, state=None, direction=None, id=None): - if state is not None: - self.state = state - self.__hashed_ident = None - if direction is not None: - self.direction = direction - self.__hashed_ident = None - if id is not None: - self.id = id - self.__hashed_ident = None - if self.__hashed_ident is None: - self.__hash_ident() - return self.__hashed_ident - - def decode(self, bbuff): - self.data = {} - self.length = datautils.unpack(MC_VARINT, bbuff) - encoded = bbuff.recv(self.length) - try: - pbuff = utils.BoundBuffer(encoded) - - #Ident - self.id = datautils.unpack(MC_VARINT, pbuff) - self.__hash_ident() - - #Payload - for dtype, name in mcdata.hashed_structs[self.__hashed_ident]: - try: - self.data[name] = datautils.unpack(dtype, pbuff) - except BufferUnderflowException: - raise Exception("Failed to parse field {0}:{1} from packet {2}".format(name, dtype, repr(self))) - - #Extension - if self.__hashed_ident in hashed_extensions: - hashed_extensions[self.__hashed_ident].decode_extra(self, pbuff) - - return self - - except BufferUnderflowException: - raise Exception("Failed to parse packet: ", repr(self)) - - def encode(self): - #Ident - o = datautils.pack(MC_VARINT, self.id) - #Payload - for dtype, name in mcdata.hashed_structs[self.__hashed_ident]: - o += datautils.pack(dtype, self.data[name]) - #Extension - if self.__hashed_ident in hashed_extensions: - o += hashed_extensions[self.__hashed_ident].encode_extra(self) - return datautils.pack(MC_VARINT, len(o)) + o - - def __repr__(self): - if self.direction == mcdata.CLIENT_TO_SERVER: - s = ">>>" - else: - s = "<<<" - - if self.length is None: - length = "?" - else: - length = str(self.length) - - data = copy.copy(self.data) - if self.ident() == mcdata.packet_idents['PLAY= proto_comp_threshold: + o = zlib.compress(o, comp_level) + ulen_varint = datautils.pack(MC_VARINT, uncompressed_len) + header = datautils.pack(MC_VARINT, uncompressed_len + len(ulen_varint)) + header += ulen_varint + return header + o + elif proto_comp_state == mcdata.PROTO_COMP_OFF: + return datautils.pack(MC_VARINT, len(o)) + o + else: + return None + + def __repr__(self): + if self.ident[1] == mcdata.CLIENT_TO_SERVER: s = ">>>" + else: s = "<<<" + format = "[%s] %s (0x%02X, 0x%02X): %-"+str(max([len(i) for i in mcdata.hashed_names.values()])+1)+"s%s" + return format % (strftime("%H:%M:%S", gmtime()), s, self.ident[0], self.ident[2], mcdata.hashed_names[self.ident], str(self.data)) diff --git a/spock/mcp/mcpacket_extensions.py b/spock/mcp/mcpacket_extensions.py index 4095902..b59cb0f 100644 --- a/spock/mcp/mcpacket_extensions.py +++ b/spock/mcp/mcpacket_extensions.py @@ -5,355 +5,587 @@ from spock.mcp import nbt from spock import utils from spock.mcp.mcdata import ( - MC_BOOL, MC_UBYTE, MC_BYTE, MC_USHORT, MC_SHORT, MC_UINT, MC_INT, - MC_LONG, MC_FLOAT, MC_DOUBLE, MC_STRING, MC_VARINT, MC_SLOT, MC_META + MC_BOOL, MC_UBYTE, MC_BYTE, MC_USHORT, MC_SHORT, MC_UINT, MC_INT, + MC_LONG, MC_FLOAT, MC_DOUBLE, MC_VARINT, MC_VARLONG, MC_UUID, MC_POSITION, + MC_STRING, MC_CHAT, MC_SLOT, MC_META ) hashed_extensions = {} extensions = tuple(tuple({} for i in j) for j in mcdata.packet_structs) def extension(ident): - def inner(cl): - hashed_extensions[ident] = cl - extensions[ident[0]][ident[1]][ident[2]] = cl - return cl - return inner + def inner(cl): + hashed_extensions[ident] = cl + extensions[ident[0]][ident[1]][ident[2]] = cl + return cl + return inner #Login SERVER_TO_CLIENT 0x01 Encryption Request @extension((mcdata.LOGIN_STATE, mcdata.SERVER_TO_CLIENT, 0x01)) class ExtensionLSTC01: - def decode_extra(packet, bbuff): - length = datautils.unpack(MC_SHORT, bbuff) - packet.data['public_key'] = bbuff.recv(length) - length = datautils.unpack(MC_SHORT, bbuff) - packet.data['verify_token'] = bbuff.recv(length) - return packet - - def encode_extra(packet): - o = datautils.pack(MC_SHORT, len(packet.data['public_key'])) - o += packet.data['public_key'] - o += datautils.pack(MC_SHORT, len(packet.data['verify_token'])) - o += packet.data['verify_token'] - return o + def decode_extra(packet, bbuff): + length = datautils.unpack(MC_VARINT, bbuff) + packet.data['public_key'] = bbuff.recv(length) + length = datautils.unpack(MC_VARINT, bbuff) + packet.data['verify_token'] = bbuff.recv(length) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['public_key'])) + o += packet.data['public_key'] + o += datautils.pack(MC_VARINT, len(packet.data['verify_token'])) + o += packet.data['verify_token'] + return o #Login CLIENT_TO_SERVER 0x01 Encryption Response @extension((mcdata.LOGIN_STATE, mcdata.CLIENT_TO_SERVER, 0x01)) class ExtensionLCTS01: - def decode_extra(packet, bbuff): - length = datautils.unpack(MC_SHORT, bbuff) - packet.data['shared_secret'] = bbuff.recv(length) - length = datautils.unpack(MC_SHORT, bbuff) - packet.data['verify_token'] = bbuff.recv(length) - return packet - - def encode_extra(packet): - o = datautils.pack(MC_SHORT, len(packet.data['shared_secret'])) - o += packet.data['shared_secret'] - o += datautils.pack(MC_SHORT, len(packet.data['verify_token'])) - o += packet.data['verify_token'] - return o + def decode_extra(packet, bbuff): + length = datautils.unpack(MC_VARINT, bbuff) + packet.data['shared_secret'] = bbuff.recv(length) + length = datautils.unpack(MC_VARINT, bbuff) + packet.data['verify_token'] = bbuff.recv(length) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['shared_secret'])) + o += packet.data['shared_secret'] + o += datautils.pack(MC_VARINT, len(packet.data['verify_token'])) + o += packet.data['verify_token'] + return o #Play SERVER_TO_CLIENT 0x0E Spawn Object @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x0E)) class ExtensionPSTC0E: - def decode_extra(packet, bbuff): - if packet.data['obj_data']: - packet.data['speed_x'] = datautils.unpack(MC_SHORT, bbuff) - packet.data['speed_y'] = datautils.unpack(MC_SHORT, bbuff) - packet.data['speed_z'] = datautils.unpack(MC_SHORT, bbuff) - return packet - - def encode_extra(packet): - if packet.data['obj_data']: - o = datautils.pack(MC_SHORT, packet.data['speed_x']) - o += datautils.pack(MC_SHORT, packet.data['speed_y']) - o += datautils.pack(MC_SHORT, packet.data['speed_z']) - return o + def decode_extra(packet, bbuff): + if packet.data['obj_data']: + packet.data['speed_x'] = datautils.unpack(MC_SHORT, bbuff) + packet.data['speed_y'] = datautils.unpack(MC_SHORT, bbuff) + packet.data['speed_z'] = datautils.unpack(MC_SHORT, bbuff) + return packet + + def encode_extra(packet): + if packet.data['obj_data']: + o = datautils.pack(MC_SHORT, packet.data['speed_x']) + o += datautils.pack(MC_SHORT, packet.data['speed_y']) + o += datautils.pack(MC_SHORT, packet.data['speed_z']) + return o #Play SERVER_TO_CLIENT 0x13 Destroy Entities @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x13)) class ExtensionPSTC13: - def decode_extra(packet, bbuff): - count = datautils.unpack(MC_BYTE, bbuff) - packet.data['eids'] = [ - datautils.unpack(MC_INT, bbuff) for i in range(count) - ] - return packet - - def encode_extra(packet): - o = datautils.pack(MC_INT, len(packet.data['eids'])) - for eid in packet.data['eids']: - o += datautils.pack(MC_INT, eid) - return o + def decode_extra(packet, bbuff): + count = datautils.unpack(MC_VARINT, bbuff) + packet.data['eids'] = [ + datautils.unpack(MC_VARINT, bbuff) for i in range(count) + ] + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['eids'])) + for eid in packet.data['eids']: + o += datautils.pack(MC_VARINT, eid) + return o #Play SERVER_TO_CLIENT 0x20 Entity Properties @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x20)) class ExtensionPSTC20: - def decode_extra(packet, bbuff): - packet.data['properties'] = [] - for i in range(datautils.unpack(MC_INT, bbuff)): - prop = { - 'key': datautils.unpack(MC_STRING, bbuff), - 'value': datautils.unpack(MC_DOUBLE, bbuff), - 'modifiers': [], - } - for j in range(datautils.unpack(MC_SHORT, bbuff)): - a, b = struct.unpack('>QQ', bbuff.recv(16)) - prop['modifiers'].append({ - 'uuid': (a<<64)|b, - 'amount': datautils.unpack(MC_DOUBLE, bbuff), - 'operation': datautils.unpack(MC_BYTE, bbuff), - }) - packet.data['properties'].append(prop) - return packet - - def encode_extra(packet): - o = datautils.pack(MC_INT, len(packet.data['properties'])) - for prop in packet.data['properties']: - o += datautils.pack(MC_STRING, prop['key']) - o += datautils.pack(MC_DOUBLE, prop['value']) - o += datautils.pack(MC_SHORT, len(prop['modifiers'])) - for modifier in prop['modifiers']: - o += struct.pack('>QQ', - (modifier['uuid']>>64)&((1<<64)-1), - modifier['uuid']&((1<<64)-1) - ) - o += datautils.pack(MC_DOUBLE, modifier['amount']) - o += datautils.pack(MC_BYTE, modifier['operation']) - return o + def decode_extra(packet, bbuff): + packet.data['properties'] = [] + for i in range(datautils.unpack(MC_INT, bbuff)): + prop = { + 'key': datautils.unpack(MC_STRING, bbuff), + 'value': datautils.unpack(MC_DOUBLE, bbuff), + 'modifiers': [], + } + for j in range(datautils.unpack(MC_VARINT, bbuff)): + prop['modifiers'].append({ + 'uuid': datautils.unpack(MC_UUID, bbuff), + 'amount': datautils.unpack(MC_DOUBLE, bbuff), + 'operation': datautils.unpack(MC_BYTE, bbuff), + }) + packet.data['properties'].append(prop) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_INT, len(packet.data['properties'])) + for prop in packet.data['properties']: + o += datautils.pack(MC_STRING, prop['key']) + o += datautils.pack(MC_DOUBLE, prop['value']) + o += datautils.pack(MC_SHORT, len(prop['modifiers'])) + for modifier in prop['modifiers']: + o += datautils.pack(MC_UUID, modifier['uuid']) + o += datautils.pack(MC_DOUBLE, modifier['amount']) + o += datautils.pack(MC_BYTE, modifier['operation']) + return o #Play SERVER_TO_CLIENT 0x21 Chunk Data @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x21)) class ExtensionPSTC21: - def decode_extra(packet, bbuff): - packet.data['data'] = zlib.decompress( - bbuff.recv(datautils.unpack(MC_INT, bbuff)) - ) - return packet - - def encode_extra(packet): - data = zlib.compress(packet.data['data']) - o = datautils.pack(MC_INT, len(data)) - o += data - return o + def decode_extra(packet, bbuff): + packet.data['data'] = bbuff.recv(datautils.unpack(MC_VARINT, bbuff)) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(data)) + o += packet.data['data'] + return o #Play SERVER_TO_CLIENT 0x22 Multi Block Change @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x22)) class ExtensionPSTC22: - def decode_extra(packet, bbuff): - count = datautils.unpack(MC_SHORT, bbuff) - assert(datautils.unpack(MC_INT, bbuff) == 4*count) - packet.data['blocks'] = [] - for i in range(count): - data = datautils.unpack(MC_UINT, bbuff) - packet.data['blocks'].append({ - 'metadata': (data )&0xF, - 'block_id': (data>> 4)&0xFFF, - 'y': (data>>16)&0xFF, - 'z': (data>>24)&0xF, - 'x': (data>>28)&0xF, - }) - return packet - - def encode_extra(packet): - o = datautils.pack(MC_SHORT, len(packet.data['blocks'])) - o += datautils.pack(MC_INT, 4*len(packet.data['blocks'])) - for block in packet.data['blocks']: - o += datautils.pack(MC_UINT, - block['metadata'] + - (block['type']<<4) + - (block['y'] << 16) + - (block['z'] << 24) + - (block['x'] << 28) - ) - return o + def decode_extra(packet, bbuff): + packet.data['blocks'] = [] + for i in range(datautils.unpack(MC_VARINT, bbuff)): + data = datautils.unpack(MC_USHORT, bbuff) + packet.data['blocks'].append({ + 'y': data&0xFF, + 'z': (data>>8)&0xF, + 'x': (data>>12)&0xF, + 'block_data': datautils.unpack(MC_VARINT, bbuff), + }) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['blocks'])) + for block in packet.data['blocks']: + o += datautils.pack(MC_USHORT, + (block['y']&0xFF) + + ((block['z']&0xF)<<8) + + ((block['x']&0xF)<<12) + ) + o += datautils.pack(MC_VARINT, block['block_data']) + return o #Play SERVER_TO_CLIENT 0x26 Map Chunk Bulk @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x26)) class ExtensionPSTC26: - def decode_extra(packet, bbuff): - count = datautils.unpack(MC_SHORT, bbuff) - size = datautils.unpack(MC_INT, bbuff) - packet.data['sky_light'] = datautils.unpack(MC_BOOL, bbuff) - packet.data['data'] = zlib.decompress(bbuff.recv(size)) - packet.data['metadata'] = [{ - 'chunk_x': datautils.unpack(MC_INT, bbuff), - 'chunk_z': datautils.unpack(MC_INT, bbuff), - 'primary_bitmap': datautils.unpack(MC_USHORT, bbuff), - 'add_bitmap': datautils.unpack(MC_USHORT, bbuff), - } for i in range(count)] - return packet - - def encode_extra(packet): - data = zlib.compress(packet.data['data']) - o = datautils.pack(MC_SHORT, len(packet.data['metadata'])) - o += datautils.pack(MC_INT, len(data)) - o += datautils.pack(MC_BOOL, packet.data['sky_light']) - for metadata in packet.data['metadata']: - o += datautils.pack(MC_INT, metadata['chunk_x']) - o += datautils.pack(MC_INT, metadata['chunk_z']) - o += datautils.pack(MC_USHORT, metadata['primary_bitmap']) - o += datautils.pack(MC_USHORT, metadata['add_bitmap']) - return o + def decode_extra(packet, bbuff): + sky_light = datautils.unpack(MC_BOOL, bbuff) + count = datautils.unpack(MC_VARINT, bbuff) + packet.data['sky_light'] = sky_light + packet.data['metadata'] = [{ + 'chunk_x': datautils.unpack(MC_INT, bbuff), + 'chunk_z': datautils.unpack(MC_INT, bbuff), + 'primary_bitmap': datautils.unpack(MC_USHORT, bbuff), + } for i in range(count)] + packet.data['data'] = bbuff.flush() + return packet + + def encode_extra(packet): + o = datautils.pack(MC_BOOL, packet.data['sky_light']) + o += datautils.pack(MC_VARINT, packet.data['metadata']) + for metadata in packet.data['metadata']: + o += datautils.pack(MC_INT, metadata['chunk_x']) + o += datautils.pack(MC_INT, metadata['chunk_z']) + o += datautils.pack(MC_USHORT, metadata['primary_bitmap']) + o += packet.data['data'] + return o #Play SERVER_TO_CLIENT 0x27 Explosion @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x27)) class ExtensionPSTC27: - def decode_extra(packet, bbuff): - packet.data['blocks'] = [ - [datautils.unpack(MC_BYTE, bbuff) for j in range(3)] - for i in range(datautils.unpack(MC_INT, bbuff))] - packet.data['player_x'] = datautils.unpack(MC_FLOAT, bbuff) - packet.data['player_y'] = datautils.unpack(MC_FLOAT, bbuff) - packet.data['player_z'] = datautils.unpack(MC_FLOAT, bbuff) - return packet - - def encode_extra(packet): - o = datautils.pack(MC_INT, len(packet.data['blocks'])) - for block in packet.data['blocks']: - for coord in block: - o += datautils.pack(MC_BYTE, coord) - o += datautils.pack(MC_FLOAT, packet.data['player_x']) - o += datautils.pack(MC_FLOAT, packet.data['player_y']) - o += datautils.pack(MC_FLOAT, packet.data['player_z']) - return o + def decode_extra(packet, bbuff): + packet.data['blocks'] = [ + [datautils.unpack(MC_BYTE, bbuff) for j in range(3)] + for i in range(datautils.unpack(MC_INT, bbuff))] + packet.data['player_x'] = datautils.unpack(MC_FLOAT, bbuff) + packet.data['player_y'] = datautils.unpack(MC_FLOAT, bbuff) + packet.data['player_z'] = datautils.unpack(MC_FLOAT, bbuff) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_INT, len(packet.data['blocks'])) + for block in packet.data['blocks']: + for coord in block: + o += datautils.pack(MC_BYTE, coord) + o += datautils.pack(MC_FLOAT, packet.data['player_x']) + o += datautils.pack(MC_FLOAT, packet.data['player_y']) + o += datautils.pack(MC_FLOAT, packet.data['player_z']) + return o + +#Play SERVER_TO_CLIENT 0x2A Particle +@extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x2A)) +class ExtensionPSTC2A: + def decode_extra(packet, bbuff): + packet.data['data'] = [ + datautils.unpack(MC_VARINT, bbuff) + for i in range(mcdata.particles[packet.data['id']][1])] + return packet + + def encode_extra(packet): + o = b'' + for i in range(mcdata.particles[packet.data['id']][1]): + o += datautils.pack(MC_VARINT, packet.data['data'][i]) + return o + +#Play SERVER_TO_CLIENT 0x2D Open Window +@extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x2D)) +class ExtensionPSTC2D: + def decode_extra(packet, bbuff): + if packet.data['inv_type'] == 'EntityHorse': + packet.data['eid'] = datautils.unpack(MC_INT, bbuff) + return packet + + def encode_extra(packet): + if packet.data['inv_type'] == 'EntityHorse': + return datautils.pack(MC_INT, packet.data['eid']) + #Play SERVER_TO_CLIENT 0x30 Window Items @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x30)) class ExtensionPSTC30: - def decode_extra(packet, bbuff): - packet.data['slots'] = [ - datautils.unpack(MC_SLOT, bbuff) - for i in range(datautils.unpack(MC_SHORT, bbuff))] - return packet - - def encode_extra(packet): - o = datautils.pack(MC_SHORT, len(packet.data['slots'])) - for slot in packet.data['slots']: - o += datautils.pack(MC_SLOT, slot) - return o + def decode_extra(packet, bbuff): + packet.data['slots'] = [ + datautils.unpack(MC_SLOT, bbuff) + for i in range(datautils.unpack(MC_SHORT, bbuff))] + return packet + + def encode_extra(packet): + o = datautils.pack(MC_SHORT, len(packet.data['slots'])) + for slot in packet.data['slots']: + o += datautils.pack(MC_SLOT, slot) + return o #TODO: Actually decode the map data into a useful format #Play SERVER_TO_CLIENT 0x34 Maps @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x34)) class ExtensionPSTC34: - def decode_extra(packet, bbuff): - packet.data['data'] = bbuff.recv(datautils.unpack(MC_SHORT, bbuff)) - return packet - - def encode_extra(packet): - o = datautils.pack(len(packet.data['data'])) - o += packet.data['data'] - return o + def decode_extra(packet, bbuff): + packet.data['icons'] = [] + for i in range(datautils.unpack(MC_VARINT, bbuff)): + byte = datautils.unpack(MC_UBYTE, bbuff) + packet.icons.append({ + 'direction': byte>>8, + 'type': byte&0x0F, + 'x': datautils.unpack(MC_BYTE, bbuff), + 'y': datautils.unpack(MC_BYTE, bbuff), + }) + packet.data['columns'] = datautils.unpack(MC_BYTE, bbuff) + if packet.data['columns']: + packet.data['rows'] = datautils.unpack(MC_BYTE, bbuff) + packet.data['x'] = datautils.unpack(MC_BYTE, bbuff) + packet.data['y'] = datautils.unpack(MC_BYTE, bbuff) + packet.data['data'] = bbuff.recv(datautils.unpack(MC_VARINT, bbuff)) + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['icons'])) + for icon in packet.data['icons']: + byte = (packet.data['direction']<<8)|(packet.data['type']&0x0F) + o += datautils.pack(MC_UBYTE, byte) + o += datautils.pack(MC_BYTE, packet.data['x']) + o += datautils.pack(MC_BYTE, packet.data['y']) + o += datautils.pack(MC_BYTE, packet.data['columns']) + if packet.data['columns']: + o += datautils.pack(MC_BYTE, packet.data['rows']) + o += datautils.pack(MC_BYTE, packet.data['x']) + o += datautils.pack(MC_BYTE, packet.data['y']) + o += datautils.pack(MC_VARINT, len(packet.data['data'])) + o += packet.data['data'] + return o #Play SERVER_TO_CLIENT 0x35 Update Block Entity +#Play SERVER_TO_CLIENT 0x48 Update Entity NBT @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x35)) -class ExtensionPSTC35: - def decode_extra(packet, bbuff): - data = bbuff.recv(datautils.unpack(MC_SHORT, bbuff)) - data = utils.BoundBuffer(zlib.decompress(data, 16+zlib.MAX_WBITS)) - assert(datautils.unpack(MC_BYTE, data) == nbt.TAG_COMPOUND) - name = nbt.TAG_String(buffer = data) - nbt_data = nbt.TAG_Compound(buffer = data) - nbt_data.name = name - packet.data['nbt'] = nbt_data - return packet - - def encode_extra(packet): - bbuff = utils.BoundBuffer() - TAG_Byte(packet.data['nbt'].id)._render_buffer(bbuff) - TAG_String(packet.data['nbt'].name)._render_buffer(bbuff) - packet.data['nbt']._render_buffer(bbuff) - compress = zlib.compressobj(wbits = 16+zlib.MAX_WBITS) - data = compress.compress(bbuff.flush()) - data += compress.flush() - o = datautils.pack(MC_SHORT, len(data)) - o += data - return o +@extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x48)) +class ExtensionUpdateNBT: + def decode_extra(packet, bbuff): + assert(datautils.unpack(MC_BYTE, bbuff) == nbt.TAG_COMPOUND) + name = nbt.TAG_String(buffer = bbuff) + nbt_data = nbt.TAG_Compound(buffer = bbuff) + nbt_data.name = name + packet.data['nbt'] = nbt_data + return packet + + def encode_extra(packet): + bbuff = utils.BoundBuffer() + TAG_Byte(packet.data['nbt'].id)._render_buffer(bbuff) + TAG_String(packet.data['nbt'].name)._render_buffer(bbuff) + packet.data['nbt']._render_buffer(bbuff) + return bbuff.flush() #Play SERVER_TO_CLIENT 0x37 Statistics @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x37)) class ExtensionPSTC37: - def decode_extra(packet, bbuff): - packet.data['entries'] = [[ - datautils.unpack(MC_STRING, bbuff), - datautils.unpack(MC_VARINT, bbuff) - ] for i in range(datautils.unpack(MC_VARINT, bbuff))] - return packet - - def encode_extra(packet): - o = datautils.pack(MC_VARINT, len(packet.data['entries'])) - for entry in packet.data['entries']: - o += datautils.pack(MC_STRING, entry[0]) - o += datautils.pack(MC_VARINT, entry[1]) - return o + def decode_extra(packet, bbuff): + packet.data['entries'] = [[ + datautils.unpack(MC_STRING, bbuff), + datautils.unpack(MC_VARINT, bbuff) + ] for i in range(datautils.unpack(MC_VARINT, bbuff))] + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['entries'])) + for entry in packet.data['entries']: + o += datautils.pack(MC_STRING, entry[0]) + o += datautils.pack(MC_VARINT, entry[1]) + return o + +#Play SERVER_TO_CLIENT 0x38 Player List Item +@extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x38)) +class ExtensionPSTC38: + def decode_extra(packet, bbuff): + action = packet.data['action'] + packet.data['player_list'] = [] + for i in range(datautils.unpack(MC_VARINT, bbuff)): + item = {'uuid': datautils.unpack(MC_UUID, bbuff)} + if action == mcdata.PL_ADD_PLAYER: + item['name'] = datautils.unpack(MC_STRING, bbuff) + item['properties'] = [] + for i in range(datautils.unpack(MC_VARINT, bbuff)): + prop = { + 'name': datautils.unpack(MC_STRING, bbuff), + 'value': datautils.unpack(MC_STRING, bbuff), + 'signed': datautils.unpack(MC_BOOL, bbuff), + } + if prop['signed']: + prop['signature'] = datautils.unpack(MC_STRING, bbuff) + item['properties'].append(prop) + if action == mcdata.PL_ADD_PLAYER or action == mcdata.PL_UPDATE_GAMEMODE: + item['gamemode'] = datautils.unpack(MC_VARINT, bbuff) + if action == mcdata.PL_ADD_PLAYER or action == mcdata.PL_UPDATE_LATENCY: + item['ping'] = datautils.unpack(MC_VARINT, bbuff) + if action == mcdata.PL_ADD_PLAYER or action == mcdata.PL_UPDATE_DISPLAY: + item['has_display'] = datautils.unpack(MC_BOOL, bbuff) + if item['has_display']: + item['display_name'] = datautils.unpack(MC_CHAT, bbuff) + packet.data['player_list'].append(item) + + + def encode_extra(packet): + action = packet.data['action'] + o = datautils.pack(MC_VARINT, len(packet.data['player_list'])) + for item in packet.data['player_list']: + o += datautils.pack(MC_UUID, item['uuid']) + if action == mcdata.PL_ADD_PLAYER: + o += datautils.pack(MC_STRING, item['name']) + o += datautils.pack(MC_VARINT, len(item['properties'])) + for prop in item['properties']: + o += datautils.pack(MC_STRING, prop['name']) + o += datautils.pack(MC_STRING, prop['value']) + o += datautils.pack(MC_BOOL, prop['signed']) + if prop['signed']: + o += datautils.pack(MC_STRING, prop['signature']) + if action == mcdata.PL_ADD_PLAYER or action == mcdata.PL_UPDATE_GAMEMODE: + o += datautils.pack(MC_VARINT, item['gamemode']) + if action == mcdata.PL_ADD_PLAYER or action == mcdata.PL_UPDATE_LATENCY: + o += datautils.pack(MC_VARINT, item['ping']) + if action == mcdata.PL_ADD_PLAYER or action == mcdata.PL_UPDATE_DISPLAY: + o += datautils.pack(MC_BOOL, item['has_display']) + if item['has_display']: + o += datautils.pack(MC_CHAT, item['display_name']) + return o #Play SERVER_TO_CLIENT 0x3A Tab-Complete @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x3A)) class ExtensionPSTC3A: - def decode_extra(packet, bbuff): - packet.data['matches'] = [ - datautils.unpack(MC_STRING, bbuff) - for i in range(datautils.unpack(MC_VARINT, bbuff))] - return packet - - def encode_extra(packet): - o = datautils.pack(MC_VARINT, len(packet.data['matches'])) - for match in packet.data['matches']: - o += datautils.pack(MC_STRING, match) - return o + def decode_extra(packet, bbuff): + packet.data['matches'] = [ + datautils.unpack(MC_STRING, bbuff) + for i in range(datautils.unpack(MC_VARINT, bbuff))] + return packet + + def encode_extra(packet): + o = datautils.pack(MC_VARINT, len(packet.data['matches'])) + for match in packet.data['matches']: + o += datautils.pack(MC_STRING, match) + return o + +#Play SERVER_TO_CLIENT 0x3B Scoreboard Objective +@extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x3B)) +class ExtensionPSTC3B: + def decode_extra(packet, bbuff): + action = packet.data['action'] + if action == mcdata.SO_CREATE_BOARD or action == mcdata.SO_UPDATE_BOARD: + packet.data['obj_val'] = datautils.unpack(MC_STRING, bbuff) + packet.data['type'] = datautils.unpack(MC_STRING, bbuff) + return packet + + def encode_extra(packet): + o = b'' + if packet.data['action'] == 0 or packet.data['action'] == 2: + o += datautils.pack(MC_STRING, packet.data['obj_val']) + o += datautils.pack(MC_STRING, packet.data['type']) + return o + +#Play SERVER_TO_CLIENT 0x3C Update Score +@extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x3C)) +class ExtensionPSTC3C: + def decode_extra(packet, bbuff): + if packet.data['action'] == 0: + packet.data['value'] = datautils.unpack(MC_VARINT, bbuff) + return packet + + def encode_extra(packet): + o = b'' + if packet.data['action'] == 0: + o += datautils.pack(MC_VARINT, packet.data['value']) + return o #Play SERVER_TO_CLIENT 0x3E Teams @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x3E)) class ExtensionPSTC3E: - def decode_extra(packet, bbuff): - mode = packet.data['mode'] - if mode == 0 or mode == 2: - packet.data['display_name'] = datautils.unpack(MC_STRING, bbuff) - packet.data['team_prefix'] = datautils.unpack(MC_STRING, bbuff) - packet.data['team_suffix'] = datautils.unpack(MC_STRING, bbuff) - packet.data['friendly_fire'] = datautils.unpack(MC_BYTE, bbuff) - if mode == 0 or mode == 3 or mode == 4: - packet.data['players'] = [ - datautils.unpack(MC_STRING, bbuff) - for i in range(datautils.unpack(MC_SHORT, bbuff))] - return packet - - def encode_extra(packet): - mode = packet.data['mode'] - o = b'' - if mode == 0 or mode == 2: - o += datautils.pack(MC_STRING, packet.data['display_name']) - o += datautils.pack(MC_STRING, packet.data['team_prefix']) - o += datautils.pack(MC_STRING, packet.data['team_suffix']) - o += datautils.pack(MC_BYTE, packet.data['friendly_fire']) - if mode == 0 or mode == 3 or mode == 4: - o += datautils.pack(MC_SHORT, len(packet.data['players'])) - for player in packet.data['players']: - o += datautils.pack(MC_STRING, player) - return o + def decode_extra(packet, bbuff): + action = packet.data['action'] + if action == 0 or action == 2: + packet.data['display_name'] = datautils.unpack(MC_STRING, bbuff) + packet.data['team_prefix'] = datautils.unpack(MC_STRING, bbuff) + packet.data['team_suffix'] = datautils.unpack(MC_STRING, bbuff) + packet.data['friendly_fire'] = datautils.unpack(MC_BYTE, bbuff) + packet.data['name_visibility'] = datautils.unpack(MC_STRING, bbuff) + if action == 0 or action == 3 or action == 4: + packet.data['players'] = [ + datautils.unpack(MC_STRING, bbuff) + for i in range(datautils.unpack(MC_VARINT, bbuff))] + return packet + + def encode_extra(packet): + action = packet.data['action'] + o = b'' + if action == 0 or action == 2: + o += datautils.pack(MC_STRING, packet.data['display_name']) + o += datautils.pack(MC_STRING, packet.data['team_prefix']) + o += datautils.pack(MC_STRING, packet.data['team_suffix']) + o += datautils.pack(MC_BYTE, packet.data['friendly_fire']) + o += datautils.pack(MC_STRING, packet.data['name_visibility']) + if action == 0 or action == 3 or action == 4: + o += datautils.pack(MC_VARINT, len(packet.data['players'])) + for player in packet.data['players']: + o += datautils.pack(MC_STRING, player) + return o #Play SERVER_TO_CLIENT 0x3F Plugin Message #Play CLIENT_TO_SERVER 0x17 Plugin Message @extension((mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x3F)) @extension((mcdata.PLAY_STATE, mcdata.CLIENT_TO_SERVER, 0x17)) class ExtensionPluginMessage: - def decode_extra(packet, bbuff): - packet.data['data'] = bbuff.recv(datautils.unpack(MC_SHORT, bbuff)) - return packet - - def encode_extra(packet): - o = datautils.pack(MC_SHORT, len(packet.data['data'])) - o += packet.data['data'] - return o - -@extension(mcdata.packet_idents['PLAY" % (self.__class__.__name__,self.name,id(self)) + """TAG, a variable with an intrinsic name.""" + id = None + + def __init__(self, value=None, name=None): + self.name = name + self.value = value + + #Parsers and Generators + def _parse_buffer(self, buffer): + raise NotImplementedError(self.__class__.__name__) + + def _render_buffer(self, buffer): + raise NotImplementedError(self.__class__.__name__) + + #Printing and Formatting of tree + def tag_info(self): + """Return Unicode string with class, name and unnested value.""" + return self.__class__.__name__ + \ + ('(%r)' % self.name if self.name else "") + \ + ": " + self.valuestr() + def valuestr(self): + """Return Unicode string of unnested value. For iterators, this returns a summary.""" + return unicode(self.value) + + def pretty_tree(self, indent=0): + """Return formated Unicode string of self, where iterable items are recursively listed in detail.""" + return ("\t"*indent) + self.tag_info() + + # Python 2 compatibility; Python 3 uses __str__ instead. + def __unicode__(self): + """Return a unicode string with the result in human readable format. Unlike valuestr(), the result is recursive for iterators till at least one level deep.""" + return unicode(self.value) + + def __str__(self): + """Return a string (ascii formated for Python 2, unicode for Python 3) with the result in human readable format. Unlike valuestr(), the result is recursive for iterators till at least one level deep.""" + return str(self.value) + # Unlike regular iterators, __repr__() is not recursive. + # Use pretty_tree for recursive results. + # iterators should use __repr__ or tag_info for each item, like regular iterators + def __repr__(self): + """Return a string (ascii formated for Python 2, unicode for Python 3) describing the class, name and id for debugging purposes.""" + return "<%s(%r) at 0x%x>" % (self.__class__.__name__,self.name,id(self)) class _TAG_Numeric(TAG): - """_TAG_Numeric, comparable to int with an intrinsic name""" - def __init__(self, value=None, name=None, buffer=None): - super(_TAG_Numeric, self).__init__(value, name) - if buffer: - self._parse_buffer(buffer) + """_TAG_Numeric, comparable to int with an intrinsic name""" + def __init__(self, value=None, name=None, buffer=None): + super(_TAG_Numeric, self).__init__(value, name) + if buffer: + self._parse_buffer(buffer) - #Parsers and Generators - def _parse_buffer(self, buffer): - # Note: buffer.read() may raise an IOError, for example if buffer is a corrupt gzip.GzipFile - self.value = self.fmt.unpack(buffer.read(self.fmt.size))[0] + #Parsers and Generators + def _parse_buffer(self, buffer): + # Note: buffer.read() may raise an IOError, for example if buffer is a corrupt gzip.GzipFile + self.value = self.fmt.unpack(buffer.read(self.fmt.size))[0] - def _render_buffer(self, buffer): - buffer.write(self.fmt.pack(self.value)) + def _render_buffer(self, buffer): + buffer.write(self.fmt.pack(self.value)) class _TAG_End(TAG): - id = TAG_END - fmt = Struct(">b") + id = TAG_END + fmt = Struct(">b") - def _parse_buffer(self, buffer): - # Note: buffer.read() may raise an IOError, for example if buffer is a corrupt gzip.GzipFile - value = self.fmt.unpack(buffer.read(1))[0] - if value != 0: - raise ValueError("A Tag End must be rendered as '0', not as '%d'." % (value)) + def _parse_buffer(self, buffer): + # Note: buffer.read() may raise an IOError, for example if buffer is a corrupt gzip.GzipFile + value = self.fmt.unpack(buffer.read(1))[0] + if value != 0: + raise ValueError("A Tag End must be rendered as '0', not as '%d'." % (value)) - def _render_buffer(self, buffer): - buffer.write(b'\x00') + def _render_buffer(self, buffer): + buffer.write(b'\x00') #== Value Tags ==# class TAG_Byte(_TAG_Numeric): - """Represent a single tag storing 1 byte.""" - id = TAG_BYTE - fmt = Struct(">b") + """Represent a single tag storing 1 byte.""" + id = TAG_BYTE + fmt = Struct(">b") class TAG_Short(_TAG_Numeric): - """Represent a single tag storing 1 short.""" - id = TAG_SHORT - fmt = Struct(">h") + """Represent a single tag storing 1 short.""" + id = TAG_SHORT + fmt = Struct(">h") class TAG_Int(_TAG_Numeric): - """Represent a single tag storing 1 int.""" - id = TAG_INT - fmt = Struct(">i") - """Struct(">i"), 32-bits integer, big-endian""" + """Represent a single tag storing 1 int.""" + id = TAG_INT + fmt = Struct(">i") + """Struct(">i"), 32-bits integer, big-endian""" class TAG_Long(_TAG_Numeric): - """Represent a single tag storing 1 long.""" - id = TAG_LONG - fmt = Struct(">q") + """Represent a single tag storing 1 long.""" + id = TAG_LONG + fmt = Struct(">q") class TAG_Float(_TAG_Numeric): - """Represent a single tag storing 1 IEEE-754 floating point number.""" - id = TAG_FLOAT - fmt = Struct(">f") + """Represent a single tag storing 1 IEEE-754 floating point number.""" + id = TAG_FLOAT + fmt = Struct(">f") class TAG_Double(_TAG_Numeric): - """Represent a single tag storing 1 IEEE-754 double precision floating point number.""" - id = TAG_DOUBLE - fmt = Struct(">d") + """Represent a single tag storing 1 IEEE-754 double precision floating point number.""" + id = TAG_DOUBLE + fmt = Struct(">d") class TAG_Byte_Array(TAG, MutableSequence): - """ - TAG_Byte_Array, comparable to a collections.UserList with - an intrinsic name whose values must be bytes - """ - id = TAG_BYTE_ARRAY - def __init__(self, name=None, buffer=None): - super(TAG_Byte_Array, self).__init__(name=name) - if buffer: - self._parse_buffer(buffer) - - #Parsers and Generators - def _parse_buffer(self, buffer): - length = TAG_Int(buffer=buffer) - self.value = bytearray(buffer.read(length.value)) - - def _render_buffer(self, buffer): - length = TAG_Int(len(self.value)) - length._render_buffer(buffer) - buffer.write(bytes(self.value)) - - # Mixin methods - def __len__(self): - return len(self.value) - - def __iter__(self): - return iter(self.value) - - def __contains__(self, item): - return item in self.value - - def __getitem__(self, key): - return self.value[key] - - def __setitem__(self, key, value): - # TODO: check type of value - self.value[key] = value - - def __delitem__(self, key): - del(self.value[key]) - - def insert(self, key, value): - # TODO: check type of value, or is this done by self.value already? - self.value.insert(key, value) - - #Printing and Formatting of tree - def valuestr(self): - return "[%i byte(s)]" % len(self.value) - - def __unicode__(self): - return '['+",".join([unicode(x) for x in self.value])+']' - def __str__(self): - return '['+",".join([str(x) for x in self.value])+']' + """ + TAG_Byte_Array, comparable to a collections.UserList with + an intrinsic name whose values must be bytes + """ + id = TAG_BYTE_ARRAY + def __init__(self, name=None, buffer=None): + super(TAG_Byte_Array, self).__init__(name=name) + if buffer: + self._parse_buffer(buffer) + + #Parsers and Generators + def _parse_buffer(self, buffer): + length = TAG_Int(buffer=buffer) + self.value = bytearray(buffer.read(length.value)) + + def _render_buffer(self, buffer): + length = TAG_Int(len(self.value)) + length._render_buffer(buffer) + buffer.write(bytes(self.value)) + + # Mixin methods + def __len__(self): + return len(self.value) + + def __iter__(self): + return iter(self.value) + + def __contains__(self, item): + return item in self.value + + def __getitem__(self, key): + return self.value[key] + + def __setitem__(self, key, value): + # TODO: check type of value + self.value[key] = value + + def __delitem__(self, key): + del(self.value[key]) + + def insert(self, key, value): + # TODO: check type of value, or is this done by self.value already? + self.value.insert(key, value) + + #Printing and Formatting of tree + def valuestr(self): + return "[%i byte(s)]" % len(self.value) + + def __unicode__(self): + return '['+",".join([unicode(x) for x in self.value])+']' + def __str__(self): + return '['+",".join([str(x) for x in self.value])+']' class TAG_Int_Array(TAG, MutableSequence): - """ - TAG_Int_Array, comparable to a collections.UserList with - an intrinsic name whose values must be integers - """ - id = TAG_INT_ARRAY - def __init__(self, name=None, buffer=None): - super(TAG_Int_Array, self).__init__(name=name) - if buffer: - self._parse_buffer(buffer) + """ + TAG_Int_Array, comparable to a collections.UserList with + an intrinsic name whose values must be integers + """ + id = TAG_INT_ARRAY + def __init__(self, name=None, buffer=None): + super(TAG_Int_Array, self).__init__(name=name) + if buffer: + self._parse_buffer(buffer) - def update_fmt(self, length): - """ Adjust struct format description to length given """ - self.fmt = Struct(">" + str(length) + "i") + def update_fmt(self, length): + """ Adjust struct format description to length given """ + self.fmt = Struct(">" + str(length) + "i") - #Parsers and Generators - def _parse_buffer(self, buffer): - length = TAG_Int(buffer=buffer).value - self.update_fmt(length) - self.value = list(self.fmt.unpack(buffer.read(self.fmt.size))) + #Parsers and Generators + def _parse_buffer(self, buffer): + length = TAG_Int(buffer=buffer).value + self.update_fmt(length) + self.value = list(self.fmt.unpack(buffer.read(self.fmt.size))) - def _render_buffer(self, buffer): - length = len(self.value) - self.update_fmt(length) - TAG_Int(length)._render_buffer(buffer) - buffer.write(self.fmt.pack(*self.value)) + def _render_buffer(self, buffer): + length = len(self.value) + self.update_fmt(length) + TAG_Int(length)._render_buffer(buffer) + buffer.write(self.fmt.pack(*self.value)) - # Mixin methods - def __len__(self): - return len(self.value) + # Mixin methods + def __len__(self): + return len(self.value) - def __iter__(self): - return iter(self.value) + def __iter__(self): + return iter(self.value) - def __contains__(self, item): - return item in self.value + def __contains__(self, item): + return item in self.value - def __getitem__(self, key): - return self.value[key] + def __getitem__(self, key): + return self.value[key] - def __setitem__(self, key, value): - self.value[key] = value + def __setitem__(self, key, value): + self.value[key] = value - def __delitem__(self, key): - del(self.value[key]) + def __delitem__(self, key): + del(self.value[key]) - def insert(self, key, value): - self.value.insert(key, value) + def insert(self, key, value): + self.value.insert(key, value) - #Printing and Formatting of tree - def valuestr(self): - return "[%i int(s)]" % len(self.value) + #Printing and Formatting of tree + def valuestr(self): + return "[%i int(s)]" % len(self.value) class TAG_String(TAG, Sequence): - """ - TAG_String, comparable to a collections.UserString with an - intrinsic name - """ - id = TAG_STRING - def __init__(self, value=None, name=None, buffer=None): - super(TAG_String, self).__init__(value, name) - if buffer: - self._parse_buffer(buffer) - - #Parsers and Generators - def _parse_buffer(self, buffer): - length = TAG_Short(buffer=buffer) - read = buffer.read(length.value) - if len(read) != length.value: - raise StructError() - self.value = read.decode("utf-8") - - def _render_buffer(self, buffer): - save_val = self.value.encode("utf-8") - length = TAG_Short(len(save_val)) - length._render_buffer(buffer) - buffer.write(save_val) - - # Mixin methods - def __len__(self): - return len(self.value) - - def __iter__(self): - return iter(self.value) - - def __contains__(self, item): - return item in self.value - - def __getitem__(self, key): - return self.value[key] - - #Printing and Formatting of tree - def __repr__(self): - return self.value + """ + TAG_String, comparable to a collections.UserString with an + intrinsic name + """ + id = TAG_STRING + def __init__(self, value=None, name=None, buffer=None): + super(TAG_String, self).__init__(value, name) + if buffer: + self._parse_buffer(buffer) + + #Parsers and Generators + def _parse_buffer(self, buffer): + length = TAG_Short(buffer=buffer) + read = buffer.read(length.value) + if len(read) != length.value: + raise StructError() + self.value = read.decode("utf-8") + + def _render_buffer(self, buffer): + save_val = self.value.encode("utf-8") + length = TAG_Short(len(save_val)) + length._render_buffer(buffer) + buffer.write(save_val) + + # Mixin methods + def __len__(self): + return len(self.value) + + def __iter__(self): + return iter(self.value) + + def __contains__(self, item): + return item in self.value + + def __getitem__(self, key): + return self.value[key] + + #Printing and Formatting of tree + def __repr__(self): + return self.value #== Collection Tags ==# class TAG_List(TAG, MutableSequence): - """ - TAG_List, comparable to a collections.UserList with an intrinsic name - """ - id = TAG_LIST - def __init__(self, type=None, value=None, name=None, buffer=None): - super(TAG_List, self).__init__(value, name) - if type: - self.tagID = type.id - else: - self.tagID = None - self.tags = [] - if buffer: - self._parse_buffer(buffer) - if self.tagID == None: - raise ValueError("No type specified for list: %s" % (name)) - - #Parsers and Generators - def _parse_buffer(self, buffer): - self.tagID = TAG_Byte(buffer=buffer).value - self.tags = [] - length = TAG_Int(buffer=buffer) - for x in range(length.value): - self.tags.append(TAGLIST[self.tagID](buffer=buffer)) - - def _render_buffer(self, buffer): - TAG_Byte(self.tagID)._render_buffer(buffer) - length = TAG_Int(len(self.tags)) - length._render_buffer(buffer) - for i, tag in enumerate(self.tags): - if tag.id != self.tagID: - raise ValueError("List element %d(%s) has type %d != container type %d" % - (i, tag, tag.id, self.tagID)) - tag._render_buffer(buffer) - - # Mixin methods - def __len__(self): - return len(self.tags) - - def __iter__(self): - return iter(self.tags) - - def __contains__(self, item): - return item in self.tags - - def __getitem__(self, key): - return self.tags[key] - - def __setitem__(self, key, value): - self.tags[key] = value - - def __delitem__(self, key): - del(self.tags[key]) - - def insert(self, key, value): - self.tags.insert(key, value) - - #Printing and Formatting of tree - def __repr__(self): - return "%i entries of type %s" % (len(self.tags), TAGLIST[self.tagID].__name__) - - #Printing and Formatting of tree - def valuestr(self): - return "[%i %s(s)]" % (len(self.tags), TAGLIST[self.tagID].__name__) - def __unicode__(self): - return "["+", ".join([tag.tag_info() for tag in self.tags])+"]" - def __str__(self): - return "["+", ".join([tag.tag_info() for tag in self.tags])+"]" - - def pretty_tree(self, indent=0): - output = [super(TAG_List, self).pretty_tree(indent)] - if len(self.tags): - output.append(("\t"*indent) + "{") - output.extend([tag.pretty_tree(indent + 1) for tag in self.tags]) - output.append(("\t"*indent) + "}") - return '\n'.join(output) + """ + TAG_List, comparable to a collections.UserList with an intrinsic name + """ + id = TAG_LIST + def __init__(self, type=None, value=None, name=None, buffer=None): + super(TAG_List, self).__init__(value, name) + if type: + self.tagID = type.id + else: + self.tagID = None + self.tags = [] + if buffer: + self._parse_buffer(buffer) + if self.tagID == None: + raise ValueError("No type specified for list: %s" % (name)) + + #Parsers and Generators + def _parse_buffer(self, buffer): + self.tagID = TAG_Byte(buffer=buffer).value + self.tags = [] + length = TAG_Int(buffer=buffer) + for x in range(length.value): + self.tags.append(TAGLIST[self.tagID](buffer=buffer)) + + def _render_buffer(self, buffer): + TAG_Byte(self.tagID)._render_buffer(buffer) + length = TAG_Int(len(self.tags)) + length._render_buffer(buffer) + for i, tag in enumerate(self.tags): + if tag.id != self.tagID: + raise ValueError("List element %d(%s) has type %d != container type %d" % + (i, tag, tag.id, self.tagID)) + tag._render_buffer(buffer) + + # Mixin methods + def __len__(self): + return len(self.tags) + + def __iter__(self): + return iter(self.tags) + + def __contains__(self, item): + return item in self.tags + + def __getitem__(self, key): + return self.tags[key] + + def __setitem__(self, key, value): + self.tags[key] = value + + def __delitem__(self, key): + del(self.tags[key]) + + def insert(self, key, value): + self.tags.insert(key, value) + + #Printing and Formatting of tree + def __repr__(self): + return "%i entries of type %s" % (len(self.tags), TAGLIST[self.tagID].__name__) + + #Printing and Formatting of tree + def valuestr(self): + return "[%i %s(s)]" % (len(self.tags), TAGLIST[self.tagID].__name__) + def __unicode__(self): + return "["+", ".join([tag.tag_info() for tag in self.tags])+"]" + def __str__(self): + return "["+", ".join([tag.tag_info() for tag in self.tags])+"]" + + def pretty_tree(self, indent=0): + output = [super(TAG_List, self).pretty_tree(indent)] + if len(self.tags): + output.append(("\t"*indent) + "{") + output.extend([tag.pretty_tree(indent + 1) for tag in self.tags]) + output.append(("\t"*indent) + "}") + return '\n'.join(output) class TAG_Compound(TAG, MutableMapping): - """ - TAG_Compound, comparable to a collections.OrderedDict with an - intrinsic name - """ - id = TAG_COMPOUND - def __init__(self, buffer=None): - super(TAG_Compound, self).__init__() - self.tags = [] - self.name = "" - if buffer: - self._parse_buffer(buffer) - - #Parsers and Generators - def _parse_buffer(self, buffer): - while True: - type = TAG_Byte(buffer=buffer) - if type.value == TAG_END: - #print("found tag_end") - break - else: - name = TAG_String(buffer=buffer).value - try: - tag = TAGLIST[type.value](buffer=buffer) - tag.name = name - self.tags.append(tag) - except KeyError: - raise ValueError("Unrecognised tag type") - - def _render_buffer(self, buffer): - for tag in self.tags: - TAG_Byte(tag.id)._render_buffer(buffer) - TAG_String(tag.name)._render_buffer(buffer) - tag._render_buffer(buffer) - buffer.write(b'\x00') #write TAG_END - - # Mixin methods - def __len__(self): - return len(self.tags) - - def __iter__(self): - for key in self.tags: - yield key.name - - def __contains__(self, key): - if isinstance(key, int): - return key <= len(self.tags) - elif isinstance(key, basestring): - for tag in self.tags: - if tag.name == key: - return True - return False - elif isinstance(key, TAG): - return key in self.tags - return False - - def __getitem__(self, key): - if isinstance(key, int): - return self.tags[key] - elif isinstance(key, basestring): - for tag in self.tags: - if tag.name == key: - return tag - else: - raise KeyError("Tag %s does not exist" % key) - else: - raise TypeError("key needs to be either name of tag, or index of tag, not a %s" % type(key).__name__) - - def __setitem__(self, key, value): - assert isinstance(value, TAG), "value must be an nbt.TAG" - if isinstance(key, int): - # Just try it. The proper error will be raised if it doesn't work. - self.tags[key] = value - elif isinstance(key, basestring): - value.name = key - for i, tag in enumerate(self.tags): - if tag.name == key: - self.tags[i] = value - return - self.tags.append(value) - - def __delitem__(self, key): - if isinstance(key, int): - del(self.tags[key]) - elif isinstance(key, basestring): - self.tags.remove(self.__getitem__(key)) - else: - raise ValueError("key needs to be either name of tag, or index of tag") - - def keys(self): - return [tag.name for tag in self.tags] - - def iteritems(self): - for tag in self.tags: - yield (tag.name, tag) - - #Printing and Formatting of tree - def __unicode__(self): - return "{"+", ".join([tag.tag_info() for tag in self.tags])+"}" - def __str__(self): - return "{"+", ".join([tag.tag_info() for tag in self.tags])+"}" - - def valuestr(self): - return '{%i Entries}' % len(self.tags) - - def pretty_tree(self, indent=0): - output = [super(TAG_Compound, self).pretty_tree(indent)] - if len(self.tags): - output.append(("\t"*indent) + "{") - output.extend([tag.pretty_tree(indent + 1) for tag in self.tags]) - output.append(("\t"*indent) + "}") - return '\n'.join(output) + """ + TAG_Compound, comparable to a collections.OrderedDict with an + intrinsic name + """ + id = TAG_COMPOUND + def __init__(self, buffer=None): + super(TAG_Compound, self).__init__() + self.tags = [] + self.name = "" + if buffer: + self._parse_buffer(buffer) + + #Parsers and Generators + def _parse_buffer(self, buffer): + while True: + type = TAG_Byte(buffer=buffer) + if type.value == TAG_END: + #print("found tag_end") + break + else: + name = TAG_String(buffer=buffer).value + try: + tag = TAGLIST[type.value](buffer=buffer) + tag.name = name + self.tags.append(tag) + except KeyError: + raise ValueError("Unrecognised tag type") + + def _render_buffer(self, buffer): + for tag in self.tags: + TAG_Byte(tag.id)._render_buffer(buffer) + TAG_String(tag.name)._render_buffer(buffer) + tag._render_buffer(buffer) + buffer.write(b'\x00') #write TAG_END + + # Mixin methods + def __len__(self): + return len(self.tags) + + def __iter__(self): + for key in self.tags: + yield key.name + + def __contains__(self, key): + if isinstance(key, int): + return key <= len(self.tags) + elif isinstance(key, basestring): + for tag in self.tags: + if tag.name == key: + return True + return False + elif isinstance(key, TAG): + return key in self.tags + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self.tags[key] + elif isinstance(key, basestring): + for tag in self.tags: + if tag.name == key: + return tag + else: + raise KeyError("Tag %s does not exist" % key) + else: + raise TypeError("key needs to be either name of tag, or index of tag, not a %s" % type(key).__name__) + + def __setitem__(self, key, value): + assert isinstance(value, TAG), "value must be an nbt.TAG" + if isinstance(key, int): + # Just try it. The proper error will be raised if it doesn't work. + self.tags[key] = value + elif isinstance(key, basestring): + value.name = key + for i, tag in enumerate(self.tags): + if tag.name == key: + self.tags[i] = value + return + self.tags.append(value) + + def __delitem__(self, key): + if isinstance(key, int): + del(self.tags[key]) + elif isinstance(key, basestring): + self.tags.remove(self.__getitem__(key)) + else: + raise ValueError("key needs to be either name of tag, or index of tag") + + def keys(self): + return [tag.name for tag in self.tags] + + def iteritems(self): + for tag in self.tags: + yield (tag.name, tag) + + #Printing and Formatting of tree + def __unicode__(self): + return "{"+", ".join([tag.tag_info() for tag in self.tags])+"}" + def __str__(self): + return "{"+", ".join([tag.tag_info() for tag in self.tags])+"}" + + def valuestr(self): + return '{%i Entries}' % len(self.tags) + + def pretty_tree(self, indent=0): + output = [super(TAG_Compound, self).pretty_tree(indent)] + if len(self.tags): + output.append(("\t"*indent) + "{") + output.extend([tag.pretty_tree(indent + 1) for tag in self.tags]) + output.append(("\t"*indent) + "}") + return '\n'.join(output) TAGLIST = {TAG_END: _TAG_End, TAG_BYTE:TAG_Byte, TAG_SHORT:TAG_Short, TAG_INT:TAG_Int, TAG_LONG:TAG_Long, TAG_FLOAT:TAG_Float, TAG_DOUBLE:TAG_Double, TAG_BYTE_ARRAY:TAG_Byte_Array, TAG_STRING:TAG_String, TAG_LIST:TAG_List, TAG_COMPOUND:TAG_Compound, TAG_INT_ARRAY:TAG_Int_Array} diff --git a/spock/mcp/yggdrasil.py b/spock/mcp/yggdrasil.py index 0c16c71..8ba55f0 100644 --- a/spock/mcp/yggdrasil.py +++ b/spock/mcp/yggdrasil.py @@ -1,103 +1,100 @@ import urllib.request as request from urllib.error import HTTPError -from urllib.error import URLError import json class YggAuth: - def __init__(self, - client_token=None, - access_token=None, - username=None, - password=None - ): - self.username = username - self.password = password - self.client_token = client_token + def __init__(self, + client_token=None, + access_token=None, + username=None, + password=None + ): + self.username = username + self.password = password + self.client_token = client_token - self.access_token = None #validate needs self.access_token to exist - self.access_token = ( - None if self.validate(access_token) else access_token - ) + self.access_token = None #validate needs self.access_token to exist + self.access_token = ( + None if self.validate(access_token) else access_token + ) - def _gen_req(self, endpoint, payload): - url = 'https://authserver.mojang.com' + endpoint - data = json.dumps(payload).encode('utf-8') - headers = {'Content-Type': 'application/json'} - return request.Request(url, data, headers, method='POST') + def _gen_req(self, endpoint, payload): + url = 'https://authserver.mojang.com' + endpoint + data = json.dumps(payload).encode('utf-8') + headers = {'Content-Type': 'application/json'} + return request.Request(url, data, headers, method='POST') - def _gen_rep(self, req): - try: - rep = request.urlopen(req) - except URLError: - return None - except HTTPError as reperr: - rep = reperr - data = rep.read().decode('utf-8') - return json.loads(data) if data else None + def _gen_rep(self, req): + try: + rep = request.urlopen(req) + except HTTPError as reperr: + rep = reperr + data = rep.read().decode('utf-8') + return json.loads(data) if data else None - def _ygg_req(self, endpoint, payload): - return self._gen_rep(self._gen_req(endpoint, payload)) + def _ygg_req(self, endpoint, payload): + return self._gen_rep(self._gen_req(endpoint, payload)) - #Generate an access token using a username and password - #(Any existing client token is invalidated if not provided) - #Returns response dict on success, otherwise error dict - def authenticate(self, username=None, password=None, client_token=None): - endpoint = '/authenticate' - payload = { - 'agent': { - 'name': 'Minecraft', - 'version': 1, - }, - } - if username: - self.username = username - payload['username'] = self.username - if password: - self.password = password - payload['password'] = self.password - payload['clientToken'] = client_token if client_token else self.client_token - rep = self._ygg_req(endpoint, payload) - if 'error' not in rep: - self.access_token = rep['accessToken'] - self.client_token = rep['clientToken'] - return rep + #Generate an access token using a username and password + #(Any existing client token is invalidated if not provided) + #Returns response dict on success, otherwise error dict + def authenticate(self, username=None, password=None, client_token=None): + endpoint = '/authenticate' + payload = { + 'agent': { + 'name': 'Minecraft', + 'version': 1, + }, + } + if username: + self.username = username + payload['username'] = self.username + if password: + self.password = password + payload['password'] = self.password + payload['clientToken'] = client_token if client_token else self.client_token + rep = self._ygg_req(endpoint, payload) + if 'error' not in rep: + self.access_token = rep['accessToken'] + self.client_token = rep['clientToken'] + return rep - #Generate an access token with a client/access token pair - #(The used access token is invalidated) - #Returns response dict on success, otherwise error dict - def refresh(self, client_token=None, access_token=None): - endpoint = '/refresh' - payload = {} - payload['accessToken'] = access_token if access_token else self.access_token - payload['clientToken'] = client_token if client_token else self.client_token - rep = self._ygg_req(endpoint, payload) - if 'error' not in rep: - self.access_token = rep['accessToken'] - self.client_token = rep['clientToken'] - return rep + #Generate an access token with a client/access token pair + #(The used access token is invalidated) + #Returns response dict on success, otherwise error dict + def refresh(self, client_token=None, access_token=None): + endpoint = '/refresh' + payload = {} + payload['accessToken'] = access_token if access_token else self.access_token + payload['clientToken'] = client_token if client_token else self.client_token + rep = self._ygg_req(endpoint, payload) + if 'error' not in rep: + self.access_token = rep['accessToken'] + self.client_token = rep['clientToken'] + return rep - #Invalidate access tokens with a username and password - #Returns None on success, otherwise error dict - def signout(self, username=None, password=None): - endpoint = '/signout' - payload = {} - payload['username'] = username if username else self.username - payload['password'] = password if password else self.password - return self._ygg_req(endpoint, payload) + #Invalidate access tokens with a username and password + #Returns None on success, otherwise error dict + def signout(self, username=None, password=None): + endpoint = '/signout' + payload = {} + payload['username'] = username if username else self.username + payload['password'] = password if password else self.password + return self._ygg_req(endpoint, payload) - #Invalidate access tokens with a client/access token pair - #Returns None on success, otherwise error dict - def invalidate(self, client_token=None, access_token=None): - endpoint = '/invalidate' - payload = {} - payload['accessToken'] = access_token if access_token else self.access_token - payload['clientToken'] = client_token if client_token else self.client_token - return self._ygg_req(endpoint, payload) + #Invalidate access tokens with a client/access token pair + #Returns None on success, otherwise error dict + def invalidate(self, client_token=None, access_token=None): + endpoint = '/invalidate' + payload = {} + payload['accessToken'] = access_token if access_token else self.access_token + payload['clientToken'] = client_token if client_token else self.client_token + return self._ygg_req(endpoint, payload) - #Check if an access token is valid - #Returns None on success (ie valid access token), otherwise error dict - def validate(self, access_token=None): - endpoint = '/validate' - payload = {} - payload['accessToken'] = access_token if access_token else self.access_token - return self._ygg_req(endpoint, payload) + #Check if an access token is valid + #Returns None on success (ie valid access token), otherwise error dict + def validate(self, access_token=None): + endpoint = '/validate' + payload = {} + payload['accessToken'] = access_token if access_token else self.access_token + return self._ygg_req(endpoint, payload) diff --git a/spock/plugins/__init__.py b/spock/plugins/__init__.py index a497a58..56a85cd 100644 --- a/spock/plugins/__init__.py +++ b/spock/plugins/__init__.py @@ -1,15 +1,16 @@ -from spock.plugins.core import event, net, timers, auth, threadpool -from spock.plugins.helpers import start, keepalive, clientinfo, world, move +from spock.plugins.core import event, net, auth, timer, ticker +from spock.plugins.helpers import start, keepalive, clientinfo, world, move, respawn DefaultPlugins = [ - event.EventPlugin, - net.NetPlugin, - timers.TimerPlugin, - auth.AuthPlugin, - threadpool.ThreadPoolPlugin, - start.StartPlugin, - keepalive.KeepalivePlugin, - #world.WorldPlugin, - #clientinfo.ClientInfoPlugin, - #move.MovePlugin, -] \ No newline at end of file + event.EventPlugin, + net.NetPlugin, + auth.AuthPlugin, + ticker.TickerPlugin, + timer.TimerPlugin, + start.StartPlugin, + keepalive.KeepalivePlugin, + respawn.RespawnPlugin, + move.MovementPlugin, + world.WorldPlugin, + clientinfo.ClientInfoPlugin, +] diff --git a/spock/plugins/core/auth.py b/spock/plugins/core/auth.py index 1700f68..bbf25ca 100644 --- a/spock/plugins/core/auth.py +++ b/spock/plugins/core/auth.py @@ -1,3 +1,7 @@ +""" +Provides authorization functions for Mojang's login and session servers +""" + import hashlib import urllib.request as request import json @@ -11,106 +15,110 @@ # This function courtesy of barneygale def JavaHexDigest(digest): - d = int(digest.hexdigest(), 16) - if d >> 39 * 4 & 0x8: - d = "-%x" % ((-d) & (2 ** (40 * 4) - 1)) - else: - d = "%x" % d - return d + d = int(digest.hexdigest(), 16) + if d >> 39 * 4 & 0x8: + d = "-%x" % ((-d) & (2 ** (40 * 4) - 1)) + else: + d = "%x" % d + return d class AuthCore: - def __init__(self, authenticated, event): - self.event = event - self.authenticated = authenticated - self.username = None - self.selected_profile = None - self.shared_secret = None - self.ygg = yggdrasil.YggAuth() + def __init__(self, authenticated, event): + self.event = event + self.authenticated = authenticated + self.username = None + self.selected_profile = None + self.shared_secret = None + if self.authenticated: + self.ygg = yggdrasil.YggAuth() - def start_session(self, username, password = ''): - if self.authenticated: - print( - "Attempting login with username:", username, - "and password:", password - ) - rep = self.ygg.authenticate(username, password) - if 'error' not in rep: - print(rep) - else: - print('Login Unsuccessful, Response:', rep) - self.event.emit('AUTH_ERR') - return rep - self.selected_profile = rep['selectedProfile'] - self.username = rep['selectedProfile']['name'] - else: - self.username = username - return ['offline'] - return rep + def start_session(self, username, password = ''): + rep = {} + if self.authenticated: + print( + "Attempting login with username:", username, + "and password:", password + ) + rep = self.ygg.authenticate(username, password) + if 'error' not in rep: + print(rep) + else: + print('Login Unsuccessful, Response:', rep) + self.event.emit('AUTH_ERR') + return rep + if 'selectedProfile' in rep: + self.selected_profile = rep['selectedProfile'] + self.username = rep['selectedProfile']['name'] + else: + self.username = username + else: + self.username = username + return rep - def gen_shared_secret(self): - self.shared_secret = Random._UserFriendlyRNG.get_random_bytes(16) - return self.shared_secret + def gen_shared_secret(self): + self.shared_secret = Random._UserFriendlyRNG.get_random_bytes(16) + return self.shared_secret @pl_announce('Auth') class AuthPlugin: - def __init__(self, ploader, settings): - self.event = ploader.requires('Event') - self.net = ploader.requires('Net') - settings = ploader.requires('Settings') - self.authenticated = settings['authenticated'] - self.sess_quit = settings['sess_quit'] - self.auth = AuthCore(self.authenticated, self.event) - self.auth.gen_shared_secret() - ploader.reg_event_handler('AUTH_ERR', self.handleAUTHERR) - ploader.reg_event_handler('SESS_ERR', self.handleSESSERR) - ploader.reg_event_handler( - (mcdata.LOGIN_STATE, mcdata.SERVER_TO_CLIENT, 0x01), - self.handle01 - ) - ploader.provides('Auth', self.auth) + def __init__(self, ploader, settings): + self.event = ploader.requires('Event') + self.net = ploader.requires('Net') + settings = ploader.requires('Settings') + self.authenticated = settings['authenticated'] + self.sess_quit = settings['sess_quit'] + self.auth = AuthCore(self.authenticated, self.event) + self.auth.gen_shared_secret() + ploader.reg_event_handler('AUTH_ERR', self.handleAUTHERR) + ploader.reg_event_handler('SESS_ERR', self.handleSESSERR) + ploader.reg_event_handler( + (mcdata.LOGIN_STATE, mcdata.SERVER_TO_CLIENT, 0x01), + self.handle01 + ) + ploader.provides('Auth', self.auth) - def handleAUTHERR(self, name, data): - self.event.kill() + def handleAUTHERR(self, name, data): + self.event.kill() - def handleSESSERR(self, name, data): - if self.sess_quit: - self.event.kill() + def handleSESSERR(self, name, data): + if self.sess_quit: + self.event.kill() - #Encryption Key Request - Request for client to start encryption - def handle01(self, name, packet): - pubkey = packet.data['public_key'] - if self.authenticated: - serverid = JavaHexDigest(hashlib.sha1( - packet.data['server_id'].encode('ascii') - + self.auth.shared_secret - + pubkey - )) - print('Attempting to authenticate session with sessionserver.mojang.com') - url = "https://sessionserver.mojang.com/session/minecraft/join" - data = json.dumps({ - 'accessToken': self.auth.ygg.access_token, - 'selectedProfile': self.auth.selected_profile, - 'serverId': serverid, - }).encode('utf-8') - headers = {'Content-Type': 'application/json'} - req = request.Request(url, data, headers, method='POST') - try: - rep = request.urlopen(req).read().decode('ascii') - except URLError: - rep = 'Couldn\'t connect to sessionserver.mojang.com' - #if rep != 'OK': - # print('Session Authentication Failed, Response:', rep) - # self.event.emit('SESS_ERR') - # return - print(rep) + #Encryption Key Request - Request for client to start encryption + def handle01(self, name, packet): + pubkey = packet.data['public_key'] + if self.authenticated: + serverid = JavaHexDigest(hashlib.sha1( + packet.data['server_id'].encode('ascii') + + self.auth.shared_secret + + pubkey + )) + print('Attempting to authenticate session with sessionserver.mojang.com') + url = "https://sessionserver.mojang.com/session/minecraft/join" + data = json.dumps({ + 'accessToken': self.auth.ygg.access_token, + 'selectedProfile': self.auth.selected_profile, + 'serverId': serverid, + }).encode('utf-8') + headers = {'Content-Type': 'application/json'} + req = request.Request(url, data, headers, method='POST') + try: + rep = request.urlopen(req).read().decode('ascii') + except URLError: + rep = 'Couldn\'t connect to sessionserver.mojang.com' + #if rep != 'OK': + # print('Session Authentication Failed, Response:', rep) + # self.event.emit('SESS_ERR') + # return + print(rep) - rsa_cipher = PKCS1_v1_5.new(RSA.importKey(pubkey)) - self.net.push(mcpacket.Packet( - ident='LOGIN>Encryption Response', - data = { - 'shared_secret': rsa_cipher.encrypt(self.auth.shared_secret), - 'verify_token': rsa_cipher.encrypt(packet.data['verify_token']), - } - )) - self.net.enable_crypto(self.auth.shared_secret) \ No newline at end of file + rsa_cipher = PKCS1_v1_5.new(RSA.importKey(pubkey)) + self.net.push(mcpacket.Packet( + ident = (mcdata.LOGIN_STATE, mcdata.CLIENT_TO_SERVER, 0x01), + data = { + 'shared_secret': rsa_cipher.encrypt(self.auth.shared_secret), + 'verify_token': rsa_cipher.encrypt(packet.data['verify_token']), + } + )) + self.net.enable_crypto(self.auth.shared_secret) diff --git a/spock/plugins/core/event.py b/spock/plugins/core/event.py index 66b57d4..9265d51 100644 --- a/spock/plugins/core/event.py +++ b/spock/plugins/core/event.py @@ -1,41 +1,41 @@ +""" +Provides the core event loop +""" + import signal import copy from spock.mcp import mcdata from spock.utils import pl_announce class EventCore: - def __init__(self): - self.kill_event = False - self.event_handlers = {} - - def event_loop(self): - signal.signal(signal.SIGINT, self.kill) - signal.signal(signal.SIGTERM, self.kill) - while not self.kill_event: - self.emit('tick') - self.emit('kill') + def __init__(self): + self.kill_event = False + self.event_handlers = {} - def reg_event_handler(self, event, handler): - if event not in self.event_handlers: - self.event_handlers[event] = [] - self.event_handlers[event].append(handler) + def event_loop(self): + # signal.signal(signal.SIGINT, self.kill) + # signal.signal(signal.SIGTERM, self.kill) + while not self.kill_event: + self.emit('event_tick') + self.emit('kill') - def unreg_event_handler(self, event, handler): - if event in self.event_handlers and handler in self.event_handlers[event]: - self.event_handlers[event].remove(handler) + def reg_event_handler(self, event, handler): + if event not in self.event_handlers: + self.event_handlers[event] = [] + self.event_handlers[event].append(handler) - def emit(self, event, data = None): - if event not in self.event_handlers: - self.event_handlers[event] = [] - for handler in self.event_handlers[event]: - handler(event, (data.clone() if hasattr(data, 'clone') - else copy.deepcopy(data) - )) + def emit(self, event, data = None): + if event not in self.event_handlers: + self.event_handlers[event] = [] + for handler in self.event_handlers[event]: + handler(event, (data.clone() if hasattr(data, 'clone') + else copy.deepcopy(data) + )) - def kill(self, *args): - self.kill_event = True + def kill(self, *args): + self.kill_event = True @pl_announce('Event') class EventPlugin: - def __init__(self, ploader, settings): - ploader.provides('Event', EventCore()) \ No newline at end of file + def __init__(self, ploader, settings): + ploader.provides('Event', EventCore()) diff --git a/spock/plugins/core/net.py b/spock/plugins/core/net.py index b9a3ff0..0342cc4 100644 --- a/spock/plugins/core/net.py +++ b/spock/plugins/core/net.py @@ -1,3 +1,9 @@ +""" +Provides an asynchronous, crypto and compression aware socket for connecting to +servers and processing incoming packet data. +Coordinates with the Timers plugin to honor clock-time timers +""" + import sys import socket import select @@ -7,234 +13,240 @@ from Crypto.Cipher import AES class AESCipher: - def __init__(self, SharedSecret): - #Name courtesy of dx - self.encryptifier = AES.new(SharedSecret, AES.MODE_CFB, IV=SharedSecret) - self.decryptifier = AES.new(SharedSecret, AES.MODE_CFB, IV=SharedSecret) + def __init__(self, SharedSecret): + #Name courtesy of dx + self.encryptifier = AES.new(SharedSecret, AES.MODE_CFB, IV=SharedSecret) + self.decryptifier = AES.new(SharedSecret, AES.MODE_CFB, IV=SharedSecret) - def encrypt(self, data): - return self.encryptifier.encrypt(data) + def encrypt(self, data): + return self.encryptifier.encrypt(data) - def decrypt(self, data): - return self.decryptifier.decrypt(data) + def decrypt(self, data): + return self.decryptifier.decrypt(data) class SelectSocket: - def __init__(self, timer): - self.sending = False - self.timer = timer - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.recv = self.sock.recv - self.send = self.sock.send - - def poll(self): - flags = [] - if self.sending: - self.sending = False - slist = [(self.sock,), (self.sock,), ()] - else: - slist = [(self.sock,), (), ()] - timeout = self.timer.get_timeout() - if timeout > 0: - slist.append(timeout) - try: - rlist, wlist, xlist = select.select(*slist) - except select.error as e: - print(str(e)) - rlist = [] - wlist = [] - if rlist: flags.append('SOCKET_RECV') - if wlist: flags.append('SOCKET_SEND') - return flags - - def reset(self): - self.sock.close() - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - def connect(self, host, port): - self.sock.connect((host, port)) - self.sock.setblocking(False) - -rmask = select.POLLIN|select.POLLPRI|select.POLLERR|select.POLLHUP|select.POLLNVAL -smask = rmask|select.POLLOUT -class PollSocket(SelectSocket): - def __init__(self, timer): - super().__init__(timer) - self.pollobj = select.poll() - - def poll(self): - flags = [] - if self.sending: - self.pollobj.register(self.sock, smask) - self.sending = False - else: - self.pollobj.register(self.sock, rmask) - try: - poll = self.pollobj.poll(self.timer.get_timeout() * 1000) - except select.error as e: - print(str(e)) - poll = [] - raise e - if poll: - poll = poll[0][1] - if poll & select.POLLERR: flags.append('SOCKET_ERR') - if poll & select.POLLHUP: flags.append('SOCKET_HUP') - if poll & select.POLLNVAL: flags.append('SOCKET_HUP') - if poll & select.POLLIN: flags.append('SOCKET_RECV') - if poll & select.POLLOUT: flags.append('SOCKET_SEND') - return flags - + def __init__(self, timer): + self.sending = False + self.timer = timer + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.setblocking(False) + self.recv = self.sock.recv + self.send = self.sock.send + + def poll(self): + flags = [] + if self.sending: + self.sending = False + slist = [(self.sock,), (self.sock,), (self.sock,)] + else: + slist = [(self.sock,), (), (self.sock,)] + timeout = self.timer.get_timeout() + if timeout>=0: + slist.append(timeout) + try: + rlist, wlist, xlist = select.select(*slist) + except select.error as e: + print(str(e)) + rlist = [] + wlist = [] + xlist = [] + if rlist: flags.append('SOCKET_RECV') + if wlist: flags.append('SOCKET_SEND') + if xlist: flags.append('SOCKET_ERR') + return flags + + def reset(self): + self.sock.close() + # self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # self.sock.setblocking(False) class NetCore: - trace = False - - def __init__(self, sock, event): - self.sock = sock - self.event = event - self.host = None - self.port = None - self.connected = False - self.encrypted = False - self.proto_state = mcdata.HANDSHAKE_STATE - self.sbuff = b'' - self.rbuff = utils.BoundBuffer() - - def connect(self, host = 'localhost', port = 25565): - self.host = host - self.port = port - print("Attempting to connect to host:", self.host, "port:", self.port) - self.sock.connect(self.host, self.port) - self.connected = True - print("Connected") - - def change_state(self, state): - if self.trace: - print("!!! Changing to state", mcdata.state_names[state]) - self.proto_state = state - - def push(self, packet): - if self.trace: - print(repr(packet)) - - if packet.state != self.proto_state: - raise ValueError("Cannot send packet {0} while in {1} state".format(repr(packet), mcdata.state_names[self.proto_state])) - if packet.direction != mcdata.CLIENT_TO_SERVER: - raise ValueError("Cannot send packet {0} from client to server".format(repr(packet))) - - data = packet.encode() - self.sbuff += (self.cipher.encrypt(data) if self.encrypted else data) - self.event.emit(packet.ident(), packet) - self.sock.sending = True - - def read_packet(self, data = b''): - self.rbuff.append(self.cipher.decrypt(data) if self.encrypted else data) - while True: - self.rbuff.save() - try: - packet = mcpacket.Packet(ident=( - self.proto_state, - mcdata.SERVER_TO_CLIENT, - )).decode(self.rbuff) - except utils.BufferUnderflowException: - self.rbuff.revert() - break - - if self.trace: - print(repr(packet)) - - self.event.emit(packet.ident(), packet) - - def enable_crypto(self, secret_key): - self.cipher = AESCipher(secret_key) - self.encrypted = True - - def disable_crypto(self): - self.cipher = None - self.encrypted = False - - def reset(self): - self.sock.reset() - self.__init__(self.sock, self.event) - - disconnect = reset + def __init__(self, event, timer): + self.timer = timer + self.event = event + self.host = None + self.port = None + self.createSocket() + self.queue = [] + + def tick(self): + if len(self.queue): + self.push(self.queue.pop(0)) + + def createSocket(self): + self.sock = SelectSocket(self.timer) + self.connected = False + self.encrypted = False + self.proto_state = mcdata.HANDSHAKE_STATE + self.comp_state = mcdata.PROTO_COMP_OFF + self.comp_threshold = -1 + self.sbuff = b'' + self.rbuff = utils.BoundBuffer() + + def connect(self, host = 'localhost', port = 25565): + self.host = host + self.port = port + try: + print("Attempting to connect to host:", host, "port:", port) + #Set the connect to be a blocking operation + self.sock.sock.setblocking(True) + self.sock.sock.connect((self.host, self.port)) + self.sock.sock.setblocking(False) + self.connected = True + self.event.emit('connect', [self.host, self.port]) + print("Connected to host:", host, "port:", port) + return True + except socket.error as error: + print("Error on Connect:", str(error)) + return False + + def set_proto_state(self, state): + self.proto_state = state + self.event.emit(mcdata.state_lookup[state] + '_STATE') + + def set_comp_state(self, threshold): + self.comp_threshold = threshold + if threshold >=0: + self.comp_state = mcdata.PROTO_COMP_ON + + def push(self, packet): + if self.proto_state != packet.ident[0]: + # play-state package during reconnect, stash for later use, if not keepAlive or Position: + if packet.ident[2] not in [0,6]: + self.queue.append(packet) + return + data = packet.encode(self.comp_state, self.comp_threshold) + self.sbuff += (self.cipher.encrypt(data) if self.encrypted else data) + self.event.emit(packet.ident, packet) + self.sock.sending = True + + def read_packet(self, data = b''): + self.rbuff.append(self.cipher.decrypt(data) if self.encrypted else data) + while True: + self.rbuff.save() + try: + packet = mcpacket.Packet(ident = ( + self.proto_state, + mcdata.SERVER_TO_CLIENT, + )).decode(self.rbuff, self.comp_state) + except utils.BufferUnderflowException: + self.rbuff.revert() + break + if packet: + self.event.emit(packet.ident, packet) + self.event.emit(packet.str_ident, packet) + + def enable_crypto(self, secret_key): + self.cipher = AESCipher(secret_key) + self.encrypted = True + + def disable_crypto(self): + self.cipher = None + self.encrypted = False + + def reset(self): + self.connected = False + if self.sock: + self.sock.reset() + self.sock = None #.reset() + + def restart(self): + self.connected = False + self.createSocket() + + disconnect = reset @pl_announce('Net') class NetPlugin: - def __init__(self, ploader, settings): - if sys.platform != 'win32': - self.sock = PollSocket(ploader.requires('Timers')) - else: - self.sock = SelectSocket(ploader.requires('Timers')) - settings = ploader.requires('Settings') - self.bufsize = settings['bufsize'] - self.sock_quit = settings['sock_quit'] - self.event = ploader.requires('Event') - self.net = NetCore(self.sock, self.event) - self.net.trace = settings['packet_trace'] - ploader.provides('Net', self.net) - - ploader.reg_event_handler('tick', self.tick) - ploader.reg_event_handler('SOCKET_RECV', self.handleRECV) - ploader.reg_event_handler('SOCKET_SEND', self.handleSEND) - ploader.reg_event_handler('SOCKET_ERR', self.handleERR) - ploader.reg_event_handler('SOCKET_HUP', self.handleHUP) - ploader.reg_event_handler(mcdata.packet_idents['PLAYHandshake'], self.handle_handshake) - ploader.reg_event_handler(mcdata.packet_idents['LOGIN0 and not self.runs<0: + self.runs-=1 + if self.runs: + self.reset() + + def stop(self): + self.runs = 0 + + def reset(self): + pass + +#Time based timer +class EventTimer(BaseTimer): + def __init__(self, wait_time, callback, runs = 1): + super().__init__(callback, runs) + self.wait_time = wait_time + self.end_time = time.time() + self.wait_time + + def countdown(self): + count = self.end_time - time.time() + return count if count > 0 else 0 + + def check(self): + if self.runs == 0: return False + return self.end_time<=time.time() + + def reset(self): + self.end_time = time.time() + self.wait_time + +#World tick based timer +class TickTimer(BaseTimer): + def __init__(self, world, wait_ticks, callback, runs = 1): + super().__init__(callback, runs) + self.world = world + self.wait_ticks = wait_ticks + self.end_tick = self.world.age + self.wait_ticks + + def check(self): + if self.runs == 0: return False + return self.end_tick<=self.world.age + + def reset(self): + self.end_tick = self.world.age + self.wait_ticks + +class TimerCore: + def __init__(self, world): + self.timers = [] + self.world = world + + def reg_timer(self, timer): + self.timers.append(timer) + + def get_timeout(self): + timeout = -1 + for timer in self.timers: + if timeout > timer.countdown() or timeout == -1: + timeout = timer.countdown() + return timeout + + def reg_event_timer(self, wait_time, callback, runs = -1): + self.reg_timer(EventTimer(wait_time, callback, runs)) + + def reg_tick_timer(self, wait_ticks, callback, runs = -1): + self.reg_timer(TickTimer(self.world, wait_ticks, callback, runs)) + +class WorldTick: + def __init__(self): + self.age = 0 + +@pl_announce('Timers') +class TimerPlugin: + def __init__(self, ploader, settings): + self.world = ploader.requires('World') + if not self.world: + self.world = WorldTick() + ploader.reg_event_handler( + (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x03), + self.handle03 + ) + self.timer_core = TimerCore(self.world) + ploader.provides('Timers', self.timer_core) + ploader.reg_event_handler('event_tick', self.tick) + ploader.reg_event_handler('disconnect', self.handle_disconnect) + + def tick(self, name, data): + for timer in self.timer_core.timers: + timer.update() + if not timer.get_runs(): + self.timer_core.timers.remove(timer) + + #Time Update - We grab world age if the world plugin isn't available + def handle03(self, name, packet): + self.world.age = packet.data['world_age'] + + def handle_disconnect(self, name, data): + self.timer_core.timers = [] diff --git a/spock/plugins/core/timers.py b/spock/plugins/core/timers.py deleted file mode 100644 index bb2179a..0000000 --- a/spock/plugins/core/timers.py +++ /dev/null @@ -1,122 +0,0 @@ -import time -from spock.mcp import mcdata -from spock.utils import pl_announce - -class BaseTimer(object): - def __init__(self, callback, runs = 1): - self.callback = callback - self.runs = runs - - def get_runs(self): - return self.runs - - def check(self): - raise NotImplementedError - - def reset(self): - raise NotImplementedError - - def update(self): - if self.check(): - self.fire() - - def fire(self): - self.callback() - if self.runs > 0: - self.runs -= 1 - if self.runs != 0: - self.reset() - - def stop(self): - self.runs = 0 - -#Time based timer -class EventTimer(BaseTimer): - def __init__(self, wait_time, callback, runs = 1): - super().__init__(callback, runs) - self.wait_time = wait_time - self.end_time = time.time() + self.wait_time - - def countdown(self): - count = self.end_time - time.time() - return count if count > 0 else 0 - - def check(self): - if self.runs == 0: return False - return self.end_time<=time.time() - - def reset(self): - self.end_time = time.time() + self.wait_time - -#World tick based timer -class TickTimer(BaseTimer): - def __init__(self, world, wait_ticks, callback, runs = 1): - super().__init__(callback, runs) - self.world = world - self.wait_ticks = wait_ticks - self.end_tick = self.world.age + self.wait_ticks - - def countdown(self): - return -1 - - def check(self): - if self.runs == 0: return False - return self.end_tick<=self.world.age - - def reset(self): - self.end_tick = self.world.age + self.wait_ticks - -class TimerCore: - def __init__(self, world): - self.timers = [] - self.world = world - - def reg_timer(self, timer): - self.timers.append(timer) - - def get_timeout(self): - timeout = -1 - for timer in self.timers: - countdown = timer.countdown() - if timeout > countdown or timeout == -1: - timeout = countdown - return timeout - - def reg_event_timer(self, wait_time, callback, runs = 1): - self.reg_timer(EventTimer(wait_time, callback, runs)) - - def reg_tick_timer(self, wait_ticks, callback, runs = 1): - self.reg_timer(TickTimer(self.world, wait_ticks, callback, runs)) - -class WorldTick: - def __init__(self): - self.age = 0 - -@pl_announce('Timers') -class TimerPlugin: - def __init__(self, ploader, settings): - self.world = ploader.requires('World') - if not self.world: - self.world = WorldTick() - ploader.reg_event_handler( - (mcdata.PLAY_STATE, mcdata.SERVER_TO_CLIENT, 0x03), - self.handle03 - ) - self.timer_core = TimerCore(self.world) - ploader.provides('Timers', self.timer_core) - ploader.reg_event_handler('tick', self.tick) - ploader.reg_event_handler('SOCKET_ERR', self.handle_disconnect) - ploader.reg_event_handler('SOCKET_HUP', self.handle_disconnect) - - def tick(self, name, data): - for timer in self.timer_core.timers: - timer.update() - if not timer.get_runs(): - self.timer_core.timers.remove(timer) - - #Time Update - We grab world age if the world plugin isn't available - def handle03(self, name, packet): - self.world.age = packet.data['world_age'] - - def handle_disconnect(self, name, data): - self.timer_core.timers = [] \ No newline at end of file diff --git a/spock/plugins/helpers/clientinfo.py b/spock/plugins/helpers/clientinfo.py index d3dc13a..18320b1 100644 --- a/spock/plugins/helpers/clientinfo.py +++ b/spock/plugins/helpers/clientinfo.py @@ -1,78 +1,99 @@ -from spock.mcp import mcdata +""" +ClientInfo is a central plugin for recording data about the client +ex. Health, position, and some auxillary information like the player list +Plugins subscribing to ClientInfo and its events don't have to independently +track this information on their own. +""" + from spock.utils import pl_announce +from spock.mcp.mcdata import ( + FLG_XPOS_REL, FLG_YPOS_REL, FLG_ZPOS_REL, FLG_YROT_REL, FLG_XROT_REL +) class ClientInfo: - def __init__(self): - self.eid = 0 - self.game_info = { - 'level_type': 0, - 'game_mode': None, - 'dimension': 0, - 'difficulty': 0, - 'max_players': 0, - } - self.spawn_position = { - 'x': 0, - 'y': 0, - 'z': 0, - } - self.health = { - 'health': 20, - 'food': 20, - 'food_saturation': 5, - } - self.position = { - 'x': 0, - 'y': 0, - 'z': 0, - 'stance': 0, - 'yaw': 0, - 'pitch': 0, - 'on_ground': False, - } - self.player_list = {} + def __init__(self): + self.eid = 0 + self.game_info = { + 'level_type': 0, + 'gamemode': None, + 'difficulty': 0, + 'max_players': 0, + } + self.spawn_position = { + 'x': 0, + 'y': 0, + 'z': 0, + } + self.health = { + 'health': 20, + 'food': 20, + 'food_saturation': 5, + } + self.position = { + 'x': 0, + 'y': 0, + 'z': 0, + 'stance': 0, + 'yaw': 0, + 'pitch': 0, + 'on_ground': False, + } + self.player_list = {} - def reset(self): - self.__init__() + def reset(self): + self.__init__() @pl_announce('ClientInfo') class ClientInfoPlugin: - def __init__(self, ploader, settings): - self.event = ploader.requires('Event') - self.emit = self.event.emit - - self.event.reg_event_handler(mcdata.packet_idents['PLAY