Skip to content

Commit 8e6981c

Browse files
Use a NamedTuple for Slot data
- Cache the classes for FixedBitset - Create an `AdvancementFrame` IntEnum - Use attrs' validators and convertors
1 parent d133a83 commit 8e6981c

40 files changed

+627
-741
lines changed

changes/297.feature.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
- Added more types to the implementation:
2+
23
- `Angle`: Represents an angle.
34
- `BitSet`: Represents a set of bits of variable length.
45
- `FixedBitSet`: Represents a set of bits of fixed length.
@@ -10,4 +11,7 @@
1011
- `Vec3`: Represents a 3D vector.
1112
- `Position`: Represents a position with packed integers.
1213
- `EntityMetadata`: Represents metadata for an entity.
13-
> There are **A LOT** of different entity metadata types, so I'm not going to list them all here.
14+
> There are **A LOT** of different entity metadata types, so I'm not going to list them all here.
15+
16+
- Removed the `validate` method from most `Serializable` classes.
17+
- Make use of validators and convertors from the `attrs` library instead.

docs/api/types/general.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ Here are documented the general types used throughout the Minecraft protocol.
77
:no-undoc-members:
88
:exclude-members: NBTag, StringNBT, CompoundNBT, EndNBT, EntityMetadata, UUID
99

10-
.. autoclass:: mcproto.types.UUID
11-
:class-doc-from: class
10+
.. autoclass:: mcproto.types.UUID
11+
:class-doc-from: class

mcproto/packets/handshaking/handshake.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from enum import IntEnum
4-
from typing import ClassVar, cast, final
4+
from typing import ClassVar, final
55

66
from typing_extensions import Self, override
77

@@ -42,19 +42,11 @@ class Handshake(ServerBoundPacket):
4242
protocol_version: int
4343
server_address: str
4444
server_port: int
45-
next_state: NextState | int
46-
47-
@override
48-
def __attrs_post_init__(self) -> None:
49-
if not isinstance(self.next_state, NextState):
50-
self.next_state = NextState(self.next_state)
51-
52-
super().__attrs_post_init__()
45+
next_state: NextState
5346

5447
@override
5548
def serialize_to(self, buf: Buffer) -> None:
5649
"""Serialize the packet."""
57-
self.next_state = cast(NextState, self.next_state) # Handled by the __attrs_post_init__ method
5850
buf.write_varint(self.protocol_version)
5951
buf.write_utf(self.server_address)
6052
buf.write_value(StructFormat.USHORT, self.server_port)
@@ -67,5 +59,5 @@ def _deserialize(cls, buf: Buffer, /) -> Self:
6759
protocol_version=buf.read_varint(),
6860
server_address=buf.read_utf(),
6961
server_port=buf.read_value(StructFormat.USHORT),
70-
next_state=buf.read_varint(),
62+
next_state=NextState(buf.read_varint()),
7163
)

mcproto/packets/login/login.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mcproto.packets.packet import ClientBoundPacket, GameState, ServerBoundPacket
1212
from mcproto.types.chat import JSONTextComponent
1313
from mcproto.types.uuid import UUID
14-
from attrs import define
14+
from attrs import define, field
1515

