chunkedge_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 chunkedge_server::client::{Client, Properties, Username};
8use chunkedge_server::keepalive::Ping;
9use chunkedge_server::layer::UpdateLayersPreClientSet;
10use chunkedge_server::protocol::encode::PacketWriter;
11use chunkedge_server::protocol::packets::play::{
12    player_info_update_s2c as packet, PlayerInfoRemoveS2c, PlayerInfoUpdateS2c, TabListS2c,
13};
14use chunkedge_server::protocol::{IntoTextComponent, WritePacket};
15use chunkedge_server::text::IntoText;
16use chunkedge_server::uuid::Uuid;
17use chunkedge_server::{Despawned, GameMode, Server, Text, UniqueId};
18use derive_more::{Deref, DerefMut};
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(&TabListS2c {
149            header: (&player_list.header).into_cow_text_component(),
150            footer: (&player_list.footer).into_cow_text_component(),
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
211                                .0
212                                .as_ref()
213                                .map(IntoTextComponent::into_cow_text_component),
214                            priority: 0,
215                            hat: false, // TODO: Hat
216                                        // priority: todo!("Implement priority"),
217                        }
218                    },
219                )
220                .collect();
221
222            if !entries.is_empty() {
223                client.write_packet(&PlayerInfoUpdateS2c {
224                    actions,
225                    entries: Cow::Owned(entries),
226                });
227            }
228
229            if !player_list.header.is_empty() || !player_list.footer.is_empty() {
230                client.write_packet(&TabListS2c {
231                    header: (&player_list.header).into_cow_text_component(),
232                    footer: (&player_list.footer).into_cow_text_component(),
233                });
234            }
235        }
236    }
237}
238
239fn remove_despawned_entries(
240    entries: Query<&UniqueId, (Added<Despawned>, With<PlayerListEntry>)>,
241    player_list: ResMut<PlayerList>,
242    server: Res<Server>,
243    mut removed: Local<Vec<Uuid>>,
244) {
245    if player_list.manage_clients {
246        debug_assert!(removed.is_empty());
247
248        removed.extend(entries.iter().map(|uuid| uuid.0));
249
250        if !removed.is_empty() {
251            let player_list = player_list.into_inner();
252
253            let mut w = PacketWriter::new(
254                &mut player_list.cached_update_packets,
255                server.compression_threshold(),
256            );
257
258            w.write_packet(&PlayerInfoRemoveS2c {
259                uuids: Cow::Borrowed(&removed),
260            });
261
262            removed.clear();
263        }
264    }
265}
266
267fn update_entries(
268    entries: Query<
269        (
270            Ref<UniqueId>,
271            Ref<Username>,
272            Ref<Properties>,
273            Ref<GameMode>,
274            Ref<Ping>,
275            Ref<DisplayName>,
276            Ref<Listed>,
277        ),
278        (
279            With<PlayerListEntry>,
280            Or<(
281                Changed<UniqueId>,
282                Changed<Username>,
283                Changed<Properties>,
284                Changed<GameMode>,
285                Changed<Ping>,
286                Changed<DisplayName>,
287                Changed<Listed>,
288            )>,
289        ),
290    >,
291    server: Res<Server>,
292    player_list: ResMut<PlayerList>,
293) {
294    let player_list = player_list.into_inner();
295
296    let mut writer = PacketWriter::new(
297        &mut player_list.cached_update_packets,
298        server.compression_threshold(),
299    );
300
301    for (uuid, username, props, game_mode, ping, display_name, listed) in &entries {
302        let mut actions = packet::PlayerListActions::new();
303
304        // Did a change occur that would force us to overwrite the entry? This also adds
305        // new entries.
306        if uuid.is_changed() || username.is_changed() || props.is_changed() {
307            actions.set_add_player(true);
308
309            if *game_mode != GameMode::default() {
310                actions.set_update_game_mode(true);
311            }
312
313            if ping.0 != 0 {
314                actions.set_update_latency(true);
315            }
316
317            if display_name.0.is_some() {
318                actions.set_update_display_name(true);
319            }
320
321            if listed.0 {
322                actions.set_update_listed(true);
323            }
324        } else {
325            if game_mode.is_changed() {
326                actions.set_update_game_mode(true);
327            }
328
329            if ping.is_changed() {
330                actions.set_update_latency(true);
331            }
332
333            if display_name.is_changed() {
334                actions.set_update_display_name(true);
335            }
336
337            if listed.is_changed() {
338                actions.set_update_listed(true);
339            }
340
341            debug_assert_ne!(u8::from(actions), 0);
342        }
343
344        let entry = packet::PlayerListEntry {
345            player_uuid: uuid.0,
346            username: &username.0,
347            properties: Cow::Borrowed(&props.0),
348            chat_data: None,
349            listed: listed.0,
350            ping: ping.0,
351            game_mode: *game_mode,
352            display_name: display_name
353                .0
354                .as_ref()
355                .map(IntoTextComponent::into_cow_text_component),
356            priority: 0, // TODO
357            hat: false,  // TODO
358        };
359
360        writer.write_packet(&PlayerInfoUpdateS2c {
361            actions,
362            entries: Cow::Borrowed(&[entry]),
363        });
364    }
365}
366
367fn write_player_list_changes(
368    mut player_list: ResMut<PlayerList>,
369    mut clients: Query<&mut Client, Without<Despawned>>,
370) {
371    if !player_list.cached_update_packets.is_empty() {
372        for mut client in &mut clients {
373            if !client.is_added() {
374                client.write_packet_bytes(&player_list.cached_update_packets);
375            }
376        }
377
378        player_list.cached_update_packets.clear();
379    }
380}