MORE PACKETS

This commit is contained in:
Casey 2023-08-26 01:12:19 +03:00
parent 79d9da259e
commit f2155a631d
Signed by: hkc
GPG Key ID: F0F6CFE11CDB0960
35 changed files with 451 additions and 45 deletions

View File

@ -4,17 +4,29 @@ import struct
class AsyncDataInputStream: class AsyncDataInputStream:
def __init__(self, queue: Queue): def __init__(self, queue: Queue):
self._queue = queue self._queue = queue
self._buffer = b''
self._last = b'' self._last = b''
def read_rest(self):
out = self._buffer
self._buffer = b''
return out
async def read_bytes(self, n: int) -> bytes: async def read_bytes(self, n: int) -> bytes:
if len(self._last) < n: if len(self._buffer) < n:
self._last += await self._queue.get() self._last = (await self._queue.get())
out, self._last = self._last[:n], self._last[n:] self._buffer += self._last
out, self._buffer = self._buffer[:n], self._buffer[n:]
return out return out
async def read(self) -> int: async def read(self) -> int:
return (await self.read_bytes(1))[0] return (await self.read_bytes(1))[0]
read_ubyte = read
async def read_byte(self) -> int:
return struct.unpack('b', await self.read_bytes(1))[0]
async def read_boolean(self) -> bool: async def read_boolean(self) -> bool:
return (await self.read()) != 0 return (await self.read()) != 0
@ -59,8 +71,12 @@ class AsyncDataInputStream:
return value return value
async def read_string(self) -> str: async def read_string(self) -> str:
last = self._last
size = await self.read_short() size = await self.read_short()
try:
return (await self.read_bytes(size)).decode('utf-8') return (await self.read_bytes(size)).decode('utf-8')
except Exception as e:
raise ValueError(f'failed reading string of size {size} in {last}') from e
class SyncDataInputStream: class SyncDataInputStream:
def __init__(self, buffer: bytes): def __init__(self, buffer: bytes):

View File

@ -1,5 +1,6 @@
from asyncio.queues import Queue from asyncio.queues import Queue
import time
from bta_proxy.datainputstream import AsyncDataInputStream from bta_proxy.datainputstream import AsyncDataInputStream
from bta_proxy.packets import * from bta_proxy.packets import *
@ -7,8 +8,13 @@ from bta_proxy.packets import *
async def inspect_client(queue: Queue, addr: tuple[str, int]): async def inspect_client(queue: Queue, addr: tuple[str, int]):
dis = AsyncDataInputStream(queue) dis = AsyncDataInputStream(queue)
last_time = time.time()
while True: while True:
pkt = await Packet.read_packet(dis) pkt = await Packet.read_packet(dis)
now = time.time()
delta = now - last_time
last_time = now
match pkt.packet_id: match pkt.packet_id:
case Packet10Flying.packet_id: case Packet10Flying.packet_id:
continue continue
@ -21,10 +27,27 @@ async def inspect_client(queue: Queue, addr: tuple[str, int]):
case Packet255KickDisconnect.packet_id: case Packet255KickDisconnect.packet_id:
break break
case _: case _:
print("C", pkt) print(f"C {delta*1000:+8.1f}ms {pkt}")
async def inspect_server(queue: Queue, addr: tuple[str, int]): async def inspect_server(queue: Queue, addr: tuple[str, int]):
dis = AsyncDataInputStream(queue) dis = AsyncDataInputStream(queue)
last_time = time.time()
while True: while True:
pkt = await Packet.read_packet(dis) pkt = await Packet.read_packet(dis)
print("S", pkt) now = time.time()
delta = now - last_time
last_time = now
match pkt.packet_id:
case Packet50PreChunk.packet_id:
continue
case Packet51MapChunk.packet_id:
continue
case Packet31RelEntityMove.packet_id:
continue
case Packet32EntityLook.packet_id:
continue
case Packet33RelEntityMoveLook.packet_id:
continue
case _:
print(f"S {delta*1000:+8.1f}ms {pkt}")

View File

