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, RegistryExt, SectionPos, SoundEventRef, SoundSource,
8 UpdateFlags, World, WorldEntityManager, entity_loot_ref, fluid_state_to_block, level_events,
9 vanilla_blocks, vanilla_game_events,
10};
11use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
12use steel_registry::sound_event::SoundEventHolder;
13use steel_registry::vanilla_particle_types::{BUBBLE, SPLASH};
14
15pub(super) fn sound_is_within_range(
16 sound: SoundEventRef,
17 volume: f32,
18 distance_squared: f64,
19) -> bool {
20 let range = f64::from(sound.range(volume));
21 distance_squared < range * range
22}
23
24fn sound_packet_for_player(
25 pos: DVec3,
26 player_pos: DVec3,
27 max_distance_squared: f64,
28 volume: f32,
29 min_volume: f32,
30) -> Option<(DVec3, f32)> {
31 let delta = pos - player_pos;
32 let distance_squared = delta.length_squared();
33 if distance_squared <= max_distance_squared {
34 return Some((pos, volume));
35 }
36 if min_volume <= 0.0 {
37 return None;
38 }
39 Some((
40 player_pos + delta / distance_squared.sqrt() * 2.0,
41 min_volume,
42 ))
43}
44
45impl World {
46 pub fn level_event(&self, event_type: i32, pos: BlockPos, data: i32, exclude: Option<i32>) {
57 let packet = CLevelEvent::new(event_type, pos, data, false);
58 let Ok(encoded) =
59 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
60 else {
61 log::warn!("Failed to encode level event packet");
62 return;
63 };
64
65 self.players.iter_players(|_, player| {
66 if exclude != Some(player.id())
67 && Self::recipient_within_64_blocks(player.position(), pos)
68 {
69 player.connection.send_encoded(encoded.clone());
70 }
71 true
72 });
73 }
74
75 pub(super) fn recipient_within_64_blocks(player_pos: DVec3, event_pos: BlockPos) -> bool {
76 const MAX_DISTANCE_SQ: f64 = 64.0 * 64.0;
77
78 let dx = f64::from(event_pos.x()) - player_pos.x;
79 let dy = f64::from(event_pos.y()) - player_pos.y;
80 let dz = f64::from(event_pos.z()) - player_pos.z;
81 dx * dx + dy * dy + dz * dz < MAX_DISTANCE_SQ
82 }
83
84 pub fn send_particles(
87 &self,
88 particle: ParticleData,
89 position: DVec3,
90 count: i32,
91 spread: DVec3,
92 speed: f64,
93 ) -> i32 {
94 self.send_particles_with_options(particle, false, false, position, count, spread, speed)
95 }
96
97 #[expect(
101 clippy::too_many_arguments,
102 reason = "keeps Vanilla's two particle visibility flags explicit"
103 )]
104 pub fn send_particles_with_options(
105 &self,
106 particle: ParticleData,
107 override_limiter: bool,
108 always_show: bool,
109 position: DVec3,
110 count: i32,
111 spread: DVec3,
112 speed: f64,
113 ) -> i32 {
114 let packet = CLevelParticles {
115 override_limiter,
116 always_show,
117 x: position.x,
118 y: position.y,
119 z: position.z,
120 x_dist: spread.x as f32,
121 y_dist: spread.y as f32,
122 z_dist: spread.z as f32,
123 max_speed: speed as f32,
124 count,
125 particle,
126 };
127 let Ok(encoded) =
128 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
129 else {
130 log::warn!("Failed to encode level particles packet");
131 return 0;
132 };
133 let mut sent = 0;
134 self.players.iter_players(|_, player| {
135 if Self::particle_recipient_in_range(
136 player.block_position(),
137 position,
138 override_limiter,
139 ) {
140 player.connection.send_encoded(encoded.clone());
141 sent += 1;
142 }
143 true
144 });
145 sent
146 }
147
148 #[expect(
151 clippy::too_many_arguments,
152 reason = "mirrors Vanilla ServerLevel.sendParticles"
153 )]
154 pub fn send_particles_to(
155 self: &Arc<Self>,
156 player: &Player,
157 particle: ParticleData,
158 override_limiter: bool,
159 always_show: bool,
160 position: DVec3,
161 count: i32,
162 spread: DVec3,
163 speed: f64,
164 ) -> bool {
165 if !Arc::ptr_eq(self, &player.get_world())
166 || !Self::particle_recipient_in_range(
167 player.block_position(),
168 position,
169 override_limiter,
170 )
171 {
172 return false;
173 }
174
175 let packet = CLevelParticles {
176 override_limiter,
177 always_show,
178 x: position.x,
179 y: position.y,
180 z: position.z,
181 x_dist: spread.x as f32,
182 y_dist: spread.y as f32,
183 z_dist: spread.z as f32,
184 max_speed: speed as f32,
185 count,
186 particle,
187 };
188 let Ok(encoded) =
189 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
190 else {
191 log::warn!("Failed to encode level particles packet");
192 return false;
193 };
194 player.connection.send_encoded(encoded);
195 true
196 }
197
198 pub(super) fn particle_recipient_in_range(
199 player_block_pos: BlockPos,
200 particle_pos: DVec3,
201 override_limiter: bool,
202 ) -> bool {
203 let (x, y, z) = player_block_pos.get_center();
204 let radius = if override_limiter { 512.0 } else { 32.0 };
205 DVec3::new(x, y, z).distance_squared(particle_pos) < radius * radius
206 }
207
208 pub fn send_bubble_column_particles(&self, pos: BlockPos) {
210 let x = f64::from(pos.x());
211 let y = f64::from(pos.y()) + 1.0;
212 let z = f64::from(pos.z());
213 for _ in 0..2 {
214 self.send_particles(
215 ParticleData::simple(&SPLASH),
216 DVec3::new(x + rand::random::<f64>(), y, z + rand::random::<f64>()),
217 1,
218 DVec3::ZERO,
219 1.0,
220 );
221 self.send_particles(
222 ParticleData::simple(&BUBBLE),
223 DVec3::new(x + rand::random::<f64>(), y, z + rand::random::<f64>()),
224 1,
225 DVec3::new(0.0, 0.01, 0.0),
226 0.2,
227 );
228 }
229 }
230
231 pub fn global_level_event(&self, event_type: i32, pos: BlockPos, data: i32) {
241 if !self.get_game_rule(&GLOBAL_SOUND_EVENTS) {
242 self.level_event(event_type, pos, data, None);
243 return;
244 }
245
246 let packet = CLevelEvent::new(event_type, pos, data, true);
247 self.players.iter_players(|_, player| {
248 player.send_packet(packet.clone());
249 true
250 });
251 }
252
253 pub fn destroy_block_effect(&self, pos: BlockPos, block_state_id: u32, exclude: Option<i32>) {
262 self.level_event(
263 level_events::PARTICLES_DESTROY_BLOCK,
264 pos,
265 block_state_id as i32,
266 exclude,
267 );
268 }
269
270 pub fn destroy_block(self: &Arc<Self>, pos: BlockPos, drop_items: bool) -> bool {
277 self.destroy_block_with_limit(pos, drop_items, Self::UPDATE_LIMIT)
278 }
279
280 pub fn remove_block(self: &Arc<Self>, pos: BlockPos, moved_by_piston: bool) -> bool {
284 let state = self.get_block_state(pos);
285 let replacement = fluid_state_to_block(state.get_fluid_state());
286 let mut flags = UpdateFlags::UPDATE_ALL;
287 if moved_by_piston {
288 flags |= UpdateFlags::UPDATE_MOVE_BY_PISTON;
289 }
290 self.set_block(pos, replacement, flags)
291 }
292
293 pub fn destroy_block_by_entity(
295 self: &Arc<Self>,
296 pos: BlockPos,
297 drop_items: bool,
298 entity: &dyn Entity,
299 ) -> bool {
300 self.destroy_block_with_limit_and_entity(pos, drop_items, Self::UPDATE_LIMIT, Some(entity))
301 }
302
303 pub fn destroy_block_with_limit(
308 self: &Arc<Self>,
309 pos: BlockPos,
310 drop_items: bool,
311 recursion_left: i32,
312 ) -> bool {
313 self.destroy_block_with_limit_and_entity(pos, drop_items, recursion_left, None)
314 }
315
316 pub(super) fn destroy_block_with_limit_and_entity(
317 self: &Arc<Self>,
318 pos: BlockPos,
319 drop_items: bool,
320 recursion_left: i32,
321 entity: Option<&dyn Entity>,
322 ) -> bool {
323 let state = self.get_block_state(pos);
324 if state.is_air() {
325 return false;
326 }
327
328 let block = state.get_block();
329 let is_fire = block == &vanilla_blocks::FIRE || block == &vanilla_blocks::SOUL_FIRE;
330 if !is_fire {
331 self.destroy_block_effect(pos, u32::from(state.0), None);
332 }
333
334 if drop_items {
335 self.drop_resources_with_entity(state, pos, entity);
336 if let Some(block_entity) = self.get_block_entity(pos)
342 && let Some(container_ref) = ContainerRef::from_block_entity(block_entity)
343 {
344 let mut guard = ContainerLockGuard::lock_all(&[&container_ref]);
345 if let Some(container) = guard.get_mut(container_ref.container_id()) {
346 for slot in 0..container.get_container_size() {
347 let item = container.remove_item_no_update(slot);
348 if !item.is_empty() {
349 self.pop_resource(pos, item);
350 }
351 }
352 }
353 }
354 }
355
356 let replacement = fluid_state_to_block(state.get_fluid_state());
359 let destroyed =
360 self.set_block_with_limit(pos, replacement, UpdateFlags::UPDATE_ALL, recursion_left);
361 if destroyed {
362 self.game_event(
363 &vanilla_game_events::BLOCK_DESTROY,
364 pos,
365 &GameEventContext::new(entity, Some(state)),
366 );
367 }
368 destroyed
369 }
370
371 pub fn drop_resources(self: &Arc<Self>, state: BlockStateId, pos: BlockPos) {
378 self.drop_resources_with_entity(state, pos, None);
379 }
380
381 pub(super) fn drop_resources_with_entity(
382 self: &Arc<Self>,
383 state: BlockStateId,
384 pos: BlockPos,
385 entity: Option<&dyn Entity>,
386 ) {
387 let context = BlockLootContext::new(self, pos).with_entity(entity);
388 for item in context.get_drops(state) {
389 if !item.is_empty() {
390 self.pop_resource(pos, item);
391 }
392 }
393 BLOCK_BEHAVIORS
394 .get_behavior(state.get_block())
395 .spawn_after_break(state, self, pos, &ItemStack::empty(), true);
396 }
397
398 pub(crate) fn block_drops(
399 state: BlockStateId,
400 context: &BlockLootContext<'_>,
401 ) -> Vec<ItemStack> {
402 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
403 behavior
404 .get_drops(state, context)
405 .unwrap_or_else(|| Self::default_block_drops(state, context))
406 }
407
408 pub(super) fn default_block_drops(
409 state: BlockStateId,
410 context: &BlockLootContext<'_>,
411 ) -> Vec<ItemStack> {
412 let block = state.get_block();
413 let loot_key = steel_utils::Identifier::vanilla(format!("blocks/{}", block.key.path));
414
415 let Some(loot_table) = REGISTRY.loot_tables.by_key(&loot_key) else {
416 return Vec::new();
417 };
418
419 let mut rng = rand::rng();
420 let mut ctx = LootContext::new(&mut rng)
421 .with_luck(context.luck())
422 .with_block_state(state)
423 .with_origin(
424 f64::from(context.pos().x()),
425 f64::from(context.pos().y()),
426 f64::from(context.pos().z()),
427 );
428 if let Some(tool) = context.tool() {
429 ctx = ctx.with_tool(tool);
430 }
431 if let Some(entity) = context.entity() {
432 ctx = ctx.with_this_entity(entity_loot_ref(entity));
433 }
434
435 loot_table.get_random_items(&mut ctx)
436 }
437
438 pub fn play_sound(
452 &self,
453 sound: SoundEventRef,
454 source: SoundSource,
455 pos: BlockPos,
456 volume: f32,
457 pitch: f32,
458 exclude: Option<i32>,
459 ) {
460 self.play_sound_at(
461 sound,
462 source,
463 DVec3::new(
464 f64::from(pos.x()) + 0.5,
465 f64::from(pos.y()) + 0.5,
466 f64::from(pos.z()) + 0.5,
467 ),
468 volume,
469 pitch,
470 exclude,
471 );
472 }
473
474 pub fn play_sound_at(
476 &self,
477 sound: SoundEventRef,
478 source: SoundSource,
479 pos: DVec3,
480 volume: f32,
481 pitch: f32,
482 exclude: Option<i32>,
483 ) {
484 let chunk = ChunkPos::new(
485 SectionPos::block_to_section_coord(pos.x.floor() as i32),
486 SectionPos::block_to_section_coord(pos.z.floor() as i32),
487 );
488
489 let seed = rand::random::<i64>();
491 let packet = CSound::new(sound, source, pos, volume, pitch, seed);
492 let Ok(encoded) =
493 EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
494 else {
495 log::warn!("Failed to encode sound packet");
496 return;
497 };
498
499 for entity_id in self.player_area_map.get_tracking_players(chunk) {
501 if exclude == Some(entity_id) {
503 continue;
504 }
505 if let Some(player) = self.players.get_by_entity_id(entity_id) {
506 let player_pos = player.position();
507 let dx = player_pos.x - pos.x;
508 let dy = player_pos.y - pos.y;
509 let dz = player_pos.z - pos.z;
510 let dist_sq = dx * dx + dy * dy + dz * dz;
511
512 if sound_is_within_range(sound, volume, dist_sq) {
513 player.connection.send_encoded(encoded.clone());
514 }
515 }
516 }
517 }
518
519 #[expect(
523 clippy::too_many_arguments,
524 reason = "keeps the vanilla playsound parameters explicit"
525 )]
526 pub fn play_sound_to_players(
527 &self,
528 sound: &SoundEventHolder,
529 source: SoundSource,
530 pos: DVec3,
531 volume: f32,
532 pitch: f32,
533 min_volume: f32,
534 targets: &[Arc<Player>],
535 ) -> Vec<Arc<Player>> {
536 let max_distance = sound.range(volume);
537 let max_distance_squared = f64::from(max_distance * max_distance);
538 let seed = rand::random::<i64>();
539 let mut played_for = Vec::new();
540
541 for player in targets {
542 let player_world = player.get_world();
543 if !ptr::eq(self, player_world.as_ref()) {
544 continue;
545 }
546
547 let player_pos = player.position();
548 let Some((packet_pos, packet_volume)) =
549 sound_packet_for_player(pos, player_pos, max_distance_squared, volume, min_volume)
550 else {
551 continue;
552 };
553
554 player.send_packet(CSound::new_holder(
555 sound.clone(),
556 source,
557 packet_pos,
558 packet_volume,
559 pitch,
560 seed,
561 ));
562 played_for.push(Arc::clone(player));
563 }
564
565 played_for
566 }
567
568 pub fn play_block_sound(
580 &self,
581 sound: SoundEventRef,
582 pos: BlockPos,
583 volume: f32,
584 pitch: f32,
585 exclude: Option<i32>,
586 ) {
587 self.play_sound(sound, SoundSource::Blocks, pos, volume, pitch, exclude);
588 }
589
590 #[must_use]
592 pub(crate) const fn entity_manager(&self) -> &WorldEntityManager {
593 &self.entity_manager
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use glam::DVec3;
600
601 use super::sound_packet_for_player;
602
603 #[test]
604 fn sound_range_fallback_matches_vanilla_boundary_and_relocation() {
605 assert_eq!(
606 sound_packet_for_player(DVec3::ZERO, DVec3::new(16.0, 0.0, 0.0), 256.0, 1.0, 0.0),
607 Some((DVec3::ZERO, 1.0))
608 );
609 assert_eq!(
610 sound_packet_for_player(DVec3::ZERO, DVec3::new(17.0, 0.0, 0.0), 256.0, 1.0, 0.0),
611 None
612 );
613 assert_eq!(
614 sound_packet_for_player(DVec3::ZERO, DVec3::new(17.0, 0.0, 0.0), 256.0, 1.0, 0.25),
615 Some((DVec3::new(15.0, 0.0, 0.0), 0.25))
616 );
617 }
618}