pyCraft icon indicating copy to clipboard operation
pyCraft copied to clipboard

How to implement ChunkDataPacket in pyCraft

Open ghost opened this issue 4 years ago • 6 comments

Hi, I am attempting to implement the ChunkDataPacket into multicraft so I can get the blocks in the game. The packet follows the structure shown here: https://wiki.vg/Chunk_Format#Packet_structure Currently, my implementation of the packet is as follows:

class ChunkDataPacket(Packet):
    @staticmethod
    def get_id(context):
        return 0x22

    packet_name = "chunk data"
    definition = [
        {'chunk_x': Integer},
        {'chunk_z': Integer},
        {'full_chunk': Boolean},
        {'primary_bit_mask': VarInt},
        {'heightmaps': ShortPrefixedByteArray},
        {'biomes': VarIntPrefixedByteArray},
        {'size': VarInt},
        {'data': ShortPrefixedByteArray},
        {'elements': VarInt},
        {'blockentities': ShortPrefixedByteArray}]

This is contained within the clientbound play group. I'm not sure what I'm doing wrong, but currently I'm not getting the right data. Here's what the response looks like:

https://hastebin.com/aloxegijox.md

Thanks, Jack.

ghost avatar Apr 30 '20 00:04 ghost

Hello,

You raise a great question. I believe the main reason this packet hasn't been implemented before is due to the the complex nature of it and scope of this project. PyCraft is designed to be compatible with all protocol versions between 1.8 and 1.15+. The ChunkDataPacket is one packet which has seen mass changes throughout different minecraft protocol versions, and proves challenging to implement a working solution for all of them. But, it's certainly not impossible to implement your own solution and is very much encouraged. I've been made aware about a recent new fork, which mostly implements the packet' structure for 1.15.2 and could be a good reference?

TheSnoozer posted his start of deserialising the ChunkDataPacket found here and I will attach where I got up to in previous years. However, do note this isn't in anywhere near a working state or polished form.

from minecraft.networking.packets import Packet
from minecraft.networking.types import (
    Integer, Boolean, VarInt, VarIntPrefixedByteArray, TrailingByteArray,
    UnsignedShort, UnsignedByte, Long, Dimension, Byte,
)

from minecraft.networking.types import (
    TAG_Long, TAG_String, TAG_Long_Array, TAG_Int, TAG_Compound, NBTFile, Compound
)


class ChunkDataPacket(Packet):
    @staticmethod
    def get_id(context):
        return 0x21 if context.protocol_version >= 471 else \
               0x22 if context.protocol_version >= 389 else \
               0x21 if context.protocol_version >= 345 else \
               0x20 if context.protocol_version >= 332 else \
               0x21 if context.protocol_version >= 318 else \
               0x20 if context.protocol_version >= 70 else \
               0x21

    packet_name = 'chunk data'

    def read(self, file_object):
        """
        A chunk is 16x256x16 (x y z) blocks.
        Each chunk has 16 chunk sections where each section represents 16x16x16 blocks.
        The number of chunk sections is equal to the number of bits set in Primary Bit Mask.

        Chunk:
           Section:
              Block Data:    2 bytes per block. Total 8192 bytes. format: blockId << 4 | meta
              Emitted Light: 4 bits per block (1/2 byte). Total 2048 bytes
              Skylight:      4 bits per block (1/2 byte). Total 2048 bytes (only included in overworld)
           Biome: 1 byte per block column. 256 bytes. (only included if all sections are in chunk)
        """
        # Read what chunk in the world we are receiving.
        self.chunk_x = Integer.read(file_object)
        self.chunk_z = Integer.read(file_object)

        # When full chunk is set to true, the chunk data packet is used to
        # create a new chunk. This includes biome data, and all (non-empty)
        # sections in the chunk. Sections not specified in the primary bit
        # mask are empty sections.
        # When full chunk is false, then the chunk data packet acts as a
        # large Multi Block Change packet, changing all of the blocks in the
        # given section at once.
        self.full_chunk = Boolean.read(file_object)

        # Bitmask with bits set to 1 for every 16×16×16 chunk section whose
        # data is included in Data. The least significant bit represents
        # the chunk section at the bottom of the chunk column
        # (from y=0 to y=15).
        if self.context.protocol_version >= 70:
            self.primary_bit_mask = VarInt.read(file_object)
        elif self.context.protocol_version >= 69:
            self.primary_bit_mask = Integer.read(file_object)
        else:  # Protocol Version 47
            self.primary_bit_mask = UnsignedShort.read(file_object)

        # In protocol version 445 this is confirmed as being heightmap.
        # Compound containing one long array named MOTION_BLOCKING, which is a
        # heightmap for the highest solid block at each position in the chunk
        # (as a compacted long array with 256 entries at 9 bits per entry).
        if self.context.protocol_version >= 443:
            # TODO Implement heightmap field for 1.14. (Uses NBT)
            return
            # self.heightmaps = TAG_Compound.(buffer=file_object)

        self.data_size = VarInt.read(file_object) # size of data in bytes
        self.read_chunk_column(file_object)

        # Number of elements in the following array
        self.num_block_entities = VarInt.read(file_object)

        # TODO Read the Array of NBT Tag.
        # All block entities in the chunk. Use the x, y, and z tags in the
        # NBT to determine their positions.
        self.block_entities = TrailingByteArray.read(file_object)

        print('END OF CHUNK PACKET')

    def read_chunk_column(self, file_object):
        for i in range(self.primary_bit_mask):
            self.read_chunk_section(file_object)

        if self.full_chunk:
            # Read biome data.
            # Only sent if full chunk is true; 256 entries if present.
            self.biomes = [Integer.read(file_object) for i in range(256)]

    def read_chunk_section(self, file_object):
        # Determines how many bits are used to encode a block.
        # Note that not all numbers are valid here.
        self.bits_per_block = UnsignedByte.read(file_object)

        # The bits per block value determines what format is used for the
        # palette. There are two types of palettes.
        self.pallete = PaletteFactory.get_palette(self.bits_per_block)
        self.pallete.read(file_object)

        # Number of longs in the following data array.
        self.data_length = VarInt.read(file_object)

        # Compacted list of 4096 (16x16x16) indices pointing to state IDs in
        # the Palette.
        self.data = [Long.read(file_object) for i in range(self.data_length)]
        print(self.data)

        # Block light and Sky Light fields were removed in protocol version 441.
        if self.context.protocol_version < 441:
            self.block_light = []
            for i in range(16 * 16 * 8):
                item = Byte.read(file_object)
                self.block_light.append(item)
                print(item)
            print(self.block_light)

            # Only if in the Overworld; half byte per block
            if self.context.dimension is Dimension.OVERWORLD:
                self.sky_light = []
                for i in range(16*16*8):
                    item = Byte.read(file_object)
                    self.sky_light.append(item)
                    print(len(self.sky_light))
                print(self.sky_light)

        print("END OF CHUNK SECTION")