1616
__all__ = [
1717
"LoginDisconnect",
@@ -71,16 +71,9 @@ class LoginEncryptionRequest(ClientBoundPacket):
7171
PACKET_ID: ClassVar[int] = 0x01
7272
GAME_STATE: ClassVar[GameState] = GameState.LOGIN
7373

74-
public_key: RSAPublicKey
75-
verify_token: bytes
76-
server_id: str | None = None
77-
78-
@override
79-
def __attrs_post_init__(self) -> None:
80-
if self.server_id is None:
81-
self.server_id = " " * 20
82-
83-
super().__attrs_post_init__()
74+
public_key: RSAPublicKey = field()
75+
verify_token: bytes = field()
76+
server_id: str | None = field(default=" " * 20)
8477

8578
@override
8679
def serialize_to(self, buf: Buffer) -> None:
@@ -243,7 +236,7 @@ class LoginPluginResponse(ServerBoundPacket):
243236
GAME_STATE: ClassVar[GameState] = GameState.LOGIN
244237

245238
message_id: int
246-
data: bytes | None
239+
data: bytes | None = None
247240

248241
@override
249242
def serialize_to(self, buf: Buffer) -> None:

mcproto/packets/status/status.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from mcproto.buffer import Buffer
99
from mcproto.packets.packet import ClientBoundPacket, GameState, ServerBoundPacket
10-
from attrs import define
10+
from attrs import define, field
1111

1212
__all__ = ["StatusRequest", "StatusResponse"]
1313

@@ -43,7 +43,7 @@ class StatusResponse(ClientBoundPacket):
4343
PACKET_ID: ClassVar[int] = 0x00
4444
GAME_STATE: ClassVar[GameState] = GameState.STATUS
4545

46-
data: dict[str, Any] # JSON response data sent back to the client.
46+
data: dict[str, Any] = field(validator=lambda self, _, value: json.dumps(value))
4747

4848
@override
4949
def serialize_to(self, buf: Buffer) -> None:
@@ -56,11 +56,3 @@ def _deserialize(cls, buf: Buffer, /) -> Self:
5656
s = buf.read_utf()
5757
data_ = json.loads(s)
5858
return cls(data_)
59-
60-
@override
61-
def validate(self) -> None:
62-
# Ensure the data is serializable to JSON
63-
try:
64-
json.dumps(self.data)
65-
except TypeError as exc:
66-
raise ValueError("Data is not serializable to JSON.") from exc

mcproto/types/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from __future__ import annotations
22

33
from mcproto.types.abc import MCType, Serializable
4-
from mcproto.types.advancement import Advancement, AdvancementProgress, AdvancementDisplay, AdvancementCriterion
4+
from mcproto.types.advancement import (
5+
Advancement,
6+
AdvancementProgress,
7+
AdvancementDisplay,
8+
AdvancementCriterion,
9+
AdvancementFrame,
10+
)
511
from mcproto.types.angle import Angle
612
from mcproto.types.bitset import Bitset, FixedBitset
713
from mcproto.types.block_entity import BlockEntity
@@ -40,7 +46,7 @@
4046
StoneCuttingRecipe,
4147
SuspiciousStewRecipe,
4248
)
43-
from mcproto.types.slot import Slot
49+
from mcproto.types.slot import Slot, SlotData
4450
from mcproto.types.registry_tag import RegistryTag
4551
from mcproto.types.trade import Trade
4652
from mcproto.types.uuid import UUID
@@ -60,6 +66,7 @@
6066
"CompoundNBT",
6167
"Quaternion",
6268
"Slot",
69+
"SlotData",
6370
"RegistryTag",
6471
"UUID",
6572
"Position",
@@ -74,6 +81,7 @@
7481
"AdvancementProgress",
7582
"AdvancementDisplay",
7683
"AdvancementCriterion",
84+
"AdvancementFrame",
7785
"ModifierData",
7886
"ModifierOperation",
7987
"Recipe",

mcproto/types/advancement.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from enum import IntEnum
34
from typing import final
45
from attrs import define
56

