Skip to main content

steel_protocol/packets/game/
c_explode.rs

1use glam::DVec3;
2use steel_macros::{ClientPacket, WriteTo};
3use steel_registry::{
4    packets::play::C_EXPLODE, particle_type::ParticleData, sound_event::SoundEventHolder,
5};
6use thiserror::Error;
7
8/// A block-debris particle and the client-side scaling applied to it.
9#[derive(WriteTo, Clone, Debug)]
10pub struct ExplosionParticleInfo {
11    pub particle: ParticleData,
12    pub scaling: f32,
13    pub speed: f32,
14}
15
16/// A weighted entry in an explosion's block-particle palette.
17#[derive(WriteTo, Clone, Debug)]
18struct WeightedExplosionParticleInfo {
19    value: ExplosionParticleInfo,
20    #[write(as = VarInt)]
21    weight: i32,
22}
23
24/// A validated weighted palette for an explosion's block particles.
25#[derive(WriteTo, Clone, Debug)]
26pub struct ExplosionParticlePalette(Vec<WeightedExplosionParticleInfo>);
27
28impl ExplosionParticlePalette {
29    /// Validates Vanilla's non-negative weights and signed 32-bit list limits.
30    pub fn try_new(
31        entries: Vec<(ExplosionParticleInfo, i32)>,
32    ) -> Result<Self, ExplosionParticleWeightError> {
33        if i32::try_from(entries.len()).is_err() {
34            return Err(ExplosionParticleWeightError::TooManyEntries(entries.len()));
35        }
36
37        let mut total = 0_i32;
38        let mut weighted = Vec::with_capacity(entries.len());
39        for (value, weight) in entries {
40            if weight < 0 {
41                return Err(ExplosionParticleWeightError::Negative(weight));
42            }
43            total = total
44                .checked_add(weight)
45                .ok_or(ExplosionParticleWeightError::TotalOverflow)?;
46            weighted.push(WeightedExplosionParticleInfo { value, weight });
47        }
48        Ok(Self(weighted))
49    }
50}
51
52/// An invalid block-particle weight supplied for an explosion packet.
53#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)]
54pub enum ExplosionParticleWeightError {
55    /// Vanilla rejects individual negative weights.
56    #[error("explosion particle weight cannot be negative: {0}")]
57    Negative(i32),
58    /// Vanilla rejects weighted lists whose total exceeds `i32::MAX`.
59    #[error("total explosion particle weight exceeds i32::MAX")]
60    TotalOverflow,
61    /// Vanilla's list codec uses a signed 32-bit entry count.
62    #[error("explosion particle palette has too many entries: {0}")]
63    TooManyEntries(usize),
64}
65
66/// Sent to apply the client-visible effects of a server-side explosion.
67#[derive(ClientPacket, WriteTo, Clone, Debug)]
68#[packet_id(Play = C_EXPLODE)]
69pub struct CExplode {
70    pub center: DVec3,
71    pub radius: f32,
72    pub block_count: i32,
73    pub player_knockback: Option<DVec3>,
74    pub explosion_particle: ParticleData,
75    pub explosion_sound: SoundEventHolder,
76    pub block_particles: ExplosionParticlePalette,
77    pub play_sound: bool,
78}
79
80#[cfg(test)]
81mod tests {
82    use glam::DVec3;
83    use steel_registry::{
84        RegistryEntry, init_vanilla_registry, particle_type::ParticleData,
85        sound_event::SoundEventHolder, sound_events, vanilla_particle_types,
86    };
87    use steel_utils::{Identifier, codec::VarInt, serial::WriteTo};
88
89    use super::{
90        CExplode, ExplosionParticleInfo, ExplosionParticlePalette, ExplosionParticleWeightError,
91    };
92
93    #[test]
94    fn writes_fields_in_vanilla_wire_order() {
95        init_vanilla_registry();
96        let packet = CExplode {
97            center: DVec3::new(1.25, -2.5, 3.75),
98            radius: 4.0,
99            block_count: 17,
100            player_knockback: Some(DVec3::new(-0.25, 0.5, 1.0)),
101            explosion_particle: ParticleData::simple(&vanilla_particle_types::EXPLOSION),
102            explosion_sound: SoundEventHolder::registry(&sound_events::ENTITY_GENERIC_EXPLODE),
103            block_particles: palette(vec![
104                (
105                    ExplosionParticleInfo {
106                        particle: ParticleData::simple(&vanilla_particle_types::POOF),
107                        scaling: 0.5,
108                        speed: 1.0,
109                    },
110                    2,
111                ),
112                (
113                    ExplosionParticleInfo {
114                        particle: ParticleData::simple(&vanilla_particle_types::SMOKE),
115                        scaling: 1.0,
116                        speed: 0.75,
117                    },
118                    3,
119                ),
120            ]),
121            play_sound: true,
122        };
123
124        let mut encoded = Vec::new();
125        assert!(packet.write(&mut encoded).is_ok());
126
127        let mut expected = Vec::new();
128        expected.extend_from_slice(&1.25_f64.to_be_bytes());
129        expected.extend_from_slice(&(-2.5_f64).to_be_bytes());
130        expected.extend_from_slice(&3.75_f64.to_be_bytes());
131        expected.extend_from_slice(&4.0_f32.to_be_bytes());
132        expected.extend_from_slice(&17_i32.to_be_bytes());
133        expected.push(1);
134        expected.extend_from_slice(&(-0.25_f64).to_be_bytes());
135        expected.extend_from_slice(&0.5_f64.to_be_bytes());
136        expected.extend_from_slice(&1.0_f64.to_be_bytes());
137        write_registry_id(&mut expected, vanilla_particle_types::EXPLOSION.id());
138        assert!(
139            VarInt(sound_events::ENTITY_GENERIC_EXPLODE.packet_holder_id())
140                .write(&mut expected)
141                .is_ok()
142        );
143
144        assert!(VarInt(2).write(&mut expected).is_ok());
145        write_registry_id(&mut expected, vanilla_particle_types::POOF.id());
146        expected.extend_from_slice(&0.5_f32.to_be_bytes());
147        expected.extend_from_slice(&1.0_f32.to_be_bytes());
148        assert!(VarInt(2).write(&mut expected).is_ok());
149        write_registry_id(&mut expected, vanilla_particle_types::SMOKE.id());
150        expected.extend_from_slice(&1.0_f32.to_be_bytes());
151        expected.extend_from_slice(&0.75_f32.to_be_bytes());
152        assert!(VarInt(3).write(&mut expected).is_ok());
153        expected.push(1);
154
155        assert_eq!(encoded, expected);
156    }
157
158    #[test]
159    fn writes_direct_explosion_sound_holder() {
160        init_vanilla_registry();
161        let sound_id = Identifier::from_steel("test_explosion");
162        let packet = CExplode {
163            center: DVec3::ZERO,
164            radius: 0.0,
165            block_count: 0,
166            player_knockback: None,
167            explosion_particle: ParticleData::simple(&vanilla_particle_types::EXPLOSION),
168            explosion_sound: SoundEventHolder::Direct {
169                sound_id: sound_id.clone(),
170                fixed_range: Some(24.0),
171            },
172            block_particles: palette(Vec::new()),
173            play_sound: false,
174        };
175
176        let mut encoded = Vec::new();
177        assert!(packet.write(&mut encoded).is_ok());
178
179        let mut expected = vec![0; 24];
180        expected.extend_from_slice(&0.0_f32.to_be_bytes());
181        expected.extend_from_slice(&0_i32.to_be_bytes());
182        expected.push(0);
183        write_registry_id(&mut expected, vanilla_particle_types::EXPLOSION.id());
184        assert!(VarInt(0).write(&mut expected).is_ok());
185        assert!(sound_id.write(&mut expected).is_ok());
186        expected.push(1);
187        expected.extend_from_slice(&24.0_f32.to_be_bytes());
188        assert!(VarInt(0).write(&mut expected).is_ok());
189
190        expected.push(0);
191        assert_eq!(encoded, expected);
192    }
193
194    #[test]
195    fn rejects_invalid_explosion_particle_weights() {
196        let particle = || ExplosionParticleInfo {
197            particle: ParticleData::simple(&vanilla_particle_types::POOF),
198            scaling: 1.0,
199            speed: 1.0,
200        };
201
202        assert!(matches!(
203            ExplosionParticlePalette::try_new(vec![(particle(), -1)]),
204            Err(ExplosionParticleWeightError::Negative(-1))
205        ));
206        assert!(matches!(
207            ExplosionParticlePalette::try_new(vec![(particle(), i32::MAX), (particle(), 1),]),
208            Err(ExplosionParticleWeightError::TotalOverflow)
209        ));
210    }
211
212    fn palette(entries: Vec<(ExplosionParticleInfo, i32)>) -> ExplosionParticlePalette {
213        let Ok(palette) = ExplosionParticlePalette::try_new(entries) else {
214            panic!("test explosion particle palette should be valid");
215        };
216        palette
217    }
218
219    fn write_registry_id(output: &mut Vec<u8>, id: usize) {
220        let Ok(id) = i32::try_from(id) else {
221            panic!("test registry id should fit in i32");
222        };
223        assert!(VarInt(id).write(output).is_ok());
224    }
225}