valence_player_list/
lib.rs

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                // Needs to happen before player entities are initialized. Otherwise, they will
48                // appear invisible.
49                PlayerListSet.before(UpdateLayersPreClientSet),
50            )
51            .add_systems(
52                PostUpdate,
53                (
54                    update_header_footer,
55                    add_new_clients_to_player_list,
56                    apply_deferred, // So new clients get the packets for their own entry.
57                    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    /// If clients should be automatically added and removed from the player
75    /// list with the proper components inserted. Enabled by default.
76    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/// Bundle for spawning new player list entries. All components are required
120/// unless otherwise stated.
121///
122/// # Despawning player list entries
123///
124/// The [`Despawned`] component must be used to despawn player list entries.
125#[derive(Bundle, Default, Debug)]
126pub struct PlayerListEntryBundle {
127    pub player_list_entry: PlayerListEntry,
128    /// Careful not to modify this!
129    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/// Marker component for player list entries.
139#[derive(Component, Default, Debug)]
140pub struct PlayerListEntry;
141
142/// Displayed name for a player list entry. Appears as [`Username`] if `None`.
143#[derive(Component, Default, Debug)]
144pub struct DisplayName(pub Option<Text>);
145
146/// If a player list entry is visible. Defaults to `true`.
147#[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        // Did a change occur that would force us to overwrite the entry? This also adds
316        // new entries.
317        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}