steel_protocol/packets/game/
c_explode.rs1use 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#[derive(WriteTo, Clone, Debug)]
10pub struct ExplosionParticleInfo {
11 pub particle: ParticleData,
12 pub scaling: f32,
13 pub speed: f32,
14}
15
16#[derive(WriteTo, Clone, Debug)]
18struct WeightedExplosionParticleInfo {
19 value: ExplosionParticleInfo,
20 #[write(as = VarInt)]
21 weight: i32,
22}
23
24#[derive(WriteTo, Clone, Debug)]
26pub struct ExplosionParticlePalette(Vec<WeightedExplosionParticleInfo>);
27
28impl ExplosionParticlePalette {
29 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#[derive(Error, Clone, Copy, Debug, PartialEq, Eq)]
54pub enum ExplosionParticleWeightError {
55 #[error("explosion particle weight cannot be negative: {0}")]
57 Negative(i32),
58 #[error("total explosion particle weight exceeds i32::MAX")]
60 TotalOverflow,
61 #[error("explosion particle palette has too many entries: {0}")]
63 TooManyEntries(usize),
64}
65
66#[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}