@ -26,6 +26,7 @@ class EntityData:
async def read_from(cls, dis: AsyncDataInputStream) -> list[DataItem]: async def read_from(cls, dis: AsyncDataInputStream) -> list[DataItem]:
items = [] items = []
while (data := await dis.read()) != 0x7F: while (data := await dis.read()) != 0x7F:
print(f"========= EntityData.read_from {data=} ({(data & 0xE0) >> 5})")
item_type = DataItemType((data & 0xE0) >> 5) item_type = DataItemType((data & 0xE0) >> 5)
item_id: int = data & 0x1F item_id: int = data & 0x1F
match item_type: match item_type:

View File

@ -1,13 +1,15 @@
from typing import Any
from bta_proxy.datainputstream import AsyncDataInputStream, SyncDataInputStream from bta_proxy.datainputstream import AsyncDataInputStream, SyncDataInputStream
class ItemStack: class ItemStack:
__slots__ = ("item_id", "count", "data") __slots__ = ("item_id", "count", "data", "tag")
def __init__(self, item_id: int, count: int, data: int): def __init__(self, item_id: int, count: int, data: int, tag: Any = None):
self.item_id = item_id self.item_id = item_id
self.count = count self.count = count
self.data = data self.data = data
self.tag = tag
@classmethod @classmethod
async def read_from(cls, stream: AsyncDataInputStream) -> 'ItemStack': async def read_from(cls, stream: AsyncDataInputStream) -> 'ItemStack':
@ -22,3 +24,8 @@ class ItemStack:
count = stream.read() count = stream.read()
data = stream.read_ushort() data = stream.read_ushort()
return cls(item_id, count, data) return cls(item_id, count, data)
def __repr__(self):
if self.tag:
return f'<ItemStack! {self.item_id}:{self.data} x{self.count}>'
return f'<ItemStack {self.item_id}:{self.data} x{self.count}>'

View File

@ -29,3 +29,32 @@ from .packet15place import Packet15Place
from .packet255kickdisconnect import Packet255KickDisconnect from .packet255kickdisconnect import Packet255KickDisconnect
from .packet255kickdisconnect import Packet255KickDisconnect from .packet255kickdisconnect import Packet255KickDisconnect
from .packet102windowclick import Packet102WindowClick from .packet102windowclick import Packet102WindowClick
from .packet7useentity import Packet7UseEntity
from .packet8updatehealth import Packet8UpdateHealth
from .packet21pickupspawn import Packet21PickupSpawn
from .packet23vehiclespawn import Packet23VehicleSpawn
from .packet28entityvelocity import Packet28EntityVelocity
from .packet41entityplayergamemode import Packet41EntityPlayerGamemode
from .packet104windowitems import Packet104WindowItems
from .packet104windowitems import Packet104WindowItems
from .packet104windowitems import Packet104WindowItems
from .packet61playsoundeffect import Packet61PlaySoundEffect
from .packet104windowitems import Packet104WindowItems
from .packet103setslot import Packet103SetSlot
from .packet31relentitymove import Packet31RelEntityMove
from .packet29destroyentity import Packet29DestroyEntity
from .packet33relentitymovelook import Packet33RelEntityMoveLook
from .packet32entitylook import Packet32EntityLook
from .packet34entityteleport import Packet34EntityTeleport
from .packet30entity import Packet30Entity
from .packet40entitymetadata import Packet40EntityMetadata
from .packet53blockchange import Packet53BlockChange
from .packet51mapchunk import Packet51MapChunk
from .packet51mapchunk import Packet51MapChunk
from .packet140tileentitydata import Packet140TileEntityData
from .packet132setmobspawner import Packet132SetMobSpawner
from .packet39attachentity import Packet39AttachEntity
from .packet35entitynickname import Packet35EntityNickname
from .packet52multiblockchange import Packet52MultiBlockChange
from .packet20statistic import Packet20Statistic
from .packet0keepalive import Packet0KeepAlive

View File

