Skip to main content

steel_core/world/
block_event.rs

1//! Server-side block-event queue and client publication.
2
3use std::collections::VecDeque;
4use std::sync::Arc;
5
6use rustc_hash::FxHashSet;
7use steel_protocol::packet_traits::EncodedPacket;
8use steel_protocol::packets::game::CBlockEvent;
9use steel_protocol::utils::ConnectionProtocol;
10use steel_registry::RegistryEntry;
11use steel_registry::blocks::BlockRef;
12use steel_registry::blocks::block_state_ext::BlockStateExt as _;
13use steel_utils::{BlockPos, ChunkPos};
14
15use super::World;
16use crate::behavior::BLOCK_BEHAVIORS;
17use crate::entity::Entity as _;
18use crate::player::connection::NetworkConnection as _;
19
20#[derive(Clone, Copy, Debug)]
21struct BlockEventData {
22    pos: BlockPos,
23    block: BlockRef,
24    param_a: i32,
25    param_b: i32,
26}
27
28impl BlockEventData {
29    fn key(self) -> BlockEventKey {
30        BlockEventKey {
31            pos: self.pos,
32            block_id: self.block.id(),
33            param_a: self.param_a,
34            param_b: self.param_b,
35        }
36    }
37}
38
39#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
40struct BlockEventKey {
41    pos: BlockPos,
42    block_id: usize,
43    param_a: i32,
44    param_b: i32,
45}
46
47/// Insertion-ordered set matching Vanilla's `ObjectLinkedOpenHashSet<BlockEventData>`.
48#[derive(Default)]
49pub(super) struct BlockEventQueue {
50    events: VecDeque<BlockEventData>,
51    members: FxHashSet<BlockEventKey>,
52}
53
54impl BlockEventQueue {
55    fn push(&mut self, event: BlockEventData) -> bool {
56        if !self.members.insert(event.key()) {
57            return false;
58        }
59        self.events.push_back(event);
60        true
61    }
62
63    fn pop_front(&mut self) -> Option<BlockEventData> {
64        let event = self.events.pop_front()?;
65        let removed = self.members.remove(&event.key());
66        debug_assert!(removed, "queued block event must have a membership entry");
67        Some(event)
68    }
69
70    #[cfg(test)]
71    fn len(&self) -> usize {
72        self.events.len()
73    }
74}
75
76impl World {
77    /// Queues a server block event for the next eligible block-event pass.
78    ///
79    /// Exact duplicates are suppressed while queued. Parameters remain `i32`
80    /// for Vanilla behavior parity and are truncated to bytes only for the
81    /// client packet.
82    pub fn block_event(&self, pos: BlockPos, block: BlockRef, param_a: i32, param_b: i32) {
83        self.block_events.lock().push(BlockEventData {
84            pos,
85            block,
86            param_a,
87            param_b,
88        });
89    }
90
91    /// Runs queued block events in Vanilla insertion order.
92    ///
93    /// Vanilla gates this queue only on simulation range and may synchronously
94    /// load chunks from a piston callback. Steel additionally requires the
95    /// confirmed radius-1 Full neighborhood so the game tick never waits for
96    /// chunk I/O. Deferred events retain their Vanilla retry behavior.
97    pub(crate) fn run_block_events(self: &Arc<Self>) {
98        let mut deferred = Vec::new();
99
100        loop {
101            let event = self.block_events.lock().pop_front();
102            let Some(event) = event else {
103                break;
104            };
105
106            let chunk_pos = ChunkPos::from_block_pos(event.pos);
107            if !self
108                .chunk_map
109                .is_block_ticking_full_chunk_simulated(chunk_pos)
110            {
111                deferred.push(event);
112                continue;
113            }
114
115            if self.do_block_event(event) {
116                self.broadcast_block_event(event);
117            }
118        }
119
120        if deferred.is_empty() {
121            return;
122        }
123        let mut queue = self.block_events.lock();
124        for event in deferred {
125            queue.push(event);
126        }
127    }
128
129    fn do_block_event(self: &Arc<Self>, event: BlockEventData) -> bool {
130        let state = self.get_block_state(event.pos);
131        if state.get_block().id() != event.block.id() {
132            return false;
133        }
134
135        BLOCK_BEHAVIORS.get_behavior(event.block).trigger_event(
136            state,
137            self,
138            event.pos,
139            event.param_a,
140            event.param_b,
141        )
142    }
143
144    fn broadcast_block_event(&self, event: BlockEventData) {
145        let packet = CBlockEvent::new(
146            event.pos,
147            event.param_a as u8,
148            event.param_b as u8,
149            event.block.id() as i32,
150        );
151        let Ok(encoded) =
152            EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
153        else {
154            tracing::warn!(
155                pos = ?event.pos,
156                block = %event.block.key,
157                "Failed to encode block event packet"
158            );
159            return;
160        };
161
162        self.players.iter_players(|_, player| {
163            if Self::recipient_within_64_blocks(player.position(), event.pos) {
164                player.connection.send_encoded(encoded.clone());
165            }
166            true
167        });
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use steel_registry::{init_vanilla_registry, vanilla_blocks};
174    use steel_utils::Downcast as _;
175    use steel_utils::types::UpdateFlags;
176
177    use super::*;
178    use crate::behavior::init_behaviors;
179    use crate::block_entity::entities::EndGatewayBlockEntity;
180    use crate::chunk::chunk_ticket_manager::ChunkTicketLevel;
181    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
182
183    #[test]
184    fn ordered_queue_suppresses_only_exact_duplicates() {
185        init_vanilla_registry();
186        let mut queue = BlockEventQueue::default();
187        let first = BlockEventData {
188            pos: BlockPos::new(1, 2, 3),
189            block: &vanilla_blocks::STONE,
190            param_a: 4,
191            param_b: 5,
192        };
193        let second = BlockEventData {
194            param_b: 6,
195            ..first
196        };
197
198        assert!(queue.push(first));
199        assert!(!queue.push(first));
200        assert!(queue.push(second));
201        assert_eq!(queue.len(), 2);
202
203        let popped_first = queue.pop_front().expect("the first event should be queued");
204        assert!(
205            queue.push(first),
206            "a callback may requeue the event after it was popped"
207        );
208        let popped_second = queue
209            .pop_front()
210            .expect("the second event should be queued");
211        let requeued_first = queue
212            .pop_front()
213            .expect("the callback event should be appended");
214        assert_eq!(popped_first.key(), first.key());
215        assert_eq!(popped_second.key(), second.key());
216        assert_eq!(requeued_first.key(), first.key());
217        assert!(queue.pop_front().is_none());
218    }
219
220    #[test]
221    fn server_queue_defers_then_dispatches_the_current_block_event() {
222        init_vanilla_registry();
223        init_behaviors();
224        let world = fresh_test_world("server_block_event_queue");
225        let pos = BlockPos::new(1, 64, 1);
226        let holder = insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
227        assert!(world.set_block(
228            pos,
229            vanilla_blocks::END_GATEWAY.default_state(),
230            UpdateFlags::UPDATE_NONE,
231        ));
232        let gateway = world
233            .get_block_entity(pos)
234            .expect("placing an end gateway should create its block entity");
235        assert!(
236            !gateway
237                .downcast_ref::<EndGatewayBlockEntity>()
238                .expect("the gateway should have its concrete block entity")
239                .is_cooling_down()
240        );
241
242        world.block_event(pos, &vanilla_blocks::STONE, 1, 0);
243        world.run_block_events();
244        assert_eq!(world.block_events.lock().len(), 0);
245        assert!(
246            !gateway
247                .downcast_ref::<EndGatewayBlockEntity>()
248                .expect("the gateway should remain concrete")
249                .is_cooling_down()
250        );
251
252        world.block_event(pos, &vanilla_blocks::END_GATEWAY, 2, 0);
253        world.run_block_events();
254        assert_eq!(world.block_events.lock().len(), 0);
255        assert!(
256            !gateway
257                .downcast_ref::<EndGatewayBlockEntity>()
258                .expect("the gateway should remain concrete")
259                .is_cooling_down()
260        );
261
262        holder.set_simulation_level(None);
263        world.block_event(pos, &vanilla_blocks::END_GATEWAY, 1, 0);
264        world.block_event(pos, &vanilla_blocks::END_GATEWAY, 1, 0);
265        assert_eq!(world.block_events.lock().len(), 1);
266        world.run_block_events();
267        assert_eq!(world.block_events.lock().len(), 1);
268        assert!(
269            !gateway
270                .downcast_ref::<EndGatewayBlockEntity>()
271                .expect("the gateway should remain concrete")
272                .is_cooling_down()
273        );
274
275        holder.set_simulation_level(Some(ChunkTicketLevel::BLOCK_TICKING_CHUNK));
276        world.run_block_events();
277
278        assert_eq!(world.block_events.lock().len(), 0);
279        assert!(
280            gateway
281                .downcast_ref::<EndGatewayBlockEntity>()
282                .expect("the gateway should remain concrete")
283                .is_cooling_down()
284        );
285    }
286}