Skip to content

Commit

Permalink
Use a NamedTuple for Slot data
Browse files Browse the repository at this point in the history
- Cache the classes for FixedBitset
- Create an `AdvancementFrame` IntEnum
- Use attrs' validators and convertors
  • Loading branch information
LiteApplication committed Jun 26, 2024
1 parent d133a83 commit 2737072
Show file tree
Hide file tree
Showing 40 changed files with 640 additions and 749 deletions.
6 changes: 5 additions & 1 deletion changes/297.feature.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- Added more types to the implementation:

- `Angle`: Represents an angle.
- `BitSet`: Represents a set of bits of variable length.
- `FixedBitSet`: Represents a set of bits of fixed length.
Expand All @@ -10,4 +11,7 @@
- `Vec3`: Represents a 3D vector.
- `Position`: Represents a position with packed integers.
- `EntityMetadata`: Represents metadata for an entity.
> There are **A LOT** of different entity metadata types, so I'm not going to list them all here.
> There are **A LOT** of different entity metadata types, so I'm not going to list them all here.
- Removed the `validate` method from most `Serializable` classes.
- Make use of validators and convertors from the `attrs` library instead.
4 changes: 2 additions & 2 deletions docs/api/types/general.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ Here are documented the general types used throughout the Minecraft protocol.
:no-undoc-members:
:exclude-members: NBTag, StringNBT, CompoundNBT, EndNBT, EntityMetadata, UUID

.. autoclass:: mcproto.types.UUID
:class-doc-from: class
.. autoclass:: mcproto.types.UUID
:class-doc-from: class
14 changes: 3 additions & 11 deletions mcproto/packets/handshaking/handshake.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from enum import IntEnum
from typing import ClassVar, cast, final
from typing import ClassVar, final

from typing_extensions import Self, override

Expand Down Expand Up @@ -42,19 +42,11 @@ class Handshake(ServerBoundPacket):
protocol_version: int
server_address: str
server_port: int
next_state: NextState | int

@override
def __attrs_post_init__(self) -> None:
if not isinstance(self.next_state, NextState):
self.next_state = NextState(self.next_state)

super().__attrs_post_init__()
next_state: NextState

@override
def serialize_to(self, buf: Buffer) -> None:
"""Serialize the packet."""
self.next_state = cast(NextState, self.next_state) # Handled by the __attrs_post_init__ method
buf.write_varint(self.protocol_version)
buf.write_utf(self.server_address)
buf.write_value(StructFormat.USHORT, self.server_port)
Expand All @@ -67,5 +59,5 @@ def _deserialize(cls, buf: Buffer, /) -> Self:
protocol_version=buf.read_varint(),
server_address=buf.read_utf(),
server_port=buf.read_value(StructFormat.USHORT),
next_state=buf.read_varint(),
next_state=NextState(buf.read_varint()),
)
17 changes: 5 additions & 12 deletions mcproto/packets/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mcproto.packets.packet import ClientBoundPacket, GameState, ServerBoundPacket
from mcproto.types.chat import JSONTextComponent
from mcproto.types.uuid import UUID
from attrs import define
from attrs import define, field

__all__ = [
"LoginDisconnect",
Expand Down Expand Up @@ -71,16 +71,9 @@ class LoginEncryptionRequest(ClientBoundPacket):
PACKET_ID: ClassVar[int] = 0x01
GAME_STATE: ClassVar[GameState] = GameState.LOGIN

public_key: RSAPublicKey
verify_token: bytes
server_id: str | None = None

@override
def __attrs_post_init__(self) -> None:
if self.server_id is None:
self.server_id = " " * 20

super().__attrs_post_init__()
public_key: RSAPublicKey = field()
verify_token: bytes = field()
server_id: str | None = field(default=" " * 20)

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

message_id: int
data: bytes | None
data: bytes | None = None

@override
def serialize_to(self, buf: Buffer) -> None:
Expand Down
12 changes: 2 additions & 10 deletions mcproto/packets/status/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from mcproto.buffer import Buffer
from mcproto.packets.packet import ClientBoundPacket, GameState, ServerBoundPacket
from attrs import define
from attrs import define, field

__all__ = ["StatusRequest", "StatusResponse"]

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

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

@override
def serialize_to(self, buf: Buffer) -> None:
Expand All @@ -56,11 +56,3 @@ def _deserialize(cls, buf: Buffer, /) -> Self:
s = buf.read_utf()
data_ = json.loads(s)
return cls(data_)

@override
def validate(self) -> None:
# Ensure the data is serializable to JSON
try:
json.dumps(self.data)
except TypeError as exc:
raise ValueError("Data is not serializable to JSON.") from exc
12 changes: 10 additions & 2 deletions mcproto/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from __future__ import annotations

from mcproto.types.abc import MCType, Serializable
from mcproto.types.advancement import Advancement, AdvancementProgress, AdvancementDisplay, AdvancementCriterion
from mcproto.types.advancement import (
Advancement,
AdvancementProgress,
AdvancementDisplay,
AdvancementCriterion,
AdvancementFrame,
)
from mcproto.types.angle import Angle
from mcproto.types.bitset import Bitset, FixedBitset
from mcproto.types.block_entity import BlockEntity
Expand Down Expand Up @@ -40,7 +46,7 @@
StoneCuttingRecipe,
SuspiciousStewRecipe,
)
from mcproto.types.slot import Slot
from mcproto.types.slot import Slot, SlotData
from mcproto.types.registry_tag import RegistryTag
from mcproto.types.trade import Trade
from mcproto.types.uuid import UUID
Expand All @@ -60,6 +66,7 @@
"CompoundNBT",
"Quaternion",
"Slot",
"SlotData",
"RegistryTag",
"UUID",
"Position",
Expand All @@ -74,6 +81,7 @@
"AdvancementProgress",
"AdvancementDisplay",
"AdvancementCriterion",
"AdvancementFrame",
"ModifierData",
"ModifierOperation",
"Recipe",
Expand Down
22 changes: 16 additions & 6 deletions mcproto/types/advancement.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from enum import IntEnum
from typing import final
from attrs import define

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


