diff --git a/.gitignore b/.gitignore index 585e7154..50b62bec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ # Distribution / packaging .Python env/ +venv/ build/ develop-eggs/ dist/ @@ -83,3 +84,4 @@ sftp-config.json ### pyCraft ### credentials +mcdata diff --git a/dl_mcdata.sh b/dl_mcdata.sh new file mode 100755 index 00000000..961091a1 --- /dev/null +++ b/dl_mcdata.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +VERSION="1.15.2" + +wget -O/tmp/mcdata.zip https://apimon.de/mcdata/$VERSION/$VERSION.zip +rm -rf mcdata +mkdir mcdata +unzip /tmp/mcdata.zip -d mcdata +rm /tmp/mcdata.zip diff --git a/minecraft/managers/__init__.py b/minecraft/managers/__init__.py new file mode 100644 index 00000000..75c311f1 --- /dev/null +++ b/minecraft/managers/__init__.py @@ -0,0 +1,5 @@ +from .data import DataManager +from .assets import AssetsManager +from .chat import ChatManager +from .chunks import ChunksManager +from .entities import EntitiesManager diff --git a/minecraft/managers/assets.py b/minecraft/managers/assets.py new file mode 100644 index 00000000..ec553423 --- /dev/null +++ b/minecraft/managers/assets.py @@ -0,0 +1,85 @@ +import os +import json +import re + +class AssetsManager: + + def __init__(self, directory, lang="en_us"): + self.lang = {} + self.directory = directory + + if not os.path.isdir(directory): + raise FileNotFoundError("%s is not a valid directory") + + if not os.path.isfile("%s/models/block/block.json"%(directory)): + raise FileNotFoundError("%s is not a valid assets directory"%(directory)) + + with open("%s/lang/%s.json"%(directory, lang)) as f: + self.lang = json.loads(f.read()) + for x in self.lang: + self.lang[x] = re.sub("\%\d+\$s", "%s", self.lang[x]) # HACK + + def translate(self, key, extra=[]): + if key not in self.lang: + return "[%?]"%(key) + if extra: + return self.lang[key]%tuple(extra) + else: + return self.lang[key] + + def get_block_variant(self, name, properties={}): + if name.startswith("minecraft:"): + name = name[10:] + + filename = "%s/blockstates/%s.json"%(self.directory, name) + if not os.path.isfile(filename): + raise FileNotFoundError("'%s' is not a valid block name"%(name)) + with open(filename) as f: + variants = json.loads(f.read())['variants'] + + if properties: + k = ",".join(["%s=%s"%(x, properties[x]) for x in sorted(properties.keys())]) + else: + k = "" + + if not k in variants: + k = "" + + v = variants[k] + if isinstance(v, list) and len(v)>0: + v=v[0] # HACK + return v + + def get_model(self, path, recursive=True): + filename = "%s/models/%s.json"%(self.directory, path) + if not os.path.isfile(filename): + raise FileNotFoundError("'%s' is not a valid model path"%(path)) + with open(filename) as f: + model = json.loads(f.read()) + + if recursive and 'parent' in model: + parent = self.get_model(model['parent']) + for x in parent: + a = parent[x] + if x in model: + a.update(model[x]) + model[x] = a + del(model['parent']) + + return model + + def get_faces_textures(self, model): + if 'textures' not in model or 'elements' not in model: + return {} + textures = model['textures'] + faces = {} + for e in model['elements']: + for x in e['faces']: + if x in faces: + continue + faces[x] = e['faces'][x] + while faces[x]['texture'].startswith("#"): + # TODO: Raise exception on max iteration + faces[x]['texture'] = textures[faces[x]['texture'][1:]] + return faces + diff --git a/minecraft/managers/chat.py b/minecraft/managers/chat.py new file mode 100644 index 00000000..4064a2f7 --- /dev/null +++ b/minecraft/managers/chat.py @@ -0,0 +1,41 @@ +import json + +from ..networking.packets import clientbound, serverbound + +class ChatManager: + + def __init__(self, assets_manager): + self.assets = assets_manager + + def translate_chat(self, data): + if isinstance(data, str): + return data + elif 'extra' in data: + return "".join([self.translate_chat(x) for x in data['extra']]) + elif 'translate' in data and 'with' in data: + params = [self.translate_chat(x) for x in data['with']] + return self.assets.translate(data['translate'], params) + elif 'translate' in data: + return self.assets.translate(data['translate']) + elif 'text' in data: + return data['text'] + else: + return "?" + + def print_chat(self, chat_packet): + # TODO: Replace with handler + try: + print("[%s] %s"%(chat_packet.field_string('position'), self.translate_chat(json.loads(chat_packet.json_data)))) + except Exception as ex: + print("Exception %r on message (%s): %s" % (ex, chat_packet.field_string('position'), chat_packet.json_data)) + + def register(self, connection): + connection.register_packet_listener(self.print_chat, clientbound.play.ChatMessagePacket) + + def send(self, connection, text): + if not text: + # Prevents connection bug when sending empty chat message + return + packet = serverbound.play.ChatPacket() + packet.message = text + connection.write_packet(packet) diff --git a/minecraft/managers/chunks.py b/minecraft/managers/chunks.py new file mode 100644 index 00000000..6dad7b10 --- /dev/null +++ b/minecraft/managers/chunks.py @@ -0,0 +1,97 @@ +from math import floor + +from ..networking.packets import clientbound + +class ChunksManager: + + def __init__(self, data_manager): + self.data = data_manager + self.chunks = {} + self.biomes = {} + + def handle_block(self, block_packet): + self.set_block_at(block_packet.location.x, block_packet.location.y, block_packet.location.z, block_packet.block_state_id) + #self.print_chunk(self.get_chunk(floor(block_packet.location.x/16), floor(block_packet.location.y/16), floor(block_packet.location.z/16)), block_packet.location.y%16) + #print('Block %s at %s'%(blocks_states[block_packet.block_state_id], block_packet.location)) + + def handle_multiblock(self, multiblock_packet): + for b in multiblock_packet.records: + self.handle_block(b) + + def handle_chunk(self, chunk_packet): + for i in chunk_packet.chunks: + self.chunks[(chunk_packet.x, i, chunk_packet.z)] = chunk_packet.chunks[i] + self.biomes[(chunk_packet.x, None, chunk_packet.z)] = chunk_packet.biomes # FIXME + + def register(self, connection): + connection.register_packet_listener(self.handle_block, clientbound.play.BlockChangePacket) + connection.register_packet_listener(self.handle_multiblock, clientbound.play.MultiBlockChangePacket) + connection.register_packet_listener(self.handle_chunk, clientbound.play.ChunkDataPacket) + + def get_chunk(self, x, y, z): + index = (x, y, z) + if not index in self.chunks: + raise ChunkNotLoadedException(index) + return self.chunks[index] + + def get_loaded_area(self, ignore_empty=False): + first = next(iter(self.chunks.keys())) + x0 = x1 = first[0] + y0 = y1 = first[1] + z0 = z1 = first[2] + for k in self.chunks.keys(): + if ignore_empty and self.chunks[k].empty: + continue + x0 = min(x0, k[0]) + x1 = max(x1, k[0]) + y0 = min(y0, k[1]) + y1 = max(y1, k[1]) + z0 = min(z0, k[2]) + z1 = max(z1, k[2]) + return ((x0,y0,z0),(x1,y1,z1)) + + def get_block_at(self, x, y, z): + c = self.get_chunk(floor(x/16), floor(y/16), floor(z/16)) + return c.get_block_at(x%16, y%16, z%16) + + def set_block_at(self, x, y, z, block): + c = self.get_chunk(floor(x/16), floor(y/16), floor(z/16)) + c.set_block_at(x%16, y%16, z%16, block) + + def print_chunk(self, chunk, y_slice): + print("This is chunk %d %d %d at slice %d:"%(chunk.x, chunk.y, chunk.z, y_slice)) + print("+%s+"%("-"*16)) + for z in range(16): + missing = [] + print("|", end="") + for x in range(16): + sid = chunk.get_block_at(x, y_slice, z) + bloc = self.data.blocks_states[sid] + if bloc == "minecraft:air" or bloc == "minecraft:cave_air": + c = " " + elif bloc == "minecraft:grass_block" or bloc == "minecraft:dirt": + c = "-" + elif bloc == "minecraft:water": + c = "~" + elif bloc == "minecraft:lava": + c = "!" + elif bloc == "minecraft:bedrock": + c = "_" + elif bloc == "minecraft:stone": + c = "X" + else: + missing.append(bloc) + c = "?" + + print(c, end="") + print("| %s"%(",".join(missing))) + print("+%s+"%("-"*16)) + if chunk.entities: + print("Entities in slice: %s"%(", ".join([x['id'].decode() for x in chunk.entities]))) + + +class ChunkNotLoadedException(Exception): + def __str__(self): + pos = self.args[0] + return "Chunk at %d %d %d not loaded (yet?)"%(pos[0], pos[1], pos[2]) + diff --git a/minecraft/managers/data.py b/minecraft/managers/data.py new file mode 100644 index 00000000..eb4509c0 --- /dev/null +++ b/minecraft/managers/data.py @@ -0,0 +1,32 @@ +import os +import json + +class DataManager: + + def __init__(self, directory): + self.blocks = {} + self.blocks_states = {} + self.blocks_properties = {} + self.registries = {} + self.biomes = {} + self.entity_type = {} + + if not os.path.isdir(directory): + raise FileNotFoundError("%s is not a valid directory") + + if not os.path.isfile("%s/registries.json"%(directory)): + raise FileNotFoundError("%s is not a valid minecraft data directory") + + with open("%s/blocks.json"%(directory)) as f: + blocks = json.loads(f.read()) + for x in blocks: + for s in blocks[x]['states']: + self.blocks_states[s['id']] = x + self.blocks_properties[s['id']] = s.get('properties', {}) + + with open("%s/registries.json"%(directory)) as f: + registries = json.loads(f.read()) + for x in registries["minecraft:biome"]["entries"]: + self.biomes[registries["minecraft:biome"]["entries"][x]["protocol_id"]] = x + for x in registries["minecraft:entity_type"]["entries"]: + self.entity_type[registries["minecraft:entity_type"]["entries"][x]["protocol_id"]] = x diff --git a/minecraft/managers/entities.py b/minecraft/managers/entities.py new file mode 100644 index 00000000..f005b279 --- /dev/null +++ b/minecraft/managers/entities.py @@ -0,0 +1,12 @@ + + +from ..networking.packets import clientbound + +class EntitiesManager: + + def __init__(self, data_manager): + self.data = data_manager + self.entities = {} + + def register(self, connection): + pass diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 30c62aa8..93e9412b 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -18,6 +18,7 @@ from .explosion_packet import ExplosionPacket from .sound_effect_packet import SoundEffectPacket from .face_player_packet import FacePlayerPacket +from .chunk_data import ChunkDataPacket # Formerly known as state_playing_clientbound. @@ -42,7 +43,8 @@ def get_packets(context): RespawnPacket, PluginMessagePacket, PlayerListHeaderAndFooterPacket, - EntityLookPacket + EntityLookPacket, + ChunkDataPacket } if context.protocol_version <= 47: packets |= { diff --git a/minecraft/networking/packets/clientbound/play/block_change_packet.py b/minecraft/networking/packets/clientbound/play/block_change_packet.py index ee61d0f0..f016b2ff 100644 --- a/minecraft/networking/packets/clientbound/play/block_change_packet.py +++ b/minecraft/networking/packets/clientbound/play/block_change_packet.py @@ -61,7 +61,7 @@ def get_id(context): chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z') class Record(MutableRecord): - __slots__ = 'x', 'y', 'z', 'block_state_id' + __slots__ = 'x', 'y', 'z', 'block_state_id', 'location' def __init__(self, **kwds): self.block_state_id = 0 @@ -91,11 +91,13 @@ def blockMeta(self, meta): # This alias is retained for backward compatibility. blockStateId = attribute_alias('block_state_id') - def read(self, file_object): + def read(self, file_object, parent): h_position = UnsignedByte.read(file_object) self.x, self.z = h_position >> 4, h_position & 0xF self.y = UnsignedByte.read(file_object) self.block_state_id = VarInt.read(file_object) + # Absolute position in world to be compatible with BlockChangePacket + self.location = Vector(self.position.x + parent.chunk_x*16, self.position.y, self.position.z + parent.chunk_z*16) def write(self, packet_buffer): UnsignedByte.send(self.x << 4 | self.z & 0xF, packet_buffer) @@ -109,7 +111,7 @@ def read(self, file_object): self.records = [] for i in range(records_count): record = self.Record() - record.read(file_object) + record.read(file_object, self) self.records.append(record) def write_fields(self, packet_buffer): diff --git a/minecraft/networking/packets/clientbound/play/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py new file mode 100644 index 00000000..1afcaa98 --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -0,0 +1,137 @@ +from math import floor + +from minecraft.networking.packets import Packet, PacketBuffer +from minecraft.networking.types import ( + VarInt, Integer, Boolean, Nbt, UnsignedByte, Long, Short, + multi_attribute_alias, Vector, UnsignedLong +) + +class ChunkDataPacket(Packet): + @staticmethod + def get_id(context): + return 0x22 # FIXME + + packet_name = 'chunk data' + fields = 'x', 'bit_mask_y', 'z', 'full_chunk' + + def read(self, file_object): + self.x = Integer.read(file_object) + self.z = Integer.read(file_object) + self.full_chunk = Boolean.read(file_object) + self.bit_mask_y = VarInt.read(file_object) + self.heightmaps = Nbt.read(file_object) + self.biomes = [] + if self.full_chunk: + for i in range(1024): + self.biomes.append(Integer.read(file_object)) + size = VarInt.read(file_object) + self.data = file_object.read(size) + size_entities = VarInt.read(file_object) + self.entities = [] + for i in range(size_entities): + self.entities.append(Nbt.read(file_object)) + + self.decode_chunk_data() + + def write_fields(self, packet_buffer): + Integer.send(self.x, packet_buffer) + Integer.send(self.z, packet_buffer) + Boolean.send(self.full_chunk, packet_buffer) + VarInt.send(self.bit_mask_y, packet_buffer) + Nbt.send(self.heightmaps, packet_buffer) + if self.full_chunk: + for i in range(1024): + Integer.send(self.biomes[i], packet_buffer) + VarInt.send(len(self.data), packet_buffer) + packet_buffer.send(self.data) + VarInt.send(len(self.entities), packet_buffer) + for e in self.entities: + Nbt.send(e, packet_buffer) + + def decode_chunk_data(self): + packet_data = PacketBuffer() + packet_data.send(self.data) + packet_data.reset_cursor() + + self.chunks = {} + for i in range(16): #0-15 + self.chunks[i] = Chunk(self.x, i, self.z) + if self.bit_mask_y & (1 << i): + self.chunks[i].read(packet_data) + + for e in self.entities: + y = e['y'] + self.chunks[floor(y/16)].entities.append(e) + + +class Chunk: + + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + def __init__(self, x, y, z, empty=True): + self.x = x + self.y = y + self.z = z + self.empty = empty + self.entities = [] + + def __repr__(self): + return 'Chunk(%r, %r, %r)' % (self.x, self.y, self.z) + + def read(self, file_object): + self.empty = False + self.block_count = Short.read(file_object) + self.bpb = UnsignedByte.read(file_object) + if self.bpb <= 4: + self.bpb = 4 + + if self.bpb <= 8: # Indirect palette + self.palette = [] + size = VarInt.read(file_object) + for i in range(size): + self.palette.append(VarInt.read(file_object)) + else: # Direct palette + self.palette = None + + size = VarInt.read(file_object) + longs = [] + for i in range(size): + longs.append(UnsignedLong.read(file_object)) + + self.blocks = [] + mask = (1 << self.bpb)-1 + for i in range(4096): + l1 = int((i*self.bpb)/64) + offset = (i*self.bpb)%64 + l2 = int(((i+1)*self.bpb-1)/64) + n = longs[l1] >> offset + if l2>l1: + n |= longs[l2] << (64-offset) + n &= mask + if self.palette: + n = self.palette[n] + self.blocks.append(n) + + def write_fields(self, packet_buffer): + pass # TODO + + def get_block_at(self, x, y, z): + if self.empty: + return 0 + return self.blocks[x+y*256+z*16] + + def set_block_at(self, x, y, z, block): + if self.empty: + self.init_empty() + self.blocks[x+y*256+z*16] = block + + def init_empty(self): + self.blocks = [] + for i in range(4096): + self.blocks.append(0) + self.empty = False + + @property + def origin(self): + return self.position*16 + diff --git a/minecraft/networking/types/__init__.py b/minecraft/networking/types/__init__.py index 2160ed82..9cf92328 100644 --- a/minecraft/networking/types/__init__.py +++ b/minecraft/networking/types/__init__.py @@ -1,3 +1,4 @@ from .basic import * # noqa: F401, F403 from .enum import * # noqa: F401, F403 from .utility import * # noqa: F401, F403 +from .nbt import * # noqa: F401, F403 diff --git a/minecraft/networking/types/basic.py b/minecraft/networking/types/basic.py index 4ccf627e..a7368935 100644 --- a/minecraft/networking/types/basic.py +++ b/minecraft/networking/types/basic.py @@ -14,7 +14,7 @@ 'Integer', 'FixedPointInteger', 'Angle', 'VarInt', 'Long', 'UnsignedLong', 'Float', 'Double', 'ShortPrefixedByteArray', 'VarIntPrefixedByteArray', 'TrailingByteArray', 'String', 'UUID', - 'Position', + 'Position', 'IntegerPrefixedByteArray', ) @@ -241,6 +241,18 @@ def send(value, socket): socket.send(value) +class IntegerPrefixedByteArray(Type): + @staticmethod + def read(file_object): + length = Integer.read(file_object) + return struct.unpack(str(length) + "s", file_object.read(length))[0] + + @staticmethod + def send(value, socket): + Integer.send(len(value), socket) + socket.send(value) + + class VarIntPrefixedByteArray(Type): @staticmethod def read(file_object): diff --git a/minecraft/networking/types/nbt.py b/minecraft/networking/types/nbt.py new file mode 100644 index 00000000..2626c28a --- /dev/null +++ b/minecraft/networking/types/nbt.py @@ -0,0 +1,93 @@ +"""Contains definition for minecraft's NBT format. +""" +from __future__ import division +import struct + +from .utility import Vector +from .basic import Type, Byte, Short, Integer, Long, Float, Double, ShortPrefixedByteArray, IntegerPrefixedByteArray + +__all__ = ( + 'Nbt', +) + +TAG_End = 0 +TAG_Byte = 1 +TAG_Short = 2 +TAG_Int = 3 +TAG_Long = 4 +TAG_Float = 5 +TAG_Double = 6 +TAG_Byte_Array = 7 +TAG_String = 8 +TAG_List = 9 +TAG_Compound = 10 +TAG_Int_Array = 11 +TAG_Long_Array = 12 + + +class Nbt(Type): + + @staticmethod + def read(file_object): + type_id = Byte.read(file_object) + if type_id != TAG_Compound: + raise Exception("Invalid NBT header") + name = ShortPrefixedByteArray.read(file_object).decode('utf-8') + a = Nbt.decode_tag(file_object, TAG_Compound) + a['_name'] = name + return a + + @staticmethod + def decode_tag(file_object, type_id): + if type_id == TAG_Byte: + return Byte.read(file_object) + elif type_id == TAG_Short: + return Short.read(file_object) + elif type_id == TAG_Int: + return Integer.read(file_object) + elif type_id == TAG_Long: + return Long.read(file_object) + elif type_id == TAG_Float: + return Float.read(file_object) + elif type_id == TAG_Double: + return Double.read(file_object) + elif type_id == TAG_Byte_Array: + return IntegerPrefixedByteArray.read(file_object).decode('utf-8') + elif type_id == TAG_String: + return ShortPrefixedByteArray.read(file_object) + elif type_id == TAG_List: + list_type_id = Byte.read(file_object) + size = Integer.read(file_object) + a = [] + for i in range(size): + a.append(Nbt.decode_tag(file_object, list_type_id)) + return a + elif type_id == TAG_Compound: + c = { } + child_type_id = Byte.read(file_object) + while child_type_id != TAG_End: + child_name = ShortPrefixedByteArray.read(file_object).decode('utf-8') + c[child_name] = Nbt.decode_tag(file_object, child_type_id) + child_type_id = Byte.read(file_object) + return c + elif type_id == TAG_Int_Array: + size = Integer.read(file_object) + a = [] + for i in range(size): + a.append(Integer.read(file_object)) + return a + elif type_id == TAG_Long_Array: + size = Integer.read(file_object) + a = [] + for i in range(size): + a.append(Long.read(file_object)) + return a + else: + raise Exception("Invalid NBT tag type") + + @staticmethod + def send(value, socket): + # TODO + pass + + diff --git a/test.py b/test.py new file mode 100755 index 00000000..c993320d --- /dev/null +++ b/test.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 + +from __future__ import print_function + +import getpass +import sys +import re +import json +import traceback +from optparse import OptionParser +from pgmagick import Image, Geometry, Color, CompositeOperator, DrawableRoundRectangle + +from minecraft import authentication +from minecraft.exceptions import YggdrasilError +from minecraft.networking.connection import Connection +from minecraft.networking.packets import Packet, clientbound, serverbound +from minecraft.compat import input +from minecraft.managers import DataManager, AssetsManager, ChatManager, ChunksManager, EntitiesManager + + +def get_options(): + parser = OptionParser() + + parser.add_option("-u", "--username", dest="username", default=None, + help="username to log in with") + + parser.add_option("-p", "--password", dest="password", default=None, + help="password to log in with") + + parser.add_option("-s", "--server", dest="server", default=None, + help="server host or host:port " + "(enclose IPv6 addresses in square brackets)") + + parser.add_option("-o", "--offline", dest="offline", action="store_true", + help="connect to a server in offline mode ") + + parser.add_option("-d", "--dump-packets", dest="dump_packets", + action="store_true", + help="print sent and received packets to standard error") + + parser.add_option("-a", "--assets", dest="assets", default='minecraft', + help="assets directory (uncompressed)") + + parser.add_option("--mcversion", dest="mcversion", default='1.15.2', + help="minecraft version") + + (options, args) = parser.parse_args() + + if not options.username: + options.username = input("Enter your username: ") + + if not options.password and not options.offline: + options.password = getpass.getpass("Enter your password (leave " + "blank for offline mode): ") + options.offline = options.offline or (options.password == "") + + if not options.server: + options.server = input("Enter server host or host:port " + "(enclose IPv6 addresses in square brackets): ") + # Try to split out port and address + match = re.match(r"((?P[^\[\]:]+)|\[(?P[^\[\]]+)\])" + r"(:(?P\d+))?$", options.server) + if match is None: + raise ValueError("Invalid server address: '%s'." % options.server) + options.address = match.group("host") or match.group("addr") + options.port = int(match.group("port") or 25565) + + return options + +def export_area(x1, y1, z1, x2, y2, z2, chunks, assets, data): + + if x1>x2: + x1, x2 = x2, x1 + if y1>y2: + y1, y2 = y2, y1 + if x1>x2: + z1, z2 = z2, z1 + + cache = {} + + unknow = Image(Geometry(16, 16), "fuchsia") + hardcoded = { + 'minecraft:air': Color('white'), + 'minecraft:cave_air': Color('black'), + 'minecraft:water': Color('blue'), # TODO use 'block/water_still' with #0080ff tint + 'minecraft:lava': 'block/lava_still', + } + for x in hardcoded: + if isinstance(hardcoded[x], Color): + hardcoded[x] = Image(Geometry(16, 16), hardcoded[x]) + else: + hardcoded[x] = Image("%s/textures/%s.png"%(assets.directory, hardcoded[x])) + hardcoded[x].crop(Geometry(16,16)) + + for y in range(y2-y1): + img = Image(Geometry(16*(x2-x1), 16*(z2-z1)), 'transparent') + for z in range(z2-z1): + for x in range(x2-x1): + try: + i = None + sid = chunks.get_block_at(x+x1, y+y1, z+z1) + if sid in cache: + i = cache[sid] + else: + bloc = data.blocks_states[sid] + if bloc in hardcoded: + i = hardcoded[bloc] + else: + prop = data.blocks_properties[sid] + variant = assets.get_block_variant(bloc, prop) + + if 'model' in variant: + faces = assets.get_faces_textures(assets.get_model(variant['model'])) + if 'up' in faces: + up = faces['up'] + i = Image("%s/textures/%s.png"%(assets.directory, up['texture'])) + if "uv" in up: + pass # TODO + i.crop(Geometry(16,16)) + if "tintindex" in up: + tint = '#80ff00' + ti = Image(Geometry(16, 16), tint) + i.composite(ti, 0, 0, CompositeOperator.MultiplyCompositeOp) + if not i: + i = unknow + cache[sid] = i + + img.composite(i, x*16, z*16, CompositeOperator.OverCompositeOp) + except Exception: + continue + + img.write("/tmp/slice_%d.png"%(y)) + + +def main(): + options = get_options() + + assets = AssetsManager(options.assets) + mcdata = DataManager("./mcdata") + + if options.offline: + print("Connecting in offline mode...") + connection = Connection( + options.address, options.port, username=options.username, + allowed_versions=[options.mcversion]) + else: + auth_token = authentication.AuthenticationToken() + try: + auth_token.authenticate(options.username, options.password) + except YggdrasilError as e: + print(e) + return + print("Logged in as %s..." % auth_token.username) + connection = Connection( + options.address, options.port, auth_token=auth_token, + allowed_versions=[options.mcversion]) + + if options.dump_packets: + def print_incoming(packet): + if type(packet) is Packet: + # This is a direct instance of the base Packet type, meaning + # that it is a packet of unknown type, so we do not print it. + return + if type(packet) in [clientbound.play.EntityVelocityPacket, clientbound.play.EntityLookPacket]: + # Prevents useless console spam + return + print('--> %s' % packet, file=sys.stderr) + + def print_outgoing(packet): + print('<-- %s' % packet, file=sys.stderr) + + connection.register_packet_listener( + print_incoming, Packet, early=True) + connection.register_packet_listener( + print_outgoing, Packet, outgoing=True) + + chat = ChatManager(assets) + chat.register(connection) + + chunks = ChunksManager(mcdata) + chunks.register(connection) + + def handle_join_game(join_game_packet): + print('Connected.') + + connection.register_packet_listener(handle_join_game, clientbound.play.JoinGamePacket) + + connection.connect() + + while True: + try: + text = input() + if text.startswith("!"): + if text == "!respawn": + print("respawning...") + packet = serverbound.play.ClientStatusPacket() + packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN + connection.write_packet(packet) + elif text.startswith("!print "): + p = text.split(" ") + chunks.print_chunk(chunks.get_chunk(int(p[1]), int(p[2]), int(p[3])), int(p[4])) + elif text == "!chunks": + area = chunks.get_loaded_area() + y_count = area[1][1] - area[0][1] + print("Bounds: %s"%(area,)) + for y in range(area[0][1], area[1][1]): + print("Slice %d:"%(y)) + for z in range(area[0][2], area[1][2]): + for x in range(area[0][0], area[1][0]): + if (x,y,z) in chunks.chunks: + c = 'X' + else: + c = '.' + print(c, end="") + print() + elif text == "!export": + area = chunks.get_loaded_area(True) + export_area(area[0][0]*16, area[0][1]*16, area[0][2]*16, area[1][0]*16, area[1][1]*16, area[1][2]*16, chunks, assets, mcdata) + else: + print("Unknow test command: %s"%(text)) + else: + chat.send(connection, text) + + except KeyboardInterrupt: + print("Bye!") + sys.exit() + + except Exception as ex: + print("Exception: %s"%(ex)) + traceback.print_exc() + + +if __name__ == "__main__": + main()