reworked compiler and types

This commit is contained in:
əlemi 2021-12-10 02:59:31 +01:00
parent d3b4a4b8b4
commit 333ba1fdd9
3 changed files with 389 additions and 299 deletions

View file

@ -23,7 +23,7 @@ class Packet:
self.definition = self._definitions[proto] self.definition = self._definitions[proto]
self.id = self._ids[proto] self.id = self._ids[proto]
for name, t in self.definition: for name, t in self.definition:
setattr(self, name, t._pytype(kwargs[name]) if name in kwargs else None) setattr(self, name, t.pytype(kwargs[name]) if name in kwargs else None)
@property @property
def processed(self) -> Event: def processed(self) -> Event:
@ -32,14 +32,18 @@ class Packet:
@classmethod @classmethod
def deserialize(cls, proto:int, buffer:io.BytesIO): def deserialize(cls, proto:int, buffer:io.BytesIO):
return cls(proto, **{ name : t.read(buffer) for (name, t) in cls._definitions[proto] }) pkt = cls(proto)
for k, t in cls._definitions[proto]:
setattr(pkt, k, t.read(buffer, ctx=pkt))
return pkt
# return cls(proto, **{ name : t.read(buffer) for (name, t) in cls._definitions[proto] })
def serialize(self) -> io.BytesIO: def serialize(self) -> io.BytesIO:
buf = io.BytesIO() buf = io.BytesIO()
VarInt.write(self.id, buf) VarInt.write(self.id, buf)
for name, t in self.definition: for name, t in self.definition:
if getattr(self, name, None) is not None: # minecraft proto has no null type: this is an optional field left unset if getattr(self, name, None) is not None: # minecraft proto has no null type: this is an optional field left unset
t.write(getattr(self, name, None), buf) t.write(getattr(self, name), buf, ctx=self)
buf.seek(0) buf.seek(0)
return buf return buf

View file

