Skip to content

Commit c550b3e

Browse files
Add more types
* Angle * Bitset and FixedBitset * Position * Vec3 * Quaternion * Slot * Identifier * TextComponent - Rename ChatMessage to JSONTextComponent
1 parent 98ee687 commit c550b3e

File tree

17 files changed

+1491
-32
lines changed

17 files changed

+1491
-32
lines changed

changes/285.internal.2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- `serialize_to(buf: Buffer)`: Abstract method to write the object to a `Buffer`.
77
- `validate()`: Validates the object's attributes; can be overridden for custom validation.
88
- `deserialize(cls, buf: Buffer) -> Self`: Abstract method to construct the object from a `Buffer`.
9-
- **Note**: Use the `dataclass` decorator when adding parameters to subclasses.
9+
- **Note**: Use the :func:`attrs.define` decorator when adding parameters to subclasses.
1010

1111
- Exemple:
1212

mcproto/packets/login/login.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from mcproto.buffer import Buffer
1111
from mcproto.packets.packet import ClientBoundPacket, GameState, ServerBoundPacket
12-
from mcproto.types.chat import ChatMessage
12+
from mcproto.types.chat import JSONTextComponent
1313
from mcproto.types.uuid import UUID
1414
from mcproto.utils.abc import define
1515

@@ -178,7 +178,7 @@ class LoginDisconnect(ClientBoundPacket):
178178
PACKET_ID: ClassVar[int] = 0x00
179179
GAME_STATE: ClassVar[GameState] = GameState.LOGIN
180180

181-
reason: ChatMessage
181+
reason: JSONTextComponent
182182

183183
@override
184184
def serialize_to(self, buf: Buffer) -> None:
@@ -187,7 +187,7 @@ def serialize_to(self, buf: Buffer) -> None:
187187
@override
188188
@classmethod
189189
def _deserialize(cls, buf: Buffer, /) -> Self:
190-
reason = ChatMessage.deserialize(buf)
190+
reason = JSONTextComponent.deserialize(buf)
191191
return cls(reason)
192192

193193

mcproto/types/angle.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
3+
from typing import final
4+
import math
5+
6+
from typing_extensions import override
7+
8+
from mcproto.buffer import Buffer
9+
from mcproto.protocol import StructFormat
10+
from mcproto.types.abc import MCType, define
11+
from mcproto.types.vec3 import Vec3
12+
13+
14+
@final
15+
@define
16+
class Angle(MCType):
17+
"""Represents a rotation angle for an entity.
18+
19+
:param value: The angle value in 1/256th of a full rotation.
20+
"""
21+
22+
angle: int
23+
24+
@override
25+
def serialize_to(self, buf: Buffer) -> None:
26+
payload = int(self.angle) & 0xFF
27+
# Convert to a signed byte.
28+
if payload & 0x80:
29+
payload -= 1 << 8
30+
buf.write_value(StructFormat.BYTE, payload)
31+
32+
@override
33+
@classmethod
34+
def deserialize(cls, buf: Buffer) -> Angle:
35+
payload = buf.read_value(StructFormat.BYTE)
36+
return cls(angle=int(payload * 360 / 256))
37+
38+
@override
39+
def validate(self) -> None:
40+
"""Constrain the angle to the range [0, 256)."""
41+
self.angle %= 256
42+
43+
def in_direction(self, base: Vec3, distance: float) -> Vec3:
44+
"""Calculate the position in the direction of the angle in the xz-plane.
45+
46+
0/256: Positive z-axis
47+
64/-192: Negative x-axis
48+
128/-128: Negative z-axis
49+
192/-64: Positive x-axis
50+
51+
:param base: The base position.
52+
:param distance: The distance to move.
53+
:return: The new position.
54+
"""
55+
x = base.x - distance * math.sin(self.to_radians())
56+
z = base.z + distance * math.cos(self.to_radians())
57+
return Vec3(x=x, y=base.y, z=z)
58+
59+
@classmethod
60+
def from_degrees(cls, degrees: float) -> Angle:
61+
"""Create an angle from degrees."""
62+
return cls(angle=int(degrees * 256 / 360))
63+
64+
def to_degrees(self) -> float:
65+
"""Return the angle in degrees."""
66+
return self.angle * 360 / 256
67+
68+
@classmethod
69+
def from_radians(cls, radians: float) -> Angle:
70+
"""Create an angle from radians."""
71+
return cls(angle=int(math.degrees(radians) * 256 / 360))
72+
73+
def to_radians(self) -> float:
74+
"""Return the angle in radians."""
75+
return math.radians(self.angle * 360 / 256)

