diff --git a/minecraft/managers/chunks.py b/minecraft/managers/chunks.py new file mode 100644 index 0000000..ce37314 --- /dev/null +++ b/minecraft/managers/chunks.py @@ -0,0 +1,98 @@ +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): + print(f"Chunk: {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 0000000..cb480ea --- /dev/null +++ b/minecraft/managers/data.py @@ -0,0 +1,36 @@ +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/networking/connection.py b/minecraft/networking/connection.py index bc15b61..74fc970 100644 --- a/minecraft/networking/connection.py +++ b/minecraft/networking/connection.py @@ -33,7 +33,8 @@ class ConnectionContext(object): def __init__(self, **kwds): self.protocol_version = kwds.get("protocol_version") - + # self.protocol_version = 340 + def protocol_earlier(self, other_pv): """Returns True if the protocol version of this context was published earlier than 'other_pv', or else False.""" @@ -447,6 +448,7 @@ def connect(self): # It is important that this is set correctly even when connecting # in status mode, as some servers, e.g. SpigotMC with the # ProtocolSupport plugin, use it to determine the correct response. + # self.context.protocol_version = 340 self.context.protocol_version \ = max(self.allowed_proto_versions, key=PROTOCOL_VERSION_INDICES.get) diff --git a/minecraft/networking/packets/clientbound/play/__init__.py b/minecraft/networking/packets/clientbound/play/__init__.py index 6959162..df6020b 100644 --- a/minecraft/networking/packets/clientbound/play/__init__.py +++ b/minecraft/networking/packets/clientbound/play/__init__.py @@ -24,6 +24,9 @@ from .join_game_and_respawn_packets import JoinGamePacket, RespawnPacket from .destroy_entities_packet import DestroyEntitiesPacket from .spawn_mob_packet import SpawnMobPacket +from .chunk_data import ChunkDataPacket + +# from .unknown_packet import UnknownPacket # Formerly known as state_playing_clientbound. @@ -58,7 +61,8 @@ def get_packets(context): BlockActionPacket, EntityHeadLookPacket, ResourcePackSendPacket, - NBTQueryPacket + NBTQueryPacket, + ChunkDataPacket } if context.protocol_earlier_eq(47): 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 0000000..baa0f56 --- /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, UnsignedByte, Long, Short, + multi_attribute_alias, Vector, UnsignedLong, Nbt +) + + +class ChunkDataPacket(Packet): + @staticmethod + def get_id(context): + return 0x3C # 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/packets/clientbound/play/unknown_packet.py b/minecraft/networking/packets/clientbound/play/unknown_packet.py new file mode 100644 index 0000000..f1c0895 --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/unknown_packet.py @@ -0,0 +1,66 @@ +from minecraft.networking.types import ( + Vector, Float, Byte, Integer, PrefixedArray, multi_attribute_alias, Type, + VarInt, +) + + +from minecraft.networking.types import ( + Type, Boolean, UnsignedByte, Byte, Short, UnsignedShort, + Integer, FixedPoint, FixedPointInteger, Angle, VarInt, VarLong, + Long, UnsignedLong, Float, Double, ShortPrefixedByteArray, + VarIntPrefixedByteArray, TrailingByteArray, String, UUID, + Position, NBT, PrefixedArray, + Type, VarInt, VarLong, UnsignedLong, Integer, UnsignedByte, Position, + Vector, MutableRecord, PrefixedArray, Boolean, attribute_alias, String +) + +from minecraft.networking.packets import Packet + + +class UnknownPacket(Packet): + @staticmethod + def get_id(context): + # return 0x20 + # return 0x16 + # return 0x4E + return 0x3F + +# 13 --> [unknown packet] 0x3C Packet +# 19 --> [unknown packet] 0x16 Packet +# 66 --> [unknown packet] 0x21 Packet +# 81 --> [unknown packet] 0x20 Packet +# 137 --> [unknown packet] 0x1E Packet +# 472 --> [unknown packet] 0x4C Packet +# 1029 --> [unknown packet] 0x27 Packet - No + + packet_name = 'unknown' + + class Record(Vector, Type): + __slots__ = () + + @classmethod + def read(cls, file_object): + return cls(*(Byte.read(file_object) for i in range(3))) + + @classmethod + def send(cls, record, socket): + for coord in record: + Byte.send(coord, socket) + + @staticmethod + def get_definition(context): + return [ + {'entity_id': VarInt}, + {'y': Angle}, + {'z': Short} + # {'player_motion_x': VarInt}, + # {'player_motion_y': Float}, + # {'player_motion_z': Float}, + ] + + # Access the 'x', 'y', 'z' fields as a Vector tuple. + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + # Access the 'player_motion_{x,y,z}' fields as a Vector tuple. + player_motion = multi_attribute_alias( + Vector, 'player_motion_x', 'player_motion_y', 'player_motion_z') diff --git a/minecraft/networking/types/nbt.py b/minecraft/networking/types/nbt.py new file mode 100644 index 0000000..2626c28 --- /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 + +