From 5b1e3cacccc77682872403ab43ce334a8f7b1448 Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Tue, 28 Apr 2020 01:46:49 +0200 Subject: [PATCH 1/8] Listen to block changes around player --- .gitignore | 2 + dl_mcdata.sh | 9 + .../clientbound/play/block_change_packet.py | 8 +- test.py | 184 ++++++++++++++++++ 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100755 dl_mcdata.sh create mode 100755 test.py 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/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/test.py b/test.py new file mode 100755 index 00000000..11327924 --- /dev/null +++ b/test.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +from __future__ import print_function + +import getpass +import sys +import re +import json +from optparse import OptionParser + +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 + + +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 main(): + options = get_options() + + lang = {} + with open("%s/lang/en_us.json"%options.assets) as f: + lang = json.loads(f.read()) + for x in lang: + lang[x] = re.sub("\%\d+\$s", "%s", lang[x]) # HACK + + blocks = {} + blocks_states = {} + with open("mcdata/blocks.json") as f: + blocks = json.loads(f.read()) + for x in blocks: + for s in blocks[x]['states']: + blocks_states[s['id']] = x + + 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) + sys.exit() + 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) + + def handle_join_game(join_game_packet): + print('Connected.') + + connection.register_packet_listener(handle_join_game, clientbound.play.JoinGamePacket) + + def translate_chat(data): + if isinstance(data, str): + return data + elif 'extra' in data: + return "".join([translate_chat(x) for x in data['extra']]) + elif 'translate' in data and 'with' in data: + params = [translate_chat(x) for x in data['with']] + return lang[data['translate']]%tuple(params) + elif 'translate' in data: + return lang[data['translate']] + elif 'text' in data: + return data['text'] + else: + return "?" + + def print_chat(chat_packet): + try: + print("[%s] %s"%(chat_packet.field_string('position'), 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)) + + connection.register_packet_listener(print_chat, clientbound.play.ChatMessagePacket) + + def handle_block(block_packet): + print('Block %s at %s'%(blocks_states[block_packet.block_state_id], block_packet.location)) + + connection.register_packet_listener(handle_block, clientbound.play.BlockChangePacket) + + def handle_multiblock(multiblock_packet): + for b in multiblock_packet.records: + handle_block(b) + + connection.register_packet_listener(handle_multiblock, clientbound.play.MultiBlockChangePacket) + + + + connection.connect() + + while True: + try: + text = input() + if not text: + continue + if text == "/respawn": + print("respawning...") + packet = serverbound.play.ClientStatusPacket() + packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN + connection.write_packet(packet) + else: + packet = serverbound.play.ChatPacket() + packet.message = text + connection.write_packet(packet) + except KeyboardInterrupt: + print("Bye!") + sys.exit() + + +if __name__ == "__main__": + main() From 79b1608eea82b495e295f98b298138a43b357ee6 Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Tue, 28 Apr 2020 02:49:25 +0200 Subject: [PATCH 2/8] ChunkDataPacket and Nbt decoder ok, still have to decode chunks data... --- .../packets/clientbound/play/__init__.py | 4 +- .../packets/clientbound/play/chunk_data.py | 45 +++++++++ minecraft/networking/types/__init__.py | 1 + minecraft/networking/types/basic.py | 14 ++- minecraft/networking/types/nbt.py | 93 +++++++++++++++++++ test.py | 19 +++- 6 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 minecraft/networking/packets/clientbound/play/chunk_data.py create mode 100644 minecraft/networking/types/nbt.py 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/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py new file mode 100644 index 00000000..88a27c27 --- /dev/null +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -0,0 +1,45 @@ +from minecraft.networking.packets import Packet +from minecraft.networking.types import ( + VarInt, Integer, Boolean, Nbt +) + + +class ChunkDataPacket(Packet): + @staticmethod + def get_id(context): + return 0x22 # FIXME + + packet_name = 'chunk data' + + 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)) + + 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) + 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 index 11327924..b895acb8 100755 --- a/test.py +++ b/test.py @@ -78,9 +78,16 @@ def main(): blocks_states = {} with open("mcdata/blocks.json") as f: blocks = json.loads(f.read()) - for x in blocks: - for s in blocks[x]['states']: - blocks_states[s['id']] = x + for x in blocks: + for s in blocks[x]['states']: + blocks_states[s['id']] = x + + registries = {} + biomes = {} + with open("mcdata/registries.json") as f: + registries = json.loads(f.read()) + for x in registries["minecraft:biome"]["entries"]: + biomes[registries["minecraft:biome"]["entries"][x]["protocol_id"]] = x if options.offline: print("Connecting in offline mode...") @@ -157,6 +164,12 @@ def handle_multiblock(multiblock_packet): connection.register_packet_listener(handle_multiblock, clientbound.play.MultiBlockChangePacket) + def handle_chunk(chunk_packet): + if chunk_packet.entities == []: + return + print('Chunk at %d,%d (%s): %s'%(chunk_packet.x, chunk_packet.z, biomes[chunk_packet.biomes[0]], chunk_packet.__dict__)) + + connection.register_packet_listener(handle_chunk, clientbound.play.ChunkDataPacket) connection.connect() From e2f9c7b5ce5b50f6f9e2ad78c0d03d572b76e4a4 Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Tue, 28 Apr 2020 05:05:07 +0200 Subject: [PATCH 3/8] Working chunk decode! --- .../packets/clientbound/play/chunk_data.py | 84 ++++++++++++++++++- test.py | 24 +++++- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/minecraft/networking/packets/clientbound/play/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py index 88a27c27..ed0a46b2 100644 --- a/minecraft/networking/packets/clientbound/play/chunk_data.py +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -1,15 +1,16 @@ -from minecraft.networking.packets import Packet +from minecraft.networking.packets import Packet, PacketBuffer from minecraft.networking.types import ( - VarInt, Integer, Boolean, Nbt + VarInt, Integer, Boolean, Nbt, UnsignedByte, Long, Short, + multi_attribute_alias, Vector ) - 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) @@ -28,6 +29,8 @@ def read(self, file_object): 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) @@ -42,4 +45,77 @@ def write_fields(self, packet_buffer): 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) + +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 + + 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(Long.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 + self.blocks.append(n) + + if self.palette: + while max(self.blocks) >= len(self.palette): + self.palette.append(len(self.palette)) # FIXME (bpb==5) + self.blocks = [self.palette[x] for x in self.blocks] + + 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] + + @property + def origin(self): + return self.position*16 + diff --git a/test.py b/test.py index b895acb8..180608bb 100755 --- a/test.py +++ b/test.py @@ -165,9 +165,29 @@ def handle_multiblock(multiblock_packet): connection.register_packet_listener(handle_multiblock, clientbound.play.MultiBlockChangePacket) def handle_chunk(chunk_packet): - if chunk_packet.entities == []: + if chunk_packet.x!=12 or chunk_packet.z!=-8: return - print('Chunk at %d,%d (%s): %s'%(chunk_packet.x, chunk_packet.z, biomes[chunk_packet.biomes[0]], chunk_packet.__dict__)) + print('Chunk pillar at %d,%d (Biome=%s)'%(chunk_packet.x, chunk_packet.z, biomes[chunk_packet.biomes[0]])) + chunk = chunk_packet.chunks[4] + for z in range(16): + missing = [] + for x in range(16): + sid = chunk.get_block_at(x, 0, z) + bloc = blocks_states[sid] + + if bloc == "minecraft:air": + c = " " + elif bloc == "minecraft:grass_block" or bloc == "minecraft:dirt": + c = "-" + elif bloc == "minecraft:blue_concrete": + c = "!" + elif bloc == "minecraft:stone": + c = "X" + else: + missing.append(bloc) + c = "?" + print(c, end="") + print(" %s"%(",".join(missing))) connection.register_packet_listener(handle_chunk, clientbound.play.ChunkDataPacket) From 7dc43bb5c08b28ba928f46964f75c4cca3ad7a07 Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Tue, 28 Apr 2020 23:05:26 +0200 Subject: [PATCH 4/8] Created dedicated managers, and chunks storage is now ok! --- minecraft/managers/__init__.py | 5 + minecraft/managers/assets.py | 27 ++++ minecraft/managers/chat.py | 41 ++++++ minecraft/managers/chunks.py | 76 +++++++++++ minecraft/managers/data.py | 27 ++++ minecraft/managers/entities.py | 5 + .../packets/clientbound/play/chunk_data.py | 11 ++ test.py | 121 ++++-------------- 8 files changed, 220 insertions(+), 93 deletions(-) create mode 100644 minecraft/managers/__init__.py create mode 100644 minecraft/managers/assets.py create mode 100644 minecraft/managers/chat.py create mode 100644 minecraft/managers/chunks.py create mode 100644 minecraft/managers/data.py create mode 100644 minecraft/managers/entities.py 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..250d04c9 --- /dev/null +++ b/minecraft/managers/assets.py @@ -0,0 +1,27 @@ +import os +import json +import re + +class AssetsManager: + + def __init__(self, directory, lang="en_us"): + self.lang = {} + + 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") + + 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] 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..0a355f60 --- /dev/null +++ b/minecraft/managers/chunks.py @@ -0,0 +1,76 @@ +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_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:grass": + c = "!" + elif bloc == "minecraft:water": + c = "~" + elif bloc == "minecraft:stone": + c = "X" + else: + missing.append(bloc) + c = "?" + print(c, end="") + print("| %s"%(",".join(missing))) + print("+%s+"%("-"*16)) + +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..fb985b5d --- /dev/null +++ b/minecraft/managers/data.py @@ -0,0 +1,27 @@ +import os +import json + +class DataManager: + + def __init__(self, directory): + self.blocks = {} + self.blocks_states = {} + self.registries = {} + self.biomes = {} + + 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 + + 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 diff --git a/minecraft/managers/entities.py b/minecraft/managers/entities.py new file mode 100644 index 00000000..1fbf8ef7 --- /dev/null +++ b/minecraft/managers/entities.py @@ -0,0 +1,5 @@ + +class EntitiesManager: + + def __init__(self, data_manager): + self.data = data_manager diff --git a/minecraft/networking/packets/clientbound/play/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py index ed0a46b2..371f4793 100644 --- a/minecraft/networking/packets/clientbound/play/chunk_data.py +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -115,6 +115,17 @@ def get_block_at(self, x, y, z): 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/test.py b/test.py index 180608bb..db382b81 100755 --- a/test.py +++ b/test.py @@ -6,6 +6,7 @@ import sys import re import json +import traceback from optparse import OptionParser from minecraft import authentication @@ -13,6 +14,7 @@ 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(): @@ -68,26 +70,8 @@ def get_options(): def main(): options = get_options() - lang = {} - with open("%s/lang/en_us.json"%options.assets) as f: - lang = json.loads(f.read()) - for x in lang: - lang[x] = re.sub("\%\d+\$s", "%s", lang[x]) # HACK - - blocks = {} - blocks_states = {} - with open("mcdata/blocks.json") as f: - blocks = json.loads(f.read()) - for x in blocks: - for s in blocks[x]['states']: - blocks_states[s['id']] = x - - registries = {} - biomes = {} - with open("mcdata/registries.json") as f: - registries = json.loads(f.read()) - for x in registries["minecraft:biome"]["entries"]: - biomes[registries["minecraft:biome"]["entries"][x]["protocol_id"]] = x + assets = AssetsManager(options.assets) + mcdata = DataManager("./mcdata") if options.offline: print("Connecting in offline mode...") @@ -100,7 +84,7 @@ def main(): auth_token.authenticate(options.username, options.password) except YggdrasilError as e: print(e) - sys.exit() + return print("Logged in as %s..." % auth_token.username) connection = Connection( options.address, options.port, auth_token=auth_token, @@ -125,93 +109,44 @@ def print_outgoing(packet): 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) - def translate_chat(data): - if isinstance(data, str): - return data - elif 'extra' in data: - return "".join([translate_chat(x) for x in data['extra']]) - elif 'translate' in data and 'with' in data: - params = [translate_chat(x) for x in data['with']] - return lang[data['translate']]%tuple(params) - elif 'translate' in data: - return lang[data['translate']] - elif 'text' in data: - return data['text'] - else: - return "?" - - def print_chat(chat_packet): - try: - print("[%s] %s"%(chat_packet.field_string('position'), 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)) - - connection.register_packet_listener(print_chat, clientbound.play.ChatMessagePacket) - - def handle_block(block_packet): - print('Block %s at %s'%(blocks_states[block_packet.block_state_id], block_packet.location)) - - connection.register_packet_listener(handle_block, clientbound.play.BlockChangePacket) - - def handle_multiblock(multiblock_packet): - for b in multiblock_packet.records: - handle_block(b) - - connection.register_packet_listener(handle_multiblock, clientbound.play.MultiBlockChangePacket) - - def handle_chunk(chunk_packet): - if chunk_packet.x!=12 or chunk_packet.z!=-8: - return - print('Chunk pillar at %d,%d (Biome=%s)'%(chunk_packet.x, chunk_packet.z, biomes[chunk_packet.biomes[0]])) - chunk = chunk_packet.chunks[4] - for z in range(16): - missing = [] - for x in range(16): - sid = chunk.get_block_at(x, 0, z) - bloc = blocks_states[sid] - - if bloc == "minecraft:air": - c = " " - elif bloc == "minecraft:grass_block" or bloc == "minecraft:dirt": - c = "-" - elif bloc == "minecraft:blue_concrete": - c = "!" - elif bloc == "minecraft:stone": - c = "X" - else: - missing.append(bloc) - c = "?" - print(c, end="") - print(" %s"%(",".join(missing))) - - connection.register_packet_listener(handle_chunk, clientbound.play.ChunkDataPacket) - - connection.connect() while True: try: text = input() - if not text: - continue - if text == "/respawn": - print("respawning...") - packet = serverbound.play.ClientStatusPacket() - packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN - connection.write_packet(packet) + 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])) + else: + print("Unknow test command: %s"%(text)) else: - packet = serverbound.play.ChatPacket() - packet.message = text - connection.write_packet(packet) + 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() From a9d03af0ff1064401a5b50c815190ef8a35ae98f Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Wed, 29 Apr 2020 03:04:22 +0200 Subject: [PATCH 5/8] Slicing image export working --- minecraft/managers/assets.py | 60 ++++++++++++++++++- minecraft/managers/chunks.py | 4 +- minecraft/managers/data.py | 5 ++ minecraft/managers/entities.py | 7 +++ .../packets/clientbound/play/chunk_data.py | 8 +++ test.py | 42 +++++++++++++ 6 files changed, 124 insertions(+), 2 deletions(-) diff --git a/minecraft/managers/assets.py b/minecraft/managers/assets.py index 250d04c9..b1e19860 100644 --- a/minecraft/managers/assets.py +++ b/minecraft/managers/assets.py @@ -6,12 +6,13 @@ 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") + 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()) @@ -25,3 +26,60 @@ def translate(self, key, 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]['texture'] + while faces[x].startswith("#"): + # TODO: Raise exception on max iteration + faces[x] = textures[faces[x][1:]] + return faces + diff --git a/minecraft/managers/chunks.py b/minecraft/managers/chunks.py index 0a355f60..71d88f41 100644 --- a/minecraft/managers/chunks.py +++ b/minecraft/managers/chunks.py @@ -51,7 +51,6 @@ def print_chunk(self, chunk, y_slice): 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": @@ -68,6 +67,9 @@ def print_chunk(self, chunk, y_slice): print(c, end="") print("| %s"%(",".join(missing))) print("+%s+"%("-"*16)) + if chunk.entities: + print("Entities in chunk: %s"%(", ".join([x['id'].decode() for x in chunk.entities]))) + class ChunkNotLoadedException(Exception): def __str__(self): diff --git a/minecraft/managers/data.py b/minecraft/managers/data.py index fb985b5d..eb4509c0 100644 --- a/minecraft/managers/data.py +++ b/minecraft/managers/data.py @@ -6,8 +6,10 @@ 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") @@ -20,8 +22,11 @@ def __init__(self, directory): 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 index 1fbf8ef7..f005b279 100644 --- a/minecraft/managers/entities.py +++ b/minecraft/managers/entities.py @@ -1,5 +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/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py index 371f4793..f870cf9a 100644 --- a/minecraft/networking/packets/clientbound/play/chunk_data.py +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -1,3 +1,5 @@ +from math import floor + from minecraft.networking.packets import Packet, PacketBuffer from minecraft.networking.types import ( VarInt, Integer, Boolean, Nbt, UnsignedByte, Long, Short, @@ -56,6 +58,11 @@ def decode_chunk_data(self): 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: @@ -66,6 +73,7 @@ def __init__(self, x, y, z, empty=True): 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) diff --git a/test.py b/test.py index db382b81..6d2e3173 100755 --- a/test.py +++ b/test.py @@ -8,6 +8,7 @@ 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 @@ -66,6 +67,44 @@ def get_options(): return options +def export_chunk(chunk, assets, data): + + hardcoded = { + #'minecraft:air': '', + #'minecraft:cave_air': '', + 'minecraft:water': 'misc/underwater', + 'minecraft:lava': 'block/lava_still', + 'minecraft:grass_block': 'block/grass_path_top', + } + + for y in range(16): + img = Image(Geometry(16*16, 16*16), 'transparent') + for z in range(16): + for x in range(16): + i = None + sid = chunk.get_block_at(x, y, z) + bloc = data.blocks_states[sid] + if bloc in hardcoded: + i = Image("%s/textures/%s.png"%(assets.directory, 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: + #print("%s => %s"%(bloc, faces['up'])) + i = Image("%s/textures/%s.png"%(assets.directory, faces['up'])) + + if i: + i.crop(Geometry(16,16)) + img.composite(i, x*16, z*16, CompositeOperator.OverCompositeOp) + + img.write("/tmp/chunk_%d_%d_%d_slice_%d.png"%(chunk.x, chunk.y, chunk.z, y)) + + #x = assets.get_block_variant("minecraft:stone") + #x = assets.get_model(x['model']) + #x = assets.get_faces_textures(x) def main(): options = get_options() @@ -134,6 +173,9 @@ def handle_join_game(join_game_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.startswith("!export "): + p = text.split(" ") + export_chunk(chunks.get_chunk(int(p[1]), int(p[2]), int(p[3])), assets, mcdata) else: print("Unknow test command: %s"%(text)) else: From 2ee6394f6055bf27f51f77eaab97ff4a27875d2d Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Wed, 29 Apr 2020 04:19:37 +0200 Subject: [PATCH 6/8] Now using -1 for invalid block id --- minecraft/managers/chunks.py | 28 ++++++----- .../packets/clientbound/play/chunk_data.py | 2 +- test.py | 49 +++++++++++-------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/minecraft/managers/chunks.py b/minecraft/managers/chunks.py index 71d88f41..fdf029a1 100644 --- a/minecraft/managers/chunks.py +++ b/minecraft/managers/chunks.py @@ -50,25 +50,27 @@ def print_chunk(self, chunk, y_slice): 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:grass": + if sid == -1: c = "!" - elif bloc == "minecraft:water": - c = "~" - elif bloc == "minecraft:stone": - c = "X" else: - missing.append(bloc) - c = "?" + 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:stone": + c = "X" + else: + missing.append(bloc) + c = "?" + print(c, end="") print("| %s"%(",".join(missing))) print("+%s+"%("-"*16)) if chunk.entities: - print("Entities in chunk: %s"%(", ".join([x['id'].decode() for x in chunk.entities]))) + print("Entities in slice: %s"%(", ".join([x['id'].decode() for x in chunk.entities]))) class ChunkNotLoadedException(Exception): diff --git a/minecraft/networking/packets/clientbound/play/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py index f870cf9a..6b59a087 100644 --- a/minecraft/networking/packets/clientbound/play/chunk_data.py +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -112,7 +112,7 @@ def read(self, file_object): if self.palette: while max(self.blocks) >= len(self.palette): - self.palette.append(len(self.palette)) # FIXME (bpb==5) + self.palette.append(-1) # FIXME (bpb==5) self.blocks = [self.palette[x] for x in self.blocks] def write_fields(self, packet_buffer): diff --git a/test.py b/test.py index 6d2e3173..3407fadd 100755 --- a/test.py +++ b/test.py @@ -69,13 +69,21 @@ def get_options(): def export_chunk(chunk, assets, data): + invalid = Image(Geometry(16, 16), "red") + unknow = Image(Geometry(16, 16), "fuchsia") hardcoded = { - #'minecraft:air': '', - #'minecraft:cave_air': '', + 'minecraft:air': Color('white'), + 'minecraft:cave_air': Color('black'), 'minecraft:water': 'misc/underwater', 'minecraft:lava': 'block/lava_still', 'minecraft:grass_block': 'block/grass_path_top', } + 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(16): img = Image(Geometry(16*16, 16*16), 'transparent') @@ -83,28 +91,29 @@ def export_chunk(chunk, assets, data): for x in range(16): i = None sid = chunk.get_block_at(x, y, z) - bloc = data.blocks_states[sid] - if bloc in hardcoded: - i = Image("%s/textures/%s.png"%(assets.directory, hardcoded[bloc])) + if sid == -1: + i = invalid 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: - #print("%s => %s"%(bloc, faces['up'])) - i = Image("%s/textures/%s.png"%(assets.directory, faces['up'])) + 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: + #print("%s => %s"%(bloc, faces['up'])) + i = Image("%s/textures/%s.png"%(assets.directory, faces['up'])) + i.crop(Geometry(16,16)) - if i: - i.crop(Geometry(16,16)) - img.composite(i, x*16, z*16, CompositeOperator.OverCompositeOp) + if not i: + i = unknow + img.composite(i, x*16, z*16, CompositeOperator.OverCompositeOp) img.write("/tmp/chunk_%d_%d_%d_slice_%d.png"%(chunk.x, chunk.y, chunk.z, y)) - - #x = assets.get_block_variant("minecraft:stone") - #x = assets.get_model(x['model']) - #x = assets.get_faces_textures(x) + def main(): options = get_options() From 2b2ab5ffc908832c807f79f22a1289dff2b2d3c1 Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Wed, 29 Apr 2020 21:21:57 +0200 Subject: [PATCH 7/8] Fixed chunk reader for bpb > 4 --- minecraft/managers/assets.py | 6 ++--- minecraft/managers/chunks.py | 27 ++++++++++--------- .../packets/clientbound/play/chunk_data.py | 11 +++----- test.py | 27 ++++++++++++------- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/minecraft/managers/assets.py b/minecraft/managers/assets.py index b1e19860..ec553423 100644 --- a/minecraft/managers/assets.py +++ b/minecraft/managers/assets.py @@ -77,9 +77,9 @@ def get_faces_textures(self, model): for x in e['faces']: if x in faces: continue - faces[x] = e['faces'][x]['texture'] - while faces[x].startswith("#"): + faces[x] = e['faces'][x] + while faces[x]['texture'].startswith("#"): # TODO: Raise exception on max iteration - faces[x] = textures[faces[x][1:]] + faces[x]['texture'] = textures[faces[x]['texture'][1:]] return faces diff --git a/minecraft/managers/chunks.py b/minecraft/managers/chunks.py index fdf029a1..5fede2e3 100644 --- a/minecraft/managers/chunks.py +++ b/minecraft/managers/chunks.py @@ -50,21 +50,22 @@ def print_chunk(self, chunk, y_slice): print("|", end="") for x in range(16): sid = chunk.get_block_at(x, y_slice, z) - if sid == -1: + 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: - 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:stone": - c = "X" - else: - missing.append(bloc) - c = "?" + missing.append(bloc) + c = "?" print(c, end="") print("| %s"%(",".join(missing))) diff --git a/minecraft/networking/packets/clientbound/play/chunk_data.py b/minecraft/networking/packets/clientbound/play/chunk_data.py index 6b59a087..1afcaa98 100644 --- a/minecraft/networking/packets/clientbound/play/chunk_data.py +++ b/minecraft/networking/packets/clientbound/play/chunk_data.py @@ -3,7 +3,7 @@ from minecraft.networking.packets import Packet, PacketBuffer from minecraft.networking.types import ( VarInt, Integer, Boolean, Nbt, UnsignedByte, Long, Short, - multi_attribute_alias, Vector + multi_attribute_alias, Vector, UnsignedLong ) class ChunkDataPacket(Packet): @@ -96,7 +96,7 @@ def read(self, file_object): size = VarInt.read(file_object) longs = [] for i in range(size): - longs.append(Long.read(file_object)) + longs.append(UnsignedLong.read(file_object)) self.blocks = [] mask = (1 << self.bpb)-1 @@ -108,13 +108,10 @@ def read(self, file_object): if l2>l1: n |= longs[l2] << (64-offset) n &= mask + if self.palette: + n = self.palette[n] self.blocks.append(n) - if self.palette: - while max(self.blocks) >= len(self.palette): - self.palette.append(-1) # FIXME (bpb==5) - self.blocks = [self.palette[x] for x in self.blocks] - def write_fields(self, packet_buffer): pass # TODO diff --git a/test.py b/test.py index 3407fadd..f3830b3d 100755 --- a/test.py +++ b/test.py @@ -69,14 +69,14 @@ def get_options(): def export_chunk(chunk, assets, data): - invalid = Image(Geometry(16, 16), "red") + cache = {} + unknow = Image(Geometry(16, 16), "fuchsia") hardcoded = { 'minecraft:air': Color('white'), 'minecraft:cave_air': Color('black'), - 'minecraft:water': 'misc/underwater', + 'minecraft:water': Color('blue'), # TODO use 'block/water_still' with #0080ff tint 'minecraft:lava': 'block/lava_still', - 'minecraft:grass_block': 'block/grass_path_top', } for x in hardcoded: if isinstance(hardcoded[x], Color): @@ -91,8 +91,8 @@ def export_chunk(chunk, assets, data): for x in range(16): i = None sid = chunk.get_block_at(x, y, z) - if sid == -1: - i = invalid + if sid in cache: + i = cache[sid] else: bloc = data.blocks_states[sid] if bloc in hardcoded: @@ -104,12 +104,19 @@ def export_chunk(chunk, assets, data): if 'model' in variant: faces = assets.get_faces_textures(assets.get_model(variant['model'])) if 'up' in faces: - #print("%s => %s"%(bloc, faces['up'])) - i = Image("%s/textures/%s.png"%(assets.directory, faces['up'])) + 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 not i: - i = unknow + 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) img.write("/tmp/chunk_%d_%d_%d_slice_%d.png"%(chunk.x, chunk.y, chunk.z, y)) From 27fa8c30116c35b311e6c428002a458021680df3 Mon Sep 17 00:00:00 2001 From: Guillaume Genty Date: Wed, 29 Apr 2020 21:22:40 +0200 Subject: [PATCH 8/8] Image export of whole area --- minecraft/managers/chunks.py | 16 ++++++ test.py | 98 ++++++++++++++++++++++-------------- 2 files changed, 77 insertions(+), 37 deletions(-) diff --git a/minecraft/managers/chunks.py b/minecraft/managers/chunks.py index 5fede2e3..6dad7b10 100644 --- a/minecraft/managers/chunks.py +++ b/minecraft/managers/chunks.py @@ -34,6 +34,22 @@ def get_chunk(self, x, y, z): 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) diff --git a/test.py b/test.py index f3830b3d..c993320d 100755 --- a/test.py +++ b/test.py @@ -67,7 +67,14 @@ def get_options(): return options -def export_chunk(chunk, assets, data): +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 = {} @@ -85,41 +92,44 @@ def export_chunk(chunk, assets, data): hardcoded[x] = Image("%s/textures/%s.png"%(assets.directory, hardcoded[x])) hardcoded[x].crop(Geometry(16,16)) - for y in range(16): - img = Image(Geometry(16*16, 16*16), 'transparent') - for z in range(16): - for x in range(16): - i = None - sid = chunk.get_block_at(x, y, z) - if sid in cache: - i = cache[sid] - else: - bloc = data.blocks_states[sid] - if bloc in hardcoded: - i = hardcoded[bloc] + 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: - 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) + 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/chunk_%d_%d_%d_slice_%d.png"%(chunk.x, chunk.y, chunk.z, y)) + img.write("/tmp/slice_%d.png"%(y)) def main(): @@ -189,9 +199,23 @@ def handle_join_game(join_game_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.startswith("!export "): - p = text.split(" ") - export_chunk(chunks.get_chunk(int(p[1]), int(p[2]), int(p[3])), assets, mcdata) + 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: