1#![doc = include_str!("../README.md")]
2#![deny(
3 rustdoc::broken_intra_doc_links,
4 rustdoc::private_intra_doc_links,
5 rustdoc::missing_crate_level_docs,
6 rustdoc::invalid_codeblock_attributes,
7 rustdoc::invalid_rust_codeblocks,
8 rustdoc::bare_urls,
9 rustdoc::invalid_html_tags
10)]
11#![warn(
12 trivial_casts,
13 trivial_numeric_casts,
14 unused_lifetimes,
15 unused_import_braces,
16 unreachable_pub,
17 clippy::dbg_macro
18)]
19#![allow(clippy::type_complexity)]
20
21use std::borrow::Cow;
22
23use bevy_app::prelude::*;
24use bevy_ecs::prelude::*;
25use valence_server::client::{Client, Properties, Username};
26use valence_server::keepalive::Ping;
27use valence_server::layer::UpdateLayersPreClientSet;
28use valence_server::protocol::encode::PacketWriter;
29use valence_server::protocol::packets::play::{
30 player_list_s2c as packet, PlayerListHeaderS2c, PlayerListS2c, PlayerRemoveS2c,
31};
32use valence_server::protocol::WritePacket;
33use valence_server::text::IntoText;
34use valence_server::uuid::Uuid;
35use valence_server::{Despawned, GameMode, Server, Text, UniqueId};
36
37pub struct PlayerListPlugin;
38
39#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
40struct PlayerListSet;
41
42impl Plugin for PlayerListPlugin {
43 fn build(&self, app: &mut App) {
44 app.insert_resource(PlayerList::new())
45 .configure_set(
46 PostUpdate,
47 PlayerListSet.before(UpdateLayersPreClientSet),
50 )
51 .add_systems(
52 PostUpdate,
53 (
54 update_header_footer,
55 add_new_clients_to_player_list,
56 apply_deferred, update_entries,
58 init_player_list_for_clients,
59 remove_despawned_entries,
60 write_player_list_changes,
61 )
62 .in_set(PlayerListSet)
63 .chain(),
64 );
65 }
66}
67
68#[derive(Resource)]
69pub struct PlayerList {
70 cached_update_packets: Vec<u8>,
71 header: Text,
72 footer: Text,
73 changed_header_or_footer: bool,
74 pub manage_clients: bool,
77}
78
79impl PlayerList {
80 fn new() -> Self {
81 Self {
82 cached_update_packets: vec![],
83 header: Text::default(),
84 footer: Text::default(),
85 changed_header_or_footer: false,
86 manage_clients: true,
87 }
88 }
89
90 pub fn header(&self) -> &Text {
91 &self.header
92 }
93
94 pub fn footer(&self) -> &Text {
95 &self.footer
96 }
97
98 pub fn set_header<'a>(&mut self, txt: impl IntoText<'a>) {
99 let txt = txt.into_cow_text().into_owned();
100
101 if txt != self.header {
102 self.changed_header_or_footer = true;
103 }
104
105 self.header = txt;
106 }
107
108 pub fn set_footer<'a>(&mut self, txt: impl IntoText<'a>) {
109 let txt = txt.into_cow_text().into_owned();
110
111 if txt != self.footer {
112 self.changed_header_or_footer = true;
113 }
114
115 self.footer = txt;
116 }
117}
118
119#[derive(Bundle, Default, Debug)]
126pub struct PlayerListEntryBundle {
127 pub player_list_entry: PlayerListEntry,
128 pub uuid: UniqueId,
130 pub username: Username,
131 pub properties: Properties,
132 pub game_mode: GameMode,
133 pub ping: Ping,
134 pub display_name: DisplayName,
135 pub listed: Listed,
136}
137
138#[derive(Component, Default, Debug)]
140pub struct PlayerListEntry;
141
142#[derive(Component, Default, Debug)]
144pub struct DisplayName(pub Option<Text>);
145
146#[derive(Component, Copy, Clone, Debug)]
148pub struct Listed(pub bool);
149
150impl Default for Listed {
151 fn default() -> Self {
152 Self(true)
153 }
154}
155
156fn update_header_footer(player_list: ResMut<PlayerList>, server: Res<Server>) {
157 if player_list.changed_header_or_footer {
158 let player_list = player_list.into_inner();
159
160 let mut w = PacketWriter::new(
161 &mut player_list.cached_update_packets,
162 server.compression_threshold(),
163 );
164
165 w.write_packet(&PlayerListHeaderS2c {
166 header: (&player_list.header).into(),
167 footer: (&player_list.footer).into(),
168 });
169
170 player_list.changed_header_or_footer = false;
171 }
172}
173
174fn add_new_clients_to_player_list(
175 clients: Query<Entity, Added<Client>>,
176 player_list: Res<PlayerList>,
177 mut commands: Commands,
178) {
179 if player_list.manage_clients {
180 for entity in &clients {
181 commands.entity(entity).insert((
182 PlayerListEntry,
183 DisplayName::default(),
184 Listed::default(),
185 ));
186 }
187 }
188}
189
190fn init_player_list_for_clients(
191 mut clients: Query<&mut Client, (Added<Client>, Without<Despawned>)>,
192 player_list: Res<PlayerList>,
193 entries: Query<
194 (
195 &UniqueId,
196 &Username,
197 &Properties,
198 &GameMode,
199 &Ping,
200 &DisplayName,
201 &Listed,
202 ),
203 With<PlayerListEntry>,
204 >,
205) {
206 if player_list.manage_clients {
207 for mut client in &mut clients {
208 let actions = packet::PlayerListActions::new()
209 .with_add_player(true)
210 .with_update_game_mode(true)
211 .with_update_listed(true)
212 .with_update_latency(true)
213 .with_update_display_name(true);
214
215 let entries: Vec<_> = entries
216 .iter()
217 .map(
218 |(uuid, username, props, game_mode, ping, display_name, listed)| {
219 packet::PlayerListEntry {
220 player_uuid: uuid.0,
221 username: &username.0,
222 properties: Cow::Borrowed(&props.0),
223 chat_data: None,
224 listed: listed.0,
225 ping: ping.0,
226 game_mode: *game_mode,
227 display_name: display_name.0.as_ref().map(Cow::Borrowed),
228 }
229 },
230 )
231 .collect();
232
233 if !entries.is_empty() {
234 client.write_packet(&PlayerListS2c {
235 actions,
236 entries: Cow::Owned(entries),
237 });
238 }
239
240 if !player_list.header.is_empty() || !player_list.footer.is_empty() {
241 client.write_packet(&PlayerListHeaderS2c {
242 header: Cow::Borrowed(&player_list.header),
243 footer: Cow::Borrowed(&player_list.footer),
244 });
245 }
246 }
247 }
248}
249
250fn remove_despawned_entries(
251 entries: Query<&UniqueId, (Added<Despawned>, With<PlayerListEntry>)>,
252 player_list: ResMut<PlayerList>,
253 server: Res<Server>,
254 mut removed: Local<Vec<Uuid>>,
255) {
256 if player_list.manage_clients {
257 debug_assert!(removed.is_empty());
258
259 removed.extend(entries.iter().map(|uuid| uuid.0));
260
261 if !removed.is_empty() {
262 let player_list = player_list.into_inner();
263
264 let mut w = PacketWriter::new(
265 &mut player_list.cached_update_packets,
266 server.compression_threshold(),
267 );
268
269 w.write_packet(&PlayerRemoveS2c {
270 uuids: Cow::Borrowed(&removed),
271 });
272
273 removed.clear();
274 }
275 }
276}
277
278fn update_entries(
279 entries: Query<
280 (
281 Ref<UniqueId>,
282 Ref<Username>,
283 Ref<Properties>,
284 Ref<GameMode>,
285 Ref<Ping>,
286 Ref<DisplayName>,
287 Ref<Listed>,
288 ),
289 (
290 With<PlayerListEntry>,
291 Or<(
292 Changed<UniqueId>,
293 Changed<Username>,
294 Changed<Properties>,
295 Changed<GameMode>,
296 Changed<Ping>,
297 Changed<DisplayName>,
298 Changed<Listed>,
299 )>,
300 ),
301 >,
302 server: Res<Server>,
303 player_list: ResMut<PlayerList>,
304) {
305 let player_list = player_list.into_inner();
306
307 let mut writer = PacketWriter::new(
308 &mut player_list.cached_update_packets,
309 server.compression_threshold(),
310 );
311
312 for (uuid, username, props, game_mode, ping, display_name, listed) in &entries {
313 let mut actions = packet::PlayerListActions::new();
314
315 if uuid.is_changed() || username.is_changed() || props.is_changed() {
318 actions.set_add_player(true);
319
320 if *game_mode != GameMode::default() {
321 actions.set_update_game_mode(true);
322 }
323
324 if ping.0 != 0 {
325 actions.set_update_latency(true);
326 }
327
328 if display_name.0.is_some() {
329 actions.set_update_display_name(true);
330 }
331
332 if listed.0 {
333 actions.set_update_listed(true);
334 }
335 } else {
336 if game_mode.is_changed() {
337 actions.set_update_game_mode(true);
338 }
339
340 if ping.is_changed() {
341 actions.set_update_latency(true);
342 }
343
344 if display_name.is_changed() {
345 actions.set_update_display_name(true);
346 }
347
348 if listed.is_changed() {
349 actions.set_update_listed(true);
350 }
351
352 debug_assert_ne!(u8::from(actions), 0);
353 }
354
355 let entry = packet::PlayerListEntry {
356 player_uuid: uuid.0,
357 username: &username.0,
358 properties: (&props.0).into(),
359 chat_data: None,
360 listed: listed.0,
361 ping: ping.0,
362 game_mode: *game_mode,
363 display_name: display_name.0.as_ref().map(|x| x.into()),
364 };
365
366 writer.write_packet(&PlayerListS2c {
367 actions,
368 entries: Cow::Borrowed(&[entry]),
369 });
370 }
371}
372
373fn write_player_list_changes(
374 mut player_list: ResMut<PlayerList>,
375 mut clients: Query<&mut Client, Without<Despawned>>,
376) {
377 if !player_list.cached_update_packets.is_empty() {
378 for mut client in &mut clients {
379 if !client.is_added() {
380 client.write_packet_bytes(&player_list.cached_update_packets);
381 }
382 }
383
384 player_list.cached_update_packets.clear();
385 }
386}