1use 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#[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 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 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}