Pull chunk data files over from Elektordi/pyCraft

https://github.com/Elektordi/pyCraft
This commit is contained in:
Tanner Collin 2020-09-05 20:42:27 -06:00
parent ef1c4498ee
commit 55ea50a6de
16 changed files with 518 additions and 0 deletions

2
.gitignore vendored
View File

@ -101,3 +101,5 @@ ENV/
# Editor # Editor
*.swp *.swp
*.swo *.swo
mcdata/

0
custom/__init__.py Normal file
View File

View File

@ -0,0 +1,2 @@
from .data import DataManager
from .chunks import ChunksManager

97
custom/managers/chunks.py Normal file
View File

@ -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])

32
custom/managers/data.py Normal file
View File

@ -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

View File

View File

View File

@ -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)

View File

@ -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

View File

View File

@ -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)

View File

@ -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

10
download_mcdata.sh Normal file
View File

@ -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

View File

@ -10,6 +10,8 @@ from minecraft.exceptions import YggdrasilError
from minecraft.networking.connection import Connection from minecraft.networking.connection import Connection
from minecraft.networking.packets import Packet, clientbound, serverbound from minecraft.networking.packets import Packet, clientbound, serverbound
from custom.managers import DataManager, ChunksManager
def get_options(): def get_options():
parser = OptionParser() parser = OptionParser()
@ -63,6 +65,8 @@ def get_options():
def main(): def main():
options = get_options() options = get_options()
mcdata = DataManager('./mcdata')
if options.offline: if options.offline:
print("Connecting in offline mode...") print("Connecting in offline mode...")
connection = Connection( connection = Connection(
@ -107,6 +111,9 @@ def main():
print("Message (%s): %s" % ( print("Message (%s): %s" % (
chat_packet.field_string('position'), chat_packet.json_data)) chat_packet.field_string('position'), chat_packet.json_data))
chunks = ChunksManager(mcdata)
chunks.register(connection)
connection.register_packet_listener( connection.register_packet_listener(
print_chat, clientbound.play.ChatMessagePacket) print_chat, clientbound.play.ChatMessagePacket)