Skip to main content

steel_registry/
particle_type.rs

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
17/// Concrete network payload behavior for a registered particle type.
18pub 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
43/// A registered particle discriminator, limiter behavior, and payload codec.
44pub 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
78/// One registry-dispatched particle value, including its concrete payload.
79pub 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
221// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
222unsafe 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
253// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
254unsafe 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
298// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
299unsafe 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
339// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
340unsafe 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
388// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
389unsafe 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
427// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
428unsafe 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
468// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
469unsafe 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
501// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
502unsafe 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
539// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
540unsafe 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
572// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
573unsafe 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
604// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
605unsafe 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
636// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
637unsafe 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
684// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
685unsafe 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
731// SAFETY: This Steel-owned key uniquely identifies the concrete particle payload.
732unsafe 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}