Skip to content

Commit

Permalink
Add more types
Browse files Browse the repository at this point in the history
 * Angle
 * Bitset and FixedBitset
 * Position
 * Vec3
 * Quaternion
 * Slot
 * Identifier
 * TextComponent
- Rename ChatMessage to JSONTextComponent
  • Loading branch information
LiteApplication committed May 21, 2024
1 parent bbab587 commit 2e98b96
Show file tree
Hide file tree
Showing 16 changed files with 1,476 additions and 29 deletions.
6 changes: 3 additions & 3 deletions mcproto/packets/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from mcproto.buffer import Buffer
from mcproto.packets.packet import ClientBoundPacket, GameState, ServerBoundPacket
from mcproto.types.chat import ChatMessage
from mcproto.types.chat import JSONTextComponent
from mcproto.types.uuid import UUID
from mcproto.utils.abc import dataclass

Expand Down Expand Up @@ -176,7 +176,7 @@ class LoginDisconnect(ClientBoundPacket):
PACKET_ID: ClassVar[int] = 0x00
GAME_STATE: ClassVar[GameState] = GameState.LOGIN

reason: ChatMessage
reason: JSONTextComponent

@override
def serialize_to(self, buf: Buffer) -> None:
Expand All @@ -185,7 +185,7 @@ def serialize_to(self, buf: Buffer) -> None:
@override
@classmethod
def _deserialize(cls, buf: Buffer, /) -> Self:
reason = ChatMessage.deserialize(buf)
reason = JSONTextComponent.deserialize(buf)
return cls(reason)


Expand Down
75 changes: 75 additions & 0 deletions mcproto/types/angle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

from typing import final
import math

from typing_extensions import override

from mcproto.buffer import Buffer
from mcproto.protocol import StructFormat
from mcproto.types.abc import MCType, dataclass
from mcproto.types.vec3 import Vec3


@dataclass
@final
class Angle(MCType):
"""Represents a rotation angle for an entity.
:param value: The angle value in 1/256th of a full rotation.
"""

angle: int

@override
def serialize_to(self, buf: Buffer) -> None:
payload = int(self.angle) & 0xFF
# Convert to a signed byte.
if payload & 0x80:
payload -= 1 << 8
buf.write_value(StructFormat.BYTE, payload)

@override
@classmethod
def deserialize(cls, buf: Buffer) -> Angle:
payload = buf.read_value(StructFormat.BYTE)
return cls(angle=int(payload * 360 / 256))

@override
def validate(self) -> None:
"""Constrain the angle to the range [0, 256)."""
self.angle %= 256

def in_direction(self, base: Vec3, distance: float) -> Vec3:
"""Calculate the position in the direction of the angle in the xz-plane.
0/256: Positive z-axis
64/-192: Negative x-axis
128/-128: Negative z-axis
192/-64: Positive x-axis
:param base: The base position.
:param distance: The distance to move.
:return: The new position.
"""
x = base.x - distance * math.sin(self.to_radians())
z = base.z + distance * math.cos(self.to_radians())
return Vec3(x=x, y=base.y, z=z)

@classmethod
def from_degrees(cls, degrees: float) -> Angle:
"""Create an angle from degrees."""
return cls(angle=int(degrees * 256 / 360))

def to_degrees(self) -> float:
"""Return the angle in degrees."""
return self.angle * 360 / 256

@classmethod
def from_radians(cls, radians: float) -> Angle:
"""Create an angle from radians."""
return cls(angle=int(math.degrees(radians) * 256 / 360))

def to_radians(self) -> float:
"""Return the angle in radians."""
return math.radians(self.angle * 360 / 256)
193 changes: 193 additions & 0 deletions mcproto/types/bitset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
from __future__ import annotations

import math

from typing import ClassVar
from typing_extensions import override

from mcproto.buffer import Buffer
from mcproto.protocol import StructFormat
from mcproto.types.abc import MCType, dataclass


@dataclass
class FixedBitset(MCType):
"""Represents a fixed-size bitset."""

__n: ClassVar[int] = -1

data: bytearray

@override
def serialize_to(self, buf: Buffer) -> None:
buf.write(bytes(self.data))

@override
@classmethod
def deserialize(cls, buf: Buffer) -> FixedBitset:
data = buf.read(math.ceil(cls.__n / 8))
return cls(data=data)

@override
def validate(self) -> None:
"""Validate the bitset."""
if self.__n == -1:
raise ValueError("Bitset size is not defined.")
if len(self.data) != math.ceil(self.__n / 8):
raise ValueError(f"Bitset size is {len(self.data) * 8}, expected {self.__n}.")

@staticmethod
def of_size(n: int) -> type[FixedBitset]:
"""Return a new FixedBitset class with the given size.
:param n: The size of the bitset.
"""
new_class = type(f"FixedBitset{n}", (FixedBitset,), {})
new_class.__n = n
return new_class

