1use std::ptr;
2
3use super::{
4 Arc, BLOCK_BEHAVIORS, BlockLootContext, BlockPos, BlockStateExt, BlockStateId, CLevelEvent,
5 CLevelParticles, CSound, ChunkPos, ConnectionProtocol, DVec3, EncodedPacket, Entity,
6 GLOBAL_SOUND_EVENTS, GameEventContext, ItemStack, LevelReader, LootContext, NetworkConnection,
7 ParticleData, Player, REGISTRY, RandomizationType, RegistryExt, SectionPos, SoundEventRef,
8 SoundSource, UpdateFlags, World, WorldEntityManager, entity_loot_ref, fluid_state_to_block,
9 level_events, vanilla_blocks, vanilla_game_events,
10};
11use crate::entity::loot_equipment;
12use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
13use steel_registry::sound_event::SoundEventHolder;
14use steel_registry::vanilla_particle_types::{BUBBLE, SPLASH};
15
16pub(super) fn sound_is_within_range(
17 sound: &SoundEventHolder,
18 volume: f32,
19 distance_squared: f64,
20) -> bool {
21 let range = f64::from(sound.range(volume));
22 distance_squared < range * range
23}
24
25fn sound_packet_for_player(
26 pos: DVec3,
27 player_pos: DVec3,
28 max_distance_squared: f64,
29 volume: f32,
30 min_volume: f32,
31) -> Option<(DVec3, f32)> {
32 let delta = pos - player_pos;
33 let distance_squared = delta.length_squared();
34 if distance_squared <= max_distance_squared {
35 return Some((pos, volume));
36 }
37 if min_volume <= 0.0 {
38 return None;
39 }
40 Some((
41 player_pos + delta / distance_squared.sqrt() * 2.0,
42 min_volume,
43 ))
44}
45
46impl World {
47 pub fn level_event(&self, event_type: i32, pos: BlockPos, data: i32, exclude: Option<i32>) {
58 let packet = CLevelEvent::new(event_type, pos, data, false);
59 let Ok(encoded) =
60 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
61 else {
62 log::warn!("Failed to encode level event packet");
63 return;
64 };
65
66 self.players.iter_players(|_, player| {
67 if exclude != Some(player.id())
68 && Self::recipient_within_64_blocks(player.position(), pos)
69 {
70 player.connection.send_encoded(encoded.clone());
71 }
72 true
73 });
74 }
75
76 pub(super) fn recipient_within_64_blocks(player_pos: DVec3, event_pos: BlockPos) -> bool {
77 const MAX_DISTANCE_SQ: f64 = 64.0 * 64.0;
78
79 let dx = f64::from(event_pos.x()) - player_pos.x;
80 let dy = f64::from(event_pos.y()) - player_pos.y;
81 let dz = f64::from(event_pos.z()) - player_pos.z;
82 dx * dx + dy * dy + dz * dz < MAX_DISTANCE_SQ
83 }
84
85 pub fn send_particles(
88 &self,
89 particle: ParticleData,
90 position: DVec3,
91 count: i32,
92 spread: DVec3,
93 speed: f64,
94 ) -> i32 {
95 self.send_particles_with_options(particle, false, false, position, count, spread, speed)
96 }
97
98 #[expect(
102 clippy::too_many_arguments,
103 reason = "keeps Vanilla's two particle visibility flags explicit"
104 )]
105 pub fn send_particles_with_options(
106 &self,
107 particle: ParticleData,
108 override_limiter: bool,
109 always_show: bool,
110 position: DVec3,
111 count: i32,
112 spread: DVec3,
113 speed: f64,
114 ) -> i32 {
115 let packet = CLevelParticles {
116 override_limiter,
117 always_show,
118 x: position.x,
119 y: position.y,
120 z: position.z,
121 x_dist: spread.x as f32,
122 y_dist: spread.y as f32,
123 z_dist: spread.z as f32,
124 x_max_speed: speed as f32,
125 y_max_speed: speed as f32,
126 z_max_speed: speed as f32,
127 count,
128 particle,
129 randomization_type: RandomizationType::default(),
130 };
131 let Ok(encoded) =
132 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
133 else {
134 log::warn!("Failed to encode level particles packet");
135 return 0;
136 };
137 let mut sent = 0;
138 self.players.iter_players(|_, player| {
139 if Self::particle_recipient_in_range(
140 player.block_position(),
141 position,
142 override_limiter,
143 ) {
144 player.connection.send_encoded(encoded.clone());
145 sent += 1;
146 }
147 true
148 });
149 sent
150 }
151
152 #[expect(
155 clippy::too_many_arguments,
156 reason = "mirrors Vanilla ServerLevel.sendParticles"
157 )]
158 pub fn send_particles_to(
159 self: &Arc<Self>,
160 player: &Player,
161 particle: ParticleData,
162 override_limiter: bool,
163 always_show: bool,
164 position: DVec3,
165 count: i32,
166 spread: DVec3,
167 speed: f64,
168 ) -> bool {
169 if !Arc::ptr_eq(self, &player.get_world())
170 || !Self::particle_recipient_in_range(
171 player.block_position(),
172 position,
173 override_limiter,
174 )
175 {
176 return false;
177 }
178
179 let packet = CLevelParticles {
180 override_limiter,
181 always_show,
182 x: position.x,
183 y: position.y,
184 z: position.z,
185 x_dist: spread.x as f32,
186 y_dist: spread.y as f32,
187 z_dist: spread.z as f32,
188 x_max_speed: speed as f32,
189 y_max_speed: speed as f32,
190 z_max_speed: speed as f32,
191 count,
192 particle,
193 randomization_type: RandomizationType::default(),
194 };
195 let Ok(encoded) =
196 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
197 else {
198 log::warn!("Failed to encode level particles packet");
199 return false;
200 };
201 player.connection.send_encoded(encoded);
202 true
203 }
204
205 pub(super) fn particle_recipient_in_range(
206 player_block_pos: BlockPos,
207 particle_pos: DVec3,
208 override_limiter: bool,
209 ) -> bool {
210 let (x, y, z) = player_block_pos.get_center();
211 let radius = if override_limiter { 512.0 } else { 32.0 };
212 DVec3::new(x, y, z).distance_squared(particle_pos) < radius * radius
213 }
214
215 pub fn send_bubble_column_particles(&self, pos: BlockPos) {
217 let x = f64::from(pos.x());
218 let y = f64::from(pos.y()) + 1.0;
219 let z = f64::from(pos.z());
220 for _ in 0..2 {
221 self.send_particles(
222 ParticleData::simple(&SPLASH),
223 DVec3::new(x + rand::random::<f64>(), y, z + rand::random::<f64>()),
224 1,
225 DVec3::ZERO,
226 1.0,
227 );
228 self.send_particles(
229 ParticleData::simple(&BUBBLE),
230 DVec3::new(x + rand::random::<f64>(), y, z + rand::random::<f64>()),
231 1,
232 DVec3::new(0.0, 0.01, 0.0),
233 0.2,
234 );
235 }
236 }
237
238 pub fn global_level_event(&self, event_type: i32, pos: BlockPos, data: i32) {
248 if !self.get_game_rule(&GLOBAL_SOUND_EVENTS) {
249 self.level_event(event_type, pos, data, None);
250 return;
251 }
252
253 let packet = CLevelEvent::new(event_type, pos, data, true);
254 self.players.iter_players(|_, player| {
255 player.send_packet(packet.clone());
256 true
257 });
258 }
259
260 pub fn destroy_block_effect(&self, pos: BlockPos, block_state_id: u32, exclude: Option<i32>) {
267 self.level_event(
268 level_events::PARTICLES_AND_SOUND_DESTROY_BLOCK,
269 pos,
270 block_state_id as i32,
271 exclude,
272 );
273 }
274
275 pub fn destroy_block(self: &Arc<Self>, pos: BlockPos, drop_items: bool) -> bool {
282 self.destroy_block_with_limit(pos, drop_items, Self::UPDATE_LIMIT)
283 }
284
285 pub fn remove_block(self: &Arc<Self>, pos: BlockPos, moved_by_piston: bool) -> bool {
289 let state = self.get_block_state(pos);
290 let replacement = fluid_state_to_block(state.get_fluid_state());
291 let mut flags = UpdateFlags::UPDATE_ALL;
292 if moved_by_piston {
293 flags |= UpdateFlags::UPDATE_MOVE_BY_PISTON;
294 }
295 self.set_block(pos, replacement, flags)
296 }
297
298 pub fn destroy_block_by_entity(
300 self: &Arc<Self>,
301 pos: BlockPos,
302 drop_items: bool,
303 entity: &dyn Entity,
304 ) -> bool {
305 self.destroy_block_with_limit_and_entity(pos, drop_items, Self::UPDATE_LIMIT, Some(entity))
306 }
307
308 pub fn destroy_block_with_limit(
313 self: &Arc<Self>,
314 pos: BlockPos,
315 drop_items: bool,
316 recursion_left: i32,
317 ) -> bool {
318 self.destroy_block_with_limit_and_entity(pos, drop_items, recursion_left, None)
319 }
320
321 pub(super) fn destroy_block_with_limit_and_entity(
322 self: &Arc<Self>,
323 pos: BlockPos,
324 drop_items: bool,
325 recursion_left: i32,
326 entity: Option<&dyn Entity>,
327 ) -> bool {
328 let state = self.get_block_state(pos);
329 if state.is_air() {
330 return false;
331 }
332
333 let block = state.get_block();
334 let is_fire = block == &vanilla_blocks::FIRE || block == &vanilla_blocks::SOUL_FIRE;
335 if !is_fire {
336 self.destroy_block_effect(pos, u32::from(state.0), None);
337 }
338
339 if drop_items {
340 self.drop_resources_with_entity(state, pos, entity);
341 if let Some(block_entity) = self.get_block_entity(pos)
347 && let Some(container_ref) = ContainerRef::from_block_entity(block_entity)
348 {
349 let mut guard = ContainerLockGuard::lock_all(&[&container_ref]);
350 if let Some(container) = guard.get_mut(container_ref.container_id()) {
351 for slot in 0..container.get_container_size() {
352 let item = container.remove_item_no_update(slot);
353 if !item.is_empty() {
354 self.pop_resource(pos, item);
355 }
356 }
357 }
358 }
359 }
360
361 let replacement = fluid_state_to_block(state.get_fluid_state());
364 let destroyed =
365 self.set_block_with_limit(pos, replacement, UpdateFlags::UPDATE_ALL, recursion_left);
366 if destroyed {
367 self.game_event(
368 &vanilla_game_events::BLOCK_DESTROY,
369 pos,
370 &GameEventContext::new(entity, Some(state)),
371 );
372 }
373 destroyed
374 }
375
376 pub fn drop_resources(self: &Arc<Self>, state: BlockStateId, pos: BlockPos) {
383 self.drop_resources_with_entity(state, pos, None);
384 }
385
386 pub(super) fn drop_resources_with_entity(
387 self: &Arc<Self>,
388 state: BlockStateId,
389 pos: BlockPos,
390 entity: Option<&dyn Entity>,
391 ) {
392 let context = BlockLootContext::new(self, pos).with_entity(entity);
393 for item in context.get_drops(state) {
394 if !item.is_empty() {
395 self.pop_resource(pos, item);
396 }
397 }
398 BLOCK_BEHAVIORS
399 .get_behavior(state.get_block())
400 .spawn_after_break(state, self, pos, &ItemStack::empty(), true);
401 }
402
403 pub(crate) fn block_drops(
404 state: BlockStateId,
405 context: &BlockLootContext<'_>,
406 ) -> Vec<ItemStack> {
407 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
408 behavior
409 .get_drops(state, context)
410 .unwrap_or_else(|| Self::default_block_drops(state, context))
411 }
412
413 pub(super) fn default_block_drops(
414 state: BlockStateId,
415 context: &BlockLootContext<'_>,
416 ) -> Vec<ItemStack> {
417 let block = state.get_block();
418 let loot_table = if let Some(loot_table) = block.config.loot_table {
419 loot_table
420 } else {
421 let loot_key = steel_utils::Identifier::vanilla(format!("blocks/{}", block.key.path));
422 let Some(loot_table) = REGISTRY.loot_tables.by_key(&loot_key) else {
423 return Vec::new();
424 };
425 loot_table
426 };
427
428 let mut rng = rand::rng();
429 let mut ctx = LootContext::new(&mut rng)
430 .with_luck(context.luck())
431 .with_block_state(state)
432 .with_origin(
433 f64::from(context.pos().x()),
434 f64::from(context.pos().y()),
435 f64::from(context.pos().z()),
436 );
437 if let Some(tool) = context.tool() {
438 ctx = ctx.with_tool(tool);
439 }
440 let entity_equipment = context.entity().and_then(loot_equipment);
441 if let Some(entity) = context.entity() {
442 ctx = ctx.with_this_entity(entity_loot_ref(entity, entity_equipment.as_ref()));
443 }
444 if let Some(radius) = context.explosion_radius() {
445 ctx = ctx.with_explosion(radius);
446 }
447
448 loot_table.get_random_items(&mut ctx)
449 }
450
451 pub fn play_sound(
465 &self,
466 sound: SoundEventRef,
467 source: SoundSource,
468 pos: BlockPos,
469 volume: f32,
470 pitch: f32,
471 exclude: Option<i32>,
472 ) {
473 self.play_sound_at(
474 sound,
475 source,
476 DVec3::new(
477 f64::from(pos.x()) + 0.5,
478 f64::from(pos.y()) + 0.5,
479 f64::from(pos.z()) + 0.5,
480 ),
481 volume,
482 pitch,
483 exclude,
484 );
485 }
486
487 pub fn play_sound_at(
489 &self,
490 sound: SoundEventRef,
491 source: SoundSource,
492 pos: DVec3,
493 volume: f32,
494 pitch: f32,
495 exclude: Option<i32>,
496 ) {
497 self.play_sound_holder_at(
498 &SoundEventHolder::Registry(sound),
499 source,
500 pos,
501 volume,
502 pitch,
503 exclude,
504 );
505 }
506
507 pub fn play_sound_holder_at(
509 &self,
510 sound: &SoundEventHolder,
511 source: SoundSource,
512 pos: DVec3,
513 volume: f32,
514 pitch: f32,
515 exclude: Option<i32>,
516 ) {
517 let chunk = ChunkPos::new(
518 SectionPos::block_to_section_coord(pos.x.floor() as i32),
519 SectionPos::block_to_section_coord(pos.z.floor() as i32),
520 );
521
522 let seed = rand::random::<i64>();
524 let packet = CSound::new_holder(sound.clone(), source, pos, volume, pitch, seed);
525 let Ok(encoded) =
526 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
527 else {
528 log::warn!("Failed to encode sound packet");
529 return;
530 };
531
532 for entity_id in self.player_area_map.get_tracking_players(chunk) {
534 if exclude == Some(entity_id) {
536 continue;
537 }
538 if let Some(player) = self.players.get_by_entity_id(entity_id) {
539 let player_pos = player.position();
540 let dx = player_pos.x - pos.x;
541 let dy = player_pos.y - pos.y;
542 let dz = player_pos.z - pos.z;
543 let dist_sq = dx * dx + dy * dy + dz * dz;
544
545 if sound_is_within_range(sound, volume, dist_sq) {
546 player.connection.send_encoded(encoded.clone());
547 }
548 }
549 }
550 }
551
552 #[expect(
556 clippy::too_many_arguments,
557 reason = "keeps the vanilla playsound parameters explicit"
558 )]
559 pub fn play_sound_to_players(
560 &self,
561 sound: &SoundEventHolder,
562 source: SoundSource,
563 pos: DVec3,
564 volume: f32,
565 pitch: f32,
566 min_volume: f32,
567 targets: &[Arc<Player>],
568 ) -> Vec<Arc<Player>> {
569 let max_distance = sound.range(volume);
570 let max_distance_squared = f64::from(max_distance * max_distance);
571 let seed = rand::random::<i64>();
572 let mut played_for = Vec::new();
573
574 for player in targets {
575 let player_world = player.get_world();
576 if !ptr::eq(self, player_world.as_ref()) {
577 continue;
578 }
579
580 let player_pos = player.position();
581 let Some((packet_pos, packet_volume)) =
582 sound_packet_for_player(pos, player_pos, max_distance_squared, volume, min_volume)
583 else {
584 continue;
585 };
586
587 player.send_packet(CSound::new_holder(
588 sound.clone(),
589 source,
590 packet_pos,
591 packet_volume,
592 pitch,
593 seed,
594 ));
595 played_for.push(Arc::clone(player));
596 }
597
598 played_for
599 }
600
601 pub fn play_block_sound(
613 &self,
614 sound: SoundEventRef,
615 pos: BlockPos,
616 volume: f32,
617 pitch: f32,
618 exclude: Option<i32>,
619 ) {
620 self.play_sound(sound, SoundSource::Blocks, pos, volume, pitch, exclude);
621 }
622
623 #[must_use]
625 pub(crate) const fn entity_manager(&self) -> &WorldEntityManager {
626 &self.entity_manager
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use glam::DVec3;
633
634 use super::sound_packet_for_player;
635
636 #[test]
637 fn sound_range_fallback_matches_vanilla_boundary_and_relocation() {
638 assert_eq!(
639 sound_packet_for_player(DVec3::ZERO, DVec3::new(16.0, 0.0, 0.0), 256.0, 1.0, 0.0),
640 Some((DVec3::ZERO, 1.0))
641 );
642 assert_eq!(
643 sound_packet_for_player(DVec3::ZERO, DVec3::new(17.0, 0.0, 0.0), 256.0, 1.0, 0.0),
644 None
645 );
646 assert_eq!(
647 sound_packet_for_player(DVec3::ZERO, DVec3::new(17.0, 0.0, 0.0), 256.0, 1.0, 0.25),
648 Some((DVec3::new(15.0, 0.0, 0.0), 0.25))
649 );
650 }
651}