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 PlayerListSet.before(UpdateLayersPreClientSet),
33 )
34 .add_systems(
35 PostUpdate,
36 (
37 update_header_footer,
38 add_new_clients_to_player_list,
39 apply_deferred, 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 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#[derive(Bundle, Default, Debug)]
109pub struct PlayerListEntryBundle {
110 pub player_list_entry: PlayerListEntry,
111 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#[derive(Component, Default, Debug)]
123pub struct PlayerListEntry;
124
125#[derive(Component, Default, Debug, Deref, DerefMut)]
127pub struct DisplayName(pub Option<Text>);
128
129#[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 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}