mcproto/types/bitset.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
from __future__ import annotations
2+
3+
import math
4+
5+
from typing import ClassVar
6+
from typing_extensions import override
7+
8+
from mcproto.buffer import Buffer
9+
from mcproto.protocol import StructFormat
10+
from mcproto.types.abc import MCType, define
11+
12+
13+
@define
14+
class FixedBitset(MCType):
15+
"""Represents a fixed-size bitset."""
16+
17+
__n: ClassVar[int] = -1
18+
19+
data: bytearray
20+
21+
@override
22+
def serialize_to(self, buf: Buffer) -> None:
23+
buf.write(bytes(self.data))
24+
25+
@override
26+
@classmethod
27+
def deserialize(cls, buf: Buffer) -> FixedBitset:
28+
data = buf.read(math.ceil(cls.__n / 8))
29+
return cls(data=data)
30+
31+
@override
32+
def validate(self) -> None:
33+
"""Validate the bitset."""
34+
if self.__n == -1:
35+
raise ValueError("Bitset size is not defined.")
36+
if len(self.data) != math.ceil(self.__n / 8):
37+
raise ValueError(f"Bitset size is {len(self.data) * 8}, expected {self.__n}.")
38+
39+
@staticmethod
40+
def of_size(n: int) -> type[FixedBitset]:
41+
"""Return a new FixedBitset class with the given size.
42+
43+
:param n: The size of the bitset.
44+
"""
45+
new_class = type(f"FixedBitset{n}", (FixedBitset,), {})
46+
new_class.__n = n
47+
return new_class
48+
49+
@classmethod
50+
def from_int(cls, n: int) -> FixedBitset:
51+
"""Return a new FixedBitset with the given integer value.
52+
53+
:param n: The integer value.
54+
"""
55+
if cls.__n == -1:
56+
raise ValueError("Bitset size is not defined.")
57+
if n < 0:
58+
# Manually compute two's complement
59+
n = -n
60+
data = bytearray(n.to_bytes(math.ceil(cls.__n / 8), "big"))
61+
for i in range(len(data)):
62+
data[i] ^= 0xFF
63+
data[-1] += 1
64+
else:
65+
data = bytearray(n.to_bytes(math.ceil(cls.__n / 8), "big"))
66+
return cls(data=data)
67+
68+
def __setitem__(self, index: int, value: bool) -> None:
69+
byte_index = index // 8
70+
bit_index = index % 8
71+
if value:
72+
self.data[byte_index] |= 1 << bit_index
73+
else:
74+
self.data[byte_index] &= ~(1 << bit_index)
75+
76+
def __getitem__(self, index: int) -> bool:
77+
byte_index = index // 8
78+
bit_index = index % 8
79+
return bool(self.data[byte_index] & (1 << bit_index))
80+
81+
def __len__(self) -> int:
82+
return self.__n
83+
84+
def __and__(self, other: FixedBitset) -> FixedBitset:
85+
if self.__n != other.__n:
86+
raise ValueError("Bitsets must have the same size.")
87+
return type(self)(data=bytearray(a & b for a, b in zip(self.data, other.data)))
88+
89+
def __or__(self, other: FixedBitset) -> FixedBitset:
90+
if self.__n != other.__n:
91+
raise ValueError("Bitsets must have the same size.")
92+
return type(self)(data=bytearray(a | b for a, b in zip(self.data, other.data)))
93+
94+
def __xor__(self, other: FixedBitset) -> FixedBitset:
95+
if self.__n != other.__n:
96+
raise ValueError("Bitsets must have the same size.")
97+
return type(self)(data=bytearray(a ^ b for a, b in zip(self.data, other.data)))
98+
99+
def __invert__(self) -> FixedBitset:
100+
return type(self)(data=bytearray(~a & 0xFF for a in self.data))
101+
102+
def __bytes__(self) -> bytes:
103+
return bytes(self.data)
104+
105+
@override
106+
def __eq__(self, value: object) -> bool:
107+
if not isinstance(value, FixedBitset):
108+
return NotImplemented
109+
return self.data == value.data and self.__n == value.__n
110+
111+
112+
@define
113+
class Bitset(MCType):
114+
"""Represents a lenght-prefixed bitset with a variable size.
115+
116+
:param size: The number of longs in the array representing the bitset.
117+
:param data: The bits of the bitset.
118+
"""
119+
120+
size: int
121+
data: list[int]
122+
123+
@override
124+
def serialize_to(self, buf: Buffer) -> None:
125+
buf.write_varint(self.size)
126+
for i in range(self.size):
127+
buf.write_value(StructFormat.LONGLONG, self.data[i])
128+
129+
@override
130+
@classmethod
131+
def deserialize(cls, buf: Buffer) -> Bitset:
132+
size = buf.read_varint()
133+
if buf.remaining < size * 8:
134+
raise IOError("Not enough data to read bitset.")
135+
data = [buf.read_value(StructFormat.LONGLONG) for _ in range(size)]
136+
return cls(size=size, data=data)
137+
138+
@override
139+
def validate(self) -> None:
140+
"""Validate the bitset."""
141+
if self.size != len(self.data):
142+
raise ValueError(f"Bitset size is {self.size}, expected {len(self.data)}.")
143+
144+
@classmethod
145+
def from_int(cls, n: int, size: int | None = None) -> Bitset:
146+
"""Return a new Bitset with the given integer value.
147+
148+
:param n: The integer value.
149+
:param size: The number of longs in the array representing the bitset.
150+
"""
151+
if size is None:
152+
size = math.ceil(float(n.bit_length()) / 64.0)
153+
data = [n >> (i * 64) & 0xFFFFFFFFFFFFFFFF for i in range(size)]
154+
return cls(size=size, data=data)
155+
156+
def __getitem__(self, index: int) -> bool:
157+
byte_index = index // 64
158+
bit_index = index % 64
159+
160+
return bool(self.data[byte_index] & (1 << bit_index))
161+
162+
def __setitem__(self, index: int, value: bool) -> None:
163+
byte_index = index // 64
164+
bit_index = index % 64
165+
166+
if value:
167+
self.data[byte_index] |= 1 << bit_index
168+
else:
169+
self.data[byte_index] &= ~(1 << bit_index)
170+
171+
def __len__(self) -> int:
172+
return self.size * 64
173+
174+
def __and__(self, other: Bitset) -> Bitset:
175+
if self.size != other.size:
176+
raise ValueError("Bitsets must have the same size.")
177+
return Bitset(size=self.size, data=[a & b for a, b in zip(self.data, other.data)])
178+
179+
def __or__(self, other: Bitset) -> Bitset:
180+
if self.size != other.size:
181+
raise ValueError("Bitsets must have the same size.")
182+
return Bitset(size=self.size, data=[a | b for a, b in zip(self.data, other.data)])
183+
184+
def __xor__(self, other: Bitset) -> Bitset:
185+
if self.size != other.size:
186+
raise ValueError("Bitsets must have the same size.")
187+
return Bitset(size=self.size, data=[a ^ b for a, b in zip(self.data, other.data)])
188+
189+
def __invert__(self) -> Bitset:
190+
return Bitset(size=self.size, data=[~a for a in self.data])
191+
192+
def __bytes__(self) -> bytes:
193+
return b"".join(a.to_bytes(8, "big") for a in self.data)

0 commit comments

Comments
 (0)