@ -1,11 +1,19 @@
from typing import Any, ClassVar, Type from typing import Any, ClassVar, Optional, Type, Union
from bta_proxy.entitydata import EntityData from bta_proxy.entitydata import EntityData
from bta_proxy.itemstack import ItemStack from bta_proxy.itemstack import ItemStack
from ..datainputstream import AsyncDataInputStream from ..datainputstream import AsyncDataInputStream
def try_int(v: str) -> Union[str, int]:
try:
return int(v)
except ValueError:
return v
class Packet: class Packet:
REGISTRY: ClassVar[dict[int, Type['Packet']]] = {} REGISTRY: ClassVar[dict[int, Type["Packet"]]] = {}
FIELDS: ClassVar[list[tuple[str, Any]]] = [] FIELDS: ClassVar[list[tuple[str, Any]]] = []
packet_id: int packet_id: int
@ -14,81 +22,157 @@ class Packet:
setattr(self, k, v) setattr(self, k, v)
@classmethod @classmethod
async def read_data_from(cls, stream: AsyncDataInputStream) -> 'Packet': async def read_data_from(cls, stream: AsyncDataInputStream) -> "Packet":
fields: dict = {} fields: dict = {}
for key, datatype in cls.FIELDS: for key, datatype in cls.FIELDS:
if "?" in key:
key, cond = key.split("?", 1)
if "==" in cond:
k, v = cond.split("==")
if fields[k] != try_int(v):
continue
elif not fields[cond]:
continue
try:
fields[key] = await cls.read_field(stream, datatype, fields) fields[key] = await cls.read_field(stream, datatype, fields)
except Exception as e:
raise ValueError(f"Failed getting key {key} on {cls}") from e
return cls(**fields) return cls(**fields)
@staticmethod @staticmethod
async def read_field(stream: AsyncDataInputStream, datatype: Any, fields: dict[str, Any] = {}): async def read_field(
stream: AsyncDataInputStream,
datatype: Any,
fields: dict[str, Any] = {},
):
match datatype: match datatype:
case 'uint': case "list", sizekey, *args:
return [
await Packet.read_field(stream, args, fields)
for _ in range(fields[sizekey])
]
case "uint":
return await stream.read_uint() return await stream.read_uint()
case 'int': case "int":
return await stream.read_int() return await stream.read_int()
case 'str': case "str":
return await stream.read_string() return await stream.read_string()
case 'str', length: case "str", length:
return (await stream.read_string())[:length] return (await stream.read_string())[:length]
case 'string': case "string":
return await stream.read_string() return await stream.read_string()
case 'string', length: case "string", length:
return (await stream.read_string())[:length] return (await stream.read_string())[:length]
case 'ulong': case "ulong":
return await stream.read_ulong() return await stream.read_ulong()
case 'long': case "long":
return await stream.read_long() return await stream.read_long()
case 'ushort': case "ushort":
return await stream.read_ushort() return await stream.read_ushort()
case 'short': case "short":
return await stream.read_short() return await stream.read_short()
case 'byte': case "byte":
return await stream.read() return await stream.read_byte()
case 'float': case "ubyte":
return await stream.read_ubyte()
case "float":
return await stream.read_float() return await stream.read_float()
case 'double': case "double":
return await stream.read_double() return await stream.read_double()
case 'bool': case "bool":
return await stream.read_boolean() return await stream.read_boolean()
case 'bytes', length_or_key: case "bytes", length_or_key:
if isinstance(length_or_key, int): if isinstance(length_or_key, int):
return await stream.read_bytes(length_or_key) return await stream.read_bytes(length_or_key)
elif isinstance(length_or_key, str): elif isinstance(length_or_key, str):
if length_or_key == ".rest":
return stream.read_rest()
if length_or_key not in fields: if length_or_key not in fields:
raise KeyError(f'failed to find {length_or_key} in {fields} to read bytes length') raise KeyError(
f"failed to find {length_or_key} in {fields} to read bytes length"
)
return await stream.read_bytes(fields[length_or_key]) return await stream.read_bytes(fields[length_or_key])
raise ValueError(f'invalid type for bytes length_or_key: {length_or_key!r}') raise ValueError(
case 'itemstack': f"invalid type for bytes length_or_key: {length_or_key!r}"
)
case "itemstack":
return await ItemStack.read_from(stream) return await ItemStack.read_from(stream)
case 'itemstack_optional': case "itemstack", length_or_key:
if isinstance(length_or_key, int):
items: list[Optional[ItemStack]] = []
for _ in range(length_or_key):
if (item_id := await stream.read_short()) >= 0:
count = await stream.read()
data = await stream.read_short()
items.append(ItemStack(item_id, count, data))
else:
items.append(None)
return items
elif isinstance(length_or_key, str):
if fields[length_or_key] <= 0:
return []
if length_or_key not in fields:
raise KeyError(
f"failed to find {length_or_key} in {fields} to read number of itemstacks"
)
items: list[Optional[ItemStack]] = []
for _ in range(fields[length_or_key]):
if (item_id := await stream.read_short()) >= 0:
count = await stream.read()
data = await stream.read_short()
items.append(ItemStack(item_id, count, data))
else:
items.append(None)
return items
raise ValueError(
f"invalid type for itemstack length_or_key: {length_or_key!r}"
)
case "itemstack_optional":
if (item_id := await stream.read_short()) >= 0: if (item_id := await stream.read_short()) >= 0:
count = await stream.read() count = await stream.read()
data = await stream.read_short() data = await stream.read_short()
return ItemStack(item_id, count, data) return ItemStack(item_id, count, data)
return None return None
case 'entitydata': case "extendeditemstack_optional":
if (item_id := await stream.read_short()) >= 0:
count = await stream.read()
data = await stream.read_short()
tag_size = await stream.read_short()
tag = await stream.read_bytes(tag_size)
return ItemStack(item_id, count, data, tag)
return None
case "entitydata":
return await EntityData.read_from(stream) return await EntityData.read_from(stream)
case _: case _:
raise ValueError(f'unknown type {datatype}') raise ValueError(f"unknown type {datatype}")
def __init_subclass__(cls, packet_id: int, **kwargs) -> None: def __init_subclass__(cls, packet_id: int, **kwargs) -> None:
Packet.REGISTRY[packet_id] = cls Packet.REGISTRY[packet_id] = cls
cls.packet_id = packet_id cls.packet_id = packet_id
super().__init_subclass__(**kwargs) super().__init_subclass__(**kwargs)
def post_creation(self):
pass
@classmethod @classmethod
async def read_packet(cls, stream: AsyncDataInputStream) -> 'Packet': async def read_packet(cls, stream: AsyncDataInputStream) -> "Packet":
packet_id: int = await stream.read() packet_id: int = await stream.read()
if packet_id not in cls.REGISTRY: if packet_id not in cls.REGISTRY:
raise ValueError(f'invalid packet 0x{packet_id:02x} ({packet_id})') raise ValueError(
f"invalid packet 0x{packet_id:02x} ({packet_id}) (rest: {stream.read_rest()[:16]}...)"
)
pkt = await cls.REGISTRY[packet_id].read_data_from(stream) pkt = await cls.REGISTRY[packet_id].read_data_from(stream)
pkt.packet_id = packet_id pkt.packet_id = packet_id
pkt.post_creation()
return pkt return pkt
def __repr__(self): def __repr__(self):
pkt_name = self.REGISTRY[self.packet_id].__name__ pkt_name = self.REGISTRY[self.packet_id].__name__
fields = [] fields = []
for name, _ in self.FIELDS: for key, _ in self.FIELDS:
fields.append(f'{name}={getattr(self, name)!r}') if "?" in key:
key, cond = key.split("?", 1)
fields.append(f"{key}={getattr(self, key, None)!r} if {cond}")
else:
fields.append(f"{key}={getattr(self, key)!r}")
return f'<{pkt_name} {str.join(", ", fields)}>' return f'<{pkt_name} {str.join(", ", fields)}>'