@@ -18,7 +19,7 @@
1819
class Advancement(MCType):
1920
"""Represents an advancement in the game.
2021
21-
https://wiki.vg/Protocol#Update_Advancements
22+
Non-standard type, see: `<https://wiki.vg/Protocol#Update_Advancements>`
2223
2324
:param parent: The parent advancement.
2425
:type parent: :class:`~mcproto.types.identifier.Identifier`, optional
@@ -56,6 +57,14 @@ def deserialize(cls, buf: Buffer) -> Advancement:
5657
return cls(parent=parent, display=display, requirements=requirements, telemetry=telemetry)
5758

5859

60+
class AdvancementFrame(IntEnum):
61+
"""Represents the shape of the frame of an advancement in the GUI."""
62+
63+
TASK = 0
64+
CHALLENGE = 1
65+
GOAL = 2
66+
67+
5968
@final
6069
@define
6170
class AdvancementDisplay(MCType):
@@ -67,7 +76,8 @@ class AdvancementDisplay(MCType):
6776
:type description: :class:`~mcproto.types.chat.TextComponent`
6877
:param icon: The icon of the advancement.
6978
:type icon: :class:`~mcproto.types.slot.Slot`
70-
:param frame: The frame of the advancement (0: task, 1: challenge, 2: goal).
79+
:param frame: The frame of the advancement.
80+
:type frame: :class:`AdvancementFrame`
7181
:param background: The background texture of the advancement.
7282
:type background: :class:`~mcproto.types.identifier.Identifier`, optional
7383
:param show_toast: Whether to show a toast notification.
@@ -83,7 +93,7 @@ class AdvancementDisplay(MCType):
8393
title: TextComponent
8494
description: TextComponent
8595
icon: Slot
86-
frame: int
96+
frame: AdvancementFrame
8797
background: Identifier | None
8898
show_toast: bool
8999
hidden: bool
@@ -95,7 +105,7 @@ def serialize_to(self, buf: Buffer) -> None:
95105
self.title.serialize_to(buf)
96106
self.description.serialize_to(buf)
97107
self.icon.serialize_to(buf)
98-
buf.write_varint(self.frame)
108+
buf.write_varint(self.frame.value)
99109

100110
flags = (self.background is not None) << 0 | self.show_toast << 1 | self.hidden << 2
101111
buf.write_value(StructFormat.BYTE, flags)
@@ -110,7 +120,7 @@ def deserialize(cls, buf: Buffer) -> AdvancementDisplay:
110120
title = TextComponent.deserialize(buf)
111121
description = TextComponent.deserialize(buf)
112122
icon = Slot.deserialize(buf)
113-
frame = buf.read_varint()
123+
frame = AdvancementFrame(buf.read_varint())
114124
flags = buf.read_value(StructFormat.BYTE)
115125
background = Identifier.deserialize(buf) if flags & 0x1 else None
116126
show_toast = bool(flags & 0x2)
@@ -166,7 +176,7 @@ class AdvancementCriterion(MCType):
166176
:type date: int, optional
167177
"""
168178

169-
date: int | None
179+
date: int | None = None
170180

171181
@override
172182
def serialize_to(self, buf: Buffer) -> None:

mcproto/types/angle.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import final
44
import math
5-
from attrs import define
5+
from attrs import define, field
66

77
from typing_extensions import override
88

@@ -18,9 +18,12 @@ class Angle(MCType):
1818
"""Represents a rotation angle for an entity.
1919
2020
:param value: The angle value in 1/256th of a full rotation.
21+
:type value: int
22+
23+
.. note:: The angle is stored as a byte, so the value is in the range [0, 255].
2124
"""
2225

23-
angle: int
26+
angle: int = field(converter=lambda x: int(x) % 256)
2427

2528
@override
2629
def serialize_to(self, buf: Buffer) -> None:
@@ -36,11 +39,6 @@ def deserialize(cls, buf: Buffer) -> Angle:
3639
payload = buf.read_value(StructFormat.BYTE)
3740
return cls(angle=int(payload * 360 / 256))
3841

