From 55ea50a6de4d410571e45cf5ca84b5b0083a408b Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sat, 5 Sep 2020 20:42:27 -0600 Subject: [PATCH] Pull chunk data files over from Elektordi/pyCraft https://github.com/Elektordi/pyCraft --- .gitignore | 2 + custom/__init__.py | 0 custom/managers/__init__.py | 2 + custom/managers/chunks.py | 97 ++++++++++++ custom/managers/data.py | 32 ++++ custom/networking/__init__.py | 0 custom/networking/packets/__init__.py | 0 .../packets/clientbound/__init__.py | 0 .../packets/clientbound/play/__init__.py | 0 .../clientbound/play/block_change_packet.py | 122 +++++++++++++++ .../packets/clientbound/play/chunk_data.py | 139 ++++++++++++++++++ custom/networking/types/__init__.py | 0 custom/networking/types/basic.py | 12 ++ custom/networking/types/nbt.py | 95 ++++++++++++ download_mcdata.sh | 10 ++ start.py | 7 + 16 files changed, 518 insertions(+) create mode 100644 custom/__init__.py create mode 100644 custom/managers/__init__.py create mode 100644 custom/managers/chunks.py create mode 100644 custom/managers/data.py create mode 100644 custom/networking/__init__.py create mode 100644 custom/networking/packets/__init__.py create mode 100644 custom/networking/packets/clientbound/__init__.py create mode 100644 custom/networking/packets/clientbound/play/__init__.py create mode 100644 custom/networking/packets/clientbound/play/block_change_packet.py create mode 100644 custom/networking/packets/clientbound/play/chunk_data.py create mode 100644 custom/networking/types/__init__.py create mode 100644 custom/networking/types/basic.py create mode 100644 custom/networking/types/nbt.py create mode 100644 download_mcdata.sh diff --git a/.gitignore b/.gitignore index 4a8da09..28d346b 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ ENV/ # Editor *.swp *.swo + +mcdata/ diff --git a/custom/__init__.py b/custom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom/managers/__init__.py b/custom/managers/__init__.py new file mode 100644 index 0000000..1f9d175 --- /dev/null +++ b/custom/managers/__init__.py @@ -0,0 +1,2 @@ +from .data import DataManager +from .chunks import ChunksManager diff --git a/custom/managers/chunks.py b/custom/managers/chunks.py new file mode 100644 index 0000000..44fb88d --- /dev/null +++ b/custom/managers/chunks.py @@ -0,0 +1,97 @@ +from math import floor + +from ..networking.packets.clientbound.play import block_change_packet, chunk_data + +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, block_change_packet.BlockChangePacket) + connection.register_packet_listener(self.handle_multiblock, block_change_packet.MultiBlockChangePacket) + connection.register_packet_listener(self.handle_chunk, chunk_data.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/custom/managers/data.py b/custom/managers/data.py new file mode 100644 index 0000000..16028d8 --- /dev/null +++ b/custom/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/custom/networking/__init__.py b/custom/networking/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom/networking/packets/__init__.py b/custom/networking/packets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom/networking/packets/clientbound/__init__.py b/custom/networking/packets/clientbound/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom/networking/packets/clientbound/play/__init__.py b/custom/networking/packets/clientbound/play/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom/networking/packets/clientbound/play/block_change_packet.py b/custom/networking/packets/clientbound/play/block_change_packet.py new file mode 100644 index 0000000..f016b2f --- /dev/null +++ b/custom/networking/packets/clientbound/play/block_change_packet.py @@ -0,0 +1,122 @@ +from minecraft.networking.packets import Packet +from minecraft.networking.types import ( + VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord, + attribute_alias, multi_attribute_alias, +) + + +class BlockChangePacket(Packet): + @staticmethod + def get_id(context): + return 0x0C if context.protocol_version >= 550 else \ + 0x0B if context.protocol_version >= 332 else \ + 0x0C if context.protocol_version >= 318 else \ + 0x0B if context.protocol_version >= 67 else \ + 0x24 if context.protocol_version >= 62 else \ + 0x23 + + packet_name = 'block change' + definition = [ + {'location': Position}, + {'block_state_id': VarInt}] + block_state_id = 0 + + # For protocols < 347: an accessor for (block_state_id >> 4). + @property + def blockId(self): + return self.block_state_id >> 4 + + @blockId.setter + def blockId(self, block_id): + self.block_state_id = (self.block_state_id & 0xF) | (block_id << 4) + + # For protocols < 347: an accessor for (block_state_id & 0xF). + @property + def blockMeta(self): + return self.block_state_id & 0xF + + @blockMeta.setter + def blockMeta(self, meta): + self.block_state_id = (self.block_state_id & ~0xF) | (meta & 0xF) + + # This alias is retained for backward compatibility. + blockStateId = attribute_alias('block_state_id') + + +class MultiBlockChangePacket(Packet): + @staticmethod + def get_id(context): + return 0x10 if context.protocol_version >= 550 else \ + 0x0F if context.protocol_version >= 343 else \ + 0x10 if context.protocol_version >= 332 else \ + 0x11 if context.protocol_version >= 318 else \ + 0x10 if context.protocol_version >= 67 else \ + 0x22 + + packet_name = 'multi block change' + + fields = 'chunk_x', 'chunk_z', 'records' + + # Access the 'chunk_x' and 'chunk_z' fields as a tuple. + chunk_pos = multi_attribute_alias(tuple, 'chunk_x', 'chunk_z') + + class Record(MutableRecord): + __slots__ = 'x', 'y', 'z', 'block_state_id', 'location' + + def __init__(self, **kwds): + self.block_state_id = 0 + super(MultiBlockChangePacket.Record, self).__init__(**kwds) + + # Access the 'x', 'y', 'z' fields as a Vector of ints. + position = multi_attribute_alias(Vector, 'x', 'y', 'z') + + # For protocols < 347: an accessor for (block_state_id >> 4). + @property + def blockId(self): + return self.block_state_id >> 4 + + @blockId.setter + def blockId(self, block_id): + self.block_state_id = self.block_state_id & 0xF | block_id << 4 + + # For protocols < 347: an accessor for (block_state_id & 0xF). + @property + def blockMeta(self): + return self.block_state_id & 0xF + + @blockMeta.setter + def blockMeta(self, meta): + self.block_state_id = self.block_state_id & ~0xF | meta & 0xF + + # This alias is retained for backward compatibility. + blockStateId = attribute_alias('block_state_id') + + 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) + UnsignedByte.send(self.y, packet_buffer) + VarInt.send(self.block_state_id, packet_buffer) + + def read(self, file_object): + self.chunk_x = Integer.read(file_object) + self.chunk_z = Integer.read(file_object) + records_count = VarInt.read(file_object) + self.records = [] + for i in range(records_count): + record = self.Record() + record.read(file_object, self) + self.records.append(record) + + def write_fields(self, packet_buffer): + Integer.send(self.chunk_x, packet_buffer) + Integer.send(self.chunk_z, packet_buffer) + VarInt.send(len(self.records), packet_buffer) + for record in self.records: + record.write(packet_buffer) diff --git a/custom/networking/packets/clientbound/play/chunk_data.py b/custom/networking/packets/clientbound/play/chunk_data.py new file mode 100644 index 0000000..7cf12da --- /dev/null +++ b/custom/networking/packets/clientbound/play/chunk_data.py @@ -0,0 +1,139 @@ +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 +) + +from ....types import nbt + +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/custom/networking/types/__init__.py b/custom/networking/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom/networking/types/basic.py b/custom/networking/types/basic.py new file mode 100644 index 0000000..64c35f6 --- /dev/null +++ b/custom/networking/types/basic.py @@ -0,0 +1,12 @@ +from minecraft.networking.types.basic import Type, Byte, Short, Integer, Long, Float, Double, ShortPrefixedByteArray + +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) diff --git a/custom/networking/types/nbt.py b/custom/networking/types/nbt.py new file mode 100644 index 0000000..b6f2ffc --- /dev/null +++ b/custom/networking/types/nbt.py @@ -0,0 +1,95 @@ +"""Contains definition for minecraft's NBT format. +""" +from __future__ import division +import struct + +from minecraft.networking.types.utility import Vector +from minecraft.networking.types.basic import Type, Byte, Short, Integer, Long, Float, Double, ShortPrefixedByteArray + +from .basic import 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/download_mcdata.sh b/download_mcdata.sh new file mode 100644 index 0000000..9190ed7 --- /dev/null +++ b/download_mcdata.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +VERSION="1.16.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/start.py b/start.py index 353a158..075880f 100644 --- a/start.py +++ b/start.py @@ -10,6 +10,8 @@ from minecraft.exceptions import YggdrasilError from minecraft.networking.connection import Connection from minecraft.networking.packets import Packet, clientbound, serverbound +from custom.managers import DataManager, ChunksManager + def get_options(): parser = OptionParser() @@ -63,6 +65,8 @@ def get_options(): def main(): options = get_options() + mcdata = DataManager('./mcdata') + if options.offline: print("Connecting in offline mode...") connection = Connection( @@ -107,6 +111,9 @@ def main(): print("Message (%s): %s" % ( chat_packet.field_string('position'), chat_packet.json_data)) + chunks = ChunksManager(mcdata) + chunks.register(connection) + connection.register_packet_listener( print_chat, clientbound.play.ChatMessagePacket)