valence_player_list/
lib.rs

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