Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
Expand Down Expand Up @@ -83,3 +84,4 @@ sftp-config.json

### pyCraft ###
credentials
mcdata
9 changes: 9 additions & 0 deletions dl_mcdata.sh
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions minecraft/managers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .data import DataManager
from .assets import AssetsManager
from .chat import ChatManager
from .chunks import ChunksManager
from .entities import EntitiesManager
85 changes: 85 additions & 0 deletions minecraft/managers/assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import os
import json
import re

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"%(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]

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]
while faces[x]['texture'].startswith("#"):
# TODO: Raise exception on max iteration
faces[x]['texture'] = textures[faces[x]['texture'][1:]]
return faces

41 changes: 41 additions & 0 deletions minecraft/managers/chat.py
Original file line number Diff line number Diff line change
@@ -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)
97 changes: 97 additions & 0 deletions minecraft/managers/chunks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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_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 changes: 32 additions & 0 deletions minecraft/managers/data.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions minecraft/managers/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +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
4 changes: 3 additions & 1 deletion minecraft/networking/packets/clientbound/play/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -42,7 +43,8 @@ def get_packets(context):
RespawnPacket,
PluginMessagePacket,
PlayerListHeaderAndFooterPacket,
EntityLookPacket
EntityLookPacket,
ChunkDataPacket
}
if context.protocol_version <= 47:
packets |= {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
Loading