@classmethod
def from_int(cls, n: int) -> FixedBitset:
"""Return a new FixedBitset with the given integer value.
:param n: The integer value.
"""
if cls.__n == -1:
raise ValueError("Bitset size is not defined.")
if n < 0:
# Manually compute two's complement
n = -n
data = bytearray(n.to_bytes(math.ceil(cls.__n / 8), "big"))
for i in range(len(data)):
data[i] ^= 0xFF
data[-1] += 1
else:
data = bytearray(n.to_bytes(math.ceil(cls.__n / 8), "big"))
return cls(data=data)

def __setitem__(self, index: int, value: bool) -> None:
byte_index = index // 8
bit_index = index % 8
if value:
self.data[byte_index] |= 1 << bit_index
else:
self.data[byte_index] &= ~(1 << bit_index)

def __getitem__(self, index: int) -> bool:
byte_index = index // 8
bit_index = index % 8
return bool(self.data[byte_index] & (1 << bit_index))

def __len__(self) -> int:
return self.__n

def __and__(self, other: FixedBitset) -> FixedBitset:
if self.__n != other.__n:
raise ValueError("Bitsets must have the same size.")
return type(self)(data=bytearray(a & b for a, b in zip(self.data, other.data)))

def __or__(self, other: FixedBitset) -> FixedBitset:
if self.__n != other.__n:
raise ValueError("Bitsets must have the same size.")
return type(self)(data=bytearray(a | b for a, b in zip(self.data, other.data)))

def __xor__(self, other: FixedBitset) -> FixedBitset:
if self.__n != other.__n:
raise ValueError("Bitsets must have the same size.")
return type(self)(data=bytearray(a ^ b for a, b in zip(self.data, other.data)))

def __invert__(self) -> FixedBitset:
return type(self)(data=bytearray(~a & 0xFF for a in self.data))

def __bytes__(self) -> bytes:
return bytes(self.data)

@override
def __eq__(self, value: object) -> bool:
if not isinstance(value, FixedBitset):
return NotImplemented
return self.data == value.data and self.__n == value.__n


@dataclass
class Bitset(MCType):
"""Represents a lenght-prefixed bitset with a variable size.
:param size: The number of longs in the array representing the bitset.
:param data: The bits of the bitset.
"""

size: int
data: list[int]

@override
def serialize_to(self, buf: Buffer) -> None:
buf.write_varint(self.size)
for i in range(self.size):
buf.write_value(StructFormat.LONGLONG, self.data[i])

@override
@classmethod
def deserialize(cls, buf: Buffer) -> Bitset:
size = buf.read_varint()
if buf.remaining < size * 8:
raise IOError("Not enough data to read bitset.")
data = [buf.read_value(StructFormat.LONGLONG) for _ in range(size)]
return cls(size=size, data=data)

@override
def validate(self) -> None:
"""Validate the bitset."""
if self.size != len(self.data):
raise ValueError(f"Bitset size is {self.size}, expected {len(self.data)}.")

@classmethod
def from_int(cls, n: int, size: int | None = None) -> Bitset:
"""Return a new Bitset with the given integer value.
:param n: The integer value.
:param size: The number of longs in the array representing the bitset.
"""
if size is None:
size = math.ceil(float(n.bit_length()) / 64.0)
data = [n >> (i * 64) & 0xFFFFFFFFFFFFFFFF for i in range(size)]
return cls(size=size, data=data)

def __getitem__(self, index: int) -> bool:
byte_index = index // 64
bit_index = index % 64

return bool(self.data[byte_index] & (1 << bit_index))

def __setitem__(self, index: int, value: bool) -> None:
byte_index = index // 64
bit_index = index % 64

if value:
self.data[byte_index] |= 1 << bit_index
else:
self.data[byte_index] &= ~(1 << bit_index)

def __len__(self) -> int:
return self.size * 64

def __and__(self, other: Bitset) -> Bitset:
if self.size != other.size:
raise ValueError("Bitsets must have the same size.")
return Bitset(size=self.size, data=[a & b for a, b in zip(self.data, other.data)])

def __or__(self, other: Bitset) -> Bitset:
if self.size != other.size:
raise ValueError("Bitsets must have the same size.")
return Bitset(size=self.size, data=[a | b for a, b in zip(self.data, other.data)])

def __xor__(self, other: Bitset) -> Bitset:
if self.size != other.size:
raise ValueError("Bitsets must have the same size.")
return Bitset(size=self.size, data=[a ^ b for a, b in zip(self.data, other.data)])

def __invert__(self) -> Bitset:
return Bitset(size=self.size, data=[~a for a in self.data])

def __bytes__(self) -> bytes:
return b"".join(a.to_bytes(8, "big") for a in self.data)
Loading

0 comments on commit 2e98b96

Please sign in to comment.