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 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(&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, }
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 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, hat: false, };
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}