From 13cd00be2109b37fdbe8abc691a0a162b913b8ec Mon Sep 17 00:00:00 2001 From: Alexis Rossfelder Date: Mon, 8 Jul 2024 20:30:55 +0200 Subject: [PATCH] Use `Generic` classes for Entity Metadata types - Update particle IDs in Particle Data (we really need that Registry stuff) - Add tests for ParticleData - Split Masked proxy EME into BoolMasked and IntMasked for improved speed, simplicity and type checking --- docs/conf.py | 6 +- mcproto/types/entity/generated.py | 163 +++++++++--------- mcproto/types/entity/metadata.py | 130 +++++++------- mcproto/types/entity/metadata_types.py | 151 +++++++--------- mcproto/types/particle_data.py | 38 +++- scripts/entity_generator.py | 1 + scripts/entity_generator_data.py | 154 ++++++++--------- tests/mcproto/types/entity/test_metadata.py | 8 +- .../types/entity/test_metadata_types.py | 53 ++++-- tests/mcproto/types/test_particle_data.py | 84 +++++---- 10 files changed, 422 insertions(+), 366 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 21df5b75..7898caeb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,7 @@ from packaging.version import parse as parse_version from typing_extensions import override -from mcproto.types.entity.metadata import _DefaultEntityMetadataEntry, _ProxyEntityMetadataEntry +from mcproto.types.entity.metadata import DefaultEntityMetadataEntryDeclaration, ProxyEntityMetadataEntryDeclaration if sys.version_info >= (3, 11): from tomllib import load as toml_parse @@ -123,9 +123,7 @@ def autodoc_skip_member(app: Any, what: str, name: str, obj: Any, skip: bool, options: Any) -> bool: """Skip EntityMetadataEntry class fields as they are already documented in the docstring.""" - if isinstance(obj, type) and ( - issubclass(obj, _ProxyEntityMetadataEntry) or issubclass(obj, _DefaultEntityMetadataEntry) - ): + if isinstance(obj, (DefaultEntityMetadataEntryDeclaration, ProxyEntityMetadataEntryDeclaration)): return True return skip diff --git a/mcproto/types/entity/generated.py b/mcproto/types/entity/generated.py index 4cafcebc..2d4fb2f1 100644 --- a/mcproto/types/entity/generated.py +++ b/mcproto/types/entity/generated.py @@ -156,12 +156,13 @@ "ZombifiedPiglinEM", ] -from typing import Any, ClassVar +from typing import ClassVar from mcproto.types.chat import TextComponent from mcproto.types.entity.metadata import EntityMetadata, entry, proxy from mcproto.types.entity.metadata_types import ( BlockStateEME, + BoolMasked, BooleanEME, ByteEME, CatVariantEME, @@ -169,7 +170,6 @@ DragonPhaseEME, FloatEME, FrogVariantEME, - Masked, NBTagEME, OptBlockStateEME, OptPositionEME, @@ -191,6 +191,7 @@ VillagerDataEME, ) from mcproto.types.nbt import EndNBT, NBTag +from mcproto.types.particle_data import ParticleData from mcproto.types.quaternion import Quaternion from mcproto.types.slot import Slot, SlotData from mcproto.types.uuid import UUID @@ -235,14 +236,14 @@ class EntityEM(EntityMetadata): """ _entity_flags: ClassVar[int] = entry(ByteEME, 0) - is_on_fire: bool = proxy(_entity_flags, Masked, mask=0x1) - is_crouching: bool = proxy(_entity_flags, Masked, mask=0x2) - is_riding: bool = proxy(_entity_flags, Masked, mask=0x4) - is_sprinting: bool = proxy(_entity_flags, Masked, mask=0x8) - is_swimming: bool = proxy(_entity_flags, Masked, mask=0x10) - is_invisible: bool = proxy(_entity_flags, Masked, mask=0x20) - is_glowing: bool = proxy(_entity_flags, Masked, mask=0x40) - is_flying: bool = proxy(_entity_flags, Masked, mask=0x80) + is_on_fire: bool = proxy(_entity_flags, BoolMasked, mask=0x1) + is_crouching: bool = proxy(_entity_flags, BoolMasked, mask=0x2) + is_riding: bool = proxy(_entity_flags, BoolMasked, mask=0x4) + is_sprinting: bool = proxy(_entity_flags, BoolMasked, mask=0x8) + is_swimming: bool = proxy(_entity_flags, BoolMasked, mask=0x10) + is_invisible: bool = proxy(_entity_flags, BoolMasked, mask=0x20) + is_glowing: bool = proxy(_entity_flags, BoolMasked, mask=0x40) + is_flying: bool = proxy(_entity_flags, BoolMasked, mask=0x80) air: int = entry(VarIntEME, 300) custom_name: str = entry(StringEME, "") is_custom_name_visible: bool = entry(BooleanEME, False) @@ -705,11 +706,11 @@ class TextDisplayEM(DisplayEM): line_width: int = entry(VarIntEME, 200) background_color: int = entry(VarIntEME, 1073741824) _display_flags: ClassVar[int] = entry(ByteEME, 0) - has_shadow: bool = proxy(_display_flags, Masked, mask=0x1) - is_see_through: bool = proxy(_display_flags, Masked, mask=0x2) - use_default_background: bool = proxy(_display_flags, Masked, mask=0x4) - align_left: bool = proxy(_display_flags, Masked, mask=0x8) - align_right: bool = proxy(_display_flags, Masked, mask=0x10) + has_shadow: bool = proxy(_display_flags, BoolMasked, mask=0x1) + is_see_through: bool = proxy(_display_flags, BoolMasked, mask=0x2) + use_default_background: bool = proxy(_display_flags, BoolMasked, mask=0x4) + align_left: bool = proxy(_display_flags, BoolMasked, mask=0x8) + align_right: bool = proxy(_display_flags, BoolMasked, mask=0x10) __slots__ = () @@ -1057,7 +1058,7 @@ class FallingBlockEM(EntityEM): """Entity that represents a falling block. :param position: The spawn position of the falling block - :type position: tuple[int, int, int], optional, default: (0, 0, 0) + :type position: :class:`Position`, optional, default: :attr:`Position(0, 0, 0)` Inherited from :class:`EntityEM`: @@ -1095,7 +1096,7 @@ class FallingBlockEM(EntityEM): """ - position: tuple[int, int, int] = entry(PositionEME, (0, 0, 0)) + position: Position = entry(PositionEME, Position(0, 0, 0)) __slots__ = () @@ -1109,8 +1110,8 @@ class AreaEffectCloudEM(EntityEM): :type color: int, optional, default: 0 :param single_point_effect: Whether to ignore the radius and show the effect as a single point, not an area. :type single_point_effect: bool, optional, default: False - :param effect: The particle effect of the area effect cloud. - :type effect: tuple[int, Any], optional, default: (0, None) + :param effect: The particle effect of the area effect cloud (default: `minecraft:effect`). + :type effect: :class:`ParticleData`, optional, default: :attr:`ParticleData(15)` Inherited from :class:`EntityEM`: @@ -1151,7 +1152,7 @@ class AreaEffectCloudEM(EntityEM): radius: float = entry(FloatEME, 0.5) color: int = entry(VarIntEME, 0) single_point_effect: bool = entry(BooleanEME, False) - effect: tuple[int, Any] = entry(ParticleEME, (0, None)) + effect: ParticleData = entry(ParticleEME, ParticleData(15)) __slots__ = () @@ -1254,8 +1255,8 @@ class AbstractArrowEM(EntityEM): """ _arrow_flags: ClassVar[int] = entry(ByteEME, 0) - is_critical: bool = proxy(_arrow_flags, Masked, mask=0x1) - is_noclip: bool = proxy(_arrow_flags, Masked, mask=0x2) + is_critical: bool = proxy(_arrow_flags, BoolMasked, mask=0x1) + is_noclip: bool = proxy(_arrow_flags, BoolMasked, mask=0x2) piercing_level: int = entry(ByteEME, 0) __slots__ = () @@ -2175,7 +2176,7 @@ class EndCrystalEM(EntityEM): """Entity that represents an end crystal. :param beam_target: The position of the beam target. - :type beam_target: tuple[int, int, int]|None, optional, default: None + :type beam_target: :class:`Position | None`, optional, default: :attr:`None` :param show_bottom: Whether the bottom of the end crystal is shown. :type show_bottom: bool, optional, default: True @@ -2215,7 +2216,7 @@ class EndCrystalEM(EntityEM): """ - beam_target: tuple[int, int, int] | None = entry(OptPositionEME, None) + beam_target: Position | None = entry(OptPositionEME, None) show_bottom: bool = entry(BooleanEME, True) __slots__ = () @@ -2711,9 +2712,9 @@ class LivingEntityEM(EntityEM): """ _hand_states: ClassVar[int] = entry(ByteEME, 0) - is_hand_active: bool = proxy(_hand_states, Masked, mask=0x1) - active_hand: int = proxy(_hand_states, Masked, mask=0x2) - is_riptide_spin_attack: bool = proxy(_hand_states, Masked, mask=0x4) + is_hand_active: bool = proxy(_hand_states, BoolMasked, mask=0x1) + active_hand: int = proxy(_hand_states, BoolMasked, mask=0x2) + is_riptide_spin_attack: bool = proxy(_hand_states, BoolMasked, mask=0x4) health: float = entry(FloatEME, 1.0) potion_effect_color: int = entry(VarIntEME, 0) is_potion_effect_ambient: bool = entry(BooleanEME, False) @@ -2815,13 +2816,13 @@ class PlayerEM(LivingEntityEM): additional_hearts: float = entry(FloatEME, 0.0) score: int = entry(VarIntEME, 0) _displayed_skin_parts: ClassVar[int] = entry(ByteEME, 0) - cape_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x1) - jacket_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x2) - left_sleeve_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x4) - right_sleeve_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x8) - left_pants_leg_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x10) - right_pants_leg_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x20) - hat_enabled: bool = proxy(_displayed_skin_parts, Masked, mask=0x40) + cape_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x1) + jacket_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x2) + left_sleeve_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x4) + right_sleeve_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x8) + left_pants_leg_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x10) + right_pants_leg_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x20) + hat_enabled: bool = proxy(_displayed_skin_parts, BoolMasked, mask=0x40) main_hand: int = entry(ByteEME, 1) left_shoulder_entity_data: NBTag = entry(NBTagEME, EndNBT()) right_shoulder_entity_data: NBTag = entry(NBTagEME, EndNBT()) @@ -2913,10 +2914,10 @@ class ArmorStandEM(LivingEntityEM): """ _armorstand_flags: ClassVar[int] = entry(ByteEME, 0) - is_small: bool = proxy(_armorstand_flags, Masked, mask=0x1) - has_arms: bool = proxy(_armorstand_flags, Masked, mask=0x4) - has_no_base_plate: bool = proxy(_armorstand_flags, Masked, mask=0x8) - is_marker: bool = proxy(_armorstand_flags, Masked, mask=0x10) + is_small: bool = proxy(_armorstand_flags, BoolMasked, mask=0x1) + has_arms: bool = proxy(_armorstand_flags, BoolMasked, mask=0x4) + has_no_base_plate: bool = proxy(_armorstand_flags, BoolMasked, mask=0x8) + is_marker: bool = proxy(_armorstand_flags, BoolMasked, mask=0x10) head_rotation: Vec3 = entry(RotationEME, Vec3(0.0, 0.0, 0.0)) body_rotation: Vec3 = entry(RotationEME, Vec3(0.0, 0.0, 0.0)) left_arm_rotation: Vec3 = entry(RotationEME, Vec3(-10.0, 0.0, -10.0)) @@ -2997,9 +2998,9 @@ class MobEM(LivingEntityEM): """ _mob_flags: ClassVar[int] = entry(ByteEME, 0) - no_ai: bool = proxy(_mob_flags, Masked, mask=0x1) - is_left_handed: bool = proxy(_mob_flags, Masked, mask=0x2) - is_aggressive: bool = proxy(_mob_flags, Masked, mask=0x4) + no_ai: bool = proxy(_mob_flags, BoolMasked, mask=0x1) + is_left_handed: bool = proxy(_mob_flags, BoolMasked, mask=0x2) + is_aggressive: bool = proxy(_mob_flags, BoolMasked, mask=0x4) __slots__ = () @@ -3155,7 +3156,7 @@ class BatEM(AmbientCreatureEM): """ _bat_flags: ClassVar[int] = entry(ByteEME, 0) - is_hanging: bool = proxy(_bat_flags, Masked, mask=0x1) + is_hanging: bool = proxy(_bat_flags, BoolMasked, mask=0x1) __slots__ = () @@ -3392,7 +3393,7 @@ class DolphinEM(WaterAnimalEM): """Entity that represents a dolphin. :param treasure_position: The position of the dolphin's treasure. - :type treasure_position: tuple[int, int, int], optional, default: (0, 0, 0) + :type treasure_position: :class:`Position`, optional, default: :attr:`Position(0, 0, 0)` :param has_fish: Whether the dolphin has fish. :type has_fish: bool, optional, default: False :param moisture_level: The moisture level of the dolphin. @@ -3470,7 +3471,7 @@ class DolphinEM(WaterAnimalEM): """ - treasure_position: tuple[int, int, int] = entry(PositionEME, (0, 0, 0)) + treasure_position: Position = entry(PositionEME, Position(0, 0, 0)) has_fish: bool = entry(BooleanEME, False) moisture_level: int = entry(VarIntEME, 2400) @@ -4332,12 +4333,12 @@ class AbstractHorseEM(AnimalEM): """ _horse_flags: ClassVar[int] = entry(ByteEME, 0) - is_tame: bool = proxy(_horse_flags, Masked, mask=0x2) - is_saddled: bool = proxy(_horse_flags, Masked, mask=0x4) - has_bred: bool = proxy(_horse_flags, Masked, mask=0x8) - is_eating: bool = proxy(_horse_flags, Masked, mask=0x10) - is_rearing: bool = proxy(_horse_flags, Masked, mask=0x20) - is_mouth_open: bool = proxy(_horse_flags, Masked, mask=0x40) + is_tame: bool = proxy(_horse_flags, BoolMasked, mask=0x2) + is_saddled: bool = proxy(_horse_flags, BoolMasked, mask=0x4) + has_bred: bool = proxy(_horse_flags, BoolMasked, mask=0x8) + is_eating: bool = proxy(_horse_flags, BoolMasked, mask=0x10) + is_rearing: bool = proxy(_horse_flags, BoolMasked, mask=0x20) + is_mouth_open: bool = proxy(_horse_flags, BoolMasked, mask=0x40) __slots__ = () @@ -5470,9 +5471,9 @@ class BeeEM(AnimalEM): """ _bee_flags: ClassVar[int] = entry(ByteEME, 0) - is_angry: bool = proxy(_bee_flags, Masked, mask=0x2) - has_stung: bool = proxy(_bee_flags, Masked, mask=0x4) - has_nectar: bool = proxy(_bee_flags, Masked, mask=0x8) + is_angry: bool = proxy(_bee_flags, BoolMasked, mask=0x2) + has_stung: bool = proxy(_bee_flags, BoolMasked, mask=0x4) + has_nectar: bool = proxy(_bee_flags, BoolMasked, mask=0x8) anger_time: int = entry(VarIntEME, 0) __slots__ = () @@ -5581,13 +5582,13 @@ class FoxEM(AnimalEM): fox_type: int = entry(VarIntEME, 0) _fox_flags: ClassVar[int] = entry(ByteEME, 0) - is_sitting: bool = proxy(_fox_flags, Masked, mask=0x1) - is_fox_crouching: bool = proxy(_fox_flags, Masked, mask=0x4) - is_interested: bool = proxy(_fox_flags, Masked, mask=0x8) - is_pouncing: bool = proxy(_fox_flags, Masked, mask=0x10) - is_sleeping: bool = proxy(_fox_flags, Masked, mask=0x20) - is_faceplanted: bool = proxy(_fox_flags, Masked, mask=0x40) - is_defending: bool = proxy(_fox_flags, Masked, mask=0x80) + is_sitting: bool = proxy(_fox_flags, BoolMasked, mask=0x1) + is_fox_crouching: bool = proxy(_fox_flags, BoolMasked, mask=0x4) + is_interested: bool = proxy(_fox_flags, BoolMasked, mask=0x8) + is_pouncing: bool = proxy(_fox_flags, BoolMasked, mask=0x10) + is_sleeping: bool = proxy(_fox_flags, BoolMasked, mask=0x20) + is_faceplanted: bool = proxy(_fox_flags, BoolMasked, mask=0x40) + is_defending: bool = proxy(_fox_flags, BoolMasked, mask=0x80) trusted_uuid: UUID | None = entry(OptUUIDEME, None) trusted_uuid_2: UUID | None = entry(OptUUIDEME, None) @@ -5600,7 +5601,7 @@ class FrogEM(AnimalEM): :param variant: The variant of the frog. :type variant: int, optional, default: 0 :param tongue_target: The target of the frog's tongue. - :type tongue_target: int, optional, default: 0 + :type tongue_target: int | None, optional, default: 0 Inherited from :class:`AnimalEM`: @@ -5680,7 +5681,7 @@ class FrogEM(AnimalEM): """ variant: int = entry(FrogVariantEME, 0) - tongue_target: int = entry(OptVarIntEME, 0) + tongue_target: int | None = entry(OptVarIntEME, 0) __slots__ = () @@ -5878,10 +5879,10 @@ class PandaEM(AnimalEM): main_gene: int = entry(ByteEME, 0) hidden_gene: int = entry(ByteEME, 0) _panda_flags: ClassVar[int] = entry(ByteEME, 0) - is_sneezing: bool = proxy(_panda_flags, Masked, mask=0x2) - is_rolling: bool = proxy(_panda_flags, Masked, mask=0x4) - is_sitting: bool = proxy(_panda_flags, Masked, mask=0x8) - is_on_back: bool = proxy(_panda_flags, Masked, mask=0x10) + is_sneezing: bool = proxy(_panda_flags, BoolMasked, mask=0x2) + is_rolling: bool = proxy(_panda_flags, BoolMasked, mask=0x4) + is_sitting: bool = proxy(_panda_flags, BoolMasked, mask=0x8) + is_on_back: bool = proxy(_panda_flags, BoolMasked, mask=0x10) __slots__ = () @@ -6069,13 +6070,13 @@ class TurtleEM(AnimalEM): """Entity that represents a turtle. :param home_pos: The home position of the turtle. - :type home_pos: tuple[int, int, int], optional, default: (0, 0, 0) + :type home_pos: :class:`Position`, optional, default: :attr:`Position(0, 0, 0)` :param has_egg: Whether the turtle has an egg. :type has_egg: bool, optional, default: False :param is_laying_egg: Whether the turtle is laying an egg. :type is_laying_egg: bool, optional, default: False :param travel_pos: The travel position of the turtle. - :type travel_pos: tuple[int, int, int], optional, default: (0, 0, 0) + :type travel_pos: :class:`Position`, optional, default: :attr:`Position(0, 0, 0)` :param is_going_home: Whether the turtle is going home. :type is_going_home: bool, optional, default: False :param is_traveling: Whether the turtle is traveling. @@ -6158,10 +6159,10 @@ class TurtleEM(AnimalEM): """ - home_pos: tuple[int, int, int] = entry(PositionEME, (0, 0, 0)) + home_pos: Position = entry(PositionEME, Position(0, 0, 0)) has_egg: bool = entry(BooleanEME, False) is_laying_egg: bool = entry(BooleanEME, False) - travel_pos: tuple[int, int, int] = entry(PositionEME, (0, 0, 0)) + travel_pos: Position = entry(PositionEME, Position(0, 0, 0)) is_going_home: bool = entry(BooleanEME, False) is_traveling: bool = entry(BooleanEME, False) @@ -6686,8 +6687,8 @@ class SheepEM(AnimalEM): """ _sheep_data: ClassVar[int] = entry(ByteEME, 0) - color_id: int = proxy(_sheep_data, Masked, mask=0xF) - is_sheared: bool = proxy(_sheep_data, Masked, mask=0x10) + color_id: int = proxy(_sheep_data, BoolMasked, mask=0xF) + is_sheared: bool = proxy(_sheep_data, BoolMasked, mask=0x10) __slots__ = () @@ -6969,8 +6970,8 @@ class TameableAnimalEM(AnimalEM): """ _tameable_data: ClassVar[int] = entry(ByteEME, 0) - is_sitting: bool = proxy(_tameable_data, Masked, mask=0x1) - is_tamed: bool = proxy(_tameable_data, Masked, mask=0x4) + is_sitting: bool = proxy(_tameable_data, BoolMasked, mask=0x1) + is_tamed: bool = proxy(_tameable_data, BoolMasked, mask=0x4) owner_uuid: UUID | None = entry(OptUUIDEME, None) __slots__ = () @@ -7700,7 +7701,7 @@ class IronGolemEM(AbstractGolemEM): """ _iron_golem_flags: ClassVar[int] = entry(ByteEME, 0) - is_player_created: bool = proxy(_iron_golem_flags, Masked, mask=0x1) + is_player_created: bool = proxy(_iron_golem_flags, BoolMasked, mask=0x1) __slots__ = () @@ -7784,7 +7785,7 @@ class SnowGolemEM(AbstractGolemEM): """ _snow_golem_flags: ClassVar[int] = entry(ByteEME, 16) - has_pumpkin: bool = proxy(_snow_golem_flags, Masked, mask=0x10) + has_pumpkin: bool = proxy(_snow_golem_flags, BoolMasked, mask=0x10) __slots__ = () @@ -8293,7 +8294,7 @@ class BlazeEM(MonsterEM): """ _blaze_flags: ClassVar[int] = entry(ByteEME, 0) - is_blaze_on_fire: bool = proxy(_blaze_flags, Masked, mask=0x1) + is_blaze_on_fire: bool = proxy(_blaze_flags, BoolMasked, mask=0x1) __slots__ = () @@ -9698,7 +9699,7 @@ class VexEM(MonsterEM): """ _vex_flags: ClassVar[int] = entry(ByteEME, 0) - is_attacking: bool = proxy(_vex_flags, Masked, mask=0x1) + is_attacking: bool = proxy(_vex_flags, BoolMasked, mask=0x1) __slots__ = () @@ -10100,7 +10101,7 @@ class SpiderEM(MonsterEM): """ _spider_flags: ClassVar[int] = entry(ByteEME, 0) - is_climbing: bool = proxy(_spider_flags, Masked, mask=0x1) + is_climbing: bool = proxy(_spider_flags, BoolMasked, mask=0x1) __slots__ = () @@ -10812,7 +10813,7 @@ class EndermanEM(MonsterEM): """Entity representing an enderman. :param carried_block: The block the enderman is carrying. - :type carried_block: str, optional, default: "Absent" + :type carried_block: int | None, optional, default: None :param is_screaming: Indicates if the enderman is screaming. :type is_screaming: bool, optional, default: False :param is_staring: Indicates if the enderman is staring. @@ -10890,7 +10891,7 @@ class EndermanEM(MonsterEM): """ - carried_block: str = entry(OptBlockStateEME, "Absent") + carried_block: int | None = entry(OptBlockStateEME, None) is_screaming: bool = entry(BooleanEME, False) is_staring: bool = entry(BooleanEME, False) diff --git a/mcproto/types/entity/metadata.py b/mcproto/types/entity/metadata.py index d8743b55..78a1faa0 100644 --- a/mcproto/types/entity/metadata.py +++ b/mcproto/types/entity/metadata.py @@ -1,16 +1,20 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import Any, ClassVar, Literal, TypeVar, cast, final, overload +from typing import Any, ClassVar, Generic, Literal, TypeVar, cast, final, overload +from attrs import define from typing_extensions import dataclass_transform, override from mcproto.buffer import Buffer from mcproto.protocol import StructFormat from mcproto.types.abc import MCType +T = TypeVar("T") +T_2 = TypeVar("T_2") -class EntityMetadataEntry(MCType): + +class EntityMetadataEntry(MCType, Generic[T]): """Represents an entry in an entity metadata list. :param index: The index of the entry. @@ -24,26 +28,31 @@ class EntityMetadataEntry(MCType): ENTRY_TYPE: ClassVar[int] = None # type: ignore index: int - value: Any + value: T __slots__ = ("index", "value", "hidden", "default", "name") def __init__( - self, index: int, value: Any = None, default: Any = None, hidden: bool = False, name: str | None = None + self, + index: int, + value: T | None = None, + default: T | None = None, + hidden: bool = False, + name: str | None = None, ): self.index = index - self.value = default if value is None else value + self.value = value if value is not None else default # type: ignore self.hidden = hidden self.default = default self.name = name # for debugging purposes self.validate() - def setter(self, value: Any) -> None: + def setter(self, value: T) -> None: """Set the value of the entry.""" self.value = value - def getter(self) -> Any: + def getter(self) -> T: """Get the value of the entry.""" return self.value @@ -117,7 +126,7 @@ def read_value(cls, buf: Buffer) -> Any: @override @classmethod @final - def deserialize(cls, buf: Buffer) -> EntityMetadataEntry: + def deserialize(cls, buf: Buffer) -> EntityMetadataEntry[T]: """Deserialize the entity metadata entry. :param buf: The buffer to read from. @@ -128,7 +137,7 @@ def deserialize(cls, buf: Buffer) -> EntityMetadataEntry: return cls(index=index, value=value) -class ProxyEntityMetadataEntry(MCType): +class ProxyEntityMetadataEntry(MCType, Generic[T, T_2]): """A proxy entity metadata entry which is used to designate a part of a metadata entry in a human-readable format. For example, this can be used to represent a certain mask for a ByteEME entry. @@ -136,11 +145,11 @@ class ProxyEntityMetadataEntry(MCType): ENTRY_TYPE: ClassVar[int] = None # type: ignore - bound_entry: EntityMetadataEntry + bound_entry: EntityMetadataEntry[T_2] __slots__ = ("bound_entry",) - def __init__(self, bound_entry: EntityMetadataEntry, *args: Any, **kwargs: Any): + def __init__(self, bound_entry: EntityMetadataEntry[T_2], *args: Any, **kwargs: Any): self.bound_entry = bound_entry self.validate() @@ -150,55 +159,43 @@ def serialize_to(self, buf: Buffer) -> None: @override @classmethod - def deserialize(cls, buf: Buffer) -> ProxyEntityMetadataEntry: + def deserialize(cls, buf: Buffer) -> ProxyEntityMetadataEntry[T, T_2]: raise NotImplementedError("Proxy entity metadata entries cannot be deserialized.") @abstractmethod - def setter(self, value: Any) -> None: + def setter(self, value: T) -> None: """Set the value of the entry by modifying the bound entry.""" @abstractmethod - def getter(self) -> Any: + def getter(self) -> T: """Get the value of the entry by reading the bound entry.""" def validate(self) -> None: """Validate that the proxy metadata entry has valid values.""" -EntityDefault = TypeVar("EntityDefault") - +@define +class DefaultEntityMetadataEntryDeclaration(Generic[T]): + """Class used to pass the default metadata to the entity metadata.""" -class _DefaultEntityMetadataEntry: m_default: Any - m_type: type[EntityMetadataEntry] + m_type: type[EntityMetadataEntry[T]] m_index: int - __slots__ = ("m_default", "m_type") - -def entry(entry_type: type[EntityMetadataEntry], value: EntityDefault) -> EntityDefault: +def entry(entry_type: type[EntityMetadataEntry[T]], value: T) -> T: """Create a entity metadata entry with the given value. :param entry_type: The type of the entry. :param default: The default value of the entry. :return: The default entity metadata entry. """ - - class DefaultEntityMetadataEntry(_DefaultEntityMetadataEntry): - m_default = value - m_type = entry_type - m_index = -1 - - __slots__ = () - # This will be taken care of by EntityMetadata - return DefaultEntityMetadataEntry # type: ignore - + return DefaultEntityMetadataEntryDeclaration(m_default=value, m_type=entry_type, m_index=-1) # type: ignore -ProxyInitializer = TypeVar("ProxyInitializer") - -class _ProxyEntityMetadataEntry: +@define +class ProxyEntityMetadataEntryDeclaration(Generic[T, T_2]): """Class used to pass the bound entry and additional arguments to the proxy entity metadata entry. Explanation: @@ -212,21 +209,19 @@ class _ProxyEntityMetadataEntry: This is set by the EntityMetadataCreator. """ - m_bound_entry: EntityMetadataEntry + m_bound_entry: EntityMetadataEntry[T_2] m_args: tuple[Any] m_kwargs: dict[str, Any] - m_type: type[ProxyEntityMetadataEntry] + m_type: type[ProxyEntityMetadataEntry[T, T_2]] m_bound_index: int - __slots__ = ("m_bound_entry", "m_args", "m_kwargs", "m_type", "m_bound_index") - def proxy( - bound_entry: EntityDefault, # type: ignore # Used only once but I prefer to keep the type hint - proxy: type[ProxyEntityMetadataEntry], + bound_entry: T_2, # This will in fact be an EntityMetadataEntry, but treated as a T_2 during type checking + proxy: type[ProxyEntityMetadataEntry[T, T_2]], *args: Any, **kwargs: Any, -) -> ProxyInitializer: # type: ignore +) -> T: """Initialize the proxy entity metadata entry with the given bound entry and additional arguments. :param bound_entry: The bound entry. @@ -236,22 +231,16 @@ def proxy( :return: The proxy entity metadata entry initializer. """ - if not isinstance(bound_entry, type): + if not isinstance(bound_entry, DefaultEntityMetadataEntryDeclaration): raise TypeError("The bound entry must be an entity metadata entry type.") - if not issubclass(bound_entry, _DefaultEntityMetadataEntry): - raise TypeError("The bound entry must be an entity metadata entry.") - - class ProxyEntityMetadataEntry(_ProxyEntityMetadataEntry): - m_bound_entry = bound_entry # type: ignore # This will be taken care of by EntityMetadata - m_args = args - m_kwargs = kwargs - m_type = proxy - - m_bound_index = -1 - __slots__ = () - - return ProxyEntityMetadataEntry # type: ignore + return ProxyEntityMetadataEntryDeclaration( # type: ignore + m_bound_entry=bound_entry, # type: ignore + m_args=args, + m_kwargs=kwargs, + m_type=proxy, + m_bound_index=-1, + ) @dataclass_transform(kw_only_default=True) # field_specifiers=(entry, proxy)) @@ -265,10 +254,16 @@ class EntityMetadataCreator(ABCMeta): ``` """ - m_defaults: ClassVar[dict[str, type[_DefaultEntityMetadataEntry | _ProxyEntityMetadataEntry]]] + m_defaults: ClassVar[ + dict[ + str, + DefaultEntityMetadataEntryDeclaration[Any] + | ProxyEntityMetadataEntryDeclaration[Any, EntityMetadataEntry[Any]], + ] + ] m_index: ClassVar[dict[int, str]] m_metadata: ClassVar[ - dict[str, EntityMetadataEntry | ProxyEntityMetadataEntry] + dict[str, EntityMetadataEntry[Any] | ProxyEntityMetadataEntry[Any, EntityMetadataEntry[Any]]] ] # This is not an actual classvar, but I # Do not want it to appear in the __init__ signature @@ -309,7 +304,8 @@ def setup_class(cls: type[EntityMetadata]) -> None: if default is None: raise ValueError(f"Default value for {name} is not set. Use the entry() or proxy() functions.") # Check if we have a default entry - if isinstance(default, type) and issubclass(default, _DefaultEntityMetadataEntry): + if isinstance(default, DefaultEntityMetadataEntryDeclaration): + default = cast(DefaultEntityMetadataEntryDeclaration[Any], default) # Set the index of the entry default.m_index = current_index @@ -321,7 +317,8 @@ def setup_class(cls: type[EntityMetadata]) -> None: # Increment the index current_index += 1 - elif isinstance(default, type) and issubclass(default, _ProxyEntityMetadataEntry): + elif isinstance(default, ProxyEntityMetadataEntryDeclaration): + default = cast(ProxyEntityMetadataEntryDeclaration[Any, EntityMetadataEntry[Any]], default) # Find the bound entry if id(default.m_bound_entry) not in bound_index: raise ValueError(f"Bound entry for {name} is not set.") @@ -364,14 +361,16 @@ def __init__(self, *args: None, **kwargs: Any) -> None: raise ValueError( "EntityMetadata does not accept positional arguments. Specify all metadata entries by name." ) - self.m_metadata: dict[str, EntityMetadataEntry | ProxyEntityMetadataEntry] = {} + self.m_metadata: dict[ + str, EntityMetadataEntry[Any] | ProxyEntityMetadataEntry[Any, EntityMetadataEntry[Any]] + ] = {} for name, default in self.m_defaults.items(): - if issubclass(default, _DefaultEntityMetadataEntry): + if isinstance(default, DefaultEntityMetadataEntryDeclaration): self.m_metadata[name] = default.m_type(index=default.m_index, default=default.m_default, name=name) - elif issubclass(default, _ProxyEntityMetadataEntry): # type: ignore # We want to check anyways + elif isinstance(default, ProxyEntityMetadataEntryDeclaration): # type: ignore # Check anyway # Bound entry bound_name = self.m_index[default.m_bound_index] - bound_entry = cast(EntityMetadataEntry, self.m_metadata[bound_name]) + bound_entry = cast(EntityMetadataEntry[Any], self.m_metadata[bound_name]) self.m_metadata[name] = default.m_type(bound_entry, *default.m_args, **default.m_kwargs) else: # pragma: no cover raise ValueError(f"Invalid default value for {name}. Use the entry() or proxy() functions.") # noqa: TRY004 @@ -383,6 +382,7 @@ def __init__(self, *args: None, **kwargs: Any) -> None: @override def __setattr__(self, name: str, value: Any) -> None: + """Any is used here because the type will be discovered statically by other means (dataclass_transform).""" if name != "m_metadata" and hasattr(self, "m_metadata") and name in self.m_metadata: self.m_metadata[name].setter(value) else: @@ -390,6 +390,10 @@ def __setattr__(self, name: str, value: Any) -> None: @override def __getattribute__(self, name: str) -> Any: + """Get the value of the metadata entry. + + .. seealso:: :meth:`__setattr__` + """ if name != "m_metadata" and hasattr(self, "m_metadata") and name in self.m_metadata: return self.m_metadata[name].getter() return super().__getattribute__(name) diff --git a/mcproto/types/entity/metadata_types.py b/mcproto/types/entity/metadata_types.py index 71388cbb..9b2dc4e4 100644 --- a/mcproto/types/entity/metadata_types.py +++ b/mcproto/types/entity/metadata_types.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import IntEnum -from typing import Any, ClassVar +from typing import Any, ClassVar, Generic, TypeVar, Union, cast from typing_extensions import override @@ -12,20 +12,19 @@ from mcproto.types.entity.metadata import EntityMetadataEntry, ProxyEntityMetadataEntry from mcproto.types.identifier import Identifier from mcproto.types.nbt import NBTag +from mcproto.types.particle_data import ParticleData from mcproto.types.quaternion import Quaternion from mcproto.types.slot import Slot from mcproto.types.uuid import UUID from mcproto.types.vec3 import Position, Vec3 -class ByteEME(EntityMetadataEntry): +class ByteEME(EntityMetadataEntry[int]): """Represents a byte entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 0 __slots__ = () - value: int - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -43,12 +42,39 @@ def validate(self) -> None: super().validate() -class Masked(ProxyEntityMetadataEntry): +class BoolMasked(ProxyEntityMetadataEntry[bool, int]): + """Represents a masked entry in an entity metadata list.""" + + __slots__ = ("mask", "_mask_shift") + + def __init__(self, bound_entry: EntityMetadataEntry[int], *, mask: int): + self.mask = mask + super().__init__(bound_entry) + self._mask_shift = self.mask.bit_length() - 1 + + @override + def setter(self, value: int) -> None: + bound = self.bound_entry.getter() + bound = (bound & ~self.mask) | (int(value) << self._mask_shift) + self.bound_entry.setter(bound) + + @override + def getter(self) -> bool: + return bool(self.bound_entry.getter() & self.mask) + + @override + def validate(self) -> None: + # Ensure that there is only one bit set in the mask + if self.mask & (self.mask - 1): + raise ValueError(f"Mask {self.mask} is not a power of 2.") + + +class IntMasked(ProxyEntityMetadataEntry[int, int]): """Represents a masked entry in an entity metadata list.""" __slots__ = ("mask",) - def __init__(self, bound_entry: EntityMetadataEntry, *, mask: int): + def __init__(self, bound_entry: EntityMetadataEntry[int], *, mask: int): super().__init__(bound_entry) self.mask = mask @@ -78,14 +104,12 @@ def getter(self) -> int: return value -class VarIntEME(EntityMetadataEntry): +class VarIntEME(EntityMetadataEntry[int]): """Represents a varint entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 1 __slots__ = () - value: int - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -110,15 +134,16 @@ class _RegistryVarIntEME(VarIntEME): __slots__ = () -class _VarIntEnumEME(EntityMetadataEntry): +IntEnum_T = TypeVar("IntEnum_T", bound=IntEnum) + + +class _VarIntEnumEME(EntityMetadataEntry[IntEnum_T], Generic[IntEnum_T]): """Represents a varint entry that refereces an enum in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = -1 __slots__ = () ENUM: ClassVar[type[IntEnum]] = None # type: ignore - value: IntEnum - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -126,13 +151,13 @@ def serialize_to(self, buf: Buffer) -> None: @override @classmethod - def read_value(cls, buf: Buffer) -> IntEnum: + def read_value(cls, buf: Buffer) -> IntEnum_T: value = buf.read_varint() try: value_enum = cls.ENUM(value) except ValueError as e: raise ValueError(f"Invalid value {value} for enum {cls.ENUM.__name__}.") from e - return value_enum + return cast(IntEnum_T, value_enum) @override def validate(self) -> None: @@ -143,14 +168,12 @@ def validate(self) -> None: raise ValueError(f"Invalid value {self.value} for enum {self.ENUM.__name__}.") from e -class VarLongEME(EntityMetadataEntry): +class VarLongEME(EntityMetadataEntry[int]): """Represents a varlong entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 2 __slots__ = () - value: int - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -168,14 +191,12 @@ def validate(self) -> None: super().validate() -class FloatEME(EntityMetadataEntry): +class FloatEME(EntityMetadataEntry[float]): """Represents a float entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 3 __slots__ = () - value: float - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -187,14 +208,12 @@ def read_value(cls, buf: Buffer) -> float: return buf.read_value(StructFormat.FLOAT) -class StringEME(EntityMetadataEntry): +class StringEME(EntityMetadataEntry[str]): """Represents a string entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 4 __slots__ = () - value: str - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -212,14 +231,12 @@ def validate(self) -> None: super().validate() -class TextComponentEME(EntityMetadataEntry): +class TextComponentEME(EntityMetadataEntry[TextComponent]): """Represents a text component entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 5 __slots__ = () - value: TextComponent - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -231,14 +248,12 @@ def read_value(cls, buf: Buffer) -> TextComponent: return TextComponent.deserialize(buf) -class OptTextComponentEME(EntityMetadataEntry): +class OptTextComponentEME(EntityMetadataEntry[Union[TextComponent, None]]): """Represents an optional text component entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 6 __slots__ = () - value: TextComponent | None - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -256,14 +271,12 @@ def read_value(cls, buf: Buffer) -> TextComponent | None: return None -class SlotEME(EntityMetadataEntry): +class SlotEME(EntityMetadataEntry[Slot]): """Represents a slot entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 7 __slots__ = () - value: Slot - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -275,14 +288,12 @@ def read_value(cls, buf: Buffer) -> Slot: return Slot.deserialize(buf) -class BooleanEME(EntityMetadataEntry): +class BooleanEME(EntityMetadataEntry[bool]): """Represents a boolean entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 8 __slots__ = () - value: bool - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -294,14 +305,12 @@ def read_value(cls, buf: Buffer) -> bool: return bool(buf.read_value(StructFormat.BYTE)) -class RotationEME(EntityMetadataEntry): +class RotationEME(EntityMetadataEntry[Vec3]): """Represents a rotation entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 9 __slots__ = () - value: Vec3 - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -313,14 +322,12 @@ def read_value(cls, buf: Buffer) -> Vec3: return Vec3.deserialize(buf) -class PositionEME(EntityMetadataEntry): +class PositionEME(EntityMetadataEntry[Position]): """Represents a position entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 10 __slots__ = () - value: Position - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -332,14 +339,12 @@ def read_value(cls, buf: Buffer) -> Position: return Position.deserialize(buf) -class OptPositionEME(EntityMetadataEntry): +class OptPositionEME(EntityMetadataEntry[Union[Position, None]]): """Represents an optional position entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 11 __slots__ = () - value: Position | None - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -357,14 +362,12 @@ def read_value(cls, buf: Buffer) -> Position | None: return None -class DirectionEME(EntityMetadataEntry): +class DirectionEME(EntityMetadataEntry[Direction]): """Represents a direction in the world.""" ENTRY_TYPE: ClassVar[int] = 12 __slots__ = () - value: Direction - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -376,14 +379,12 @@ def read_value(cls, buf: Buffer) -> Direction: return Direction(buf.read_value(StructFormat.BYTE)) -class OptUUIDEME(EntityMetadataEntry): +class OptUUIDEME(EntityMetadataEntry[Union[UUID, None]]): """Represents an optional UUID entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 13 __slots__ = () - value: UUID | None - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -408,14 +409,12 @@ class BlockStateEME(VarIntEME): __slots__ = () -class OptBlockStateEME(EntityMetadataEntry): +class OptBlockStateEME(EntityMetadataEntry[Union[int, None]]): """Represents an optional block state in the world.""" ENTRY_TYPE: ClassVar[int] = 15 __slots__ = () - value: int | None - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -433,14 +432,12 @@ def read_value(cls, buf: Buffer) -> int | None: return value -class NBTagEME(EntityMetadataEntry): +class NBTagEME(EntityMetadataEntry[NBTag]): """Represents an NBT entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 16 __slots__ = () - value: NBTag - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -452,28 +449,24 @@ def read_value(cls, buf: Buffer) -> NBTag: return NBTag.deserialize(buf, with_name=False) -class ParticleEME(EntityMetadataEntry): +class ParticleEME(EntityMetadataEntry[ParticleData]): """Represents a particle entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 17 __slots__ = () - value: tuple[int, Any] - @override def serialize_to(self, buf: Buffer) -> None: - self._write_header(buf) # pragma: no cover - buf.write_varint(self.value[0]) # pragma: no cover - raise NotImplementedError("The rest of the particle data is not implemented yet.") # pragma: no cover + self._write_header(buf) + self.value.serialize_to(buf, with_id=True) @override @classmethod - def read_value(cls, buf: Buffer) -> tuple[int, Any]: - value = buf.read_varint() # pragma: no cover # noqa: F841 - raise NotImplementedError("The rest of the particle data is not implemented yet.") + def read_value(cls, buf: Buffer) -> ParticleData: + return ParticleData.deserialize(buf, particle_id=None) # Read the particle ID from the buffer -class VillagerDataEME(EntityMetadataEntry): +class VillagerDataEME(EntityMetadataEntry["tuple[int, int, int]"]): """Represents a villager data entry in an entity metadata list. This includes the type, profession, and level of the villager. @@ -482,8 +475,6 @@ class VillagerDataEME(EntityMetadataEntry): ENTRY_TYPE: ClassVar[int] = 18 __slots__ = () - value: tuple[int, int, int] - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -497,14 +488,12 @@ def read_value(cls, buf: Buffer) -> tuple[int, int, int]: return (buf.read_varint(), buf.read_varint(), buf.read_varint()) -class OptVarIntEME(EntityMetadataEntry): +class OptVarIntEME(EntityMetadataEntry[Union[int, None]]): """Represents an optional varint entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 19 __slots__ = () - value: int | None - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -523,7 +512,7 @@ def read_value(cls, buf: Buffer) -> int | None: return value -class PoseEME(_VarIntEnumEME): +class PoseEME(_VarIntEnumEME[Pose]): """Represents a pose entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 20 @@ -545,10 +534,10 @@ class FrogVariantEME(_RegistryVarIntEME): __slots__ = () -class DragonPhaseEME(_VarIntEnumEME): +class DragonPhaseEME(_VarIntEnumEME[DragonPhase]): """Represents a dragon phase entry in an entity metadata list. - This is not a real type, because the type is VarInt, but the values are predefined. + .. note:: This is not a real type, because the type is `VarInt`, but the values are predefined. """ ENTRY_TYPE: ClassVar[int] = 1 @@ -556,7 +545,7 @@ class DragonPhaseEME(_VarIntEnumEME): ENUM: ClassVar[type[IntEnum]] = DragonPhase -class OptGlobalPositionEME(EntityMetadataEntry): +class OptGlobalPositionEME(EntityMetadataEntry[Union["tuple[Identifier, Position]", None]]): """Represents an optional global position entry in an entity metadata list. This includes an identifier for the dimension as well as the position in it. @@ -565,8 +554,6 @@ class OptGlobalPositionEME(EntityMetadataEntry): ENTRY_TYPE: ClassVar[int] = 23 __slots__ = () - value: tuple[Identifier, Position] | None - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -592,7 +579,7 @@ class PaintingVariantEME(_RegistryVarIntEME): __slots__ = () -class SnifferStateEME(_VarIntEnumEME): +class SnifferStateEME(_VarIntEnumEME[SnifferState]): """Represents a sniffer state entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 25 @@ -600,14 +587,12 @@ class SnifferStateEME(_VarIntEnumEME): ENUM: ClassVar[type[IntEnum]] = SnifferState -class Vector3EME(EntityMetadataEntry): +class Vector3EME(EntityMetadataEntry[Vec3]): """Represents a vector3 entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 26 __slots__ = () - value: Vec3 - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) @@ -619,14 +604,12 @@ def read_value(cls, buf: Buffer) -> Vec3: return Vec3.deserialize(buf) -class QuaternionEME(EntityMetadataEntry): +class QuaternionEME(EntityMetadataEntry[Quaternion]): """Represents a quaternion entry in an entity metadata list.""" ENTRY_TYPE: ClassVar[int] = 27 __slots__ = () - value: Quaternion - @override def serialize_to(self, buf: Buffer) -> None: self._write_header(buf) diff --git a/mcproto/types/particle_data.py b/mcproto/types/particle_data.py index 538ccbd7..8f784984 100644 --- a/mcproto/types/particle_data.py +++ b/mcproto/types/particle_data.py @@ -7,6 +7,7 @@ from mcproto.buffer import Buffer from mcproto.protocol.base_io import StructFormat +from mcproto.protocol.utils import from_twos_complement, to_twos_complement from mcproto.types.abc import MCType from mcproto.types.slot import Slot from mcproto.types.vec3 import Position @@ -66,38 +67,46 @@ class ParticleData(MCType): :param delay: The delay before the particle is displayed. :type delay: int, optional + + :param color: The color of the particle. + :type color: int, optional """ # TODO: Use registries for particle IDs WITH_BLOCK_STATE = ( - 2, # minecraft:block - 3, # minecraft:block_marker - 27, # minecraft:falling_dust + 1, # minecraft:block + 2, # minecraft:block_marker + 28, # minecraft:falling_dust + 105, # minecraft:dust_pillar ) WITH_RGB_SCALE = ( - 14, # minecraft:dust + 13, # minecraft:dust ) WITH_RGB_TRANSITION = ( - 15, # minecraft:dust_color_transition + 14, # minecraft:dust_color_transition ) WITH_ROLL = ( - 33, # minecraft:sculk_charge + 35, # minecraft:sculk_charge ) WITH_ITEM = ( - 42, # minecraft:item + 44, # minecraft:item ) WITH_VIBRATION = ( - 43, # minecraft:vibration + 45, # minecraft:vibration ) WITH_DELAY = ( - 96, # minecraft:shriek + 99, # minecraft:shriek + ) + + WITH_COLOR = ( + 20, # minecraft:entity_effect ) particle_id: int @@ -120,6 +129,7 @@ class ParticleData(MCType): entity_eye_height: float | None = None tick: int | None = None delay: int | None = None + color: int | None = None def __attrs_post_init__(self) -> None: # noqa: PLR0912 # I know but what can I do? """Run all the sanity checks for the particle data.""" @@ -158,6 +168,9 @@ def __attrs_post_init__(self) -> None: # noqa: PLR0912 # I know but what can I elif self.particle_id in self.WITH_DELAY: if self.delay is None: raise ValueError("delay is required for this particle ID") + elif self.particle_id in self.WITH_COLOR: + if self.color is None: + raise ValueError("color is required for this particle ID") @override def serialize_to(self, buf: Buffer, with_id: bool = True) -> None: @@ -210,6 +223,9 @@ def serialize_to(self, buf: Buffer, with_id: bool = True) -> None: if self.particle_id in self.WITH_DELAY: self.delay = cast(int, self.delay) buf.write_varint(self.delay) + if self.particle_id in self.WITH_COLOR: + self.color = cast(int, self.color) + buf.write_value(StructFormat.INT, from_twos_complement(self.color, 32)) @classmethod @override @@ -234,6 +250,7 @@ def deserialize(cls, buf: Buffer, particle_id: int | None = None) -> Self: entity_eye_height = None tick = None delay = None + color = None if particle_id in cls.WITH_BLOCK_STATE: block_state = buf.read_varint() @@ -263,6 +280,8 @@ def deserialize(cls, buf: Buffer, particle_id: int | None = None) -> Self: tick = buf.read_varint() if particle_id in cls.WITH_DELAY: delay = buf.read_varint() + if particle_id in cls.WITH_COLOR: + color = to_twos_complement(buf.read_value(StructFormat.INT), 32) return cls( particle_id=particle_id, @@ -285,4 +304,5 @@ def deserialize(cls, buf: Buffer, particle_id: int | None = None) -> Self: entity_eye_height=entity_eye_height, tick=tick, delay=delay, + color=color, ) diff --git a/scripts/entity_generator.py b/scripts/entity_generator.py index 9dc00cc6..6fbcf76b 100644 --- a/scripts/entity_generator.py +++ b/scripts/entity_generator.py @@ -63,6 +63,7 @@ class {name}({base}): from mcproto.types.slot import Slot, SlotData from mcproto.types.chat import TextComponent from mcproto.types.nbt import NBTag, EndNBT +from mcproto.types.particle_data import ParticleData from mcproto.types.vec3 import Position, Vec3 from mcproto.types.quaternion import Quaternion from mcproto.types.uuid import UUID diff --git a/scripts/entity_generator_data.py b/scripts/entity_generator_data.py index cfbc2e15..f2f56691 100644 --- a/scripts/entity_generator_data.py +++ b/scripts/entity_generator_data.py @@ -22,56 +22,56 @@ "input": int, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_on_fire", "input": bool, "description": "Whether the entity is on fire.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_crouching", "input": bool, "description": "Whether the entity is crouching.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_riding", "input": bool, "description": "[UNUSED] Whether the entity is riding something.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_sprinting", "input": bool, "description": "Whether the entity is sprinting.", "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_swimming", "input": bool, "description": "Whether the entity is swimming.", "mask": 0x10, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_invisible", "input": bool, "description": "Whether the entity is invisible.", "mask": 0x20, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_glowing", "input": bool, "description": "Whether the entity has a glowing effect.", "mask": 0x40, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_flying", "input": bool, "description": "Whether the entity is flying.", @@ -358,28 +358,28 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "has_shadow", "input": bool, "description": "Whether the text is displayed with shadow.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_see_through", "input": bool, "description": "Whether the text is displayed as see-through.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "use_default_background", "input": bool, "description": "Whether to use the default background color for the text\n display.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "align_left", "input": bool, "description": "Whether the text is aligned to the left.\n" @@ -387,7 +387,7 @@ "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "align_right", "input": bool, "description": "Whether the text is aligned to the right.\n" @@ -501,8 +501,8 @@ { "type": "Position", "name": "position", - "default": "(0, 0, 0)", - "input": "tuple[int, int, int]", + "default": "Position(0, 0, 0)", + "input": "Position", "description": "The spawn position of the falling block", }, ], @@ -536,9 +536,9 @@ { "type": "Particle", "name": "effect", - "default": "(0, None)", - "input": "tuple[int, Any]", - "description": "The particle effect of the area effect cloud.", + "default": "ParticleData(15)", # minecraft:effect + "input": "ParticleData", + "description": "The particle effect of the area effect cloud (default: `minecraft:effect`).", }, ], }, @@ -576,14 +576,14 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_critical", "input": bool, "description": "Whether the arrow is critical.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_noclip", "input": bool, "description": "Whether the arrow is noclip (used by loyalty tridents when\n returning).", @@ -820,7 +820,7 @@ "type": "OptPosition", "name": "beam_target", "default": "None", - "input": "tuple[int, int, int]|None", + "input": "Position | None", "description": "The position of the beam target.", }, { @@ -976,21 +976,21 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_hand_active", "input": bool, "description": "Whether the hand is active.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "active_hand", "input": int, "description": "Which hand is active (0 = main hand, 1 = offhand).", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_riptide_spin_attack", "input": bool, "description": "Whether the entity is in riptide spin attack.", @@ -1072,49 +1072,49 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "cape_enabled", "input": bool, "description": "Whether the cape is enabled.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "jacket_enabled", "input": bool, "description": "Whether the jacket is enabled.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "left_sleeve_enabled", "input": bool, "description": "Whether the left sleeve is enabled.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "right_sleeve_enabled", "input": bool, "description": "Whether the right sleeve is enabled.", "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "left_pants_leg_enabled", "input": bool, "description": "Whether the left pants leg is enabled.", "mask": 0x10, }, { - "type": "Masked", + "type": "BoolMasked", "name": "right_pants_leg_enabled", "input": bool, "description": "Whether the right pants leg is enabled.\n" " ", "mask": 0x20, }, { - "type": "Masked", + "type": "BoolMasked", "name": "hat_enabled", "input": bool, "description": "Whether the hat is enabled.", @@ -1159,28 +1159,28 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_small", "input": bool, "description": "Whether the armor stand is small.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "has_arms", "input": bool, "description": "Whether the armor stand has arms.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "has_no_base_plate", "input": bool, "description": "Whether the armor stand has no base plate.", "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_marker", "input": bool, "description": "Whether the armor stand is a marker.", @@ -1246,21 +1246,21 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "no_ai", "input": bool, "description": "Whether the mob has AI.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_left_handed", "input": bool, "description": "Whether the mob is left-handed.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_aggressive", "input": bool, "description": "Whether the mob is aggressive.", @@ -1290,7 +1290,7 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_hanging", "input": bool, "description": "Whether the bat is hanging upside down.", @@ -1327,8 +1327,8 @@ { "type": "Position", "name": "treasure_position", - "default": "(0, 0, 0)", - "input": "tuple[int, int, int]", + "default": "Position(0, 0, 0)", + "input": "Position", "description": "The position of the dolphin's treasure.", }, { @@ -1452,42 +1452,42 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_tame", "input": bool, "description": "Whether the horse is tame.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_saddled", "input": bool, "description": "Whether the horse is saddled.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "has_bred", "input": bool, "description": "Whether the horse has bred.", "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_eating", "input": bool, "description": "Whether the horse is eating.", "mask": 0x10, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_rearing", "input": bool, "description": "Whether the horse is rearing (on hind legs).", "mask": 0x20, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_mouth_open", "input": bool, "description": "Whether the horse's mouth is open.", @@ -1639,21 +1639,21 @@ "description": "Flags representing various properties of the bee.", "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_angry", "input": bool, "description": "Whether the bee is angry.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "has_stung", "input": bool, "description": "Whether the bee has stung.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "has_nectar", "input": bool, "description": "Whether the bee has nectar.", @@ -1691,49 +1691,49 @@ "description": "Bit mask representing various states of the fox.", "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_sitting", "input": bool, "description": "Whether the fox is sitting.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_fox_crouching", "input": bool, "description": "Whether the fox is crouching.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_interested", "input": bool, "description": "Whether the fox is interested.", "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_pouncing", "input": bool, "description": "Whether the fox is pouncing.", "mask": 0x10, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_sleeping", "input": bool, "description": "Whether the fox is sleeping.", "mask": 0x20, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_faceplanted", "input": bool, "description": "Whether the fox is faceplanted.", "mask": 0x40, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_defending", "input": bool, "description": "Whether the fox is defending.", @@ -1773,7 +1773,7 @@ "type": "OptVarInt", "name": "tongue_target", "default": 0, - "input": int, + "input": "int | None", "description": "The target of the frog's tongue.", }, ], @@ -1841,28 +1841,28 @@ "description": "Bit mask representing various states of the panda.", "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_sneezing", "input": bool, "description": "Whether the panda is sneezing.", "mask": 0x02, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_rolling", "input": bool, "description": "Whether the panda is rolling.", "mask": 0x04, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_sitting", "input": bool, "description": "Whether the panda is sitting.", "mask": 0x08, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_on_back", "input": bool, "description": "Whether the panda is on its back.", @@ -1915,8 +1915,8 @@ { "type": "Position", "name": "home_pos", - "default": "(0, 0, 0)", - "input": "tuple[int, int, int]", + "default": "Position(0, 0, 0)", + "input": "Position", "description": "The home position of the turtle.", }, { @@ -1936,8 +1936,8 @@ { "type": "Position", "name": "travel_pos", - "default": "(0, 0, 0)", - "input": "tuple[int, int, int]", + "default": "Position(0, 0, 0)", + "input": "Position", "description": "The travel position of the turtle.", }, { @@ -2023,14 +2023,14 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "color_id", "input": int, "description": "The color of the sheep.", "mask": 0x0F, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_sheared", "input": bool, "description": "Whether the sheep is sheared.", @@ -2111,14 +2111,14 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_sitting", "input": bool, "description": "Whether the animal is sitting.", "mask": 0x01, }, { - "type": "Masked", + "type": "BoolMasked", "name": "is_tamed", "input": bool, "description": "Whether the animal is tamed.", @@ -2266,7 +2266,7 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_player_created", "input": bool, "description": "Whether the iron golem was created by a player.", @@ -2290,7 +2290,7 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "has_pumpkin", "input": bool, "description": "Whether the snow golem has a pumpkin on its head.", @@ -2397,7 +2397,7 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_blaze_on_fire", "input": bool, "description": "Whether the blaze is on fire.", @@ -2587,7 +2587,7 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_attacking", "input": bool, "description": "Indicates if the vex is charging.", @@ -2635,7 +2635,7 @@ "available": False, "proxy": [ { - "type": "Masked", + "type": "BoolMasked", "name": "is_climbing", "input": bool, "description": "Whether the spider is climbing.", @@ -2784,8 +2784,8 @@ { "type": "OptBlockState", "name": "carried_block", - "default": "Absent", - "input": str, + "default": None, + "input": "int | None", "description": "The block the enderman is carrying.", }, { diff --git a/tests/mcproto/types/entity/test_metadata.py b/tests/mcproto/types/entity/test_metadata.py index f5c2164b..7e0e0627 100644 --- a/tests/mcproto/types/entity/test_metadata.py +++ b/tests/mcproto/types/entity/test_metadata.py @@ -3,7 +3,7 @@ from mcproto.buffer import Buffer from mcproto.types.entity import DisplayEM, PandaEM from mcproto.types.entity.metadata import EntityMetadata, EntityMetadataCreator, entry, proxy -from mcproto.types.entity.metadata_types import ByteEME, Masked +from mcproto.types.entity.metadata_types import BoolMasked, ByteEME from mcproto.types.vec3 import Vec3 @@ -65,10 +65,10 @@ def test_kwargs(): def test_class_error(): """Test errors for EntityMetadataCreator.""" with pytest.raises(TypeError): - proxy("test", Masked, mask=0x1) # wrong type + proxy("test", BoolMasked, mask=0x1) # type: ignore # wrong type with pytest.raises(TypeError): - proxy(object, Masked, mask=0x1) # wrong type + proxy(object, BoolMasked, mask=0x1) # type: ignore # wrong type with pytest.raises(ValueError, match=r"Default value for .* is not set\."): @@ -85,7 +85,7 @@ class Test(metaclass=EntityMetadataCreator): # type: ignore class Test(metaclass=EntityMetadataCreator): # noqa: F811 """Test class.""" - test: int = proxy(entry(ByteEME, 1), Masked, mask=0x1) + test: int = proxy(entry(ByteEME, 1), BoolMasked, mask=0x1) buf = Buffer(b"\x00\x02\x00") # Wrong metadata type with pytest.raises(TypeError): diff --git a/tests/mcproto/types/entity/test_metadata_types.py b/tests/mcproto/types/entity/test_metadata_types.py index 1d97b332..aa9da1eb 100644 --- a/tests/mcproto/types/entity/test_metadata_types.py +++ b/tests/mcproto/types/entity/test_metadata_types.py @@ -5,6 +5,7 @@ from mcproto.types.entity.enums import Direction, DragonPhase, Pose, SnifferState from mcproto.types.entity.metadata_types import ( BlockStateEME, + BoolMasked, BooleanEME, ByteEME, CatVariantEME, @@ -12,7 +13,7 @@ DragonPhaseEME, FloatEME, FrogVariantEME, - Masked, + IntMasked, NBTagEME, OptBlockStateEME, OptGlobalPositionEME, @@ -37,6 +38,7 @@ ) from mcproto.types.identifier import Identifier from mcproto.types.nbt import ByteNBT, CompoundNBT, EndNBT, NBTag, StringNBT +from mcproto.types.particle_data import ParticleData from mcproto.types.quaternion import Quaternion from mcproto.types.slot import Slot from mcproto.types.uuid import UUID @@ -387,13 +389,16 @@ cls=ParticleEME, fields=[ ("index", int), - ("value", tuple), + ("value", ParticleData), ], - deserialization_fail=[ - (b"\x01\x11\x01", NotImplementedError), + serialize_deserialize=[ + ( + (54, ParticleData(1, 2)), + b"\x36\x11" + ParticleData(1, 2).serialize(), + ), ], validation_fail=[ - ((-1, (2, None)), ValueError), + ((-1, ParticleData(1, 2)), ValueError), ], ) @@ -596,38 +601,64 @@ ], ) +# BoolMasked +gen_serializable_test( + context=globals(), + cls=BoolMasked, + fields=[ + ("bound_entry", ByteEME), + ("mask", int), + ], + validation_fail=[ + ((ByteEME(1, 0), 0b00000011), ValueError), # not a power of 2 + ], +) + def test_masked(): - """Test the Masked class.""" + """Test the IntMasked class.""" container = ByteEME(index=1, value=0) - mask1 = Masked(container, mask=0b00000001) - mask2 = Masked(container, mask=0b00000010) - mask3 = Masked(container, mask=0b00000100) - mask12 = Masked(container, mask=0b00000011) - mask13 = Masked(container, mask=0b00000101) + mask1 = IntMasked(container, mask=0b00000001) + bmask1 = BoolMasked(container, mask=0b00000001) + mask2 = IntMasked(container, mask=0b00000010) + bmask2 = BoolMasked(container, mask=0b00000010) + mask3 = IntMasked(container, mask=0b00000100) + bmask3 = BoolMasked(container, mask=0b00000100) + mask12 = IntMasked(container, mask=0b00000011) + mask13 = IntMasked(container, mask=0b00000101) assert mask1.getter() == 0 + assert bmask1.getter() is False assert mask2.getter() == 0 + assert bmask2.getter() is False assert mask3.getter() == 0 + assert bmask3.getter() is False assert mask12.getter() == 0 assert mask13.getter() == 0 mask1.setter(1) mask2.setter(1) assert mask1.getter() == 1 + assert bmask1.getter() is True assert mask2.getter() == 1 + assert bmask2.getter() is True assert mask3.getter() == 0 + assert bmask3.getter() is False assert mask12.getter() == 3 assert mask13.getter() == 1 mask1.setter(0) mask3.setter(1) assert mask1.getter() == 0 + assert bmask1.getter() is False assert mask13.getter() == 2 mask12.setter(0) assert mask12.getter() == 0 assert mask13.getter() == 2 assert mask1.getter() == 0 + assert bmask1.getter() is False assert mask2.getter() == 0 + assert bmask2.getter() is False assert mask3.getter() == 1 + assert bmask3.getter() is True diff --git a/tests/mcproto/types/test_particle_data.py b/tests/mcproto/types/test_particle_data.py index dfcc922a..948274a6 100644 --- a/tests/mcproto/types/test_particle_data.py +++ b/tests/mcproto/types/test_particle_data.py @@ -13,12 +13,13 @@ ("block_state", int), ], serialize_deserialize=[ - ((2, 2), b"\x02\x02"), # minecraft:block - ((3, 3), b"\x03\x03"), # minecraft:block_marker - ((27, 4), b"\x1b\x04"), # minecraft:falling_dust + ((1, 2), b"\x01\x02"), # minecraft:block + ((2, 3), b"\x02\x03"), # minecraft:block_marker + ((28, 4), b"\x1c\x04"), # minecraft:falling_dust + ((105, 5), b"\x69\x05"), # minecraft:dust_pillar ], validation_fail=[ - ((2, None), ValueError), # Missing block state + ((1, None), ValueError), # Missing block state ], test_suffix="BlockState", ) @@ -34,12 +35,12 @@ ("blue", float), ], serialize_deserialize=[ - ((14, 0.5, 0.5, 0.625), b"\x0e" + struct.pack("!fff", 0.5, 0.5, 0.625)), # minecraft:dust + ((13, 0.5, 0.5, 0.625), b"\x0d" + struct.pack("!fff", 0.5, 0.5, 0.625)), # minecraft:dust ], validation_fail=[ - ((14, None, 0.5, 0.75), ValueError), # Missing red - ((14, 1.0, None, 0.75), ValueError), # Missing green - ((14, 1.0, 0.5, None), ValueError), # Missing blue + ((13, None, 0.5, 0.75), ValueError), # Missing red + ((13, 1.0, None, 0.75), ValueError), # Missing green + ((13, 1.0, 0.5, None), ValueError), # Missing blue ], test_suffix="RGBScale", ) @@ -60,18 +61,18 @@ ], serialize_deserialize=[ ( - (15, 1.0, 0.5, 0.75, 2.0, 0.5, 0.0, 0.625), - b"\x0f" + struct.pack("!fffffff", 1.0, 0.5, 0.75, 2.0, 0.5, 0.0, 0.625), + (14, 1.0, 0.5, 0.75, 2.0, 0.5, 0.0, 0.625), + b"\x0e" + struct.pack("!fffffff", 1.0, 0.5, 0.75, 2.0, 0.5, 0.0, 0.625), ), # minecraft:dust_color_transition ], validation_fail=[ - ((15, None, 0.5, 0.75, 2.0, 0.5, 0.0, 0.625), ValueError), # Missing from_red - ((15, 1.0, None, 0.75, 2.0, 0.5, 0.0, 0.625), ValueError), # Missing from_green - ((15, 1.0, 0.5, None, 2.0, 0.5, 0.0, 0.625), ValueError), # Missing from_blue - ((15, 1.0, 0.5, 0.75, None, 0.5, 0.0, 0.625), ValueError), # Missing scale - ((15, 1.0, 0.5, 0.75, 2.0, None, 0.0, 0.625), ValueError), # Missing to_red - ((15, 1.0, 0.5, 0.75, 2.0, 0.5, None, 0.625), ValueError), # Missing to_green - ((15, 1.0, 0.5, 0.75, 2.0, 0.5, 0.0, None), ValueError), # Missing to_blue + ((14, None, 0.5, 0.75, 2.0, 0.5, 0.0, 0.625), ValueError), # Missing from_red + ((14, 1.0, None, 0.75, 2.0, 0.5, 0.0, 0.625), ValueError), # Missing from_green + ((14, 1.0, 0.5, None, 2.0, 0.5, 0.0, 0.625), ValueError), # Missing from_blue + ((14, 1.0, 0.5, 0.75, None, 0.5, 0.0, 0.625), ValueError), # Missing scale + ((14, 1.0, 0.5, 0.75, 2.0, None, 0.0, 0.625), ValueError), # Missing to_red + ((14, 1.0, 0.5, 0.75, 2.0, 0.5, None, 0.625), ValueError), # Missing to_green + ((14, 1.0, 0.5, 0.75, 2.0, 0.5, 0.0, None), ValueError), # Missing to_blue ], test_suffix="RGBTransition", ) @@ -85,10 +86,10 @@ ("roll", float), ], serialize_deserialize=[ - ((33, 1.0), b"\x21" + struct.pack("!f", 1.0)), # minecraft:sculk_charge + ((35, 1.0), b"\x23" + struct.pack("!f", 1.0)), # minecraft:sculk_charge ], validation_fail=[ - ((33, None), ValueError), # Missing roll + ((35, None), ValueError), # Missing roll ], test_suffix="Roll", ) @@ -103,12 +104,12 @@ ], serialize_deserialize=[ ( - (42, Slot(SlotData(23, 13))), - b"\x2a" + Slot(SlotData(23, 13)).serialize(), + (44, Slot(SlotData(23, 13))), + b"\x2c" + Slot(SlotData(23, 13)).serialize(), ), ], validation_fail=[ - ((42, None), ValueError), # Missing item + ((44, None), ValueError), # Missing item ], test_suffix="Item", ) @@ -127,20 +128,20 @@ ], serialize_deserialize=[ ( - (43, 0, Position(0, 0, 0), None, None, 10), - b"\x2b\x00" + Position(0, 0, 0).serialize() + b"\x0a", + (45, 0, Position(0, 0, 0), None, None, 10), + b"\x2d\x00" + Position(0, 0, 0).serialize() + b"\x0a", ), # minecraft:vibration with block_position ( - (43, 1, None, 3, 1.0625, 10), - b"\x2b\x01\x03" + struct.pack("!f", 1.0625) + b"\x0a", + (45, 1, None, 3, 1.0625, 10), + b"\x2d\x01\x03" + struct.pack("!f", 1.0625) + b"\x0a", ), # minecraft:vibration with entity_id and entity_eye_height ], validation_fail=[ - ((43, None, Position(0, 0, 0), None, None, 1), ValueError), # Missing source_type - ((43, 0, None, None, None, 1), ValueError), # Missing block_position - ((43, 1, None, None, None, 1), ValueError), # Missing entity_id - ((43, 1, None, 1, None, 1), ValueError), # Missing entity_eye_height - ((43, 1, None, 1, 1.8, None), ValueError), # Missing tick + ((45, None, Position(0, 0, 0), None, None, 1), ValueError), # Missing source_type + ((45, 0, None, None, None, 1), ValueError), # Missing block_position + ((45, 1, None, None, None, 1), ValueError), # Missing entity_id + ((45, 1, None, 1, None, 1), ValueError), # Missing entity_eye_height + ((45, 1, None, 1, 1.8, None), ValueError), # Missing tick ], test_suffix="Vibration", ) @@ -154,14 +155,31 @@ ("delay", int), ], serialize_deserialize=[ - ((96, 1), b"\x60\x01"), # minecraft:shriek + ((99, 1), b"\x63\x01"), # minecraft:shriek ], validation_fail=[ - ((96, None), ValueError), # Missing delay + ((99, None), ValueError), # Missing delay ], test_suffix="Delay", ) +# WITH_COLOR +gen_serializable_test( + context=globals(), + cls=ParticleData, + fields=[ + ("particle_id", int), + ("color", int), + ], + serialize_deserialize=[ + ((20, 0xCC00FF66), b"\x14\xcc\x00\xff\x66"), # minecraft:entity_effect + ], + validation_fail=[ + ((20, None), ValueError), # Missing color + ], + test_suffix="Color", +) + def test_particle_data_without_id(): """Test ParticleData without particle_id during serialization."""