Skip to main content

mcproto_types/entity_metadata/
particle.rs

1//! Type-safe particle definitions used by entity metadata.
2
3use std::io::{Read, Write};
4
5use mcproto_codec::error::{CodecError, CodecKind, InvalidEncodingReason};
6
7use crate::{
8    Double, Float, Int, Position, PrefixedArray, Slot, TypeCodec, TypeStructCodec, VarInt,
9};
10
11use super::BlockStateId;
12
13macro_rules! particle_struct {
14    ($(#[$meta:meta])* $name:ident { $($(#[$field_meta:meta])* $field:ident: $ty:ty),* $(,)? }) => {
15        $(#[$meta])*
16        #[derive(Debug, Clone, PartialEq, TypeStructCodec)]
17        #[type_struct_codec(kind = Particle)]
18        pub struct $name {
19            $($(#[$field_meta])* pub $field: $ty,)*
20        }
21    };
22}
23
24particle_struct!(/// Payload of `minecraft:block`.
25    BlockParticleData { block_state: BlockStateId });
26particle_struct!(/// Payload of `minecraft:block_marker`.
27    BlockMarkerParticleData { block_state: BlockStateId });
28particle_struct!(/// Payload of `minecraft:geyser`.
29    GeyserParticleData { water_blocks: Int });
30particle_struct!(/// Payload of `minecraft:geyser_base`.
31GeyserBaseParticleData {
32    water_blocks: Int,
33    burst_impulse_base: Float,
34});
35particle_struct!(/// Payload of `minecraft:geyser_poof`.
36GeyserPoofParticleData {
37    water_blocks: Int,
38    burst_impulse_base: Float,
39});
40particle_struct!(/// Payload of `minecraft:geyser_plume`.
41    GeyserPlumeParticleData { water_blocks: Int });
42particle_struct!(/// Payload of `minecraft:dragon_breath`.
43    DragonBreathParticleData { power: Float });
44particle_struct!(/// Payload of `minecraft:dust`.
45DustParticleData {
46    /// RGB color encoded as `0xRRGGBB`; upper bits are ignored.
47    color: Int,
48    /// Display scale, clamped by the client to `0.01..=4.0`.
49    scale: Float,
50});
51particle_struct!(/// Payload of `minecraft:dust_color_transition`.
52DustColorTransitionParticleData {
53    /// Initial RGB color encoded as `0xRRGGBB`.
54    from_color: Int,
55    /// Final RGB color encoded as `0xRRGGBB`.
56    to_color: Int,
57    /// Display scale, clamped by the client to `0.01..=4.0`.
58    scale: Float,
59});
60particle_struct!(/// Payload of `minecraft:effect`.
61EffectParticleData {
62    /// RGB color encoded as `0xRRGGBB`.
63    color: Int,
64    power: Float,
65});
66particle_struct!(/// Payload of `minecraft:entity_effect`.
67    EntityEffectParticleData { color: Int });
68particle_struct!(/// Payload of `minecraft:falling_dust`.
69    FallingDustParticleData { block_state: BlockStateId });
70particle_struct!(/// Payload of `minecraft:tinted_leaves`.
71    TintedLeavesParticleData { color: Int });
72particle_struct!(/// Payload of `minecraft:sculk_charge`.
73    SculkChargeParticleData { roll: Float });
74particle_struct!(/// Payload of `minecraft:flash`.
75    FlashParticleData { color: Int });
76particle_struct!(/// Payload of `minecraft:instant_effect`.
77InstantEffectParticleData {
78    /// RGB color encoded as `0xRRGGBB`.
79    color: Int,
80    power: Float,
81});
82particle_struct!(/// Payload of `minecraft:item`.
83    ItemParticleData { item: Slot });
84particle_struct!(/// Payload of `minecraft:trail`.
85TrailParticleData {
86    target_x: Double,
87    target_y: Double,
88    target_z: Double,
89    /// Trail RGB color encoded as `0xRRGGBB`.
90    color: Int,
91    /// Lifetime in ticks.
92    duration: VarInt,
93});
94particle_struct!(/// Payload of `minecraft:shriek`.
95    ShriekParticleData { delay: VarInt });
96particle_struct!(/// Payload of `minecraft:dust_pillar`.
97    DustPillarParticleData { block_state: BlockStateId });
98particle_struct!(/// Payload of `minecraft:block_crumble`.
99    BlockCrumbleParticleData { block_state: BlockStateId });
100
101/// Block position payload of the `minecraft:block` vibration source.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, TypeStructCodec)]
103#[type_struct_codec(kind = VibrationSource)]
104pub struct BlockVibrationSource {
105    /// Position from which the vibration originated.
106    pub position: Position,
107}
108
109/// Entity payload of the `minecraft:entity` vibration source.
110#[derive(Debug, Clone, Copy, PartialEq, TypeStructCodec)]
111#[type_struct_codec(kind = VibrationSource)]
112pub struct EntityVibrationSource {
113    /// Runtime entity ID from which the vibration originated.
114    pub entity_id: VarInt,
115    /// Eye height relative to the entity.
116    pub eye_height: Float,
117}
118
119/// Position source selected by a vibration particle's registry type ID.
120#[derive(Debug, Clone, Copy, PartialEq)]
121pub enum VibrationSource {
122    /// Type ID 0, `minecraft:block`.
123    Block(BlockVibrationSource),
124    /// Type ID 1, `minecraft:entity`.
125    Entity(EntityVibrationSource),
126}
127
128impl TypeCodec for VibrationSource {
129    fn encode(&self, writer: &mut impl Write) -> Result<(), CodecError> {
130        match self {
131            Self::Block(value) => {
132                encode_vibration_source_type(0, writer)?;
133                value.encode(writer)
134            }
135            Self::Entity(value) => {
136                encode_vibration_source_type(1, writer)?;
137                value.encode(writer)
138            }
139        }
140    }
141
142    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
143        let source_type = VarInt::decode(reader)
144            .map_err(|error| error.with_context(CodecKind::VibrationSource))?
145            .0;
146        match source_type {
147            0 => BlockVibrationSource::decode(reader).map(Self::Block),
148            1 => EntityVibrationSource::decode(reader).map(Self::Entity),
149            value => Err(CodecError::invalid_encoding(
150                CodecKind::VibrationSource,
151                0,
152                InvalidEncodingReason::InvalidEnumValue {
153                    value: i128::from(value),
154                },
155            )),
156        }
157    }
158}
159
160fn encode_vibration_source_type(
161    source_type: i32,
162    writer: &mut impl Write,
163) -> Result<(), CodecError> {
164    VarInt(source_type)
165        .encode(writer)
166        .map_err(|error| error.with_context(CodecKind::VibrationSource))
167}
168
169particle_struct!(/// Payload of `minecraft:vibration`.
170VibrationParticleData {
171    /// Type-safe block or entity position source.
172    source: VibrationSource,
173    /// Travel time in ticks.
174    ticks: VarInt,
175});
176
177macro_rules! define_particles {
178    (
179        unit {
180            $($unit_id:literal => $unit_variant:ident = $unit_name:literal,)*
181        }
182        data {
183            $($data_id:literal => $data_variant:ident($data_type:ty) = $data_name:literal,)*
184        }
185    ) => {
186        /// A particle type ID bound to exactly the payload required by that type.
187        ///
188        /// Unit variants encode only their registry ID. Data variants encode the
189        /// registry ID followed by their dedicated payload structure, preventing
190        /// particle IDs and payload layouts from being mismatched in memory.
191        ///
192        /// See the official [particle protocol table].
193        ///
194        /// [particle protocol table]: https://minecraft.wiki/w/Java_Edition_protocol/Particles
195        #[derive(Debug, Clone, PartialEq)]
196        pub enum Particle {
197            $(
198                #[doc = concat!("The `", $unit_name, "` particle (type ID `", stringify!($unit_id), "`) with no payload.")]
199                $unit_variant,
200            )*
201            $(
202                #[doc = concat!("The `", $data_name, "` particle (type ID `", stringify!($data_id), "`).")]
203                $data_variant($data_type),
204            )*
205        }
206
207        impl Particle {
208            /// Returns the numeric ID in the `minecraft:particle_type` registry.
209            #[must_use]
210            pub const fn type_id(&self) -> i32 {
211                match self {
212                    $(Self::$unit_variant => $unit_id,)*
213                    $(Self::$data_variant(_) => $data_id,)*
214                }
215            }
216
217            /// Returns the namespaced particle registry name.
218            #[must_use]
219            pub const fn registry_name(&self) -> &'static str {
220                match self {
221                    $(Self::$unit_variant => $unit_name,)*
222                    $(Self::$data_variant(_) => $data_name,)*
223                }
224            }
225
226            /// Returns whether this particle has no type-specific payload.
227            #[must_use]
228            pub const fn is_unit(&self) -> bool {
229                match self {
230                    $(Self::$unit_variant => true,)*
231                    $(Self::$data_variant(_) => false,)*
232                }
233            }
234        }
235
236        impl TypeCodec for Particle {
237            fn encode(&self, writer: &mut impl Write) -> Result<(), CodecError> {
238                match self {
239                    $(Self::$unit_variant => encode_particle_type($unit_id, writer),)*
240                    $(
241                        Self::$data_variant(value) => {
242                            encode_particle_type($data_id, writer)?;
243                            value.encode(writer)
244                        }
245                    )*
246                }
247            }
248
249            fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
250                let particle_type = VarInt::decode(reader)
251                    .map_err(|error| error.with_context(CodecKind::Particle))?
252                    .0;
253                match particle_type {
254                    $($unit_id => Ok(Self::$unit_variant),)*
255                    $($data_id => <$data_type>::decode(reader).map(Self::$data_variant),)*
256                    value => Err(CodecError::invalid_encoding(
257                        CodecKind::Particle,
258                        0,
259                        InvalidEncodingReason::InvalidEnumValue {
260                            value: i128::from(value),
261                        },
262                    )),
263                }
264            }
265        }
266    };
267}
268
269define_particles! {
270    unit {
271        0 => AngryVillager = "minecraft:angry_villager",
272        3 => Bubble = "minecraft:bubble",
273        4 => SulfurBubbles = "minecraft:sulfur_bubbles",
274        5 => NoxiousGas = "minecraft:noxious_gas",
275        6 => NoxiousGasCloud = "minecraft:noxious_gas_cloud",
276        11 => Cloud = "minecraft:cloud",
277        12 => CopperFireFlame = "minecraft:copper_fire_flame",
278        13 => Crit = "minecraft:crit",
279        14 => DamageIndicator = "minecraft:damage_indicator",
280        16 => DrippingLava = "minecraft:dripping_lava",
281        17 => FallingLava = "minecraft:falling_lava",
282        18 => LandingLava = "minecraft:landing_lava",
283        19 => DrippingWater = "minecraft:dripping_water",
284        20 => FallingWater = "minecraft:falling_water",
285        24 => ElderGuardian = "minecraft:elder_guardian",
286        25 => EnchantedHit = "minecraft:enchanted_hit",
287        26 => Enchant = "minecraft:enchant",
288        27 => EndRod = "minecraft:end_rod",
289        29 => ExplosionEmitter = "minecraft:explosion_emitter",
290        30 => Explosion = "minecraft:explosion",
291        31 => Gust = "minecraft:gust",
292        32 => SmallGust = "minecraft:small_gust",
293        33 => GustEmitterLarge = "minecraft:gust_emitter_large",
294        34 => GustEmitterSmall = "minecraft:gust_emitter_small",
295        35 => SonicBoom = "minecraft:sonic_boom",
296        37 => Firework = "minecraft:firework",
297        38 => Fishing = "minecraft:fishing",
298        39 => Flame = "minecraft:flame",
299        40 => Infested = "minecraft:infested",
300        41 => CherryLeaves = "minecraft:cherry_leaves",
301        42 => PaleOakLeaves = "minecraft:pale_oak_leaves",
302        44 => SculkSoul = "minecraft:sculk_soul",
303        46 => SculkChargePop = "minecraft:sculk_charge_pop",
304        47 => SoulFireFlame = "minecraft:soul_fire_flame",
305        48 => Soul = "minecraft:soul",
306        50 => HappyVillager = "minecraft:happy_villager",
307        51 => Composter = "minecraft:composter",
308        52 => Heart = "minecraft:heart",
309        57 => PauseMobGrowth = "minecraft:pause_mob_growth",
310        58 => ResetMobGrowth = "minecraft:reset_mob_growth",
311        59 => ItemSlime = "minecraft:item_slime",
312        60 => ItemCobweb = "minecraft:item_cobweb",
313        61 => ItemSnowball = "minecraft:item_snowball",
314        62 => LargeSmoke = "minecraft:large_smoke",
315        63 => Lava = "minecraft:lava",
316        64 => Mycelium = "minecraft:mycelium",
317        65 => Note = "minecraft:note",
318        66 => Poof = "minecraft:poof",
319        67 => Portal = "minecraft:portal",
320        68 => Rain = "minecraft:rain",
321        69 => Smoke = "minecraft:smoke",
322        70 => WhiteSmoke = "minecraft:white_smoke",
323        71 => Sneeze = "minecraft:sneeze",
324        72 => Spit = "minecraft:spit",
325        73 => SquidInk = "minecraft:squid_ink",
326        74 => SweepAttack = "minecraft:sweep_attack",
327        75 => TotemOfUndying = "minecraft:totem_of_undying",
328        76 => Underwater = "minecraft:underwater",
329        77 => Splash = "minecraft:splash",
330        78 => Witch = "minecraft:witch",
331        79 => BubblePop = "minecraft:bubble_pop",
332        80 => CurrentDown = "minecraft:current_down",
333        81 => BubbleColumnUp = "minecraft:bubble_column_up",
334        82 => Nautilus = "minecraft:nautilus",
335        83 => Dolphin = "minecraft:dolphin",
336        84 => CampfireCosySmoke = "minecraft:campfire_cosy_smoke",
337        85 => CampfireSignalSmoke = "minecraft:campfire_signal_smoke",
338        86 => DrippingHoney = "minecraft:dripping_honey",
339        87 => FallingHoney = "minecraft:falling_honey",
340        88 => LandingHoney = "minecraft:landing_honey",
341        89 => FallingNectar = "minecraft:falling_nectar",
342        90 => FallingSporeBlossom = "minecraft:falling_spore_blossom",
343        91 => Ash = "minecraft:ash",
344        92 => CrimsonSpore = "minecraft:crimson_spore",
345        93 => WarpedSpore = "minecraft:warped_spore",
346        94 => SporeBlossomAir = "minecraft:spore_blossom_air",
347        95 => DrippingObsidianTear = "minecraft:dripping_obsidian_tear",
348        96 => FallingObsidianTear = "minecraft:falling_obsidian_tear",
349        97 => LandingObsidianTear = "minecraft:landing_obsidian_tear",
350        98 => ReversePortal = "minecraft:reverse_portal",
351        99 => WhiteAsh = "minecraft:white_ash",
352        100 => SmallFlame = "minecraft:small_flame",
353        101 => Snowflake = "minecraft:snowflake",
354        102 => DrippingDripstoneLava = "minecraft:dripping_dripstone_lava",
355        103 => FallingDripstoneLava = "minecraft:falling_dripstone_lava",
356        104 => DrippingDripstoneWater = "minecraft:dripping_dripstone_water",
357        105 => FallingDripstoneWater = "minecraft:falling_dripstone_water",
358        106 => GlowSquidInk = "minecraft:glow_squid_ink",
359        107 => Glow = "minecraft:glow",
360        108 => WaxOn = "minecraft:wax_on",
361        109 => WaxOff = "minecraft:wax_off",
362        110 => ElectricSpark = "minecraft:electric_spark",
363        111 => Scrape = "minecraft:scrape",
364        113 => EggCrack = "minecraft:egg_crack",
365        114 => DustPlume = "minecraft:dust_plume",
366        115 => TrialSpawnerDetection = "minecraft:trial_spawner_detection",
367        116 => TrialSpawnerDetectionOminous = "minecraft:trial_spawner_detection_ominous",
368        117 => VaultConnection = "minecraft:vault_connection",
369        119 => OminousSpawning = "minecraft:ominous_spawning",
370        120 => RaidOmen = "minecraft:raid_omen",
371        121 => TrialOmen = "minecraft:trial_omen",
372        123 => Firefly = "minecraft:firefly",
373        124 => SulfurCubeGoo = "minecraft:sulfur_cube_goo",
374    }
375    data {
376        1 => Block(BlockParticleData) = "minecraft:block",
377        2 => BlockMarker(BlockMarkerParticleData) = "minecraft:block_marker",
378        7 => Geyser(GeyserParticleData) = "minecraft:geyser",
379        8 => GeyserBase(GeyserBaseParticleData) = "minecraft:geyser_base",
380        9 => GeyserPoof(GeyserPoofParticleData) = "minecraft:geyser_poof",
381        10 => GeyserPlume(GeyserPlumeParticleData) = "minecraft:geyser_plume",
382        15 => DragonBreath(DragonBreathParticleData) = "minecraft:dragon_breath",
383        21 => Dust(DustParticleData) = "minecraft:dust",
384        22 => DustColorTransition(DustColorTransitionParticleData) = "minecraft:dust_color_transition",
385        23 => Effect(EffectParticleData) = "minecraft:effect",
386        28 => EntityEffect(EntityEffectParticleData) = "minecraft:entity_effect",
387        36 => FallingDust(FallingDustParticleData) = "minecraft:falling_dust",
388        43 => TintedLeaves(TintedLeavesParticleData) = "minecraft:tinted_leaves",
389        45 => SculkCharge(SculkChargeParticleData) = "minecraft:sculk_charge",
390        49 => Flash(FlashParticleData) = "minecraft:flash",
391        53 => InstantEffect(InstantEffectParticleData) = "minecraft:instant_effect",
392        54 => Item(ItemParticleData) = "minecraft:item",
393        55 => Vibration(VibrationParticleData) = "minecraft:vibration",
394        56 => Trail(TrailParticleData) = "minecraft:trail",
395        112 => Shriek(ShriekParticleData) = "minecraft:shriek",
396        118 => DustPillar(DustPillarParticleData) = "minecraft:dust_pillar",
397        122 => BlockCrumble(BlockCrumbleParticleData) = "minecraft:block_crumble",
398    }
399}
400
401fn encode_particle_type(particle_type: i32, writer: &mut impl Write) -> Result<(), CodecError> {
402    VarInt(particle_type)
403        .encode(writer)
404        .map_err(|error| error.with_context(CodecKind::Particle))
405}
406
407/// A VarInt-length-prefixed list of complete particle definitions.
408#[repr(transparent)]
409#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
410#[type_struct_codec(kind = EntityMetadataValue)]
411pub struct Particles(pub PrefixedArray<Particle>);
412
413impl Particles {
414    /// Creates a particle list.
415    #[must_use]
416    pub const fn new(values: Vec<Particle>) -> Self {
417        Self(PrefixedArray(values))
418    }
419
420    /// Returns the particle definitions.
421    #[must_use]
422    pub fn as_slice(&self) -> &[Particle] {
423        self.0.as_slice()
424    }
425
426    /// Returns the number of particle definitions.
427    #[must_use]
428    pub const fn len(&self) -> usize {
429        self.0.len()
430    }
431
432    /// Returns whether the list is empty.
433    #[must_use]
434    pub const fn is_empty(&self) -> bool {
435        self.0.is_empty()
436    }
437
438    /// Extracts the particle definitions.
439    #[must_use]
440    pub fn into_vec(self) -> Vec<Particle> {
441        self.0.into_vec()
442    }
443}
444
445impl From<Vec<Particle>> for Particles {
446    fn from(values: Vec<Particle>) -> Self {
447        Self::new(values)
448    }
449}
450
451impl From<Particles> for Vec<Particle> {
452    fn from(values: Particles) -> Self {
453        values.into_vec()
454    }
455}
456
457impl Default for Particles {
458    fn default() -> Self {
459        Self::new(Vec::new())
460    }
461}