@ -3,99 +3,77 @@ import struct
import asyncio import asyncio
import uuid import uuid
from typing import List, Any, Optional, Type as Class from typing import List, Tuple, Dict, Any, Optional, Type as Class
class Type(object): class Type(object):
_pytype : type pytype : type
_size : int
_fmt : str
# These methods will work only for fixed size data, if _size and _fmt are defined. def write(self, data:Any, buffer:io.BytesIO, ctx:object=None) -> None:
# For anything variabile in size, define custom read() and write() classmethods """Write data to a packet buffer"""
raise NotImplementedError
def read(self, buffer:io.BytesIO, ctx:object=None) -> Any:
"""Read data off a packet buffer"""
raise NotImplementedError
@classmethod def check(self, ctx:object) -> bool:
def write(cls, data:Any, buffer:io.BytesIO): """Check if this type exists in this context"""
buffer.write(struct.pack(cls._fmt, data)) return True
@classmethod class UnimplementedDataType(Type):
def read(cls, buffer:io.BytesIO) -> Any: pytype : type = bytes
return struct.unpack(cls._fmt, buffer.read(cls._size))[0]
class TrailingByteArray(Type): def write(self, data:bytes, buffer:io.BytesIO, ctx:object=None):
_pytype : type = bytes
@classmethod
def write(cls, data:bytes, buffer:io.BytesIO):
if data: if data:
buffer.write(data) buffer.write(data)
@classmethod def read(self, buffer:io.BytesIO, ctx:object=None) -> bytes:
def read(cls, buffer:io.BytesIO) -> bytes:
return buffer.read() return buffer.read()
class Boolean(Type): TrailingData = UnimplementedDataType()
_pytype : type = bool EntityMetadata = UnimplementedDataType()
_size : int = 1 EntityMetadataItem = UnimplementedDataType()
_fmt : str = ">?" NBTTag = UnimplementedDataType()
Slot = UnimplementedDataType()
class Byte(Type): class PrimitiveType(Type):
_pytype : type = int size : int
_size : int = 1 fmt : str
_fmt : str = ">b"
class UnsignedByte(Type): def __init__(self, pytype:type, fmt:str, size:int):
_pytype : type = int self.pytype = pytype
_size : int = 1 self.fmt = fmt
_fmt : str = ">B" self.size = size
class Short(Type): def write(self, data:Any, buffer:io.BytesIO, ctx:object=None):
_pytype : type = int buffer.write(struct.pack(self.fmt, data))
_size : int = 2
_fmt : str = ">h"
class UnsignedShort(Type): def read(self, buffer:io.BytesIO, ctx:object=None) -> Any:
_pytype : type = int return struct.unpack(self.fmt, buffer.read(self.size))[0]
_size : int = 2
_fmt : str = ">H"
class Int(Type): Boolean = PrimitiveType(bool, ">?", 1)
_pytype : type = int Byte = PrimitiveType(int, ">b", 1)
_size : int = 4 UnsignedByte = PrimitiveType(int, ">B", 1)
_fmt : str = ">i" Short = PrimitiveType(int, ">h", 2)
UnsignedShort = PrimitiveType(int, ">H", 2)
Int = PrimitiveType(int, ">i", 4)
UnsignedInt = PrimitiveType(int, ">I", 4)
Long = PrimitiveType(int, ">q", 8)
UnsignedLong = PrimitiveType(int, ">Q", 8)
Float = PrimitiveType(float, ">f", 4)
Double = PrimitiveType(float, ">d", 8)
Angle = PrimitiveType(int, ">b", 1)
class UnsignedInt(Type): class VarLenPrimitive(Type):
_pytype : type = int pytype : type = int
_size : int = 4 max_bytes : int
_fmt : str = ">I"
class Long(Type): def __init__(self, max_bytes:int):
_pytype : type = int self.max_bytes = max_bytes
_size : int = 8
_fmt : str = ">q"
class UnsignedLong(Type): def write(self, data:int, buffer:io.BytesIO, ctx:object=None):
_pytype : type = int
_size : int = 8
_fmt : str = ">Q"
class Float(Type):
_pytype : type = float
_size : int = 4
_fmt : str = ">f"
class Double(Type):
_pytype : type = float
_size : int = 8
_fmt : str = ">d"
class VarInt(Type):
_pytype : type = int
_size = 5
@classmethod
def write(cls, data:int, buffer:io.BytesIO):
count = 0 count = 0
while count < cls._size: while count < self.max_bytes:
byte = data & 0b01111111 byte = data & 0b01111111
data >>= 7 data >>= 7
if data > 0: if data > 0:
@ -105,179 +83,173 @@ class VarInt(Type):
if not data: if not data:
break break
@classmethod def read(self, buffer:io.BytesIO, ctx:object=None) -> int:
def read(cls, buffer:io.BytesIO) -> int:
numRead = 0 numRead = 0
result = 0 result = 0
while True: while True:
data = buffer.read(1) data = buffer.read(1)
if len(data) < 1: if len(data) < 1:
raise ValueError("VarInt is too short") raise ValueError("VarInt/VarLong is too short")
buf = int.from_bytes(data, 'little') buf = int.from_bytes(data, 'little')
result |= (buf & 0b01111111) << (7 * numRead) result |= (buf & 0b01111111) << (7 * numRead)
numRead +=1 numRead +=1
if numRead > cls._size: if numRead > self.max_bytes:
raise ValueError("VarInt is too big") raise ValueError("VarInt/VarLong is too big")
if buf & 0b10000000 == 0: if buf & 0b10000000 == 0:
break break
return result return result
@classmethod # utility methods since VarInt is super used
def serialize(cls, data:int) -> bytes:
def serialize(self, data:int) -> bytes:
buf = io.BytesIO() buf = io.BytesIO()
cls.write(data, buf) self.write(data, buf)
buf.seek(0) buf.seek(0)
return buf.read() return buf.read()
@classmethod def deserialize(self, data:bytes) -> int:
def deserialize(cls, data:bytes) -> int:
buf = io.BytesIO(data) buf = io.BytesIO(data)
return cls.read(buf) return self.read(buf, ctx=ctx)
class VarLong(VarInt): VarInt = VarLenPrimitive(5)
_pytype : type = int VarLong = VarLenPrimitive(10)
_size = 10
class EntityMetadata(TrailingByteArray): class StringType(Type):
# TODO pytype : type = str
pass
class Slot(TrailingByteArray): def write(self, data:str, buffer:io.BytesIO, ctx:object=None):
_pytype : type = bytes
# TODO
pass
class Maybe(Type): # TODO better name without
_t : Class[Type] = TrailingByteArray
_pytype : type = bytes
def __init__(self, t:Class[Type]):
self._t = t
self._pytype = t._pytype
self._size = Boolean._size + t._size
@classmethod
def write(cls, data:Optional[Any], buffer:io.BytesIO):
Boolean.write(bool(data), buffer)
if data:
cls._t.write(data, buffer)
@classmethod
def read(cls, buffer:io.BytesIO) -> Optional[Any]:
if Boolean.read(buffer):
return cls._t.read(buffer)
return None
class Array(Type):
_counter : Class[Type] = VarInt
_content : Class[Type] = Byte
_pytype : type = bytes
def __init__(self, content:Class[Type] = Byte, counter:Class[Type] = VarInt):
self._content = content
self._counter = counter
@classmethod
def write(cls, data:List[Any], buffer:io.BytesIO):
cls._counter.write(len(data), buffer)
for el in data:
cls._content.write(el, buffer)
@classmethod
def read(cls, buffer:io.BytesIO) -> List[Any]:
length = cls._counter.read(buffer)
return [ cls._content.read(buffer) for _ in range(length) ]
class String(Type):
_pytype : type = str
@classmethod
def write(cls, data:str, buffer:io.BytesIO):
encoded = data.encode('utf-8') encoded = data.encode('utf-8')
VarInt.write(len(encoded), buffer) VarInt.write(len(encoded), buffer, ctx=ctx)
buffer.write(encoded) buffer.write(encoded)
@classmethod def read(self, buffer:io.BytesIO, ctx:object=None) -> str:
def read(cls, buffer:io.BytesIO) -> str: length = VarInt.read(buffer, ctx=ctx)
length = VarInt.read(buffer)
return buffer.read(length).decode('utf-8') return buffer.read(length).decode('utf-8')
class ByteArray(Type): String = StringType()
_pytype : type = bytes Chat = StringType()
Identifier = StringType()
@classmethod class BufferType(Type):
def write(cls, data:bytes, buffer:io.BytesIO): pytype : type = bytes
VarInt.write(len(data), buffer) count : Type
def __init__(self, count:Type = VarInt):
self.count = count
def write(self, data:bytes, buffer:io.BytesIO, ctx:object=None):
self.count.write(len(data), buffer, ctx=ctx)
buffer.write(data) buffer.write(data)
@classmethod def read(self, buffer:io.BytesIO, ctx:object=None) -> bytes:
def read(cls, buffer:io.BytesIO) -> bytes: length = self.count.read(buffer, ctx=ctx)
length = VarInt.read(buffer)
return buffer.read(length) return buffer.read(length)
class IntegerByteArray(Type): ByteArray = BufferType()
_pytype : type = bytes IntegerByteArray = BufferType(Int)
@classmethod class PositionType(Type):
def write(cls, data:bytes, buffer:io.BytesIO): pytype : type = tuple
Int.write(len(data), buffer) MAX_SIZE : int = 8
buffer.write(data)
@classmethod # TODO THIS IS FOR 1.12.2!!! Make a generic version-less?
def read(cls, buffer:io.BytesIO) -> bytes:
length = Int.read(buffer)
return buffer.read(length)
class Chat(String): def write(self, data:tuple, buffer:io.BytesIO, ctx:object=None):
_pytype : type = str
class Identifier(String):
_pytype : type = str
class Angle(Type):
_pytype : type = int
_size : int = 1
_fmt : str = ">b"
class EntityMetadataItem(Type):
_pytype : type = bytes
# TODO
pass
class NBTTag(Type):
_pytype : type = bytes
# TODO
pass
class Position(Type):
_pytype : type = tuple
_size = 8
# TODO THIS IS FOR 1.12.2!!!
@classmethod
def write(cls, data:tuple, buffer:io.BytesIO):
packed = ((0x3FFFFFF & data[0]) << 38) | ((0xFFF & data[1]) << 26) | (0x3FFFFFF & data[2]) packed = ((0x3FFFFFF & data[0]) << 38) | ((0xFFF & data[1]) << 26) | (0x3FFFFFF & data[2])
UnsignedLong.write(packed, buffer) UnsignedLong.write(packed, buffer, ctx=ctx)
@classmethod def read(self, buffer:io.BytesIO, ctx:object=None) -> tuple:
def read(cls, buffer:io.BytesIO) -> tuple:
packed = UnsignedLong.read(buffer) packed = UnsignedLong.read(buffer)
x = packed >> 38 x = packed >> 38
y = (packed >> 24) & 0xFFF y = (packed >> 24) & 0xFFF
z = packed & 0x3FFFFFF z = packed & 0x3FFFFFF
return (x, y, z) return (x, y, z)
class UUID(Type): Position = PositionType()
_pytype : type = str
_size = 16
@classmethod class UUIDType(Type):
def write(cls, data:uuid.UUID, buffer:io.BytesIO): pytype : type = str # TODO maybe use partial with uuid constructor?
buffer.write(int(data).to_bytes(cls._size, 'big')) MAX_SIZE : int = 16
def write(self, data:uuid.UUID, buffer:io.BytesIO, ctx:object=None):
buffer.write(int(data).to_bytes(self.MAX_SIZE, 'big'))
def read(self, buffer:io.BytesIO, ctx:object=None) -> uuid.UUID:
return uuid.UUID(int=int.from_bytes(buffer.read(self.MAX_SIZE), 'big'))
UUID = UUIDType()
class ArrayType(Type):
pytype : type = list
counter : Type
content : Type
def __init__(self, content:Type, counter:Type = VarInt):
self.content = content
self.counter = counter
def write(self, data:List[Any], buffer:io.BytesIO, ctx:object=None):
self.counter.write(len(data), buffer, ctx=ctx)
for el in data:
self.content.write(el, buffer, ctx=ctx)
def read(self, buffer:io.BytesIO, ctx:object=None) -> List[Any]:
length = self.counter.read(buffer, ctx=ctx)
return [ self.content.read(buffer, ctx=ctx) for _ in range(length) ]
class OptionalType(Type):
t : Type
def __init__(self, t:Type):
self.t = t
self.pytype = t.pytype
def write(self, data:Optional[Any], buffer:io.BytesIO, ctx:object=None):
Boolean.write(bool(data), buffer, ctx=ctx)
if data:
self.t.write(data, buffer, ctx=ctx)
def read(self, buffer:io.BytesIO, ctx:object=None) -> Optional[Any]:
if Boolean.read(buffer, ctx=ctx):
return self.t.read(buffer, ctx=ctx)
return None
class SwitchType(Type):
field : str
mappings : Dict[Any, Type]
def __init__(self, watch:str, mappings:Dict[Any, Type], default:Type = None):
self.field = watch
self.mappings = mappings
self.default = default
def write(self, data:Any, buffer:io.BytesIO, ctx:object=None):
watched = getattr(ctx, self.field)
if watched in self.mappings:
return self.mappings[watched].write(data, buffer, ctx=ctx)
elif self.default:
return self.default.write(data, buffer, ctx=ctx)
def read(self, buffer:io.BytesIO, ctx:object=None) -> Dict[str, Any]:
watched = getattr(ctx, self.field)
if watched in self.mappings:
return self.mappings[watched].read(buffer, ctx=ctx)
elif self.default:
return self.default.read(buffer, ctx=ctx)
return {}
class StructType(Type):
pytype : type = dict
fields : Tuple[Tuple[str, Type], ...]
def __init__(self, *args:Tuple[str, Type]):
self.fields = args
def write(self, data:Dict[str, Any], buffer:io.BytesIO, ctx:object=None):
for k, t in self.fields:
t.write(data[k], buffer, ctx=ctx)
def read(self, buffer:io.BytesIO, ctx:object=None) -> Dict[str, Any]:
return { k : t.read(buffer, ctx=ctx) for k, t in self.fields }
@classmethod
def read(cls, buffer:io.BytesIO) -> uuid.UUID:
return uuid.UUID(int=int.from_bytes(buffer.read(cls._size), 'big'))

