Skip to main content

steel_core/world/
broadcasts.rs

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