1use std::fmt::{self, Debug, Formatter};
2use std::io::{Cursor, Error, Result, Write};
3
4use glam::DVec3;
5use rustc_hash::FxHashMap;
6use steel_utils::codec::VarInt;
7use steel_utils::serial::{ReadFrom, WriteTo};
8use steel_utils::{
9 ArgbColor, BlockStateId, Downcast as _, DowncastType, DowncastTypeKey, ErasedType, Identifier,
10 RgbColor,
11};
12
13use crate::item_stack_template::ItemStackTemplate;
14use crate::position_source::PositionSource;
15use crate::{REGISTRY, RegistryExt};
16
17pub trait ParticleOptions:
19 DowncastType + Clone + Debug + PartialEq + Send + Sync + 'static
20{
21 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self>;
22 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()>;
23}
24
25trait ErasedParticleOptions: ErasedType + Debug + Send + Sync {
26 fn clone_options(&self) -> Box<dyn ErasedParticleOptions>;
27 fn options_eq(&self, other: &dyn ErasedParticleOptions) -> bool;
28}
29
30impl<T: ParticleOptions> ErasedParticleOptions for T {
31 fn clone_options(&self) -> Box<dyn ErasedParticleOptions> {
32 Box::new(self.clone())
33 }
34
35 fn options_eq(&self, other: &dyn ErasedParticleOptions) -> bool {
36 other.downcast_ref::<T>() == Some(self)
37 }
38}
39
40type NetworkReader = fn(&mut Cursor<&[u8]>) -> Result<Box<dyn ErasedParticleOptions>>;
41type NetworkWriter = fn(&dyn ErasedParticleOptions, &mut Vec<u8>) -> Result<()>;
42
43pub struct ParticleType {
45 pub key: Identifier,
46 pub override_limiter: bool,
47 expected_type_key: DowncastTypeKey,
48 network_reader: NetworkReader,
49 network_writer: NetworkWriter,
50}
51
52impl ParticleType {
53 #[must_use]
54 pub const fn of<T: ParticleOptions>(key: Identifier, override_limiter: bool) -> Self {
55 Self {
56 key,
57 override_limiter,
58 expected_type_key: T::TYPE_KEY,
59 network_reader: read_network::<T>,
60 network_writer: write_network::<T>,
61 }
62 }
63}
64
65impl Debug for ParticleType {
66 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
67 formatter
68 .debug_struct("ParticleType")
69 .field("key", &self.key)
70 .field("override_limiter", &self.override_limiter)
71 .field("expected_type_key", &self.expected_type_key)
72 .finish_non_exhaustive()
73 }
74}
75
76pub type ParticleTypeRef = &'static ParticleType;
77
78pub struct ParticleData {
80 particle_type: ParticleTypeRef,
81 options: Box<dyn ErasedParticleOptions>,
82}
83
84impl ParticleData {
85 #[must_use]
86 pub fn new<T: ParticleOptions>(particle_type: ParticleTypeRef, options: T) -> Self {
87 assert_eq!(
88 particle_type.expected_type_key,
89 T::TYPE_KEY,
90 "particle options do not match their registered type"
91 );
92 Self {
93 particle_type,
94 options: Box::new(options),
95 }
96 }
97
98 #[must_use]
99 pub fn simple(particle_type: ParticleTypeRef) -> Self {
100 Self::new(particle_type, SimpleParticleOptions)
101 }
102
103 #[must_use]
104 pub const fn particle_type(&self) -> ParticleTypeRef {
105 self.particle_type
106 }
107
108 #[must_use]
109 pub fn downcast_ref<T: DowncastType>(&self) -> Option<&T> {
110 self.options.downcast_ref::<T>()
111 }
112}
113
114impl Clone for ParticleData {
115 fn clone(&self) -> Self {
116 Self {
117 particle_type: self.particle_type,
118 options: self.options.clone_options(),
119 }
120 }
121}
122
123impl Debug for ParticleData {
124 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
125 formatter
126 .debug_struct("ParticleData")
127 .field("particle_type", &self.particle_type.key)
128 .field("options", &self.options)
129 .finish()
130 }
131}
132
133impl PartialEq for ParticleData {
134 fn eq(&self, other: &Self) -> bool {
135 self.particle_type.key == other.particle_type.key
136 && self.options.options_eq(other.options.as_ref())
137 }
138}
139
140impl WriteTo for ParticleData {
141 fn write(&self, writer: &mut impl Write) -> Result<()> {
142 let (id, particle_type) = REGISTRY
143 .particle_types
144 .registered_entry_with_id(self.particle_type)
145 .ok_or_else(|| {
146 Error::other(format!(
147 "Particle type is not the registered value for key: {}",
148 self.particle_type.key
149 ))
150 })?;
151 let id = i32::try_from(id)
152 .map_err(|_| Error::other(format!("Particle type id out of range: {id}")))?;
153 VarInt(id).write(writer)?;
154
155 let mut payload = Vec::new();
156 (particle_type.network_writer)(self.options.as_ref(), &mut payload)?;
157 writer.write_all(&payload)
158 }
159}
160
161impl ReadFrom for ParticleData {
162 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
163 let id = VarInt::read(data)?.0;
164 let id = usize::try_from(id)
165 .map_err(|_| Error::other(format!("Negative particle type id: {id}")))?;
166 let particle_type = REGISTRY
167 .particle_types
168 .by_id(id)
169 .ok_or_else(|| Error::other(format!("Unknown particle type id: {id}")))?;
170 let options = (particle_type.network_reader)(data)?;
171 Ok(Self {
172 particle_type,
173 options,
174 })
175 }
176}
177
178pub struct ParticleTypeRegistry {
179 particle_types_by_id: Vec<ParticleTypeRef>,
180 particle_types_by_key: FxHashMap<Identifier, usize>,
181 allows_registering: bool,
182}
183
184impl ParticleTypeRegistry {
185 #[must_use]
186 pub fn new() -> Self {
187 Self {
188 particle_types_by_id: Vec::new(),
189 particle_types_by_key: FxHashMap::default(),
190 allows_registering: true,
191 }
192 }
193
194 fn registered_entry_with_id(&self, entry: ParticleTypeRef) -> Option<(usize, ParticleTypeRef)> {
195 let id = self.particle_types_by_key.get(&entry.key).copied()?;
196 let registered = self.particle_types_by_id.get(id).copied()?;
197 std::ptr::eq(registered, entry).then_some((id, registered))
198 }
199}
200
201crate::impl_standard_methods!(
202 ParticleTypeRegistry,
203 ParticleTypeRef,
204 particle_types_by_id,
205 particle_types_by_key,
206 allows_registering,
207 "Cannot register duplicate particle type key: {}"
208);
209
210crate::impl_registry!(
211 ParticleTypeRegistry,
212 ParticleType,
213 particle_types_by_id,
214 particle_types_by_key,
215 particle_types
216);
217
218#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
219pub struct SimpleParticleOptions;
220
221unsafe impl DowncastType for SimpleParticleOptions {
223 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/simple");
224}
225
226impl ParticleOptions for SimpleParticleOptions {
227 fn read_network(_data: &mut Cursor<&[u8]>) -> Result<Self> {
228 Ok(Self)
229 }
230
231 fn write_network(&self, _writer: &mut Vec<u8>) -> Result<()> {
232 Ok(())
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub struct BlockParticleOption {
238 state: BlockStateId,
239}
240
241impl BlockParticleOption {
242 #[must_use]
243 pub const fn new(state: BlockStateId) -> Self {
244 Self { state }
245 }
246
247 #[must_use]
248 pub const fn state(&self) -> BlockStateId {
249 self.state
250 }
251}
252
253unsafe impl DowncastType for BlockParticleOption {
255 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/block");
256}
257
258impl ParticleOptions for BlockParticleOption {
259 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
260 let id = VarInt::read(data)?.0;
261 let id = u16::try_from(id)
262 .map_err(|_| Error::other(format!("Block state id out of range: {id}")))?;
263 let state = BlockStateId(id);
264 if REGISTRY.blocks.by_state_id(state).is_none() {
265 return Err(Error::other(format!("Unknown block state id: {id}")));
266 }
267 Ok(Self::new(state))
268 }
269
270 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
271 if REGISTRY.blocks.by_state_id(self.state).is_none() {
272 return Err(Error::other(format!(
273 "Unknown block state id: {}",
274 self.state.0
275 )));
276 }
277 self.state.write(writer)
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct ColorParticleOption {
283 color: ArgbColor,
284}
285
286impl ColorParticleOption {
287 #[must_use]
288 pub const fn new(color: ArgbColor) -> Self {
289 Self { color }
290 }
291
292 #[must_use]
293 pub const fn color(&self) -> ArgbColor {
294 self.color
295 }
296}
297
298unsafe impl DowncastType for ColorParticleOption {
300 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/color");
301}
302
303impl ParticleOptions for ColorParticleOption {
304 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
305 Ok(Self::new(ArgbColor::read(data)?))
306 }
307
308 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
309 self.color.write(writer)
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq)]
314pub struct DustParticleOptions {
315 color: RgbColor,
316 scale: f32,
317}
318
319impl DustParticleOptions {
320 #[must_use]
321 pub const fn new(color: RgbColor, scale: f32) -> Self {
322 Self {
323 color,
324 scale: scale.clamp(0.01, 4.0),
325 }
326 }
327
328 #[must_use]
329 pub const fn color(&self) -> RgbColor {
330 self.color
331 }
332
333 #[must_use]
334 pub const fn scale(&self) -> f32 {
335 self.scale
336 }
337}
338
339unsafe impl DowncastType for DustParticleOptions {
341 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/dust");
342}
343
344impl ParticleOptions for DustParticleOptions {
345 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
346 Ok(Self::new(RgbColor::read(data)?, f32::read(data)?))
347 }
348
349 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
350 self.color.write(writer)?;
351 self.scale.write(writer)
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq)]
356pub struct DustColorTransitionOptions {
357 from_color: RgbColor,
358 to_color: RgbColor,
359 scale: f32,
360}
361
362impl DustColorTransitionOptions {
363 #[must_use]
364 pub const fn new(from_color: RgbColor, to_color: RgbColor, scale: f32) -> Self {
365 Self {
366 from_color,
367 to_color,
368 scale: scale.clamp(0.01, 4.0),
369 }
370 }
371
372 #[must_use]
373 pub const fn source_color(&self) -> RgbColor {
374 self.from_color
375 }
376
377 #[must_use]
378 pub const fn target_color(&self) -> RgbColor {
379 self.to_color
380 }
381
382 #[must_use]
383 pub const fn scale(&self) -> f32 {
384 self.scale
385 }
386}
387
388unsafe impl DowncastType for DustColorTransitionOptions {
390 const TYPE_KEY: DowncastTypeKey =
391 DowncastTypeKey::new("steel:particle_options/dust_color_transition");
392}
393
394impl ParticleOptions for DustColorTransitionOptions {
395 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
396 Ok(Self::new(
397 RgbColor::read(data)?,
398 RgbColor::read(data)?,
399 f32::read(data)?,
400 ))
401 }
402
403 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
404 self.from_color.write(writer)?;
405 self.to_color.write(writer)?;
406 self.scale.write(writer)
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub struct GeyserParticleOptions {
412 water_blocks: i32,
413}
414
415impl GeyserParticleOptions {
416 #[must_use]
417 pub const fn new(water_blocks: i32) -> Self {
418 Self { water_blocks }
419 }
420
421 #[must_use]
422 pub const fn water_blocks(&self) -> i32 {
423 self.water_blocks
424 }
425}
426
427unsafe impl DowncastType for GeyserParticleOptions {
429 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/geyser");
430}
431
432impl ParticleOptions for GeyserParticleOptions {
433 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
434 Ok(Self::new(i32::read(data)?))
435 }
436
437 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
438 self.water_blocks.write(writer)
439 }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq)]
443pub struct GeyserBaseParticleOptions {
444 water_blocks: i32,
445 burst_impulse_base: f32,
446}
447
448impl GeyserBaseParticleOptions {
449 #[must_use]
450 pub const fn new(water_blocks: i32, burst_impulse_base: f32) -> Self {
451 Self {
452 water_blocks,
453 burst_impulse_base,
454 }
455 }
456
457 #[must_use]
458 pub const fn water_blocks(&self) -> i32 {
459 self.water_blocks
460 }
461
462 #[must_use]
463 pub const fn burst_impulse_base(&self) -> f32 {
464 self.burst_impulse_base
465 }
466}
467
468unsafe impl DowncastType for GeyserBaseParticleOptions {
470 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/geyser_base");
471}
472
473impl ParticleOptions for GeyserBaseParticleOptions {
474 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
475 Ok(Self::new(i32::read(data)?, f32::read(data)?))
476 }
477
478 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
479 self.water_blocks.write(writer)?;
480 self.burst_impulse_base.write(writer)
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq)]
485pub struct PowerParticleOption {
486 power: f32,
487}
488
489impl PowerParticleOption {
490 #[must_use]
491 pub const fn new(power: f32) -> Self {
492 Self { power }
493 }
494
495 #[must_use]
496 pub const fn power(&self) -> f32 {
497 self.power
498 }
499}
500
501unsafe impl DowncastType for PowerParticleOption {
503 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/power");
504}
505
506impl ParticleOptions for PowerParticleOption {
507 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
508 Ok(Self::new(f32::read(data)?))
509 }
510
511 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
512 self.power.write(writer)
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq)]
517pub struct SpellParticleOption {
518 color: RgbColor,
519 power: f32,
520}
521
522impl SpellParticleOption {
523 #[must_use]
524 pub const fn new(color: RgbColor, power: f32) -> Self {
525 Self { color, power }
526 }
527
528 #[must_use]
529 pub const fn color(&self) -> RgbColor {
530 self.color
531 }
532
533 #[must_use]
534 pub const fn power(&self) -> f32 {
535 self.power
536 }
537}
538
539unsafe impl DowncastType for SpellParticleOption {
541 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/spell");
542}
543
544impl ParticleOptions for SpellParticleOption {
545 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
546 Ok(Self::new(RgbColor::read(data)?, f32::read(data)?))
547 }
548
549 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
550 self.color.write(writer)?;
551 self.power.write(writer)
552 }
553}
554
555#[derive(Debug, Clone, PartialEq)]
556pub struct ItemParticleOption {
557 item: ItemStackTemplate,
558}
559
560impl ItemParticleOption {
561 #[must_use]
562 pub const fn new(item: ItemStackTemplate) -> Self {
563 Self { item }
564 }
565
566 #[must_use]
567 pub const fn item(&self) -> &ItemStackTemplate {
568 &self.item
569 }
570}
571
572unsafe impl DowncastType for ItemParticleOption {
574 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/item");
575}
576
577impl ParticleOptions for ItemParticleOption {
578 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
579 Ok(Self::new(ItemStackTemplate::read(data)?))
580 }
581
582 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
583 self.item.write(writer)
584 }
585}
586
587#[derive(Debug, Clone, Copy, PartialEq)]
588pub struct SculkChargeParticleOptions {
589 roll: f32,
590}
591
592impl SculkChargeParticleOptions {
593 #[must_use]
594 pub const fn new(roll: f32) -> Self {
595 Self { roll }
596 }
597
598 #[must_use]
599 pub const fn roll(&self) -> f32 {
600 self.roll
601 }
602}
603
604unsafe impl DowncastType for SculkChargeParticleOptions {
606 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/sculk_charge");
607}
608
609impl ParticleOptions for SculkChargeParticleOptions {
610 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
611 Ok(Self::new(f32::read(data)?))
612 }
613
614 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
615 self.roll.write(writer)
616 }
617}
618
619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub struct ShriekParticleOption {
621 delay: i32,
622}
623
624impl ShriekParticleOption {
625 #[must_use]
626 pub const fn new(delay: i32) -> Self {
627 Self { delay }
628 }
629
630 #[must_use]
631 pub const fn delay(&self) -> i32 {
632 self.delay
633 }
634}
635
636unsafe impl DowncastType for ShriekParticleOption {
638 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/shriek");
639}
640
641impl ParticleOptions for ShriekParticleOption {
642 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
643 Ok(Self::new(VarInt::read(data)?.0))
644 }
645
646 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
647 VarInt(self.delay).write(writer)
648 }
649}
650
651#[derive(Debug, Clone, Copy, PartialEq)]
652pub struct TrailParticleOption {
653 target: DVec3,
654 color: RgbColor,
655 duration: i32,
656}
657
658impl TrailParticleOption {
659 #[must_use]
660 pub const fn new(target: DVec3, color: RgbColor, duration: i32) -> Self {
661 Self {
662 target,
663 color,
664 duration,
665 }
666 }
667
668 #[must_use]
669 pub const fn target(&self) -> DVec3 {
670 self.target
671 }
672
673 #[must_use]
674 pub const fn color(&self) -> RgbColor {
675 self.color
676 }
677
678 #[must_use]
679 pub const fn duration(&self) -> i32 {
680 self.duration
681 }
682}
683
684unsafe impl DowncastType for TrailParticleOption {
686 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/trail");
687}
688
689impl ParticleOptions for TrailParticleOption {
690 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
691 Ok(Self::new(
692 DVec3::read(data)?,
693 RgbColor::read(data)?,
694 VarInt::read(data)?.0,
695 ))
696 }
697
698 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
699 self.target.write(writer)?;
700 self.color.write(writer)?;
701 VarInt(self.duration).write(writer)
702 }
703}
704
705#[derive(Debug, Clone, PartialEq)]
706pub struct VibrationParticleOption {
707 destination: PositionSource,
708 arrival_in_ticks: i32,
709}
710
711impl VibrationParticleOption {
712 #[must_use]
713 pub const fn new(destination: PositionSource, arrival_in_ticks: i32) -> Self {
714 Self {
715 destination,
716 arrival_in_ticks,
717 }
718 }
719
720 #[must_use]
721 pub const fn destination(&self) -> &PositionSource {
722 &self.destination
723 }
724
725 #[must_use]
726 pub const fn arrival_in_ticks(&self) -> i32 {
727 self.arrival_in_ticks
728 }
729}
730
731unsafe impl DowncastType for VibrationParticleOption {
733 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:particle_options/vibration");
734}
735
736impl ParticleOptions for VibrationParticleOption {
737 fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
738 Ok(Self::new(
739 PositionSource::read(data)?,
740 VarInt::read(data)?.0,
741 ))
742 }
743
744 fn write_network(&self, writer: &mut Vec<u8>) -> Result<()> {
745 self.destination.write(writer)?;
746 VarInt(self.arrival_in_ticks).write(writer)
747 }
748}
749
750fn read_network<T: ParticleOptions>(
751 data: &mut Cursor<&[u8]>,
752) -> Result<Box<dyn ErasedParticleOptions>> {
753 Ok(Box::new(T::read_network(data)?))
754}
755
756fn write_network<T: ParticleOptions>(
757 options: &dyn ErasedParticleOptions,
758 writer: &mut Vec<u8>,
759) -> Result<()> {
760 let options = options.downcast_ref::<T>().ok_or_else(|| {
761 Error::other(format!(
762 "Particle options payload does not match {}",
763 T::TYPE_KEY
764 ))
765 })?;
766 options.write_network(writer)
767}
768
769#[cfg(test)]
770mod tests {
771 use std::io::Cursor;
772
773 use glam::DVec3;
774 use steel_utils::codec::VarInt;
775 use steel_utils::serial::{ReadFrom, WriteTo};
776 use steel_utils::{ArgbColor, BlockPos, BlockStateId, Identifier, RgbColor};
777
778 use crate::item_stack_template::ItemStackTemplate;
779 use crate::position_source::{BlockPositionSource, EntityPositionSource, PositionSource};
780 use crate::{
781 REGISTRY, init_vanilla_registry, vanilla_items, vanilla_particle_types,
782 vanilla_position_source_types,
783 };
784
785 use super::{
786 BlockParticleOption, ColorParticleOption, DustColorTransitionOptions, DustParticleOptions,
787 GeyserBaseParticleOptions, GeyserParticleOptions, ItemParticleOption, ParticleData,
788 ParticleOptions, ParticleType, ParticleTypeRegistry, PowerParticleOption,
789 SculkChargeParticleOptions, ShriekParticleOption, SpellParticleOption, TrailParticleOption,
790 VibrationParticleOption,
791 };
792
793 static FORGED_FLAME: ParticleType =
794 ParticleType::of::<BlockParticleOption>(Identifier::vanilla_static("flame"), false);
795
796 fn assert_round_trip(particle: ParticleData) {
797 let expected = particle.clone();
798 let mut encoded = Vec::new();
799 let result = particle.write(&mut encoded);
800 assert!(result.is_ok(), "{result:?}");
801
802 let mut cursor = Cursor::new(encoded.as_slice());
803 let decoded = ParticleData::read(&mut cursor);
804 let Ok(decoded) = decoded else {
805 panic!("failed to decode particle: {decoded:?}");
806 };
807 assert_eq!(cursor.position() as usize, encoded.len());
808 assert_eq!(decoded, expected);
809 }
810
811 #[test]
812 fn every_vanilla_particle_payload_family_round_trips() {
813 init_vanilla_registry();
814
815 assert_round_trip(ParticleData::simple(&vanilla_particle_types::FLAME));
816 assert_round_trip(ParticleData::new(
817 &vanilla_particle_types::BLOCK,
818 BlockParticleOption::new(BlockStateId(321)),
819 ));
820 assert_round_trip(ParticleData::new(
821 &vanilla_particle_types::ENTITY_EFFECT,
822 ColorParticleOption::new(ArgbColor::new(i32::from_be_bytes([0xAA, 0xBB, 0xCC, 0xDD]))),
823 ));
824 assert_round_trip(ParticleData::new(
825 &vanilla_particle_types::DUST,
826 DustParticleOptions::new(RgbColor::new(0x123456), 1.25),
827 ));
828 assert_round_trip(ParticleData::new(
829 &vanilla_particle_types::DUST_COLOR_TRANSITION,
830 DustColorTransitionOptions::new(RgbColor::new(0x123456), RgbColor::new(0x654321), 2.5),
831 ));
832 assert_round_trip(ParticleData::new(
833 &vanilla_particle_types::GEYSER,
834 GeyserParticleOptions::new(4),
835 ));
836 assert_round_trip(ParticleData::new(
837 &vanilla_particle_types::GEYSER_BASE,
838 GeyserBaseParticleOptions::new(7, 0.75),
839 ));
840 assert_round_trip(ParticleData::new(
841 &vanilla_particle_types::DRAGON_BREATH,
842 PowerParticleOption::new(0.4),
843 ));
844 assert_round_trip(ParticleData::new(
845 &vanilla_particle_types::EFFECT,
846 SpellParticleOption::new(RgbColor::new(0xABCDEF), 0.8),
847 ));
848 assert_round_trip(ParticleData::new(
849 &vanilla_particle_types::ITEM,
850 ItemParticleOption::new(ItemStackTemplate::new(&vanilla_items::STONE)),
851 ));
852 assert_round_trip(ParticleData::new(
853 &vanilla_particle_types::SCULK_CHARGE,
854 SculkChargeParticleOptions::new(0.25),
855 ));
856 assert_round_trip(ParticleData::new(
857 &vanilla_particle_types::SHRIEK,
858 ShriekParticleOption::new(17),
859 ));
860 assert_round_trip(ParticleData::new(
861 &vanilla_particle_types::TRAIL,
862 TrailParticleOption::new(DVec3::new(1.25, -2.5, 3.75), RgbColor::new(0x345678), 40),
863 ));
864 assert_round_trip(ParticleData::new(
865 &vanilla_particle_types::VIBRATION,
866 VibrationParticleOption::new(
867 PositionSource::new(
868 &vanilla_position_source_types::BLOCK,
869 BlockPositionSource::new(BlockPos::new(12, -34, 56)),
870 ),
871 9,
872 ),
873 ));
874 assert_round_trip(ParticleData::new(
875 &vanilla_particle_types::VIBRATION,
876 VibrationParticleOption::new(
877 PositionSource::new(
878 &vanilla_position_source_types::ENTITY,
879 EntityPositionSource::new(1234, 1.5),
880 ),
881 22,
882 ),
883 ));
884 }
885
886 #[test]
887 fn particle_write_rejects_noncanonical_same_key_codec() {
888 init_vanilla_registry();
889
890 let particle = ParticleData::new(
891 &FORGED_FLAME,
892 BlockParticleOption::new(BlockStateId::default()),
893 );
894 let mut encoded = Vec::new();
895 let result = particle.write(&mut encoded);
896
897 assert!(result.is_err());
898 assert_eq!(encoded.len(), 0);
899 }
900
901 #[test]
902 #[should_panic(expected = "Cannot register duplicate particle type key")]
903 fn particle_type_registry_rejects_duplicate_keys() {
904 let mut registry = ParticleTypeRegistry::new();
905 registry.register(&vanilla_particle_types::FLAME);
906 registry.register(&FORGED_FLAME);
907 }
908
909 #[test]
910 fn block_particle_network_codec_rejects_invalid_state_ids() {
911 init_vanilla_registry();
912
913 for id in [-1, i32::from(u16::MAX) + 1, i32::from(u16::MAX)] {
914 let mut encoded = Vec::new();
915 let encoded_result = VarInt(id).write(&mut encoded);
916 assert!(encoded_result.is_ok());
917
918 let mut cursor = Cursor::new(encoded.as_slice());
919 let decoded = BlockParticleOption::read_network(&mut cursor);
920 assert!(decoded.is_err(), "accepted invalid block state id {id}");
921 }
922
923 let invalid_state = BlockStateId(u16::MAX);
924 assert!(REGISTRY.blocks.by_state_id(invalid_state).is_none());
925
926 let mut encoded = Vec::new();
927 let result = BlockParticleOption::new(invalid_state).write_network(&mut encoded);
928 assert!(result.is_err());
929 assert_eq!(encoded.len(), 0);
930 }
931
932 #[test]
933 fn dust_scale_matches_vanilla_constructor_clamping() {
934 assert_eq!(
935 DustParticleOptions::new(RgbColor::new(0), 0.0).scale(),
936 0.01
937 );
938 assert_eq!(DustParticleOptions::new(RgbColor::new(0), 5.0).scale(), 4.0);
939 assert_eq!(
940 DustColorTransitionOptions::new(RgbColor::new(0), RgbColor::new(0), -1.0).scale(),
941 0.01
942 );
943 }
944}