class PaletteFactory(object):
    @staticmethod
    def get_palette(bits_per_block):
        # Returns the correct Palette class corresponding to the number of
        # bits per block.
        return IndirectPalette(4) if bits_per_block <= 4 else \
               IndirectPalette(bits_per_block) if bits_per_block <= 8 else \
               DirectPalette()


class IndirectPalette(object):
    def __init__(self, bits_per_block):
        print('BPP : ' + str(bits_per_block))
        self.bits_per_block = bits_per_block
        self.id_to_state = {}
        self.state_to_id = {}
        self.palette = {}

    def read(self, file_object):
        self.length = VarInt.read(file_object)

        # TODO Check if this is done correctly.
        self.palette = dict((block_state_id, VarInt.read(file_object)) for
                             block_state_id in range(self.length))


class DirectPalette(object):
    def read(self, file_object):
        pass

Its complex nature means that we can't simply depend upon overriding the definition function which pyCraft offers. This is reserved for packets with little logic. The ChunkDataPacket requires additional logic, so to implement the ChunkDataPacket you must instead override the the packet's base class read and write_fields methods, which by default tries to read/write fields corresponding to its definition. https://github.com/ammaraskar/pyCraft/blob/ff9a0813b64a0afdf3cd089ad9000350bb4122bc/minecraft/networking/packets/packet.py#L61

Polished implementation of using these methods can be found here. https://github.com/ammaraskar/pyCraft/blob/ff9a0813b64a0afdf3cd089ad9000350bb4122bc/minecraft/networking/packets/clientbound/play/face_player_packet.py#L28

Also, to fully parse the data field in the ChunkDataPacket there contains additional sub-fields. However, you could if you wish ignore this by just reading the size in bytes of the field, as given by the prepending size field.

Anyways. Hope this gives you some guidance on how to proceed should you choose to. Let me know how it goes :-)

zacholade avatar Apr 30 '20 13:04 zacholade

Hi @Zachy24 , I'm a python bigginer but I try to make a "bot" for minecraft servers so I need to get some block data. So I tried your code but I'm getting this error

return struct.unpack('>b', file_object.read(1))[0] struct.error: unpack requires a buffer of 1 bytes

(server versions is 1.8.8) in the read_chunk_section function at line

item = Byte.read(file_object)

Can you explain me why I have this and how to fix it ?

jj136975 avatar May 10 '20 16:05 jj136975

Hey. So as I mentioned my code isn't in a working or polished state. I merely posted it hoping it may provide someone with help when they implement it. In order to use the ChunkDataPacket for block data, a proper, working implementation of the packet would be needed.

Alternatives to using this packet to get block data could be using the BlockChangePacket or MultiBlockChangePacket. https://github.com/ammaraskar/pyCraft/blob/master/minecraft/networking/packets/clientbound/play/block_change_packet.py However, the full chunk data would be incomplete and pyCraft would only know what blocks were were if they have been updated using either of these packets since connecting (unless you cache these changes using sql or another data storage type between instances).

zacholade avatar May 10 '20 16:05 zacholade

Ok so if I've understoud, MultiBlockChangePacket provide all data of blocks in chunk and is received on every changes in chunk ?

jj136975 avatar May 10 '20 16:05 jj136975

https://wiki.vg/Protocol#Block_Change https://wiki.vg/Protocol#Multi_Block_Change Full details are on the wiki. These do not contain entire chunk data. It simply contains the position of block(s) if they are changed and the new type of block it is.

zacholade avatar May 10 '20 16:05 zacholade

Allright, thx ^^ it solved my problem

jj136975 avatar May 10 '20 16:05 jj136975