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,
|
init_clients,
|
||||||
despawn_disconnected_clients,
|
despawn_disconnected_clients,
|
||||||
))
|
))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ pub fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_systems((init_clients, despawn_disconnected_clients))
|
.add_systems((init_clients, despawn_disconnected_clients))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ pub fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_systems((event_handler, init_clients, despawn_disconnected_clients))
|
.add_systems((event_handler, init_clients, despawn_disconnected_clients))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ pub fn main() {
|
||||||
digging_survival_mode,
|
digging_survival_mode,
|
||||||
place_blocks,
|
place_blocks,
|
||||||
))
|
))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ pub fn main() {
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_systems((toggle_gamemode_on_sneak, open_chest))
|
.add_systems((toggle_gamemode_on_sneak, open_chest))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ pub fn main() {
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_system(handle_combat_events.in_schedule(EventLoopSchedule))
|
.add_system(handle_combat_events.in_schedule(EventLoopSchedule))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.add_system(teleport_oob_clients)
|
.add_system(teleport_oob_clients)
|
||||||
.run();
|
.run();
|
||||||
|
|
|
@ -27,7 +27,6 @@ pub fn main() {
|
||||||
.add_startup_system(setup_biomes.before(setup))
|
.add_startup_system(setup_biomes.before(setup))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_systems((
|
.add_systems((
|
||||||
despawn_disconnected_clients,
|
despawn_disconnected_clients,
|
||||||
toggle_cell_on_dig,
|
toggle_cell_on_dig,
|
||||||
|
|
|
@ -26,7 +26,6 @@ fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(update_sphere)
|
.add_system(update_sphere)
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.run();
|
.run();
|
||||||
|
|
|
@ -12,7 +12,6 @@ pub fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
|
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_systems((init_clients, squat_and_die, necromancy))
|
.add_systems((init_clients, squat_and_die, necromancy))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ pub fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_systems((
|
.add_systems((
|
||||||
reset_clients.after(init_clients),
|
reset_clients.after(init_clients),
|
||||||
manage_chunks.after(reset_clients).before(manage_blocks),
|
manage_chunks.after(reset_clients).before(manage_blocks),
|
||||||
|
|
|
@ -13,7 +13,6 @@ pub fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.add_system(manage_particles)
|
.add_system(manage_particles)
|
||||||
.run();
|
.run();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use valence::player_list::Entry;
|
use valence::player_list::{DisplayName, PlayerListEntryBundle};
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
@ -14,11 +14,10 @@ fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_systems((
|
.add_systems((
|
||||||
init_clients,
|
init_clients,
|
||||||
|
override_display_name,
|
||||||
update_player_list,
|
update_player_list,
|
||||||
remove_disconnected_clients_from_player_list,
|
|
||||||
despawn_disconnected_clients,
|
despawn_disconnected_clients,
|
||||||
))
|
))
|
||||||
.run();
|
.run();
|
||||||
|
@ -26,7 +25,6 @@ fn main() {
|
||||||
|
|
||||||
fn setup(
|
fn setup(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut player_list: ResMut<PlayerList>,
|
|
||||||
server: Res<Server>,
|
server: Res<Server>,
|
||||||
dimensions: Query<&DimensionType>,
|
dimensions: Query<&DimensionType>,
|
||||||
biomes: Query<&Biome>,
|
biomes: Query<&Biome>,
|
||||||
|
@ -47,29 +45,18 @@ fn setup(
|
||||||
|
|
||||||
commands.spawn(instance);
|
commands.spawn(instance);
|
||||||
|
|
||||||
player_list.insert(
|
commands.spawn(PlayerListEntryBundle {
|
||||||
PLAYER_UUID_1,
|
uuid: UniqueId(PLAYER_UUID_1),
|
||||||
PlayerListEntry::new().with_display_name(Some("persistent entry with no ping")),
|
display_name: DisplayName(Some("persistent entry with no ping".into())),
|
||||||
);
|
..Default::default()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_clients(
|
fn init_clients(
|
||||||
mut clients: Query<
|
mut clients: Query<(&mut Client, &mut Position, &mut Location, &mut GameMode), Added<Client>>,
|
||||||
(
|
|
||||||
&mut Client,
|
|
||||||
&mut Position,
|
|
||||||
&mut Location,
|
|
||||||
&mut GameMode,
|
|
||||||
&Username,
|
|
||||||
&Properties,
|
|
||||||
&UniqueId,
|
|
||||||
),
|
|
||||||
Added<Client>,
|
|
||||||
>,
|
|
||||||
instances: Query<Entity, With<Instance>>,
|
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]);
|
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
|
||||||
loc.0 = instances.single();
|
loc.0 = instances.single();
|
||||||
*game_mode = GameMode::Creative;
|
*game_mode = GameMode::Creative;
|
||||||
|
@ -79,19 +66,21 @@ fn init_clients(
|
||||||
.italic()
|
.italic()
|
||||||
.color(Color::WHITE),
|
.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();
|
let tick = server.current_tick();
|
||||||
|
|
||||||
player_list.set_header("Current tick: ".into_text() + 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));
|
.set_footer("Current tick but in purple: ".into_text() + tick.color(Color::LIGHT_PURPLE));
|
||||||
|
|
||||||
if tick % 5 == 0 {
|
if tick % 5 == 0 {
|
||||||
|
for (_, uuid, mut display_name) in &mut entries {
|
||||||
|
if uuid.0 == PLAYER_UUID_1 {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let color = Color::new(rng.gen(), rng.gen(), rng.gen());
|
let color = Color::new(rng.gen(), rng.gen(), rng.gen());
|
||||||
|
|
||||||
let entry = player_list.get_mut(PLAYER_UUID_1).unwrap();
|
let new_name = display_name.0.clone().unwrap_or_default().color(color);
|
||||||
let new_display_name = entry.display_name().unwrap().clone().color(color);
|
display_name.0 = Some(new_name);
|
||||||
entry.set_display_name(Some(new_display_name));
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tick % 20 == 0 {
|
if tick % 20 == 0 {
|
||||||
match player_list.entry(PLAYER_UUID_2) {
|
if let Some((entity, _, _)) = entries.iter().find(|(_, uuid, _)| uuid.0 == PLAYER_UUID_2) {
|
||||||
Entry::Occupied(oe) => {
|
commands.entity(entity).insert(Despawned);
|
||||||
oe.remove();
|
} else {
|
||||||
}
|
commands.spawn(PlayerListEntryBundle {
|
||||||
Entry::Vacant(ve) => {
|
uuid: UniqueId(PLAYER_UUID_2),
|
||||||
let entry = PlayerListEntry::new()
|
display_name: DisplayName(Some("Hello!".into())),
|
||||||
.with_display_name(Some("Hello!"))
|
ping: Ping(300),
|
||||||
.with_ping(300);
|
..Default::default()
|
||||||
|
});
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ pub fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_systems((init_clients, prompt_on_punch, on_resource_pack_status))
|
.add_systems((init_clients, prompt_on_punch, on_resource_pack_status))
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,6 @@ pub fn main() {
|
||||||
.chain(),
|
.chain(),
|
||||||
)
|
)
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ pub fn main() {
|
||||||
.add_plugin(ServerPlugin::new(()))
|
.add_plugin(ServerPlugin::new(()))
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(init_clients)
|
.add_system(init_clients)
|
||||||
.add_systems(PlayerList::default_systems())
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ pub(crate) struct ClientBundle {
|
||||||
client: Client,
|
client: Client,
|
||||||
settings: settings::ClientSettings,
|
settings: settings::ClientSettings,
|
||||||
scratch: ScratchBuf,
|
scratch: ScratchBuf,
|
||||||
entity_remove_buffer: EntityRemoveBuf,
|
entity_remove_buf: EntityRemoveBuf,
|
||||||
username: Username,
|
username: Username,
|
||||||
ip: Ip,
|
ip: Ip,
|
||||||
properties: Properties,
|
properties: Properties,
|
||||||
|
@ -151,7 +151,7 @@ impl ClientBundle {
|
||||||
client: Client { conn, enc },
|
client: Client { conn, enc },
|
||||||
settings: settings::ClientSettings::default(),
|
settings: settings::ClientSettings::default(),
|
||||||
scratch: ScratchBuf::default(),
|
scratch: ScratchBuf::default(),
|
||||||
entity_remove_buffer: EntityRemoveBuf(vec![]),
|
entity_remove_buf: EntityRemoveBuf(vec![]),
|
||||||
username: Username(info.username),
|
username: Username(info.username),
|
||||||
ip: Ip(info.ip),
|
ip: Ip(info.ip),
|
||||||
properties: Properties(info.properties),
|
properties: Properties(info.properties),
|
||||||
|
|
|
@ -40,6 +40,7 @@ fn send_keepalive(
|
||||||
) {
|
) {
|
||||||
if server.current_tick() % (server.tps() * 10) == 0 {
|
if server.current_tick() % (server.tps() * 10) == 0 {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
for (entity, mut client, mut state) in &mut clients {
|
for (entity, mut client, mut state) in &mut clients {
|
||||||
if state.got_keepalive {
|
if state.got_keepalive {
|
||||||
|
@ -48,8 +49,7 @@ fn send_keepalive(
|
||||||
|
|
||||||
state.got_keepalive = false;
|
state.got_keepalive = false;
|
||||||
state.last_keepalive_id = id;
|
state.last_keepalive_id = id;
|
||||||
// TODO: start timing when the packets are flushed.
|
state.keepalive_sent_time = now;
|
||||||
state.keepalive_sent_time = Instant::now();
|
|
||||||
} else {
|
} else {
|
||||||
warn!("Client {entity:?} timed out (no keepalive response)");
|
warn!("Client {entity:?} timed out (no keepalive response)");
|
||||||
commands.entity(entity).remove::<Client>();
|
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::util::{from_yaw_and_pitch, to_yaw_and_pitch};
|
||||||
use crate::view::ChunkPos;
|
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
|
/// A [`Component`] for marking entities that should be despawned at the end of
|
||||||
/// the tick.
|
/// 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);
|
pub struct Username(pub String);
|
||||||
|
|
||||||
impl fmt::Display for Username {
|
impl fmt::Display for Username {
|
||||||
|
@ -235,40 +268,9 @@ impl Look {
|
||||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
|
||||||
pub struct OnGround(pub bool);
|
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)]
|
#[derive(Component, Default, Debug)]
|
||||||
pub struct ScratchBuf(pub Vec<u8>);
|
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::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::prelude::*;
|
||||||
use bevy_ecs::schedule::SystemConfigs;
|
|
||||||
use tracing::warn;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use valence_protocol::packet::s2c::play::player_list::{
|
use valence_protocol::packet::s2c::play::player_list::{Actions, Entry, PlayerListS2c};
|
||||||
Actions, Entry as PlayerInfoEntry, PlayerListS2c,
|
|
||||||
};
|
|
||||||
use valence_protocol::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c};
|
use valence_protocol::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c};
|
||||||
use valence_protocol::text::Text;
|
use valence_protocol::text::Text;
|
||||||
use valence_protocol::types::Property;
|
|
||||||
|
|
||||||
use crate::client::{Client, FlushPacketsSet};
|
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::packet::{PacketWriter, WritePacket};
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
|
|
||||||
/// The global list of players on a server visible by pressing the tab key by
|
pub(crate) struct PlayerListPlugin;
|
||||||
/// default.
|
|
||||||
///
|
impl Plugin for PlayerListPlugin {
|
||||||
/// Each entry in the player list is intended to represent a connected client to
|
fn build(&self, app: &mut App) {
|
||||||
/// the server. In addition to a list of players, the player list has a header
|
app.insert_resource(PlayerList::new()).add_systems(
|
||||||
/// and a footer which can contain arbitrary text.
|
(
|
||||||
///
|
update_header_footer,
|
||||||
/// ```ignore
|
add_new_clients_to_player_list,
|
||||||
/// # use uuid::Uuid;
|
apply_system_buffers, // So new clients get the packets for their own entry.
|
||||||
/// # use valence::player_list::{PlayerList, PlayerListEntry};
|
update_entries,
|
||||||
///
|
init_player_list_for_clients,
|
||||||
/// # let mut player_list = PlayerList::new();
|
remove_despawned_entries,
|
||||||
/// player_list.set_header("Hello, world!");
|
write_player_list_changes,
|
||||||
/// player_list.set_footer("Goodbye, world!");
|
)
|
||||||
/// player_list.insert(
|
.chain()
|
||||||
/// Uuid::new_v4(),
|
.before(FlushPacketsSet)
|
||||||
/// PlayerListEntry::new()
|
.in_base_set(CoreSet::PostUpdate),
|
||||||
/// .with_username("Notch")
|
);
|
||||||
/// .with_display_name(Some("Herobrine")),
|
}
|
||||||
/// );
|
}
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Resource)]
|
#[derive(Resource)]
|
||||||
pub struct PlayerList {
|
pub struct PlayerList {
|
||||||
cached_update_packets: Vec<u8>,
|
cached_update_packets: Vec<u8>,
|
||||||
entries: HashMap<Uuid, Option<PlayerListEntry>>,
|
scratch: Vec<u8>,
|
||||||
header: Text,
|
header: Text,
|
||||||
footer: Text,
|
footer: Text,
|
||||||
modified_header_or_footer: bool,
|
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 {
|
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 PlayerList {
|
|
||||||
/// Create a new empty player list.
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cached_update_packets: vec![],
|
cached_update_packets: vec![],
|
||||||
entries: HashMap::new(),
|
scratch: vec![],
|
||||||
header: Text::default(),
|
header: Text::default(),
|
||||||
footer: Text::default(),
|
footer: Text::default(),
|
||||||
modified_header_or_footer: false,
|
changed_header_or_footer: false,
|
||||||
}
|
manage_clients: true,
|
||||||
}
|
|
||||||
|
|
||||||
/// 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),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,58 +61,121 @@ impl PlayerList {
|
||||||
&self.header
|
&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 {
|
pub fn footer(&self) -> &Text {
|
||||||
&self.footer
|
&self.footer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the footer text for the player list. Returns the previous footer.
|
pub fn set_header(&mut self, txt: impl Into<Text>) {
|
||||||
pub fn set_footer(&mut self, footer: impl Into<Text>) -> Text {
|
let txt = txt.into();
|
||||||
let footer = footer.into();
|
|
||||||
|
|
||||||
if footer != self.footer {
|
if txt != self.header {
|
||||||
self.modified_header_or_footer = true;
|
self.changed_header_or_footer = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
mem::replace(&mut self.footer, footer)
|
self.header = txt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retains only the elements specified by the predicate.
|
pub fn set_footer(&mut self, txt: impl Into<Text>) {
|
||||||
///
|
let txt = txt.into();
|
||||||
/// In other words, remove all pairs `(k, v)` for which `f(&k, &mut v)`
|
|
||||||
/// returns `false`. The elements are visited in unsorted (and
|
if txt != self.footer {
|
||||||
/// unspecified) order.
|
self.changed_header_or_footer = true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
self.footer = txt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bundle for spawning new player list entries. All components are required
|
||||||
|
/// unless otherwise stated.
|
||||||
|
///
|
||||||
|
/// # Despawning player list entries
|
||||||
|
///
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(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(),
|
||||||
|
&mut player_list.scratch,
|
||||||
|
);
|
||||||
|
|
||||||
|
w.write_packet(&PlayerListHeaderS2c {
|
||||||
|
header: (&player_list.header).into(),
|
||||||
|
footer: (&player_list.footer).into(),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the player list.
|
player_list.changed_header_or_footer = false;
|
||||||
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) {
|
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 = Actions::new()
|
let actions = Actions::new()
|
||||||
.with_add_player(true)
|
.with_add_player(true)
|
||||||
.with_update_game_mode(true)
|
.with_update_game_mode(true)
|
||||||
|
@ -230,471 +183,175 @@ impl PlayerList {
|
||||||
.with_update_latency(true)
|
.with_update_latency(true)
|
||||||
.with_update_display_name(true);
|
.with_update_display_name(true);
|
||||||
|
|
||||||
let entries: Vec<_> = self
|
let entries: Vec<_> = entries
|
||||||
.entries
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(&uuid, opt)| {
|
.map(
|
||||||
opt.as_ref().map(|entry| PlayerInfoEntry {
|
|(uuid, username, props, game_mode, ping, display_name, listed)| Entry {
|
||||||
player_uuid: uuid,
|
player_uuid: uuid.0,
|
||||||
username: &entry.username,
|
username: &username.0,
|
||||||
properties: entry.properties().into(),
|
properties: Cow::Borrowed(&props.0),
|
||||||
chat_data: None,
|
chat_data: None,
|
||||||
listed: entry.listed,
|
listed: listed.0,
|
||||||
ping: entry.ping,
|
ping: ping.0,
|
||||||
game_mode: entry.game_mode.into(),
|
game_mode: (*game_mode).into(),
|
||||||
display_name: entry.display_name.as_ref().map(|t| t.into()),
|
display_name: display_name.0.as_ref().map(Cow::Borrowed),
|
||||||
})
|
},
|
||||||
})
|
)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !entries.is_empty() {
|
if !entries.is_empty() {
|
||||||
writer.write_packet(&PlayerListS2c {
|
client.write_packet(&PlayerListS2c {
|
||||||
actions,
|
actions,
|
||||||
entries: entries.into(),
|
entries: Cow::Owned(entries),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.header.is_empty() || !self.footer.is_empty() {
|
if !player_list.header.is_empty() || !player_list.footer.is_empty() {
|
||||||
writer.write_packet(&PlayerListHeaderS2c {
|
client.write_packet(&PlayerListHeaderS2c {
|
||||||
header: (&self.header).into(),
|
header: Cow::Borrowed(&player_list.header),
|
||||||
footer: (&self.footer).into(),
|
footer: Cow::Borrowed(&player_list.footer),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a player entry in the [`PlayerList`].
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use valence::player_list::PlayerListEntry;
|
|
||||||
///
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PlayerListEntry {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerListEntry {
|
fn remove_despawned_entries(
|
||||||
/// Create a new player list entry.
|
entries: Query<&UniqueId, (Added<Despawned>, With<PlayerListEntry>)>,
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use valence::player_list::PlayerListEntry;
|
|
||||||
///
|
|
||||||
/// PlayerListEntry::new()
|
|
||||||
/// .with_username("Notch")
|
|
||||||
/// .with_display_name(Some("Herobrine"));
|
|
||||||
/// ```
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manage all player lists on the server and send updates to clients.
|
|
||||||
fn update_player_list(
|
|
||||||
player_list: ResMut<PlayerList>,
|
player_list: ResMut<PlayerList>,
|
||||||
server: Res<Server>,
|
server: Res<Server>,
|
||||||
mut clients: Query<&mut Client>,
|
mut removed: Local<Vec<Uuid>>,
|
||||||
) {
|
) {
|
||||||
let pl = player_list.into_inner();
|
if player_list.manage_clients {
|
||||||
|
debug_assert!(removed.is_empty());
|
||||||
|
|
||||||
let mut scratch = vec![];
|
removed.extend(entries.iter().map(|uuid| uuid.0));
|
||||||
pl.cached_update_packets.clear();
|
|
||||||
|
|
||||||
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(),
|
|
||||||
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()),
|
|
||||||
}]),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
});
|
|
||||||
|
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
writer.write_packet(&PlayerRemoveS2c {
|
let player_list = player_list.into_inner();
|
||||||
uuids: removed.into(),
|
|
||||||
|
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),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if pl.modified_header_or_footer {
|
removed.clear();
|
||||||
pl.modified_header_or_footer = false;
|
|
||||||
|
|
||||||
writer.write_packet(&PlayerListHeaderS2c {
|
|
||||||
header: (&pl.header).into(),
|
|
||||||
footer: (&pl.footer).into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::biome::BiomePlugin;
|
||||||
use crate::client::{ClientBundle, ClientPlugin};
|
use crate::client::{ClientBundle, ClientPlugin};
|
||||||
|
use crate::component::ComponentPlugin;
|
||||||
use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
|
use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
|
||||||
use crate::dimension::DimensionPlugin;
|
use crate::dimension::DimensionPlugin;
|
||||||
use crate::entity::EntityPlugin;
|
use crate::entity::EntityPlugin;
|
||||||
|
@ -24,7 +25,6 @@ use crate::event_loop::{EventLoopPlugin, RunEventLoopSet};
|
||||||
use crate::instance::InstancePlugin;
|
use crate::instance::InstancePlugin;
|
||||||
use crate::inventory::InventoryPlugin;
|
use crate::inventory::InventoryPlugin;
|
||||||
use crate::player_list::PlayerListPlugin;
|
use crate::player_list::PlayerListPlugin;
|
||||||
use crate::prelude::ComponentPlugin;
|
|
||||||
use crate::registry_codec::RegistryCodecPlugin;
|
use crate::registry_codec::RegistryCodecPlugin;
|
||||||
use crate::server::connect::do_accept_loop;
|
use crate::server::connect::do_accept_loop;
|
||||||
use crate::weather::WeatherPlugin;
|
use crate::weather::WeatherPlugin;
|
||||||
|
|
Loading…
Reference in a new issue