Skip to main content

steel_core/world/
broadcasts.rs

1use super::{
2    Arc, CPlayerChat, CSystemChat, ChunkPos, ClientPacket, ConnectionProtocol, EncodedPacket,
3    Entity, EntityMovementSyncPacket, LastSeen, NetworkConnection, Player, PlayerChunkView, World,
4};
5
6impl World {
7    /// Broadcasts a signed chat message to all players in the world.
8    ///
9    /// # Panics
10    /// Panics if `message_signature` is `None` after checking `is_some()` (should never happen).
11    pub fn broadcast_chat(
12        &self,
13        mut packet: CPlayerChat,
14        _sender: Arc<Player>,
15        sender_last_seen: LastSeen,
16        message_signature: Option<&[u8; 256]>,
17    ) {
18        log::debug!(
19            "broadcast_chat: sender_last_seen has {} signatures, message_signature present: {}",
20            sender_last_seen.len(),
21            message_signature.is_some()
22        );
23
24        self.players.iter_players(|_, recipient| {
25            let messages_received = recipient.get_and_increment_messages_received();
26            packet.global_index = messages_received;
27
28            log::debug!(
29                "Broadcasting to player {} (UUID: {}), global_index={}",
30                recipient.gameprofile.name,
31                recipient.gameprofile.id,
32                messages_received
33            );
34
35            // IMPORTANT: Index previous messages BEFORE updating the cache
36            // This matches vanilla's order: pack() then push()
37            let previous_messages = {
38                let chat = recipient.chat.lock();
39                chat.signature_cache
40                    .index_previous_messages(&sender_last_seen)
41            };
42
43            log::debug!(
44                "  Indexed {} previous messages for recipient",
45                previous_messages.len()
46            );
47
48            packet.previous_messages.clone_from(&previous_messages);
49
50            // Send the packet
51            recipient.send_packet(packet.clone());
52
53            // AFTER sending, update the recipient's cache using vanilla's push algorithm
54            // This adds all lastSeen signatures + current signature to the cache
55            {
56                let mut chat = recipient.chat.lock();
57                if let Some(signature) = message_signature {
58                    chat.signature_cache
59                        .push(&sender_last_seen, Some(signature));
60
61                    log::debug!("  Added signature to recipient's cache and pending list");
62
63                    // Add to pending messages for acknowledgment tracking
64                    chat.message_validator
65                        .add_pending(Some(Box::new(*signature) as Box<[u8]>));
66                } else {
67                    // Even unsigned messages update the pending tracker
68                    chat.message_validator.add_pending(None);
69                    log::debug!("  Added unsigned message to pending list");
70                }
71            }
72
73            true
74        });
75    }
76
77    /// Broadcasts a system chat message to all players.
78    pub fn broadcast_system_chat(&self, packet: CSystemChat) {
79        self.broadcast_to_all(packet);
80    }
81
82    /// Broadcasts a packet to all players in the world.
83    pub fn broadcast_to_all<P: ClientPacket>(&self, packet: P) {
84        let Ok(encoded) =
85            EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
86        else {
87            return;
88        };
89        self.broadcast_to_all_encoded(encoded);
90    }
91
92    /// Broadcasts a packet to all players in the world except one (identified by entity ID).
93    pub fn broadcast_to_all_except<P: ClientPacket>(&self, packet: P, exclude: i32) {
94        let Ok(encoded) =
95            EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
96        else {
97            return;
98        };
99        self.broadcast_to_all_encoded_except(encoded, exclude);
100    }
101
102    /// Broadcasts a packet to all players in the world.
103    ///
104    /// This method handles encoding the packets produced from the function passed.
105    pub fn broadcast_to_all_with<P: ClientPacket, F: Fn(&Player) -> P>(&self, packet: F) {
106        self.players.iter_players(|_, player| {
107            let Ok(encoded) = EncodedPacket::from_bare(
108                packet(player),
109                self.compression,
110                ConnectionProtocol::Play,
111            ) else {
112                return false;
113            };
114            player.connection.send_encoded(encoded);
115            true
116        });
117    }
118
119    /// Broadcasts an already-encoded packet to all players in the world.
120    pub fn broadcast_to_all_encoded(&self, packet: EncodedPacket) {
121        self.players.iter_players(|_, player| {
122            player.connection.send_encoded(packet.clone());
123            true
124        });
125    }
126
127    /// Broadcasts an already-encoded packet to all players except one.
128    pub fn broadcast_to_all_encoded_except(&self, packet: EncodedPacket, exclude: i32) {
129        self.players.iter_players(|_, player| {
130            if player.id() != exclude {
131                player.connection.send_encoded(packet.clone());
132            }
133            true
134        });
135    }
136
137    /// Broadcasts an unsigned player chat message to all players.
138    pub fn broadcast_unsigned_chat(&self, mut packet: CPlayerChat) {
139        self.players.iter_players(|_, recipient| {
140            let messages_received = recipient.get_and_increment_messages_received();
141            packet.global_index = messages_received;
142
143            recipient.send_packet(packet.clone());
144            true
145        });
146    }
147
148    /// Broadcasts a packet to all players tracking the given chunk.
149    ///
150    /// This method handles encoding the packet internally, avoiding boilerplate at call sites.
151    /// If encoding fails, the broadcast is silently skipped.
152    pub fn broadcast_to_nearby<P: ClientPacket>(
153        &self,
154        chunk: ChunkPos,
155        packet: P,
156        exclude: Option<i32>,
157    ) {
158        let Ok(encoded) =
159            EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
160        else {
161            return;
162        };
163        self.broadcast_to_nearby_encoded(chunk, encoded, exclude);
164    }
165
166    /// Broadcasts an already-encoded packet to all players tracking the given chunk.
167    ///
168    /// Use this when you have a pre-encoded packet to avoid re-encoding.
169    pub fn broadcast_to_nearby_encoded(
170        &self,
171        chunk: ChunkPos,
172        packet: EncodedPacket,
173        exclude: Option<i32>,
174    ) {
175        let tracking_players = self.get_packet_tracking_players(chunk);
176        for entity_id in tracking_players {
177            if Some(entity_id) == exclude {
178                continue;
179            }
180            if let Some(player) = self.players.get_by_entity_id(entity_id) {
181                player.connection.send_encoded(packet.clone());
182            }
183        }
184    }
185
186    /// Returns players whose view includes the chunk and whose client has the base chunk packet.
187    pub fn get_packet_tracking_players(&self, chunk: ChunkPos) -> Vec<i32> {
188        self.player_area_map
189            .get_tracking_players(chunk)
190            .into_iter()
191            .filter(|entity_id| {
192                self.players
193                    .get_by_entity_id(*entity_id)
194                    .is_some_and(|player| player.chunk_sender.lock().is_chunk_sent(chunk))
195            })
196            .collect()
197    }
198
199    /// Returns players on the tracked border of a chunk whose client has its base chunk packet.
200    pub fn get_light_packet_tracking_players(&self, chunk: ChunkPos) -> Vec<i32> {
201        self.player_area_map
202            .get_tracking_players(chunk)
203            .into_iter()
204            .filter(|entity_id| {
205                let Some(player) = self.players.get_by_entity_id(*entity_id) else {
206                    return false;
207                };
208                let Some(view) = *player.last_tracking_view.lock() else {
209                    return false;
210                };
211                let chunk_sender = player.chunk_sender.lock();
212                let is_chunk_sent = |pos| chunk_sender.is_chunk_sent(pos);
213                Self::chunk_is_on_packet_tracked_border(view, chunk, &is_chunk_sent)
214            })
215            .collect()
216    }
217
218    pub(super) fn chunk_is_on_packet_tracked_border(
219        view: PlayerChunkView,
220        chunk: ChunkPos,
221        is_chunk_sent: &impl Fn(ChunkPos) -> bool,
222    ) -> bool {
223        if !Self::chunk_is_packet_tracked(view, chunk, is_chunk_sent) {
224            return false;
225        }
226
227        for dx in -1..=1 {
228            for dz in -1..=1 {
229                if dx == 0 && dz == 0 {
230                    continue;
231                }
232
233                let neighbor = ChunkPos::new(chunk.0.x + dx, chunk.0.y + dz);
234                if !Self::chunk_is_packet_tracked(view, neighbor, is_chunk_sent) {
235                    return true;
236                }
237            }
238        }
239
240        false
241    }
242
243    pub(super) fn chunk_is_packet_tracked(
244        view: PlayerChunkView,
245        chunk: ChunkPos,
246        is_chunk_sent: &impl Fn(ChunkPos) -> bool,
247    ) -> bool {
248        view.contains(chunk) && is_chunk_sent(chunk)
249    }
250
251    /// Broadcasts a packet to players currently tracking an entity.
252    pub fn broadcast_to_entity_trackers<P: ClientPacket>(
253        &self,
254        entity_id: i32,
255        packet: P,
256        exclude: Option<i32>,
257    ) {
258        let Ok(encoded) =
259            EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
260        else {
261            return;
262        };
263        self.broadcast_to_entity_trackers_encoded(entity_id, encoded, exclude);
264    }
265
266    /// Broadcasts a packet to players tracking an entity, excluding several players.
267    pub fn broadcast_to_entity_trackers_except_many<P: ClientPacket>(
268        &self,
269        entity_id: i32,
270        packet: P,
271        excluded_player_ids: &[i32],
272    ) {
273        let Ok(encoded) =
274            EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
275        else {
276            return;
277        };
278
279        for player_id in self.entity_tracker.tracking_player_ids(entity_id) {
280            if excluded_player_ids.contains(&player_id) {
281                continue;
282            }
283            if let Some(player) = self.players.get_by_entity_id(player_id) {
284                player.connection.send_encoded(encoded.clone());
285            }
286        }
287    }
288
289    /// Broadcasts an entity movement sync packet to players currently tracking an entity.
290    pub fn broadcast_movement_sync_to_entity_trackers(
291        &self,
292        entity_id: i32,
293        packet: EntityMovementSyncPacket,
294        exclude: Option<i32>,
295    ) {
296        let Some(encoded) = self.encode_movement_sync_packet(packet) else {
297            return;
298        };
299        self.broadcast_to_entity_trackers_encoded(entity_id, encoded, exclude);
300    }
301
302    /// Broadcasts an already-encoded packet to players currently tracking an entity.
303    pub fn broadcast_to_entity_trackers_encoded(
304        &self,
305        entity_id: i32,
306        packet: EncodedPacket,
307        exclude: Option<i32>,
308    ) {
309        for player_id in self.entity_tracker.tracking_player_ids(entity_id) {
310            if Some(player_id) == exclude {
311                continue;
312            }
313            if let Some(player) = self.players.get_by_entity_id(player_id) {
314                player.connection.send_encoded(packet.clone());
315            }
316        }
317    }
318
319    pub(super) fn encode_movement_sync_packet(
320        &self,
321        packet: EntityMovementSyncPacket,
322    ) -> Option<EncodedPacket> {
323        let encoded = match packet {
324            EntityMovementSyncPacket::Position(packet) => {
325                EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
326            }
327            EntityMovementSyncPacket::PositionRotation(packet) => {
328                EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
329            }
330            EntityMovementSyncPacket::Rotation(packet) => {
331                EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
332            }
333            EntityMovementSyncPacket::HeadRotation(packet) => {
334                EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
335            }
336            EntityMovementSyncPacket::PositionSync(packet) => {
337                EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
338            }
339            EntityMovementSyncPacket::Velocity(packet) => {
340                EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
341            }
342        };
343        encoded.ok()
344    }
345}