View File

@ -0,0 +1,6 @@
from .base import Packet
class Packet0KeepAlive(Packet, packet_id=0):
__slots__ = ()
FIELDS = [
]

View File

@ -0,0 +1,8 @@
from .base import Packet
class Packet103SetSlot(Packet, packet_id=103):
FIELDS = [
('window_id', 'byte'),
('slot', 'short'),
('item', 'extendeditemstack_optional'),
]

View File

@ -0,0 +1,8 @@
from .base import Packet
class Packet104WindowItems(Packet, packet_id=104):
FIELDS = [
('window_id', 'byte'),
('n_items', 'short'),
('items', ('itemstack', 'n_items')),
]

View File

@ -0,0 +1,10 @@
from .base import Packet
class Packet132SetMobSpawner(Packet, packet_id=132):
__slots__ = ('x', 'y', 'z', 'type')
FIELDS = [
('x', 'int'),
('y', 'short'),
('z', 'int'),
('type', 'str'),
]

View File

@ -0,0 +1,12 @@
from .base import Packet
import gzip
class Packet140TileEntityData(Packet, packet_id=140):
__slots__ = ('size', 'data')
FIELDS = [
('size', 'short'),
('data', ('bytes', 'size')),
]
def post_creation(self):
self.data = gzip.decompress(self.data)