View file

@ -5,7 +5,7 @@ import keyword
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Dict, Union, Type as Class from typing import Tuple, List, Dict, Union, Set, Type as Class
from aiocraft.mc.types import * from aiocraft.mc.types import *
@ -13,7 +13,7 @@ from aiocraft.mc.types import *
DIR_MAP = {"toClient": "clientbound", "toServer": "serverbound"} DIR_MAP = {"toClient": "clientbound", "toServer": "serverbound"}
PREFACE = """\"\"\"[!] This file is autogenerated\"\"\"\n\n""" PREFACE = """\"\"\"[!] This file is autogenerated\"\"\"\n\n"""
IMPORTS = """from typing import Tuple, List, Dict IMPORTS = """from typing import Tuple, List, Dict, Union
from ....packet import Packet from ....packet import Packet
from ....types import *\n""" from ....types import *\n"""
IMPORT_ALL = """__all__ = [\n\t{all}\n]\n""" IMPORT_ALL = """__all__ = [\n\t{all}\n]\n"""
@ -30,91 +30,226 @@ class {name}(Packet):
_definitions : Dict[int, List[Tuple[str, Type]]] = {definitions} _definitions : Dict[int, List[Tuple[str, Type]]] = {definitions}
""" """
class Ref:
name : str
args : tuple
def __equals__(self, other) -> bool:
if self.args:
return self.name == other.name and self.args == other.args
return self.name == other.name
def __init__(self, name:str, *args):
self.name = name or "anon"
self.args = args
def __repr__(self) -> str:
if self.args:
out = self.name + "("
for arg in self.args:
out += repr(arg) + ", "
out += ")"
return out
return self.name
TYPE_MAP = { TYPE_MAP = {
"varint": VarInt, "varint": Ref('VarInt'),
"u8": Byte, "u8": Ref('Byte'),
"i8": Byte, "i8": Ref('Byte'),
"u16": UnsignedShort, "u16": Ref('UnsignedShort'),
"u32": UnsignedInt, "u32": Ref('UnsignedInt'),
"u64": UnsignedLong, "u64": Ref('UnsignedLong'),
"i16": Short, "i16": Ref('Short'),
"i32": Int, "i32": Ref('Int'),
"i64": Long, "i64": Ref('Long'),
"f32": Float, "f32": Ref('Float'),
"f64": Double, "f64": Ref('Double'),
"bool": Boolean, "bool": Ref('Boolean'),
"UUID": UUID, "UUID": Ref('UUID'),
"string": String, "string": Ref('String'),
"nbt": NBTTag, "nbt": Ref('NBTTag'),
"slot": Slot, "slot": Ref('Slot'),
"position": Position, "position": Ref('Position'),
"entityMetadataItem": EntityMetadataItem, "entityMetadataItem": Ref('EntityMetadataItem'),
"entityMetadata": EntityMetadata, "entityMetadata": Ref('EntityMetadata'),
} }
def mctype(slot_type:Any) -> Class[Type]: HINT_MAP = {
"varint": 'int',
"u8": 'int',
"i8": 'int',
"u16": 'int',
"u32": 'int',
"u64": 'int',
"i16": 'int',
"i32": 'int',
"i64": 'int',
"f32": 'float',
"f64": 'float',
"bool": 'bool',
"UUID": 'str',
"string": 'str',
"nbt": 'bytes',
"slot": 'dict',
"position": 'tuple',
"entityMetadataItem": 'bytes',
"entityMetadata": 'bytes',
}
def _format_line(i, depth:int=0) -> str:
nl = ('\n' if depth > 0 else " ")
tab = '\t' * depth
return nl + tab + \
f",{nl}{tab}".join(f"{repr(e)}" for e in i) + \
nl + ('\t' * (depth-1))
def format_dict(d:dict, depth:int=1) -> str:
return "{" + _format_line((Ref(f"{k} : {v}") for k,v in sorted(d.items())), depth) + "}"
def format_list(l:list, depth:int=0) -> str:
return "[" + _format_line(l, depth) + "]"
def format_tuple(l:list, depth:int=0) -> str:
return "(" + _format_line(l, depth) + ")"
def mctype(slot_type:Any) -> Ref:
if isinstance(slot_type, str) and slot_type in TYPE_MAP: if isinstance(slot_type, str) and slot_type in TYPE_MAP:
return TYPE_MAP[slot_type] return TYPE_MAP[slot_type]
if isinstance(slot_type, list): if isinstance(slot_type, list):
name = slot_type[0] t = slot_type[0]
if name == "buffer": v = slot_type[1]
if "countType" in slot_type[1] and slot_type[1]["countType"] == "integer": if t == "buffer": # Array of bytes
return IntegerByteArray if "countType" in v and v["countType"] == "integer":
return ByteArray return Ref('IntegerByteArray')
# TODO composite data types return Ref('ByteArray')
return TrailingByteArray elif t == "array": # Generic array
return Ref('ArrayType', mctype(v["type"]), (mctype(v["countType"]) if "countType" in v else 'VarInt'))
elif t == "container": # Struct
return Ref('StructType', Ref(", ".join(format_tuple((p["name"], mctype(p["type"]))) for p in v if "name" in p))) # some fields are anonymous???
elif t == "option": # Optional
return Ref('OptionalType', mctype(v))
elif t == "switch": # Union
return Ref('SwitchType',
v["compareTo"].split('/')[-1],
Ref(format_dict({int(k) if k.isnumeric() else repr(k):mctype(x) for k,x in v["fields"].items()}, depth=0)),
v["default"] if "default" in v and v['default'] != 'void' else None,
)
# return SwitchType(mctype(v)) # TODO
# elif t == "mapper": # ????
# return TrailingData
else:
logging.error("Encountered unknown composite data type : %s", t)
return Ref('TrailingData')
def mchint(slot_type:Any) -> Ref:
if isinstance(slot_type, str) and slot_type in HINT_MAP:
return Ref(HINT_MAP[slot_type])
if isinstance(slot_type, list):
t = slot_type[0]
if t == "buffer": # Array of bytes
return Ref('bytes')
elif t == "array": # Generic array
return Ref('list')
elif t == "container": # Struct
return Ref('dict')
elif t == "option": # Optional
return Ref('tuple')
elif t == "switch": # Union
return Ref('bytes')
# return SwitchType(mctype(v)) # TODO
# elif t == "mapper": # ????
# return TrailingData
return Ref('bytes')
def pytype(t:list) -> str:
vals = set(str(x) for x in t)
if len(vals) <= 1:
return next(iter(vals))
return 'Union[' + ','.join(x for x in vals) + ']'
def snake_to_camel(name:str) -> str: def snake_to_camel(name:str) -> str:
return "".join(x.capitalize() for x in name.split("_")) return "".join(x.capitalize() for x in name.split("_"))
def parse_slot(slot: dict) -> str: # def parse_slot(slot: dict) -> str:
name = slot["name"] if "name" in slot else "anon" # name = slot["name"] if "name" in slot else "anon"
if keyword.iskeyword(name): # if keyword.iskeyword(name):
name = "is_" + name # name = "is_" + name
t = mctype(slot["type"] if "type" in slot else "restBuffer") # t = mctype(slot["type"] if "type" in slot else "restBuffer")
return f"(\"{name}\", {t.__name__})" # return f"(\"{name}\", {t.__name__})"
#
def parse_field(slot: dict) -> str: # def parse_field(slot: dict) -> str:
name = slot["name"] if "name" in slot else "anon" # name = slot["name"] if "name" in slot else "anon"
if keyword.iskeyword(name): # if keyword.iskeyword(name):
name = "is_" + name # name = "is_" + name
t = mctype(slot["type"] if "type" in slot else "restBuffer") # t = mctype(slot["type"] if "type" in slot else "restBuffer")
return f"{name} : {t._pytype.__name__}" # return f"{name} : {t._pytype.__name__}"
class PacketClassWriter: class PacketClassWriter:
title : str name : str
ids : str attrs : Set[str]
attrs : List[str] types : Dict[str, List[Type]]
slots : str hints : Dict[str, List[Type]]
fields: str ids : Dict[int, int]
definitions : Dict[int, List[Tuple[str, Type]]]
state : int state : int
def __init__(self, pkt:dict, state:int):
def __init__(self, title:str, ids:str, attrs:List[str], slots:str, fields:str, state:int): self.name = pkt["name"]
self.title = title
self.ids = ids
self.attrs = attrs
self.slots = slots
self.fields = fields
self.state = state self.state = state
self.attrs = set()
self.ids = {}
self.types = {}
self.hints = {}
self.definitions = {}
for v, defn in pkt["definitions"].items():
self.ids[v] = defn["id"]
self.definitions[v] = []
for field in defn["slots"]:
if "name" not in field:
logging.error("Skipping anonymous field %s", str(field))
continue
field_name = field["name"] if not keyword.iskeyword(field["name"]) else "is_" + field["name"]
self.attrs.add(field_name)
self.definitions[v].append((field_name, mctype(field["type"])))
if field_name not in self.types:
self.types[field_name] = set()
self.types[field_name].add(mctype(field["type"]))
if field_name not in self.hints:
self.hints[field_name] = set()
self.hints[field_name].add(mchint(field["type"]))
def compile(self) -> str: def compile(self) -> str:
return PREFACE + \ return PREFACE + \
IMPORTS + \ IMPORTS + \
OBJECT.format( OBJECT.format(
name=self.title, name=self.name,
ids='{\n\t\t' + ',\n\t\t'.join(self.ids) + '\n\t}\n', ids=format_dict(self.ids, depth=2),
definitions='{\n\t\t' + '\n\t\t'.join(self.slots) + '\n\t}\n', definitions=format_dict({ k : Ref(format_list(Ref(format_tuple(x)) for x in v)) for k,v in self.definitions.items() }, depth=2),
slots=', '.join((f"'is_{x}'" if keyword.iskeyword(x) else f"'{x}'") for x in (list(self.attrs) + ["id"])), # TODO de-jank! slots=format_tuple(["id"] + list(self.attrs), depth=0), # TODO jank fix when no slots
fields='\n\t'.join(self.fields), fields="\n\t" + "\n\t".join(f"{a} : {pytype(self.hints[a])}" for a in self.attrs),
state=self.state, state=self.state,
) )
class RegistryClassWriter:
registry : dict
def __init__(self, registry:dict):
self.registry = registry
def compile(self) -> str:
return REGISTRY_ENTRY.format(
entries='{\n\t' + ",\n\t".join((
str(v) + " : { " + ", ".join(
f"{pid}:{clazz}" for (pid, clazz) in self.registry[v].items()
) + ' }' ) for v in self.registry.keys()
) + '\n}'
)
def _make_module(path:Path, contents:dict): def _make_module(path:Path, contents:dict):
os.mkdir(path) os.mkdir(path)
imports = "" imports = ""
for key in contents: for key, value in contents.items():
imports += f"from .{key} import {contents[key]}\n" imports += f"from .{key} import {value}\n"
with open(path / "__init__.py", "w") as f: with open(path / "__init__.py", "w") as f:
f.write(PREFACE + imports) f.write(PREFACE + imports)
@ -156,7 +291,6 @@ def compile():
} }
} }
# TODO load all versions!
all_versions = os.listdir(mc_path / f'{folder_name}/data/pc/') all_versions = os.listdir(mc_path / f'{folder_name}/data/pc/')
all_versions.remove("common") all_versions.remove("common")
all_proto_numbers = [] all_proto_numbers = []
@ -175,6 +309,7 @@ def compile():
with open(mc_path / f'{folder_name}/data/pc/{v}/protocol.json') as f: with open(mc_path / f'{folder_name}/data/pc/{v}/protocol.json') as f:
data = json.load(f) data = json.load(f)
# Build data structure containing all packets with all their definitions for different versions
for state in ("handshaking", "status", "login", "play"): for state in ("handshaking", "status", "login", "play"):
for _direction in ("toClient", "toServer"): for _direction in ("toClient", "toServer"):
direction = DIR_MAP[_direction] direction = DIR_MAP[_direction]
@ -212,45 +347,24 @@ def compile():
_make_module(mc_path / f"proto/{state}", { k:"*" for k in PACKETS[state].keys() }) _make_module(mc_path / f"proto/{state}", { k:"*" for k in PACKETS[state].keys() })
for direction in PACKETS[state].keys(): for direction in PACKETS[state].keys():
registry = {} registry = {}
_make_module(mc_path / f"proto/{state}/{direction}", { k:snake_to_camel(k) for k in PACKETS[state][direction].keys() }) _make_module(mc_path / f"proto/{state}/{direction}", { k:snake_to_camel(k) for k in PACKETS[state][direction].keys() })
for packet in PACKETS[state][direction].keys(): for packet in PACKETS[state][direction].keys():
pkt = PACKETS[state][direction][packet] pkt = PACKETS[state][direction][packet]
slots = []
fields = set() for v, defn in pkt["definitions"].items():
attrs = set()
ids = []
for v in sorted(PACKETS[state][direction][packet]["definitions"].keys()):
defn = pkt["definitions"][v]
if v not in registry: if v not in registry:
registry[v] = {} registry[v] = {}
registry[v][defn['id']] = snake_to_camel(packet) registry[v][defn['id']] = snake_to_camel(packet)
ids.append(f"{v} : 0x{defn['id']:02X}")
v_slots = []
v_fields = []
for slot in defn["slots"]:
v_slots.append(parse_slot(slot))
fields.add(parse_field(slot))
if "name" in slot:
attrs.add(slot["name"])
slots.append(f"{v} : [ {','.join(v_slots)} ],")
with open(mc_path / f"proto/{state}/{direction}/{packet}.py", "w") as f: with open(mc_path / f"proto/{state}/{direction}/{packet}.py", "w") as f:
f.write( f.write(PacketClassWriter(pkt, _STATE_MAP[state]).compile())
PacketClassWriter(
pkt["name"], ids, attrs, slots, fields, _STATE_MAP[state]
).compile()
)
with open(mc_path / f"proto/{state}/{direction}/__init__.py", "a") as f: with open(mc_path / f"proto/{state}/{direction}/__init__.py", "a") as f:
f.write( # TODO make this thing actually readable, maybe not using nested joins and generators f.write(RegistryClassWriter(registry).compile())
REGISTRY_ENTRY.format(
entries='{\n\t' + ",\n\t".join(( if __name__ == "__main__":
str(v) + " : { " + ", ".join( compile()
f"{pid}:{clazz}" for (pid, clazz) in registry[v].items()
) + ' }' ) for v in registry.keys()
) + '\n}'
)
)