class AdvancementFrame(IntEnum):
"""Represents the shape of the frame of an advancement in the GUI."""

TASK = 0
CHALLENGE = 1
GOAL = 2


@final
@define
class AdvancementDisplay(MCType):
Expand All @@ -67,7 +76,8 @@ class AdvancementDisplay(MCType):
:type description: :class:`~mcproto.types.chat.TextComponent`
:param icon: The icon of the advancement.
:type icon: :class:`~mcproto.types.slot.Slot`
:param frame: The frame of the advancement (0: task, 1: challenge, 2: goal).
:param frame: The frame of the advancement.
:type frame: :class:`AdvancementFrame`
:param background: The background texture of the advancement.
:type background: :class:`~mcproto.types.identifier.Identifier`, optional
:param show_toast: Whether to show a toast notification.
Expand All @@ -83,7 +93,7 @@ class AdvancementDisplay(MCType):
title: TextComponent
description: TextComponent
icon: Slot
frame: int
frame: AdvancementFrame
background: Identifier | None
show_toast: bool
hidden: bool
Expand All @@ -95,7 +105,7 @@ def serialize_to(self, buf: Buffer) -> None:
self.title.serialize_to(buf)
self.description.serialize_to(buf)
self.icon.serialize_to(buf)
buf.write_varint(self.frame)
buf.write_varint(self.frame.value)

flags = (self.background is not None) << 0 | self.show_toast << 1 | self.hidden << 2
buf.write_value(StructFormat.BYTE, flags)
Expand All @@ -110,7 +120,7 @@ def deserialize(cls, buf: Buffer) -> AdvancementDisplay:
title = TextComponent.deserialize(buf)
description = TextComponent.deserialize(buf)
icon = Slot.deserialize(buf)
frame = buf.read_varint()
frame = AdvancementFrame(buf.read_varint())
flags = buf.read_value(StructFormat.BYTE)
background = Identifier.deserialize(buf) if flags & 0x1 else None
show_toast = bool(flags & 0x2)
Expand Down Expand Up @@ -166,7 +176,7 @@ class AdvancementCriterion(MCType):
:type date: int, optional
"""

date: int | None
date: int | None = None

@override
def serialize_to(self, buf: Buffer) -> None:
Expand Down
12 changes: 5 additions & 7 deletions mcproto/types/angle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import final
import math
from attrs import define
from attrs import define, field

from typing_extensions import override

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

angle: int
angle: int = field(converter=lambda x: int(x) % 256)

@override
def serialize_to(self, buf: Buffer) -> None:
Expand All @@ -36,11 +39,6 @@ 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.
Expand Down
55 changes: 36 additions & 19 deletions mcproto/types/bitset.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from functools import lru_cache
import math

from typing import ClassVar
Expand All @@ -8,17 +9,34 @@
from mcproto.buffer import Buffer
from mcproto.protocol import StructFormat
from mcproto.types.abc import MCType
from attrs import define
from attrs import define, field, Attribute, validators
from mcproto.protocol.utils import to_twos_complement


@define
class FixedBitset(MCType):
"""Represents a fixed-size bitset."""
"""Represents a fixed-size bitset.
The size of the bitset must be defined using the :meth:`of_size` method.
Each :class:`FixedBitset` class is unique to its size, and the size must be defined before using the class.
:param data: The bits of the bitset.
"""

__BIT_COUNT: ClassVar[int] = -1

data: bytearray
@staticmethod
def data_length_check(_self: FixedBitset, attribute: Attribute[bytearray], value: bytearray) -> None:
"""Check that the data length matches the bitset size.
:raises ValueError: If the data length doesn't match the bitset size.
"""
if _self.__BIT_COUNT == -1:
raise ValueError("Bitset size is not defined.")
if len(value) != math.ceil(_self.__BIT_COUNT / 8):
raise ValueError(f"Bitset size is {_self.__BIT_COUNT}, but data length is {len(value)}.")

data: bytearray = field(validator=data_length_check.__get__(object))

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

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

@staticmethod
@lru_cache(maxsize=None)
def of_size(n: int) -> type[FixedBitset]:
"""Return a new FixedBitset class with the given size.
The result of this method is cached, so calling it multiple times with the same value will return the same
class.
:param n: The size of the bitset.
"""
new_class = type(f"FixedBitset{n}", (FixedBitset,), {})
Expand Down Expand Up @@ -113,8 +127,17 @@ class Bitset(MCType):
:param data: The bits of the bitset.
"""

size: int
data: list[int]
@staticmethod
def data_length_check(_self: Bitset, attribute: Attribute[list[int]], value: list[int]) -> None:
"""Check that the data length matches the bitset size.
:raises ValueError: If the data length doesn't match the bitset size.
"""
if len(value) != _self.size:
raise ValueError(f"Bitset size is {_self.size}, but data length is {len(value)}.")

size: int = field(validator=validators.gt(0))
data: list[int] = field(validator=data_length_check.__get__(object))

@override
def serialize_to(self, buf: Buffer) -> None:
Expand All @@ -129,12 +152,6 @@ def deserialize(cls, buf: Buffer) -> 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}) doesn't match data size ({len(self.data)}).")

@classmethod
def from_int(cls, n: int, size: int | None = None) -> Bitset:
"""Return a new Bitset with the given integer value.
Expand Down
Loading

0 comments on commit 2737072

Please sign in to comment.