View File

@ -2,9 +2,9 @@ from .base import Packet
class Packet14BlockDig(Packet, packet_id=14): class Packet14BlockDig(Packet, packet_id=14):
FIELDS = [ FIELDS = [
('status', 'byte'), ('status', 'ubyte'),
('x', 'int'), ('x', 'int'),
('y', 'byte'), ('y', 'ubyte'),
('z', 'int'), ('z', 'int'),
('side', 'byte'), ('side', 'ubyte'),
] ]

View File

@ -3,9 +3,9 @@ from .base import Packet
class Packet15Place(Packet, packet_id=15): class Packet15Place(Packet, packet_id=15):
FIELDS = [ FIELDS = [
('x', 'int'), ('x', 'int'),
('y', 'byte'), ('y', 'ubyte'),
('z', 'int'), ('z', 'int'),
('direction', 'byte'), ('direction', 'ubyte'),
('y_placed', 'double'), ('y_placed', 'double'),
('item', 'itemstack_optional') ('item', 'itemstack_optional')
] ]

View File

@ -0,0 +1,8 @@
from .base import Packet
class Packet20Statistic(Packet, packet_id=20):
__slots__ = ('field_27052_a', 'field_27051')
FIELDS = [
('field_27052_a', 'int'),
('field_27051', 'byte'),
]

View File

@ -0,0 +1,17 @@
from .base import Packet
class Packet21PickupSpawn(Packet, packet_id=21):
FIELDS = [
('entity_id', 'int'),
('item_id', 'short'),
('count', 'byte'),
('damage', 'short'),
('tag_size', 'short'),
('tag', ('bytes', 'tag_size')),
('x', 'int'),
('y', 'int'),
('z', 'int'),
('yaw', 'byte'),
('pitch', 'byte'),
('roll', 'byte'),
]

View File

@ -0,0 +1,17 @@
from .base import Packet
class Packet23VehicleSpawn(Packet, packet_id=23):
FIELDS = [
('entity_id', 'int'),
('type', 'byte'),
('x', 'int'),
('y', 'int'),
('z', 'int'),
('pitch', 'float'),
('yaw', 'float'),
('vehicle_type', 'int'),
('dx?vehicle_type', 'short'),
('dy?vehicle_type', 'short'),
('dz?vehicle_type', 'short'),
('arrow_type?type==60', 'int'),
]

View File

@ -0,0 +1,9 @@
from .base import Packet
class Packet28EntityVelocity(Packet, packet_id=28):
FIELDS = [
('entity_id', 'int'),
('dx', 'short'),
('dy', 'short'),
('dz', 'short'),
]

View File

@ -0,0 +1,6 @@
from .base import Packet
class Packet29DestroyEntity(Packet, packet_id=29):
FIELDS = [
('entity_id', 'int'),
]

View File

@ -0,0 +1,6 @@
from .base import Packet
class Packet30Entity(Packet, packet_id=30):
FIELDS = [
('entity_id', 'int'),
]

View File

@ -0,0 +1,9 @@
from .base import Packet
class Packet31RelEntityMove(Packet, packet_id=31):
FIELDS = [
('entity_id', 'int'),
('x', 'byte'),
('y', 'byte'),
('z', 'byte'),
]

View File

@ -0,0 +1,8 @@
from .base import Packet
class Packet32EntityLook(Packet, packet_id=32):
FIELDS = [
('entity_id', 'int'),
('yaw', 'byte'),
('pitch', 'byte'),
]

View File

@ -0,0 +1,11 @@
from .base import Packet
class Packet33RelEntityMoveLook(Packet, packet_id=33):
FIELDS = [
('entity_id', 'int'),
('x', 'byte'),
('y', 'byte'),
('z', 'byte'),
('yaw', 'byte'),
('pitch', 'byte'),
]

View File

