mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
Player list rework (#317)
## Description - Redesigned the player list so that every entry is an entity in the ECS. Entry components overlap with client components where possible. - Clients are now automatically added and removed from the player list unless configured not to. - Updated player list example. ## Test Plan Steps: 1. Run any of the examples. 2. Run player list example.
This commit is contained in:
parent
a68792e605
commit
d78627e478
|
@ -27,7 +27,6 @@ fn main() {
|
|||
init_clients,
|
||||
despawn_disconnected_clients,
|
||||
))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.run();
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ pub fn main() {
|
|||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_systems((init_clients, despawn_disconnected_clients))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.run();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ pub fn main() {
|
|||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_systems((event_handler, init_clients, despawn_disconnected_clients))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.run();
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ pub fn main() {
|
|||
digging_survival_mode,
|
||||
place_blocks,
|
||||
))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.run();
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ pub fn main() {
|
|||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_systems((toggle_gamemode_on_sneak, open_chest))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.run();
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ pub fn main() {
|
|||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_system(handle_combat_events.in_schedule(EventLoopSchedule))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.add_system(teleport_oob_clients)
|
||||
.run();
|
||||
|
|
|
@ -27,7 +27,6 @@ pub fn main() {
|
|||
.add_startup_system(setup_biomes.before(setup))
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_systems((
|
||||
despawn_disconnected_clients,
|
||||
toggle_cell_on_dig,
|
||||
|
|
|
@ -26,7 +26,6 @@ fn main() {
|
|||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(update_sphere)
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.run();
|
||||
|
|
|
@ -12,7 +12,6 @@ pub fn main() {
|
|||
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
|
||||
.add_startup_system(setup)
|
||||
.add_systems((init_clients, squat_and_die, necromancy))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.run();
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ pub fn main() {
|
|||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_system(init_clients)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_systems((
|
||||
reset_clients.after(init_clients),
|
||||
manage_chunks.after(reset_clients).before(manage_blocks),
|
||||
|
|
|
@ -13,7 +13,6 @@ pub fn main() {
|
|||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.add_system(manage_particles)
|
||||
.run();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use rand::Rng;
|
||||
use valence::player_list::Entry;
|
||||
use valence::player_list::{DisplayName, PlayerListEntryBundle};
|
||||
use valence::prelude::*;
|
||||
|
||||
const SPAWN_Y: i32 = 64;
|
||||
|
@ -14,11 +14,10 @@ fn main() {
|
|||
App::new()
|
||||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_systems((
|
||||
init_clients,
|
||||
override_display_name,
|
||||
update_player_list,
|
||||
remove_disconnected_clients_from_player_list,
|
||||
despawn_disconnected_clients,
|
||||
))
|
||||
.run();
|
||||
|
@ -26,7 +25,6 @@ fn main() {
|
|||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
server: Res<Server>,
|
||||
dimensions: Query<&DimensionType>,
|
||||
biomes: Query<&Biome>,
|
||||
|
@ -47,29 +45,18 @@ fn setup(
|
|||
|
||||
commands.spawn(instance);
|
||||
|
||||
player_list.insert(
|
||||
PLAYER_UUID_1,
|
||||
PlayerListEntry::new().with_display_name(Some("persistent entry with no ping")),
|
||||
);
|
||||
commands.spawn(PlayerListEntryBundle {
|
||||
uuid: UniqueId(PLAYER_UUID_1),
|
||||
display_name: DisplayName(Some("persistent entry with no ping".into())),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn init_clients(
|
||||
mut clients: Query<
|
||||
(
|
||||
&mut Client,
|
||||
&mut Position,
|
||||
&mut Location,
|
||||
&mut GameMode,
|
||||
&Username,
|
||||
&Properties,
|
||||
&UniqueId,
|
||||
),
|
||||
Added<Client>,
|
||||
>,
|
||||
mut clients: Query<(&mut Client, &mut Position, &mut Location, &mut GameMode), Added<Client>>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
) {
|
||||
for (mut client, mut pos, mut loc, mut game_mode, username, props, uuid) in &mut clients {
|
||||
for (mut client, mut pos, mut loc, mut game_mode) in &mut clients {
|
||||
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
|
||||
loc.0 = instances.single();
|
||||
*game_mode = GameMode::Creative;
|
||||
|
@ -79,19 +66,21 @@ fn init_clients(
|
|||
.italic()
|
||||
.color(Color::WHITE),
|
||||
);
|
||||
|
||||
let entry = PlayerListEntry::new()
|
||||
.with_username(&username.0)
|
||||
.with_properties(props.0.clone()) // For the player's skin and cape.
|
||||
.with_game_mode(*game_mode)
|
||||
.with_ping(0) // Use negative values to indicate missing.
|
||||
.with_display_name(Some("ඞ".color(Color::new(255, 87, 66))));
|
||||
|
||||
player_list.insert(uuid.0, entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_player_list(mut player_list: ResMut<PlayerList>, server: Res<Server>) {
|
||||
fn override_display_name(mut clients: Query<&mut DisplayName, (Added<DisplayName>, With<Client>)>) {
|
||||
for mut display_name in &mut clients {
|
||||
display_name.0 = Some("ඞ".color(Color::new(255, 87, 66)));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_player_list(
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
server: Res<Server>,
|
||||
mut entries: Query<(Entity, &UniqueId, &mut DisplayName), With<PlayerListEntry>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let tick = server.current_tick();
|
||||
|
||||
player_list.set_header("Current tick: ".into_text() + tick);
|
||||
|
@ -99,38 +88,27 @@ fn update_player_list(mut player_list: ResMut<PlayerList>, server: Res<Server>)
|
|||
.set_footer("Current tick but in purple: ".into_text() + tick.color(Color::LIGHT_PURPLE));
|
||||
|
||||
if tick % 5 == 0 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let color = Color::new(rng.gen(), rng.gen(), rng.gen());
|
||||
for (_, uuid, mut display_name) in &mut entries {
|
||||
if uuid.0 == PLAYER_UUID_1 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let color = Color::new(rng.gen(), rng.gen(), rng.gen());
|
||||
|
||||
let entry = player_list.get_mut(PLAYER_UUID_1).unwrap();
|
||||
let new_display_name = entry.display_name().unwrap().clone().color(color);
|
||||
entry.set_display_name(Some(new_display_name));
|
||||
let new_name = display_name.0.clone().unwrap_or_default().color(color);
|
||||
display_name.0 = Some(new_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tick % 20 == 0 {
|
||||
match player_list.entry(PLAYER_UUID_2) {
|
||||
Entry::Occupied(oe) => {
|
||||
oe.remove();
|
||||
}
|
||||
Entry::Vacant(ve) => {
|
||||
let entry = PlayerListEntry::new()
|
||||
.with_display_name(Some("Hello!"))
|
||||
.with_ping(300);
|
||||
|
||||
ve.insert(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_disconnected_clients_from_player_list(
|
||||
mut clients: RemovedComponents<Client>,
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
uuids: Query<&UniqueId>,
|
||||
) {
|
||||
for client in clients.iter() {
|
||||
if let Ok(UniqueId(uuid)) = uuids.get(client) {
|
||||
player_list.remove(*uuid);
|
||||
if let Some((entity, _, _)) = entries.iter().find(|(_, uuid, _)| uuid.0 == PLAYER_UUID_2) {
|
||||
commands.entity(entity).insert(Despawned);
|
||||
} else {
|
||||
commands.spawn(PlayerListEntryBundle {
|
||||
uuid: UniqueId(PLAYER_UUID_2),
|
||||
display_name: DisplayName(Some("Hello!".into())),
|
||||
ping: Ping(300),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ pub fn main() {
|
|||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_systems((init_clients, prompt_on_punch, on_resource_pack_status))
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.run();
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ pub fn main() {
|
|||
.chain(),
|
||||
)
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.run();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ pub fn main() {
|
|||
.add_plugin(ServerPlugin::new(()))
|
||||
.add_startup_system(setup)
|
||||
.add_system(init_clients)
|
||||
.add_systems(PlayerList::default_systems())
|
||||
.add_system(despawn_disconnected_clients)
|
||||
.run();
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ pub(crate) struct ClientBundle {
|
|||
client: Client,
|
||||
settings: settings::ClientSettings,
|
||||
scratch: ScratchBuf,
|
||||
entity_remove_buffer: EntityRemoveBuf,
|
||||
entity_remove_buf: EntityRemoveBuf,
|
||||
username: Username,
|
||||
ip: Ip,
|
||||
properties: Properties,
|
||||
|
@ -151,7 +151,7 @@ impl ClientBundle {
|
|||
client: Client { conn, enc },
|
||||
settings: settings::ClientSettings::default(),
|
||||
scratch: ScratchBuf::default(),
|
||||
entity_remove_buffer: EntityRemoveBuf(vec![]),
|
||||
entity_remove_buf: EntityRemoveBuf(vec![]),
|
||||
username: Username(info.username),
|
||||
ip: Ip(info.ip),
|
||||
properties: Properties(info.properties),
|
||||
|
|
|
@ -40,6 +40,7 @@ fn send_keepalive(
|
|||
) {
|
||||
if server.current_tick() % (server.tps() * 10) == 0 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let now = Instant::now();
|
||||
|
||||
for (entity, mut client, mut state) in &mut clients {
|
||||
if state.got_keepalive {
|
||||
|
@ -48,8 +49,7 @@ fn send_keepalive(
|
|||
|
||||
state.got_keepalive = false;
|
||||
state.last_keepalive_id = id;
|
||||
// TODO: start timing when the packets are flushed.
|
||||
state.keepalive_sent_time = Instant::now();
|
||||
state.keepalive_sent_time = now;
|
||||
} else {
|
||||
warn!("Client {entity:?} timed out (no keepalive response)");
|
||||
commands.entity(entity).remove::<Client>();
|
||||
|
|
|
@ -11,6 +11,39 @@ use crate::client::FlushPacketsSet;
|
|||
use crate::util::{from_yaw_and_pitch, to_yaw_and_pitch};
|
||||
use crate::view::ChunkPos;
|
||||
|
||||
pub(crate) struct ComponentPlugin;
|
||||
|
||||
impl Plugin for ComponentPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.add_systems(
|
||||
(update_old_position, update_old_location)
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.after(FlushPacketsSet),
|
||||
)
|
||||
.add_system(despawn_marked_entities.in_base_set(CoreSet::Last));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_old_position(mut query: Query<(&Position, &mut OldPosition)>) {
|
||||
for (pos, mut old_pos) in &mut query {
|
||||
old_pos.0 = pos.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_old_location(mut query: Query<(&Location, &mut OldLocation)>) {
|
||||
for (loc, mut old_loc) in &mut query {
|
||||
old_loc.0 = loc.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Despawns all the entities marked as despawned with the [`Despawned`]
|
||||
/// component.
|
||||
fn despawn_marked_entities(mut commands: Commands, entities: Query<Entity, With<Despawned>>) {
|
||||
for entity in &entities {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Component`] for marking entities that should be despawned at the end of
|
||||
/// the tick.
|
||||
///
|
||||
|
@ -41,7 +74,7 @@ impl Default for UniqueId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Component, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct Username(pub String);
|
||||
|
||||
impl fmt::Display for Username {
|
||||
|
@ -235,40 +268,9 @@ impl Look {
|
|||
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
||||
pub struct OnGround(pub bool);
|
||||
|
||||
/// General-purpose reusable byte buffer.
|
||||
///
|
||||
/// No guarantees are made about the buffer's contents between systems.
|
||||
/// Therefore, the inner `Vec` should be cleared before use.
|
||||
#[derive(Component, Default, Debug)]
|
||||
pub struct ScratchBuf(pub Vec<u8>);
|
||||
|
||||
pub(crate) struct ComponentPlugin;
|
||||
|
||||
impl Plugin for ComponentPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.add_systems(
|
||||
(update_old_position, update_old_location)
|
||||
.in_base_set(CoreSet::PostUpdate)
|
||||
.after(FlushPacketsSet),
|
||||
)
|
||||
// This is fine because we're applying system buffers later.
|
||||
.add_system(despawn_marked_entities.in_base_set(CoreSet::PostUpdate
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_old_position(mut query: Query<(&Position, &mut OldPosition), Changed<Position>>) {
|
||||
for (pos, mut old_pos) in &mut query {
|
||||
old_pos.0 = pos.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_old_location(mut query: Query<(&Location, &mut OldLocation), Changed<Location>>) {
|
||||
for (loc, mut old_loc) in &mut query {
|
||||
old_loc.0 = loc.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Despawns all the entities marked as despawned with the [`Despawned`]
|
||||
/// component.
|
||||
fn despawn_marked_entities(mut commands: Commands, entities: Query<Entity, With<Despawned>>) {
|
||||
for entity in &entities {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,169 +1,59 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::{Entry as MapEntry, OccupiedEntry as OccupiedMapEntry};
|
||||
use std::collections::HashMap;
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
|
||||
use bevy_app::{CoreSet, Plugin};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::schedule::SystemConfigs;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
use valence_protocol::packet::s2c::play::player_list::{
|
||||
Actions, Entry as PlayerInfoEntry, PlayerListS2c,
|
||||
};
|
||||
use valence_protocol::packet::s2c::play::player_list::{Actions, Entry, PlayerListS2c};
|
||||
use valence_protocol::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c};
|
||||
use valence_protocol::text::Text;
|
||||
use valence_protocol::types::Property;
|
||||
|
||||
use crate::client::{Client, FlushPacketsSet};
|
||||
use crate::component::{GameMode, Ping, Properties, UniqueId, Username};
|
||||
use crate::component::{Despawned, GameMode, Ping, Properties, UniqueId, Username};
|
||||
use crate::packet::{PacketWriter, WritePacket};
|
||||
use crate::server::Server;
|
||||
|
||||
/// The global list of players on a server visible by pressing the tab key by
|
||||
/// default.
|
||||
///
|
||||
/// Each entry in the player list is intended to represent a connected client to
|
||||
/// the server. In addition to a list of players, the player list has a header
|
||||
/// and a footer which can contain arbitrary text.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use uuid::Uuid;
|
||||
/// # use valence::player_list::{PlayerList, PlayerListEntry};
|
||||
///
|
||||
/// # let mut player_list = PlayerList::new();
|
||||
/// player_list.set_header("Hello, world!");
|
||||
/// player_list.set_footer("Goodbye, world!");
|
||||
/// player_list.insert(
|
||||
/// Uuid::new_v4(),
|
||||
/// PlayerListEntry::new()
|
||||
/// .with_username("Notch")
|
||||
/// .with_display_name(Some("Herobrine")),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Resource)]
|
||||
pub struct PlayerList {
|
||||
cached_update_packets: Vec<u8>,
|
||||
entries: HashMap<Uuid, Option<PlayerListEntry>>,
|
||||
header: Text,
|
||||
footer: Text,
|
||||
modified_header_or_footer: bool,
|
||||
}
|
||||
pub(crate) struct PlayerListPlugin;
|
||||
|
||||
impl PlayerList {
|
||||
/// Returns a set of systems for maintaining the player list in a reasonable
|
||||
/// default way. When clients connect, they are added to the player list.
|
||||
/// When clients disconnect, they are removed from the player list.
|
||||
pub fn default_systems() -> SystemConfigs {
|
||||
fn add_new_clients_to_player_list(
|
||||
clients: Query<(&Username, &Properties, &GameMode, &Ping, &UniqueId), Added<Client>>,
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
) {
|
||||
for (username, properties, game_mode, ping, uuid) in &clients {
|
||||
let entry = PlayerListEntry::new()
|
||||
.with_username(&username.0)
|
||||
.with_properties(properties.0.clone())
|
||||
.with_game_mode(*game_mode)
|
||||
.with_ping(ping.0);
|
||||
|
||||
player_list.insert(uuid.0, entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_disconnected_clients_from_player_list(
|
||||
mut clients: RemovedComponents<Client>,
|
||||
uuids: Query<&UniqueId>,
|
||||
mut player_list: ResMut<PlayerList>,
|
||||
) {
|
||||
for entity in clients.iter() {
|
||||
if let Ok(UniqueId(uuid)) = uuids.get(entity) {
|
||||
player_list.remove(*uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
add_new_clients_to_player_list,
|
||||
remove_disconnected_clients_from_player_list,
|
||||
)
|
||||
.into_configs()
|
||||
impl Plugin for PlayerListPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(PlayerList::new()).add_systems(
|
||||
(
|
||||
update_header_footer,
|
||||
add_new_clients_to_player_list,
|
||||
apply_system_buffers, // So new clients get the packets for their own entry.
|
||||
update_entries,
|
||||
init_player_list_for_clients,
|
||||
remove_despawned_entries,
|
||||
write_player_list_changes,
|
||||
)
|
||||
.chain()
|
||||
.before(FlushPacketsSet)
|
||||
.in_base_set(CoreSet::PostUpdate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct PlayerList {
|
||||
cached_update_packets: Vec<u8>,
|
||||
scratch: Vec<u8>,
|
||||
header: Text,
|
||||
footer: Text,
|
||||
changed_header_or_footer: bool,
|
||||
/// If clients should be automatically added and removed from the player
|
||||
/// list with the proper components inserted. Enabled by default.
|
||||
pub manage_clients: bool,
|
||||
}
|
||||
|
||||
impl PlayerList {
|
||||
/// Create a new empty player list.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_update_packets: vec![],
|
||||
entries: HashMap::new(),
|
||||
scratch: vec![],
|
||||
header: Text::default(),
|
||||
footer: Text::default(),
|
||||
modified_header_or_footer: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the entry for the given UUID, if it exists, otherwise return None.
|
||||
pub fn get(&self, uuid: Uuid) -> Option<&PlayerListEntry> {
|
||||
self.entries.get(&uuid).and_then(|opt| opt.as_ref())
|
||||
}
|
||||
|
||||
/// Mutably get the entry for the given UUID, if it exists, otherwise return
|
||||
/// None.
|
||||
pub fn get_mut(&mut self, uuid: Uuid) -> Option<&mut PlayerListEntry> {
|
||||
self.entries.get_mut(&uuid).and_then(|opt| opt.as_mut())
|
||||
}
|
||||
|
||||
/// Get an iterator over all entries in the player list. The order of this
|
||||
/// iterator is not guaranteed.
|
||||
pub fn iter(&self) -> impl FusedIterator<Item = (Uuid, &PlayerListEntry)> + Clone + '_ {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|(&uuid, opt)| opt.as_ref().map(|entry| (uuid, entry)))
|
||||
}
|
||||
|
||||
/// Get an iterator over all entries in the player list as mutable. The
|
||||
/// order of this iterator is not guaranteed.
|
||||
pub fn iter_mut(&mut self) -> impl FusedIterator<Item = (Uuid, &mut PlayerListEntry)> + '_ {
|
||||
self.entries
|
||||
.iter_mut()
|
||||
.filter_map(|(&uuid, opt)| opt.as_mut().map(|entry| (uuid, entry)))
|
||||
}
|
||||
|
||||
/// Insert a new entry into the player list. If an entry already exists for
|
||||
/// the given UUID, it is replaced and returned.
|
||||
pub fn insert(&mut self, uuid: Uuid, entry: PlayerListEntry) -> Option<PlayerListEntry> {
|
||||
match self.entry(uuid) {
|
||||
Entry::Occupied(mut oe) => Some(oe.insert(entry)),
|
||||
Entry::Vacant(ve) => {
|
||||
ve.insert(entry);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an entry from the player list. If an entry exists for the given
|
||||
/// UUID, it is removed and returned.
|
||||
pub fn remove(&mut self, uuid: Uuid) -> Option<PlayerListEntry> {
|
||||
match self.entry(uuid) {
|
||||
Entry::Occupied(oe) => Some(oe.remove()),
|
||||
Entry::Vacant(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the given key’s corresponding entry in the map for in-place
|
||||
/// manipulation.
|
||||
pub fn entry(&mut self, uuid: Uuid) -> Entry {
|
||||
match self.entries.entry(uuid) {
|
||||
MapEntry::Occupied(oe) if oe.get().is_some() => {
|
||||
Entry::Occupied(OccupiedEntry { entry: oe })
|
||||
}
|
||||
MapEntry::Occupied(oe) => Entry::Vacant(VacantEntry {
|
||||
entry: MapEntry::Occupied(oe),
|
||||
}),
|
||||
MapEntry::Vacant(ve) => Entry::Vacant(VacantEntry {
|
||||
entry: MapEntry::Vacant(ve),
|
||||
}),
|
||||
changed_header_or_footer: false,
|
||||
manage_clients: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,530 +61,297 @@ impl PlayerList {
|
|||
&self.header
|
||||
}
|
||||
|
||||
/// Set the header text for the player list. Returns the previous header.
|
||||
pub fn set_header(&mut self, header: impl Into<Text>) -> Text {
|
||||
let header = header.into();
|
||||
|
||||
if header != self.header {
|
||||
self.modified_header_or_footer = true;
|
||||
}
|
||||
|
||||
mem::replace(&mut self.header, header)
|
||||
}
|
||||
|
||||
pub fn footer(&self) -> &Text {
|
||||
&self.footer
|
||||
}
|
||||
|
||||
/// Set the footer text for the player list. Returns the previous footer.
|
||||
pub fn set_footer(&mut self, footer: impl Into<Text>) -> Text {
|
||||
let footer = footer.into();
|
||||
pub fn set_header(&mut self, txt: impl Into<Text>) {
|
||||
let txt = txt.into();
|
||||
|
||||
if footer != self.footer {
|
||||
self.modified_header_or_footer = true;
|
||||
if txt != self.header {
|
||||
self.changed_header_or_footer = true;
|
||||
}
|
||||
|
||||
mem::replace(&mut self.footer, footer)
|
||||
self.header = txt;
|
||||
}
|
||||
|
||||
/// Retains only the elements specified by the predicate.
|
||||
///
|
||||
/// In other words, remove all pairs `(k, v)` for which `f(&k, &mut v)`
|
||||
/// returns `false`. The elements are visited in unsorted (and
|
||||
/// unspecified) order.
|
||||
pub fn retain<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(Uuid, &mut PlayerListEntry) -> bool,
|
||||
{
|
||||
self.entries.retain(|&uuid, opt| {
|
||||
if let Some(entry) = opt {
|
||||
if !f(uuid, entry) {
|
||||
*opt = None;
|
||||
}
|
||||
}
|
||||
pub fn set_footer(&mut self, txt: impl Into<Text>) {
|
||||
let txt = txt.into();
|
||||
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear the player list.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.values_mut().for_each(|e| *e = None);
|
||||
}
|
||||
|
||||
pub(crate) fn write_init_packets(&self, mut writer: impl WritePacket) {
|
||||
let actions = Actions::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<_> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|(&uuid, opt)| {
|
||||
opt.as_ref().map(|entry| PlayerInfoEntry {
|
||||
player_uuid: uuid,
|
||||
username: &entry.username,
|
||||
properties: entry.properties().into(),
|
||||
chat_data: None,
|
||||
listed: entry.listed,
|
||||
ping: entry.ping,
|
||||
game_mode: entry.game_mode.into(),
|
||||
display_name: entry.display_name.as_ref().map(|t| t.into()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !entries.is_empty() {
|
||||
writer.write_packet(&PlayerListS2c {
|
||||
actions,
|
||||
entries: entries.into(),
|
||||
});
|
||||
if txt != self.footer {
|
||||
self.changed_header_or_footer = true;
|
||||
}
|
||||
|
||||
if !self.header.is_empty() || !self.footer.is_empty() {
|
||||
writer.write_packet(&PlayerListHeaderS2c {
|
||||
header: (&self.header).into(),
|
||||
footer: (&self.footer).into(),
|
||||
});
|
||||
}
|
||||
self.footer = txt;
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a player entry in the [`PlayerList`].
|
||||
/// Bundle for spawning new player list entries. All components are required
|
||||
/// unless otherwise stated.
|
||||
///
|
||||
/// ```
|
||||
/// use valence::player_list::PlayerListEntry;
|
||||
/// # Despawning player list entries
|
||||
///
|
||||
/// PlayerListEntry::new()
|
||||
/// .with_username("Notch")
|
||||
/// .with_display_name(Some("Herobrine"));
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlayerListEntry {
|
||||
username: String,
|
||||
properties: Vec<Property>,
|
||||
game_mode: GameMode,
|
||||
old_game_mode: GameMode,
|
||||
ping: i32,
|
||||
display_name: Option<Text>,
|
||||
listed: bool,
|
||||
old_listed: bool,
|
||||
is_new: bool,
|
||||
modified_ping: bool,
|
||||
modified_display_name: bool,
|
||||
/// The [`Despawned`] component must be used to despawn player list entries.
|
||||
#[derive(Bundle, Default, Debug)]
|
||||
pub struct PlayerListEntryBundle {
|
||||
pub player_list_entry: PlayerListEntry,
|
||||
/// Careful not to modify this!
|
||||
pub uuid: UniqueId,
|
||||
pub username: Username,
|
||||
pub properties: Properties,
|
||||
pub game_mode: GameMode,
|
||||
pub ping: Ping,
|
||||
pub display_name: DisplayName,
|
||||
pub listed: Listed,
|
||||
}
|
||||
|
||||
impl Default for PlayerListEntry {
|
||||
/// Marker component for player list entries.
|
||||
#[derive(Component, Default, Debug)]
|
||||
pub struct PlayerListEntry;
|
||||
|
||||
/// Displayed name for a player list entry. Appears as [`Username`] if `None`.
|
||||
#[derive(Component, Default, Debug)]
|
||||
pub struct DisplayName(pub Option<Text>);
|
||||
|
||||
/// If a player list entry is visible. Defaults to `true`.
|
||||
#[derive(Component, Copy, Clone, Debug)]
|
||||
pub struct Listed(pub bool);
|
||||
|
||||
impl Default for Listed {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
username: String::new(),
|
||||
properties: vec![],
|
||||
game_mode: GameMode::default(),
|
||||
old_game_mode: GameMode::default(),
|
||||
ping: -1, // Negative indicates absence.
|
||||
display_name: None,
|
||||
old_listed: true,
|
||||
listed: true,
|
||||
is_new: true,
|
||||
modified_ping: false,
|
||||
modified_display_name: false,
|
||||
}
|
||||
Self(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerListEntry {
|
||||
/// Create a new player list entry.
|
||||
///
|
||||
/// ```
|
||||
/// use valence::player_list::PlayerListEntry;
|
||||
///
|
||||
/// PlayerListEntry::new()
|
||||
/// .with_username("Notch")
|
||||
/// .with_display_name(Some("Herobrine"));
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
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();
|
||||
|
||||
/// Set the username for the player list entry. Returns `Self` to chain
|
||||
/// other options.
|
||||
#[must_use]
|
||||
pub fn with_username(mut self, username: impl Into<String>) -> Self {
|
||||
self.username = username.into();
|
||||
|
||||
if self.username.chars().count() > 16 {
|
||||
warn!("player list username is longer than 16 characters");
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the properties for the player list entry. Returns `Self` to chain
|
||||
/// other options.
|
||||
///
|
||||
/// A property is a key-value pair that can be used to customize the
|
||||
/// appearance of the player list entry. For example, the skin of the
|
||||
/// player can be set by adding a property with the key `textures` and
|
||||
/// the value being a base64 encoded JSON object.
|
||||
#[must_use]
|
||||
pub fn with_properties(mut self, properties: impl Into<Vec<Property>>) -> Self {
|
||||
self.properties = properties.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the game mode for the player list entry. Returns `Self` to chain
|
||||
/// other options.
|
||||
#[must_use]
|
||||
pub fn with_game_mode(mut self, game_mode: GameMode) -> Self {
|
||||
self.game_mode = game_mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the ping for the player list entry. Returns `Self` to chain other
|
||||
/// options.
|
||||
///
|
||||
/// The ping is the number of milliseconds it takes for the server to
|
||||
/// receive a response from the player. The client will display the
|
||||
/// ping as a number of green bars, where more bars indicate a lower
|
||||
/// ping.
|
||||
///
|
||||
/// Use a value of `-1` to hide the ping.
|
||||
#[must_use]
|
||||
pub fn with_ping(mut self, ping: i32) -> Self {
|
||||
self.ping = ping;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the display name for the player list entry. Returns `Self` to
|
||||
/// chain other options.
|
||||
///
|
||||
/// The display name is the literal text that is displayed in the player
|
||||
/// list. If this is not set, the username will be used instead.
|
||||
#[must_use]
|
||||
pub fn with_display_name(mut self, display_name: Option<impl Into<Text>>) -> Self {
|
||||
self.display_name = display_name.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether the player list entry is listed. Returns `Self` to chain
|
||||
/// other options. Setting this to `false` will hide the entry from the
|
||||
/// player list.
|
||||
#[must_use]
|
||||
pub fn with_listed(mut self, listed: bool) -> Self {
|
||||
self.listed = listed;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn username(&self) -> &str {
|
||||
&self.username
|
||||
}
|
||||
|
||||
pub fn properties(&self) -> &[Property] {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
pub fn game_mode(&self) -> GameMode {
|
||||
self.game_mode
|
||||
}
|
||||
|
||||
/// Set the game mode for the player list entry.
|
||||
pub fn set_game_mode(&mut self, game_mode: GameMode) {
|
||||
self.game_mode = game_mode;
|
||||
}
|
||||
|
||||
pub fn ping(&self) -> i32 {
|
||||
self.ping
|
||||
}
|
||||
|
||||
/// Set the ping for the player list entry.
|
||||
///
|
||||
/// The ping is the number of milliseconds it takes for the server to
|
||||
/// receive a response from the player. The client will display the
|
||||
/// ping as a number of green bars, where more bars indicate a lower
|
||||
/// ping.
|
||||
///
|
||||
/// Use a value of `-1` to hide the ping.
|
||||
pub fn set_ping(&mut self, ping: i32) {
|
||||
if self.ping != ping {
|
||||
self.ping = ping;
|
||||
self.modified_ping = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Option<&Text> {
|
||||
self.display_name.as_ref()
|
||||
}
|
||||
|
||||
/// Set the display name for the player list entry. Returns the previous
|
||||
/// display name, if any.
|
||||
///
|
||||
/// The display name is the literal text that is displayed in the player
|
||||
/// list. If this is not set, the username will be used instead.
|
||||
pub fn set_display_name(&mut self, display_name: Option<impl Into<Text>>) -> Option<Text> {
|
||||
let display_name = display_name.map(Into::into);
|
||||
|
||||
if self.display_name != display_name {
|
||||
self.modified_display_name = true;
|
||||
}
|
||||
|
||||
mem::replace(&mut self.display_name, display_name)
|
||||
}
|
||||
|
||||
pub fn is_listed(&self) -> bool {
|
||||
self.listed
|
||||
}
|
||||
|
||||
/// Set whether the player list entry is listed. Setting this to `false`
|
||||
/// will hide the entry from the player list.
|
||||
pub fn set_listed(&mut self, listed: bool) {
|
||||
self.listed = listed;
|
||||
}
|
||||
|
||||
fn clear_trackers(&mut self) {
|
||||
self.old_game_mode = self.game_mode;
|
||||
self.old_listed = self.listed;
|
||||
self.modified_ping = false;
|
||||
self.modified_display_name = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry in the player list that corresponds to a single UUID. Works like
|
||||
/// [`std::collections::hash_map::Entry`].
|
||||
#[derive(Debug)]
|
||||
pub enum Entry<'a> {
|
||||
Occupied(OccupiedEntry<'a>),
|
||||
Vacant(VacantEntry<'a>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OccupiedEntry<'a> {
|
||||
entry: OccupiedMapEntry<'a, Uuid, Option<PlayerListEntry>>,
|
||||
}
|
||||
|
||||
impl<'a> OccupiedEntry<'a> {
|
||||
pub fn key(&self) -> &Uuid {
|
||||
self.entry.key()
|
||||
}
|
||||
|
||||
pub fn remove_entry(mut self) -> (Uuid, PlayerListEntry) {
|
||||
let mut entry = self.entry.get_mut().take().unwrap();
|
||||
let uuid = *self.entry.key();
|
||||
|
||||
entry.is_new = false;
|
||||
|
||||
(uuid, entry)
|
||||
}
|
||||
|
||||
pub fn get(&self) -> &PlayerListEntry {
|
||||
self.entry.get().as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut PlayerListEntry {
|
||||
self.entry.get_mut().as_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn into_mut(self) -> &'a mut PlayerListEntry {
|
||||
self.entry.into_mut().as_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, mut entry: PlayerListEntry) -> PlayerListEntry {
|
||||
let old_entry = self.get_mut();
|
||||
|
||||
// Need to overwrite the entry if the username or properties changed because the
|
||||
// player list update packet doesn't support modifying these. Otherwise we can
|
||||
// just modify the existing entry.
|
||||
if old_entry.username != entry.username || old_entry.properties != entry.properties {
|
||||
entry.clear_trackers();
|
||||
entry.is_new = true;
|
||||
self.entry.insert(Some(entry)).unwrap()
|
||||
} else {
|
||||
PlayerListEntry::new()
|
||||
.with_game_mode(old_entry.game_mode)
|
||||
.with_ping(old_entry.ping)
|
||||
.with_display_name(old_entry.set_display_name(entry.display_name))
|
||||
.with_listed(old_entry.listed)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self) -> PlayerListEntry {
|
||||
self.remove_entry().1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VacantEntry<'a> {
|
||||
entry: MapEntry<'a, Uuid, Option<PlayerListEntry>>,
|
||||
}
|
||||
|
||||
impl<'a> VacantEntry<'a> {
|
||||
pub fn key(&self) -> &Uuid {
|
||||
self.entry.key()
|
||||
}
|
||||
|
||||
pub fn into_key(self) -> Uuid {
|
||||
*self.entry.key()
|
||||
}
|
||||
|
||||
pub fn insert(self, mut entry: PlayerListEntry) -> &'a mut PlayerListEntry {
|
||||
entry.clear_trackers();
|
||||
entry.is_new = true;
|
||||
|
||||
match self.entry {
|
||||
MapEntry::Occupied(mut oe) => {
|
||||
oe.insert(Some(entry));
|
||||
oe.into_mut().as_mut().unwrap()
|
||||
}
|
||||
MapEntry::Vacant(ve) => ve.insert(Some(entry)).as_mut().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PlayerListPlugin;
|
||||
|
||||
impl Plugin for PlayerListPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.insert_resource(PlayerList::new()).add_system(
|
||||
update_player_list
|
||||
.before(FlushPacketsSet)
|
||||
.in_base_set(CoreSet::PostUpdate),
|
||||
let mut w = PacketWriter::new(
|
||||
&mut player_list.cached_update_packets,
|
||||
server.compression_threshold(),
|
||||
&mut player_list.scratch,
|
||||
);
|
||||
|
||||
w.write_packet(&PlayerListHeaderS2c {
|
||||
header: (&player_list.header).into(),
|
||||
footer: (&player_list.footer).into(),
|
||||
});
|
||||
|
||||
player_list.changed_header_or_footer = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Manage all player lists on the server and send updates to clients.
|
||||
fn update_player_list(
|
||||
player_list: ResMut<PlayerList>,
|
||||
server: Res<Server>,
|
||||
mut clients: Query<&mut Client>,
|
||||
fn add_new_clients_to_player_list(
|
||||
clients: Query<Entity, Added<Client>>,
|
||||
player_list: Res<PlayerList>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let pl = player_list.into_inner();
|
||||
if player_list.manage_clients {
|
||||
for entity in &clients {
|
||||
commands.entity(entity).insert((
|
||||
PlayerListEntry,
|
||||
DisplayName::default(),
|
||||
Listed::default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut scratch = vec![];
|
||||
pl.cached_update_packets.clear();
|
||||
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 = Actions::new()
|
||||
.with_add_player(true)
|
||||
.with_update_game_mode(true)
|
||||
.with_update_listed(true)
|
||||
.with_update_latency(true)
|
||||
.with_update_display_name(true);
|
||||
|
||||
let mut writer = PacketWriter::new(
|
||||
&mut pl.cached_update_packets,
|
||||
server.compression_threshold(),
|
||||
&mut scratch,
|
||||
);
|
||||
|
||||
let mut removed = vec![];
|
||||
|
||||
pl.entries.retain(|&uuid, entry| {
|
||||
let Some(entry) = entry else {
|
||||
removed.push(uuid);
|
||||
return false
|
||||
};
|
||||
|
||||
if entry.is_new {
|
||||
entry.is_new = false;
|
||||
|
||||
// Send packets to initialize this entry.
|
||||
|
||||
let mut actions = Actions::new().with_add_player(true);
|
||||
|
||||
// We don't need to send data for fields if they have the default values.
|
||||
|
||||
if entry.listed {
|
||||
actions.set_update_listed(true);
|
||||
}
|
||||
|
||||
// Negative ping indicates absence.
|
||||
if entry.ping != 0 {
|
||||
actions.set_update_latency(true);
|
||||
}
|
||||
|
||||
if entry.game_mode != GameMode::default() {
|
||||
actions.set_update_game_mode(true);
|
||||
}
|
||||
|
||||
if entry.display_name.is_some() {
|
||||
actions.set_update_display_name(true);
|
||||
}
|
||||
|
||||
entry.clear_trackers();
|
||||
|
||||
let packet_entry = PlayerInfoEntry {
|
||||
player_uuid: uuid,
|
||||
username: &entry.username,
|
||||
properties: Cow::Borrowed(&entry.properties),
|
||||
chat_data: None,
|
||||
listed: entry.listed,
|
||||
ping: entry.ping,
|
||||
game_mode: entry.game_mode.into(),
|
||||
display_name: entry.display_name.as_ref().map(|t| t.into()),
|
||||
};
|
||||
|
||||
writer.write_packet(&PlayerListS2c {
|
||||
actions,
|
||||
entries: Cow::Borrowed(&[packet_entry]),
|
||||
});
|
||||
} else {
|
||||
let mut actions = Actions::new();
|
||||
|
||||
if entry.game_mode != entry.old_game_mode {
|
||||
entry.old_game_mode = entry.game_mode;
|
||||
actions.set_update_game_mode(true);
|
||||
}
|
||||
|
||||
if entry.listed != entry.old_listed {
|
||||
entry.old_listed = entry.listed;
|
||||
actions.set_update_listed(true);
|
||||
}
|
||||
|
||||
if entry.modified_ping {
|
||||
entry.modified_ping = false;
|
||||
actions.set_update_latency(true);
|
||||
}
|
||||
|
||||
if entry.modified_display_name {
|
||||
entry.modified_display_name = false;
|
||||
actions.set_update_display_name(true);
|
||||
}
|
||||
|
||||
if u8::from(actions) != 0 {
|
||||
writer.write_packet(&PlayerListS2c {
|
||||
actions,
|
||||
entries: Cow::Borrowed(&[PlayerInfoEntry {
|
||||
player_uuid: uuid,
|
||||
username: &entry.username,
|
||||
properties: Cow::default(),
|
||||
let entries: Vec<_> = entries
|
||||
.iter()
|
||||
.map(
|
||||
|(uuid, username, props, game_mode, ping, display_name, listed)| Entry {
|
||||
player_uuid: uuid.0,
|
||||
username: &username.0,
|
||||
properties: Cow::Borrowed(&props.0),
|
||||
chat_data: None,
|
||||
listed: entry.listed,
|
||||
ping: entry.ping,
|
||||
game_mode: entry.game_mode.into(),
|
||||
display_name: entry.display_name.as_ref().map(|t| t.into()),
|
||||
}]),
|
||||
listed: listed.0,
|
||||
ping: ping.0,
|
||||
game_mode: (*game_mode).into(),
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
if !removed.is_empty() {
|
||||
writer.write_packet(&PlayerRemoveS2c {
|
||||
uuids: removed.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if pl.modified_header_or_footer {
|
||||
pl.modified_header_or_footer = false;
|
||||
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());
|
||||
|
||||
writer.write_packet(&PlayerListHeaderS2c {
|
||||
header: (&pl.header).into(),
|
||||
footer: (&pl.footer).into(),
|
||||
});
|
||||
}
|
||||
removed.extend(entries.iter().map(|uuid| uuid.0));
|
||||
|
||||
for mut client in &mut clients {
|
||||
if client.is_added() {
|
||||
pl.write_init_packets(client.into_inner());
|
||||
} else {
|
||||
client.write_packet_bytes(&pl.cached_update_packets);
|
||||
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(),
|
||||
&mut player_list.scratch,
|
||||
);
|
||||
|
||||
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(),
|
||||
&mut player_list.scratch,
|
||||
);
|
||||
|
||||
for (uuid, username, props, game_mode, ping, display_name, listed) in &entries {
|
||||
let mut actions = Actions::new();
|
||||
|
||||
// Did a change occur that would force us to overwrite the entry? This also adds
|
||||
// new entries.
|
||||
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 = Entry {
|
||||
player_uuid: uuid.0,
|
||||
username: &username.0,
|
||||
properties: (&props.0).into(),
|
||||
chat_data: None,
|
||||
listed: listed.0,
|
||||
ping: ping.0,
|
||||
game_mode: (*game_mode).into(),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use valence_protocol::types::Property;
|
|||
|
||||
use crate::biome::BiomePlugin;
|
||||
use crate::client::{ClientBundle, ClientPlugin};
|
||||
use crate::component::ComponentPlugin;
|
||||
use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
|
||||
use crate::dimension::DimensionPlugin;
|
||||
use crate::entity::EntityPlugin;
|
||||
|
@ -24,7 +25,6 @@ use crate::event_loop::{EventLoopPlugin, RunEventLoopSet};
|
|||
use crate::instance::InstancePlugin;
|
||||
use crate::inventory::InventoryPlugin;
|
||||
use crate::player_list::PlayerListPlugin;
|
||||
use crate::prelude::ComponentPlugin;
|
||||
use crate::registry_codec::RegistryCodecPlugin;
|
||||
use crate::server::connect::do_accept_loop;
|
||||
use crate::weather::WeatherPlugin;
|
||||
|
|
Loading…
Reference in a new issue