Skip to main content

steel_core/world/
events.rs

1use super::{
2    Arc, BlockEntityTypeRef, BlockPos, CBlockDestruction, ChunkPos, DVec3, ItemEntity, ItemStack,
3    NbtCompound, RegistryEntry, SectionPos, World,
4};
5
6/// Generates a random value using triangle distribution.
7///
8/// Mirrors vanilla's `RandomSource.triangle(mode, deviation)`.
9/// Produces values centered around `mode` with a spread of `deviation`.
10fn triangle_random(mode: f64, deviation: f64) -> f64 {
11    mode + deviation * (rand::random::<f64>() - rand::random::<f64>())
12}
13
14impl World {
15    /// Broadcasts block destruction progress to nearby players.
16    ///
17    /// Note: The packet is NOT sent to the player doing the breaking (matching vanilla).
18    /// The breaking player sees progress through client-side prediction.
19    ///
20    /// # Arguments
21    /// * `entity_id` - The entity ID of the player breaking the block
22    /// * `pos` - The position of the block being broken
23    /// * `progress` - The destruction progress (0-9), or -1 to clear
24    #[expect(
25        clippy::cast_sign_loss,
26        reason = "value is clamped to -1..=9 before cast; -1 wraps intentionally to 255 as sentinel"
27    )]
28    pub fn broadcast_block_destruction(&self, entity_id: i32, pos: BlockPos, progress: i32) {
29        let chunk = ChunkPos::new(
30            SectionPos::block_to_section_coord(pos.x()),
31            SectionPos::block_to_section_coord(pos.z()),
32        );
33        let packet = CBlockDestruction {
34            id: entity_id,
35            pos,
36            progress: progress.clamp(-1, 9) as u8,
37        };
38        self.broadcast_to_nearby(chunk, packet, Some(entity_id));
39    }
40
41    /// Broadcasts a block entity update to all players tracking the chunk.
42    ///
43    /// This is used when block entity data changes (e.g., sign text updated).
44    ///
45    /// # Arguments
46    /// * `pos` - The position of the block entity
47    /// * `block_entity_type` - The type of block entity
48    /// * `nbt` - The NBT data to send
49    pub fn broadcast_block_entity_update(
50        &self,
51        pos: BlockPos,
52        block_entity_type: BlockEntityTypeRef,
53        nbt: NbtCompound,
54    ) {
55        use steel_protocol::packets::game::CBlockEntityData;
56        use steel_utils::serial::OptionalNbt;
57
58        let chunk = ChunkPos::new(
59            SectionPos::block_to_section_coord(pos.x()),
60            SectionPos::block_to_section_coord(pos.z()),
61        );
62
63        // Get the block entity type ID from the registry
64        let type_id = block_entity_type.id();
65
66        let packet = CBlockEntityData {
67            pos,
68            block_entity_type: type_id as i32,
69            nbt: OptionalNbt(Some(nbt)),
70        };
71
72        self.broadcast_to_nearby(chunk, packet, None);
73    }
74
75    /// Broadcasts the current block-entity update packet when that entity type
76    /// exposes client-visible update data.
77    pub(crate) fn broadcast_block_entity_if_needed(&self, pos: BlockPos) {
78        let Some(block_entity) = self.get_block_entity(pos) else {
79            return;
80        };
81        let update = block_entity
82            .get_update_tag()
83            .map(|tag| (block_entity.get_type(), tag));
84        if let Some((block_entity_type, tag)) = update {
85            self.broadcast_block_entity_update(pos, block_entity_type, tag);
86        }
87    }
88
89    /// Drops an item stack at the given position with scatter behavior.
90    ///
91    /// Mirrors vanilla's `Containers.dropItemStack`. Splits large stacks into
92    /// multiple item entities (10-30 items each) and scatters them with random
93    /// positions and velocities.
94    ///
95    /// # Arguments
96    /// * `pos` - The block position to drop the item at
97    /// * `item` - The item stack to drop
98    pub fn drop_item_stack(self: &Arc<Self>, pos: BlockPos, mut item: ItemStack) {
99        use crate::entity::next_entity_id;
100        use steel_registry::vanilla_entities;
101
102        // Random velocity using triangle distribution (vanilla uses random.triangle)
103        // Vanilla constant: 0.05F * Mth.SQRT_OF_TWO (sqrt(2) * 0.05 ≈ 0.1148...)
104        const VELOCITY_SPREAD: f64 = 0.114_850_001_711_398_36;
105
106        if item.is_empty() {
107            return;
108        }
109
110        // Vanilla uses EntityType.ITEM dimensions for position calculation
111        let item_width = f64::from(vanilla_entities::ITEM.dimensions.width);
112        let center_range = 1.0 - item_width;
113        let half_size = item_width / 2.0;
114
115        // Keep spawning item entities until the stack is empty
116        // Vanilla splits stacks into 10-30 items each
117        while !item.is_empty() {
118            // Split off 10-30 items (or remaining if less)
119            let split_count = (rand::random::<u32>() % 21 + 10) as i32;
120            let split_stack = item.split(split_count);
121
122            if split_stack.is_empty() {
123                break;
124            }
125
126            // Random position within the block (vanilla logic)
127            let x = f64::from(pos.x()).floor() + rand::random::<f64>() * center_range + half_size;
128            let y = f64::from(pos.y()).floor() + rand::random::<f64>() * center_range;
129            let z = f64::from(pos.z()).floor() + rand::random::<f64>() * center_range + half_size;
130
131            // triangle(mode, deviation) produces values centered around mode with spread of deviation
132            let vx = triangle_random(0.0, VELOCITY_SPREAD);
133            let vy = triangle_random(0.2, VELOCITY_SPREAD);
134            let vz = triangle_random(0.0, VELOCITY_SPREAD);
135
136            let entity_id = next_entity_id();
137            let entity = Arc::new(ItemEntity::with_item_and_velocity(
138                &vanilla_entities::ITEM,
139                entity_id,
140                DVec3::new(x, y, z),
141                split_stack,
142                DVec3::new(vx, vy, vz),
143                Arc::downgrade(self),
144            ));
145            entity.set_default_pickup_delay();
146            if let Err(error) = self.try_add_entity(entity) {
147                log::warn!("Failed to drop item stack entity: {error}");
148            }
149        }
150    }
151}