@ -0,0 +1,11 @@
from .base import Packet
class Packet34EntityTeleport(Packet, packet_id=34):
FIELDS = [
('entity_id', 'int'),
('x', 'int'),
('y', 'int'),
('z', 'int'),
('yaw', 'byte'),
('pitch', 'byte'),
]

View File

@ -0,0 +1,9 @@
from .base import Packet
class Packet35EntityNickname(Packet, packet_id=35):
__slots__ = ('entity_id', 'name', 'color')
FIELDS = [
('entity_id', 'int'),
('name', 'str'),
('color', 'ubyte'),
]

View File

@ -0,0 +1,8 @@
from .base import Packet
class Packet39AttachEntity(Packet, packet_id=39):
__slots__ = ('entity_id', 'vehicle_entity_id')
FIELDS = [
('entity_id', 'int'),
('vehicle_entity_id', 'int'),
]

View File

@ -0,0 +1,7 @@
from .base import Packet
class Packet40EntityMetadata(Packet, packet_id=40):
FIELDS = [
('entity_id', 'int'),
('data', 'entitydata'),
]

View File

@ -0,0 +1,6 @@
from .base import Packet
class Packet41EntityPlayerGamemode(Packet, packet_id=41):
FIELDS = [
('gamemode', 'byte'),
]

View File

@ -0,0 +1,19 @@
from .base import Packet
class Packet51MapChunk(Packet, packet_id=51):
__slots__ = ('x', 'y', 'z', 'xs', 'ys', 'zs', 'size', 'data')
FIELDS = [
('x', 'int'),
('y', 'short'),
('z', 'int'),
('xs', 'ubyte'),
('ys', 'ubyte'),
('zs', 'ubyte'),
('size', 'int'),
('data', ('bytes', 'size')),
]
def post_creation(self):
self.xs += 1
self.ys += 1
self.zs += 1

View File

@ -0,0 +1,12 @@
from .base import Packet
class Packet52MultiBlockChange(Packet, packet_id=52):
__slots__ = ('x', 'z', 'size', 'coordinates', 'types', 'metadata')
FIELDS = [
('x', 'int'),
('z', 'int'),
('size', 'short'),
('coordinates', ('list', 'size', 'short')),
('types', ('list', 'size', 'short')),
('metadata', ('list', 'size', 'ubyte')),
]

View File

@ -0,0 +1,10 @@
from .base import Packet
class Packet53BlockChange(Packet, packet_id=53):
FIELDS = [
('x', 'int'),
('y', 'ubyte'),
('z', 'int'),
('type', 'short'),
('metadata', 'ubyte'),
]

View File

@ -0,0 +1,10 @@
from .base import Packet
class Packet61PlaySoundEffect(Packet, packet_id=61):
FIELDS = [
('sound_id', 'int'),
('x', 'int'),
('y', 'int'),
('z', 'int'),
('data', 'int'),
]

View File

@ -5,7 +5,7 @@ class Packet73WeatherStatus(Packet, packet_id=73):
FIELDS = [ FIELDS = [
('dim', 'int'), ('dim', 'int'),
('id', 'int'), ('id', 'int'),
('new_id', 'id'), ('new_id', 'int'),
('duration', 'long'), ('duration', 'long'),
('intensity', 'float'), ('intensity', 'float'),
('power', 'float') ('power', 'float')

View File

@ -0,0 +1,8 @@
from .base import Packet
class Packet7UseEntity(Packet, packet_id=7):
FIELDS = [
('player_id', 'int'),
('entity_id', 'int'),
('is_left', 'bool'),
]

View File

@ -0,0 +1,6 @@
from .base import Packet
class Packet8UpdateHealth(Packet, packet_id=8):
FIELDS = [
('health', 'short'),
]

View File

@ -12,6 +12,11 @@ def main(argv: list[str]):
f.write(f'from .base import Packet\n') f.write(f'from .base import Packet\n')
f.write(f'\n') f.write(f'\n')
f.write(f'class {packetname}(Packet, packet_id={packet_id}):\n') f.write(f'class {packetname}(Packet, packet_id={packet_id}):\n')
slots = tuple([
arg.split(':')[0].split('?')[0]
for arg in fields
])
f.write(f' __slots__ = {slots!r}\n')
f.write(f' FIELDS = [\n') f.write(f' FIELDS = [\n')
for field in fields: for field in fields:
args: list[Any] args: list[Any]