#![doc = include_str!("../README.md")]
use std::borrow::Cow;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use valence_server::client::{Client, Properties, Username};
use valence_server::keepalive::Ping;
use valence_server::layer::UpdateLayersPreClientSet;
use valence_server::protocol::encode::PacketWriter;
use valence_server::protocol::packets::play::{
player_list_s2c as packet, PlayerListHeaderS2c, PlayerListS2c, PlayerRemoveS2c,
};
use valence_server::protocol::WritePacket;
use valence_server::text::IntoText;
use valence_server::uuid::Uuid;
use valence_server::{Despawned, GameMode, Server, Text, UniqueId};
pub struct PlayerListPlugin;
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct PlayerListSet;
impl Plugin for PlayerListPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(PlayerList::new())
.configure_sets(
PostUpdate,
PlayerListSet.before(UpdateLayersPreClientSet),
)
.add_systems(
PostUpdate,
(
update_header_footer,
add_new_clients_to_player_list,
apply_deferred, update_entries,
init_player_list_for_clients,
remove_despawned_entries,
write_player_list_changes,
)
.in_set(PlayerListSet)
.chain(),
);
}
}
#[derive(Resource)]
pub struct PlayerList {
cached_update_packets: Vec<u8>,
header: Text,
footer: Text,
changed_header_or_footer: bool,
pub manage_clients: bool,
}
impl PlayerList {
fn new() -> Self {
Self {
cached_update_packets: vec![],
header: Text::default(),
footer: Text::default(),
changed_header_or_footer: false,
manage_clients: true,
}
}
pub fn header(&self) -> &Text {
&self.header
}
pub fn footer(&self) -> &Text {
&self.footer
}
pub fn set_header<'a, T: IntoText<'a>>(&mut self, txt: T) {
let txt = txt.into_cow_text().into_owned();
if txt != self.header {
self.changed_header_or_footer = true;
}
self.header = txt;
}
pub fn set_footer<'a, T: IntoText<'a>>(&mut self, txt: T) {
let txt = txt.into_cow_text().into_owned();
if txt != self.footer {
self.changed_header_or_footer = true;
}
self.footer = txt;
}
}
#[derive(Bundle, Default, Debug)]
pub struct PlayerListEntryBundle {
pub player_list_entry: PlayerListEntry,
pub uuid: UniqueId,
pub username: Username,
pub properties: Properties,
pub game_mode: GameMode,
pub ping: Ping,
pub display_name: DisplayName,
pub listed: Listed,
}
#[derive(Component, Default, Debug)]
pub struct PlayerListEntry;
#[derive(Component, Default, Debug, Deref, DerefMut)]
pub struct DisplayName(pub Option<Text>);
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut)]
pub struct Listed(pub bool);
impl Default for Listed {
fn default() -> Self {
Self(true)
}
}
fn update_header_footer(player_list: ResMut<PlayerList>, server: Res<Server>) {
if player_list.changed_header_or_footer {
let player_list = player_list.into_inner();
let mut w = PacketWriter::new(
&mut player_list.cached_update_packets,
server.compression_threshold(),
);
w.write_packet(&PlayerListHeaderS2c {
header: (&player_list.header).into(),
footer: (&player_list.footer).into(),
});
player_list.changed_header_or_footer = false;
}
}
fn add_new_clients_to_player_list(
clients: Query<Entity, Added<Client>>,
player_list: Res<PlayerList>,
mut commands: Commands,
) {
if player_list.manage_clients {
for entity in &clients {
commands.entity(entity).insert((
PlayerListEntry,
DisplayName::default(),
Listed::default(),
));
}
}
}
fn init_player_list_for_clients(
mut clients: Query<&mut Client, (Added<Client>, Without<Despawned>)>,
player_list: Res<PlayerList>,
entries: Query<
(
&UniqueId,
&Username,
&Properties,
&GameMode,
&Ping,
&DisplayName,
&Listed,
),
With<PlayerListEntry>,
>,
) {
if player_list.manage_clients {
for mut client in &mut clients {
let actions = packet::PlayerListActions::new()
.with_add_player(true)
.with_update_game_mode(true)
.with_update_listed(true)
.with_update_latency(true)
.with_update_display_name(true);
let entries: Vec<_> = entries
.iter()
.map(
|(uuid, username, props, game_mode, ping, display_name, listed)| {
packet::PlayerListEntry {
player_uuid: uuid.0,
username: &username.0,
properties: Cow::Borrowed(&props.0),
chat_data: None,
listed: listed.0,
ping: ping.0,
game_mode: *game_mode,
display_name: display_name.0.as_ref().map(Cow::Borrowed),
}
},
)
.collect();
if !entries.is_empty() {
client.write_packet(&PlayerListS2c {
actions,
entries: Cow::Owned(entries),
});
}
if !player_list.header.is_empty() || !player_list.footer.is_empty() {
client.write_packet(&PlayerListHeaderS2c {
header: Cow::Borrowed(&player_list.header),
footer: Cow::Borrowed(&player_list.footer),
});
}
}
}
}
fn remove_despawned_entries(
entries: Query<&UniqueId, (Added<Despawned>, With<PlayerListEntry>)>,
player_list: ResMut<PlayerList>,
server: Res<Server>,
mut removed: Local<Vec<Uuid>>,
) {
if player_list.manage_clients {
debug_assert!(removed.is_empty());
removed.extend(entries.iter().map(|uuid| uuid.0));
if !removed.is_empty() {
let player_list = player_list.into_inner();
let mut w = PacketWriter::new(
&mut player_list.cached_update_packets,
server.compression_threshold(),
);
w.write_packet(&PlayerRemoveS2c {
uuids: Cow::Borrowed(&removed),
});
removed.clear();
}
}
}
fn update_entries(
entries: Query<
(
Ref<UniqueId>,
Ref<Username>,
Ref<Properties>,
Ref<GameMode>,
Ref<Ping>,
Ref<DisplayName>,
Ref<Listed>,
),
(
With<PlayerListEntry>,
Or<(
Changed<UniqueId>,
Changed<Username>,
Changed<Properties>,
Changed<GameMode>,
Changed<Ping>,
Changed<DisplayName>,
Changed<Listed>,
)>,
),
>,
server: Res<Server>,
player_list: ResMut<PlayerList>,
) {
let player_list = player_list.into_inner();
let mut writer = PacketWriter::new(
&mut player_list.cached_update_packets,
server.compression_threshold(),
);
for (uuid, username, props, game_mode, ping, display_name, listed) in &entries {
let mut actions = packet::PlayerListActions::new();
if uuid.is_changed() || username.is_changed() || props.is_changed() {
actions.set_add_player(true);
if *game_mode != GameMode::default() {
actions.set_update_game_mode(true);
}
if ping.0 != 0 {
actions.set_update_latency(true);
}
if display_name.0.is_some() {
actions.set_update_display_name(true);
}
if listed.0 {
actions.set_update_listed(true);
}
} else {
if game_mode.is_changed() {
actions.set_update_game_mode(true);
}
if ping.is_changed() {
actions.set_update_latency(true);
}
if display_name.is_changed() {
actions.set_update_display_name(true);
}
if listed.is_changed() {
actions.set_update_listed(true);
}
debug_assert_ne!(u8::from(actions), 0);
}
let entry = packet::PlayerListEntry {
player_uuid: uuid.0,
username: &username.0,
properties: Cow::Borrowed(&props.0),
chat_data: None,
listed: listed.0,
ping: ping.0,
game_mode: *game_mode,
display_name: display_name.0.as_ref().map(|x| x.into()),
};
writer.write_packet(&PlayerListS2c {
actions,
entries: Cow::Borrowed(&[entry]),
});
}
}
fn write_player_list_changes(
mut player_list: ResMut<PlayerList>,
mut clients: Query<&mut Client, Without<Despawned>>,
) {
if !player_list.cached_update_packets.is_empty() {
for mut client in &mut clients {
if !client.is_added() {
client.write_packet_bytes(&player_list.cached_update_packets);
}
}
player_list.cached_update_packets.clear();
}
}