Skip to main content

steel_core/server/
broadcasting.rs

1use super::{
2    Arc, CEntityEvent, CGameEvent, CSystemChat, CTabList, CTickingState, CTickingStep, Color,
3    CommandSender, CommandSource, DisplayResolutor, Entity, GameEventType, Modifier, Player,
4    Server, SprintReport, TabListTickStats, TextComponent, Uuid, client_permission_event,
5    command_tree_packet, translations,
6};
7
8impl Server {
9    /// Logs and broadcasts a system chat message to online players.
10    fn broadcast_system_chat(&self, message: &TextComponent, excluded_player: Option<Uuid>) {
11        log::info!("{}", message.to_plain(&DisplayResolutor));
12        self.online_players.iter_players(|uuid, player| {
13            if Some(*uuid) != excluded_player {
14                player.send_packet(CSystemChat::new(message, false, player));
15            }
16            true
17        });
18    }
19
20    /// Builds the tab list header/footer with recent and five-second tick statistics.
21    pub(super) fn tab_list_components(
22        tick_stats: TabListTickStats,
23    ) -> (TextComponent, TextComponent) {
24        // Color TPS based on value
25        let tps_color = if tick_stats.tps >= 19.5 {
26            Color::Green
27        } else if tick_stats.tps >= 15.0 {
28            Color::Yellow
29        } else {
30            Color::Red
31        };
32
33        let mspt_color = |mspt: f32| {
34            if mspt <= 50.0 {
35                Color::Aqua
36            } else {
37                Color::Red
38            }
39        };
40
41        let header = TextComponent::plain("\n").add_children(vec![
42            TextComponent::plain("Steel Dev Build").color(Color::Yellow),
43            TextComponent::plain("\n"),
44        ]);
45        let footer = TextComponent::plain("\n").add_children(vec![
46            TextComponent::plain("TPS: ").color(Color::Gray),
47            TextComponent::plain(format!("{:.1}", tick_stats.tps)).color(tps_color),
48            TextComponent::plain(" | ").color(Color::DarkGray),
49            TextComponent::plain("MSPT: ").color(Color::Gray),
50            TextComponent::plain(format!("{:.2}", tick_stats.recent_mspt))
51                .color(mspt_color(tick_stats.recent_mspt)),
52            TextComponent::plain(" recent | ").color(Color::Gray),
53            TextComponent::plain(format!("{:.2}", tick_stats.average_mspt))
54                .color(mspt_color(tick_stats.average_mspt)),
55            TextComponent::plain(" avg (5s) | ").color(Color::Gray),
56            TextComponent::plain(format!("{:.2}", tick_stats.p95_mspt))
57                .color(mspt_color(tick_stats.p95_mspt)),
58            TextComponent::plain(" p95").color(Color::Gray),
59            TextComponent::plain("\n"),
60        ]);
61
62        (header, footer)
63    }
64
65    /// Broadcasts the tab list header/footer with current TPS and MSPT statistics.
66    pub(super) fn broadcast_tab_list(&self, tick_stats: TabListTickStats) {
67        let (header, footer) = Self::tab_list_components(tick_stats);
68
69        self.broadcast_to_online_with(|player| CTabList::new(&header, &footer, player));
70    }
71
72    /// Broadcasts a sprint completion report to all players.
73    pub(crate) fn broadcast_sprint_report(&self, report: &SprintReport) {
74        let message: TextComponent = translations::COMMANDS_TICK_SPRINT_REPORT
75            .message([
76                TextComponent::from(format!("{}", report.ticks_per_second)),
77                TextComponent::from(format!("{:.2}", report.ms_per_tick)),
78            ])
79            .into();
80
81        self.broadcast_system_chat(&message, None);
82    }
83
84    pub(super) fn broadcast_player_join_message(
85        &self,
86        player: &Player,
87        previous_name: Option<&str>,
88    ) {
89        let display_name = player.display_name();
90        // Fallback to the current name when the cache has no prior entry.
91        let old_name = previous_name.unwrap_or(player.gameprofile.name.as_str());
92        let message: TextComponent = if player.gameprofile.name.eq_ignore_ascii_case(old_name) {
93            translations::MULTIPLAYER_PLAYER_JOINED
94                .message([display_name])
95                .into()
96        } else {
97            translations::MULTIPLAYER_PLAYER_JOINED_RENAMED
98                .message([display_name, TextComponent::plain(old_name.to_owned())])
99                .into()
100        };
101        let message = message.color(Color::Yellow);
102        self.broadcast_system_chat(&message, Some(player.gameprofile.id));
103    }
104
105    pub(super) fn broadcast_player_leave_message(&self, player: &Player) {
106        let message: TextComponent = translations::MULTIPLAYER_PLAYER_LEFT
107            .message([player.display_name()])
108            .into();
109        let message = message.color(Color::Yellow);
110        self.broadcast_system_chat(&message, None);
111    }
112
113    /// Broadcasts the current tick rate and frozen state to all clients.
114    /// This should be called whenever the tick rate or frozen state changes.
115    pub fn broadcast_ticking_state(&self) {
116        let tick_manager = self.tick_rate_manager.read();
117        let packet = CTickingState::new(tick_manager.tick_rate(), tick_manager.is_frozen());
118        drop(tick_manager);
119
120        self.broadcast_to_online(packet);
121    }
122
123    /// Broadcasts the current step tick count to all clients.
124    /// This should be called whenever the step tick count changes.
125    pub fn broadcast_ticking_step(&self) {
126        let tick_manager = self.tick_rate_manager.read();
127        let packet = CTickingStep::new(tick_manager.frozen_ticks_to_run());
128        drop(tick_manager);
129
130        self.broadcast_to_online(packet);
131    }
132
133    /// Sends the current ticking state and step packets to a joining player.
134    /// This should be called when a player joins the server.
135    pub fn send_ticking_state_to_player(&self, player: &Player) {
136        let tick_manager = self.tick_rate_manager.read();
137        let state_packet = CTickingState::new(tick_manager.tick_rate(), tick_manager.is_frozen());
138        let step_packet = CTickingStep::new(tick_manager.frozen_ticks_to_run());
139        drop(tick_manager);
140
141        player.send_packet(state_packet);
142        player.send_packet(step_packet);
143    }
144
145    /// Resends client state that is not fully covered by `CRespawn`.
146    pub fn resend_player_context(self: &Arc<Self>, player: &Arc<Player>) {
147        player.send_difficulty();
148        player.send_inventory_to_remote();
149
150        self.resend_player_permission_context(player);
151
152        self.send_ticking_state_to_player(player);
153
154        player.send_packet(CGameEvent {
155            event: GameEventType::ChangeGameMode,
156            data: player.game_mode().into(),
157        });
158    }
159
160    /// Resends the command tree and vanilla client permission-level projection.
161    pub fn resend_player_permission_context(self: &Arc<Self>, player: &Arc<Player>) {
162        let world = player.get_world();
163        player.send_packet(CEntityEvent {
164            entity_id: player.id(),
165            event: client_permission_event(player, &world),
166        });
167
168        let server = player.server();
169        if !Arc::ptr_eq(&server, self) {
170            tracing::error!(
171                player = %player.gameprofile.name,
172                "cannot project commands from a different server"
173            );
174            return;
175        }
176        let Some(shared_player) = self.online_players.get_by_uuid(&player.gameprofile.id) else {
177            tracing::error!(
178                player = %player.gameprofile.name,
179                "cannot project commands for a player outside the online player map"
180            );
181            return;
182        };
183        if !Arc::ptr_eq(&shared_player, player) {
184            tracing::error!(
185                player = %player.gameprofile.name,
186                "cannot project commands for a stale player handle"
187            );
188            return;
189        }
190        let source = CommandSource::new(CommandSender::Player(shared_player), server);
191        let commands = {
192            let dispatcher = self.command_dispatcher.read();
193            command_tree_packet(&dispatcher, &source)
194        };
195        match commands {
196            Ok(commands) => player.send_packet(commands),
197            Err(error) => tracing::error!(
198                player = %player.gameprofile.name,
199                %error,
200                "failed to project the player's command tree"
201            ),
202        }
203    }
204}