1use 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!(BlockParticleData { block_state: BlockStateId });
26particle_struct!(BlockMarkerParticleData { block_state: BlockStateId });
28particle_struct!(GeyserParticleData { water_blocks: Int });
30particle_struct!(GeyserBaseParticleData {
32 water_blocks: Int,
33 burst_impulse_base: Float,
34});
35particle_struct!(GeyserPoofParticleData {
37 water_blocks: Int,
38 burst_impulse_base: Float,
39});
40particle_struct!(GeyserPlumeParticleData { water_blocks: Int });
42particle_struct!(DragonBreathParticleData { power: Float });
44particle_struct!(DustParticleData {
46 color: Int,
48 scale: Float,
50});
51particle_struct!(DustColorTransitionParticleData {
53 from_color: Int,
55 to_color: Int,
57 scale: Float,
59});
60particle_struct!(EffectParticleData {
62 color: Int,
64 power: Float,
65});
66particle_struct!(EntityEffectParticleData { color: Int });
68particle_struct!(FallingDustParticleData { block_state: BlockStateId });
70particle_struct!(TintedLeavesParticleData { color: Int });
72particle_struct!(SculkChargeParticleData { roll: Float });
74particle_struct!(FlashParticleData { color: Int });
76particle_struct!(InstantEffectParticleData {
78 color: Int,
80 power: Float,
81});
82particle_struct!(ItemParticleData { item: Slot });
84particle_struct!(TrailParticleData {
86 target_x: Double,
87 target_y: Double,
88 target_z: Double,
89 color: Int,
91 duration: VarInt,
93});
94particle_struct!(ShriekParticleData { delay: VarInt });
96particle_struct!(DustPillarParticleData { block_state: BlockStateId });
98particle_struct!(BlockCrumbleParticleData { block_state: BlockStateId });
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, TypeStructCodec)]
103#[type_struct_codec(kind = VibrationSource)]
104pub struct BlockVibrationSource {
105 pub position: Position,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, TypeStructCodec)]
111#[type_struct_codec(kind = VibrationSource)]
112pub struct EntityVibrationSource {
113 pub entity_id: VarInt,
115 pub eye_height: Float,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq)]
121pub enum VibrationSource {
122 Block(BlockVibrationSource),
124 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!(VibrationParticleData {
171 source: VibrationSource,
173 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 #[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 #[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 #[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 #[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#[repr(transparent)]
409#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
410#[type_struct_codec(kind = EntityMetadataValue)]
411pub struct Particles(pub PrefixedArray<Particle>);
412
413impl Particles {
414 #[must_use]
416 pub const fn new(values: Vec<Particle>) -> Self {
417 Self(PrefixedArray(values))
418 }
419
420 #[must_use]
422 pub fn as_slice(&self) -> &[Particle] {
423 self.0.as_slice()
424 }
425
426 #[must_use]
428 pub const fn len(&self) -> usize {
429 self.0.len()
430 }
431
432 #[must_use]
434 pub const fn is_empty(&self) -> bool {
435 self.0.is_empty()
436 }
437
438 #[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}