39-
@override
40-
def validate(self) -> None:
41-
"""Constrain the angle to the range [0, 256)."""
42-
self.angle %= 256
43-
4442
def in_direction(self, base: Vec3, distance: float) -> Vec3:
4543
"""Calculate the position in the direction of the angle in the xz-plane.
4644

mcproto/types/bitset.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from functools import lru_cache
34
import math
45

56
from typing import ClassVar
@@ -8,17 +9,34 @@
89
from mcproto.buffer import Buffer
910
from mcproto.protocol import StructFormat
1011
from mcproto.types.abc import MCType
11-
from attrs import define
12+
from attrs import define, field, Attribute, validators
1213
from mcproto.protocol.utils import to_twos_complement
1314

1415

1516
@define
1617
class FixedBitset(MCType):
17-
"""Represents a fixed-size bitset."""
18+
"""Represents a fixed-size bitset.
19+
20+
The size of the bitset must be defined using the :meth:`of_size` method.
21+
Each :class:`FixedBitset` class is unique to its size, and the size must be defined before using the class.
22+
23+
:param data: The bits of the bitset.
24+
"""
1825

1926
__BIT_COUNT: ClassVar[int] = -1
2027

21-
data: bytearray
28+
@staticmethod
29+
def data_length_check(_self: FixedBitset, attribute: Attribute[bytearray], value: bytearray) -> None:
30+
"""Check that the data length matches the bitset size.
31+
32+
:raises ValueError: If the data length doesn't match the bitset size.
33+
"""
34+
if _self.__BIT_COUNT == -1:
35+
raise ValueError("Bitset size is not defined.")
36+
if len(value) != math.ceil(_self.__BIT_COUNT / 8):
37+
raise ValueError(f"Bitset size is {_self.__BIT_COUNT}, but data length is {len(value)}.")
38+
39+
data: bytearray = field(validator=data_length_check.__get__(object))
2240

2341
@override
2442
def serialize_to(self, buf: Buffer) -> None:
@@ -32,18 +50,14 @@ def deserialize(cls, buf: Buffer) -> FixedBitset:
3250
data = buf.read(math.ceil(cls.__BIT_COUNT / 8))
3351
return cls(data=data)
3452

35-
@override
36-
def validate(self) -> None:
37-
"""Validate the bitset."""
38-
if self.__BIT_COUNT == -1:
39-
raise ValueError("Bitset size is not defined.")
40-
if len(self.data) != math.ceil(self.__BIT_COUNT / 8):
41-
raise ValueError(f"Bitset size is {len(self.data) * 8}, expected {self.__BIT_COUNT}.")
42-
4353
@staticmethod
54+
@lru_cache(maxsize=None)
4455
def of_size(n: int) -> type[FixedBitset]:
4556
"""Return a new FixedBitset class with the given size.
4657
58+
The result of this method is cached, so calling it multiple times with the same value will return the same
59+
class.
60+
4761
:param n: The size of the bitset.
4862
"""
4963
new_class = type(f"FixedBitset{n}", (FixedBitset,), {})
@@ -113,8 +127,17 @@ class Bitset(MCType):
113127
:param data: The bits of the bitset.
114128
"""
115129

116-
size: int
117-
data: list[int]
130+
@staticmethod
131+
def data_length_check(_self: Bitset, attribute: Attribute[list[int]], value: list[int]) -> None:
132+
"""Check that the data length matches the bitset size.
133+
134+
:raises ValueError: If the data length doesn't match the bitset size.
135+
"""
136+
if len(value) != _self.size:
137+
raise ValueError(f"Bitset size is {_self.size}, but data length is {len(value)}.")
138+
139+
size: int = field(validator=validators.gt(0))
140+
data: list[int] = field(validator=data_length_check.__get__(object))
118141

119142
@override
120143
def serialize_to(self, buf: Buffer) -> None:
@@ -129,12 +152,6 @@ def deserialize(cls, buf: Buffer) -> Bitset:
129152
data = [buf.read_value(StructFormat.LONGLONG) for _ in range(size)]
130153
return cls(size=size, data=data)
131154

132-
@override
133-
def validate(self) -> None:
134-
"""Validate the bitset."""
135-
if self.size != len(self.data):
136-
raise ValueError(f"Bitset size is ({self.size}) doesn't match data size ({len(self.data)}).")
137-
138155
@classmethod
139156
def from_int(cls, n: int, size: int | None = None) -> Bitset:
140157
"""Return a new Bitset with the given integer value.

0 commit comments

Comments
 (0)