mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-24 06:51:30 +11:00
Implement the player list
This commit is contained in:
parent
d7d922399a
commit
055dd03ffc
|
@ -39,6 +39,7 @@ sha1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
url = {version = "2.2.2", features = ["serde"] }
|
||||||
uuid = "1"
|
uuid = "1"
|
||||||
vek = "0.15"
|
vek = "0.15"
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl Config for Game {
|
||||||
|
|
||||||
fn online_mode(&self) -> bool {
|
fn online_mode(&self) -> bool {
|
||||||
// You'll want this to be true on real servers.
|
// You'll want this to be true on real servers.
|
||||||
false
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing {
|
async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing {
|
||||||
|
@ -104,6 +104,16 @@ impl Config for Game {
|
||||||
client.set_game_mode(GameMode::Creative);
|
client.set_game_mode(GameMode::Creative);
|
||||||
client.set_max_view_distance(32);
|
client.set_max_view_distance(32);
|
||||||
client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
|
client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
|
||||||
|
|
||||||
|
world.meta.player_list_mut().add_player(
|
||||||
|
client.uuid(),
|
||||||
|
client.username().to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
GameMode::Creative,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dist = client.view_distance();
|
let dist = client.view_distance();
|
||||||
|
|
51
src/chunk.rs
51
src/chunk.rs
|
@ -16,7 +16,7 @@ use crate::packets::play::s2c::{
|
||||||
use crate::protocol::{Encode, Nbt};
|
use crate::protocol::{Encode, Nbt};
|
||||||
use crate::var_int::VarInt;
|
use crate::var_int::VarInt;
|
||||||
use crate::var_long::VarLong;
|
use crate::var_long::VarLong;
|
||||||
use crate::{BiomeId, BlockPos, DimensionId, Server, Ticks};
|
use crate::{BiomeId, BlockPos, ChunkPos, DimensionId, Server, Ticks};
|
||||||
|
|
||||||
pub struct Chunks {
|
pub struct Chunks {
|
||||||
chunks: HashMap<ChunkPos, Chunk>,
|
chunks: HashMap<ChunkPos, Chunk>,
|
||||||
|
@ -390,55 +390,6 @@ impl From<BlockChangePacket> for S2cPlayPacket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The X and Z position of a chunk in a world.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
||||||
pub struct ChunkPos {
|
|
||||||
/// The X position of the chunk.
|
|
||||||
pub x: i32,
|
|
||||||
/// The Z position of the chunk.
|
|
||||||
pub z: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChunkPos {
|
|
||||||
pub const fn new(x: i32, z: i32) -> Self {
|
|
||||||
Self { x, z }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn at(x: f64, z: f64) -> Self {
|
|
||||||
Self::new((x / 16.0).floor() as i32, (z / 16.0).floor() as i32)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(i32, i32)> for ChunkPos {
|
|
||||||
fn from((x, z): (i32, i32)) -> Self {
|
|
||||||
ChunkPos { x, z }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<(i32, i32)> for ChunkPos {
|
|
||||||
fn into(self) -> (i32, i32) {
|
|
||||||
(self.x, self.z)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<[i32; 2]> for ChunkPos {
|
|
||||||
fn from([x, z]: [i32; 2]) -> Self {
|
|
||||||
(x, z).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<[i32; 2]> for ChunkPos {
|
|
||||||
fn into(self) -> [i32; 2] {
|
|
||||||
[self.x, self.z]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BlockPos> for ChunkPos {
|
|
||||||
fn from(pos: BlockPos) -> Self {
|
|
||||||
Self::new(pos.x.div_euclid(16), pos.z.div_euclid(16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A 16x16x16 section of blocks, biomes, and light in a chunk.
|
/// A 16x16x16 section of blocks, biomes, and light in a chunk.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ChunkSection {
|
struct ChunkSection {
|
||||||
|
|
50
src/chunk_pos.rs
Normal file
50
src/chunk_pos.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use crate::BlockPos;
|
||||||
|
|
||||||
|
/// The X and Z position of a chunk in a world.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct ChunkPos {
|
||||||
|
/// The X position of the chunk.
|
||||||
|
pub x: i32,
|
||||||
|
/// The Z position of the chunk.
|
||||||
|
pub z: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkPos {
|
||||||
|
pub const fn new(x: i32, z: i32) -> Self {
|
||||||
|
Self { x, z }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at(x: f64, z: f64) -> Self {
|
||||||
|
Self::new((x / 16.0).floor() as i32, (z / 16.0).floor() as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(i32, i32)> for ChunkPos {
|
||||||
|
fn from((x, z): (i32, i32)) -> Self {
|
||||||
|
ChunkPos { x, z }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<(i32, i32)> for ChunkPos {
|
||||||
|
fn into(self) -> (i32, i32) {
|
||||||
|
(self.x, self.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[i32; 2]> for ChunkPos {
|
||||||
|
fn from([x, z]: [i32; 2]) -> Self {
|
||||||
|
(x, z).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<[i32; 2]> for ChunkPos {
|
||||||
|
fn into(self) -> [i32; 2] {
|
||||||
|
[self.x, self.z]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BlockPos> for ChunkPos {
|
||||||
|
fn from(pos: BlockPos) -> Self {
|
||||||
|
Self::new(pos.x.div_euclid(16), pos.z.div_euclid(16))
|
||||||
|
}
|
||||||
|
}
|
|
@ -630,8 +630,12 @@ impl<'a> ClientMut<'a> {
|
||||||
.map(|(id, pos)| (ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0), pos)),
|
.map(|(id, pos)| (ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0), pos)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
meta.player_list()
|
||||||
|
.initial_packets(|pkt| self.send_packet(pkt));
|
||||||
|
|
||||||
self.teleport(self.position(), self.yaw(), self.pitch());
|
self.teleport(self.position(), self.yaw(), self.pitch());
|
||||||
} else if self.0.old_game_mode != self.0.new_game_mode {
|
} else {
|
||||||
|
if self.0.old_game_mode != self.0.new_game_mode {
|
||||||
self.0.old_game_mode = self.0.new_game_mode;
|
self.0.old_game_mode = self.0.new_game_mode;
|
||||||
self.send_packet(ChangeGameState {
|
self.send_packet(ChangeGameState {
|
||||||
reason: ChangeGameStateReason::ChangeGameMode,
|
reason: ChangeGameStateReason::ChangeGameMode,
|
||||||
|
@ -639,6 +643,9 @@ impl<'a> ClientMut<'a> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
meta.player_list().packets(|pkt| self.send_packet(pkt));
|
||||||
|
}
|
||||||
|
|
||||||
// Update the players spawn position (compass position)
|
// Update the players spawn position (compass position)
|
||||||
if self.0.modified_spawn_position {
|
if self.0.modified_spawn_position {
|
||||||
self.0.modified_spawn_position = false;
|
self.0.modified_spawn_position = false;
|
||||||
|
|
|
@ -394,7 +394,7 @@ impl Entity {
|
||||||
EntityMeta::Giant(_) => [3.6, 12.0, 3.6],
|
EntityMeta::Giant(_) => [3.6, 12.0, 3.6],
|
||||||
EntityMeta::GlowItemFrame(_) => todo!("account for rotation"),
|
EntityMeta::GlowItemFrame(_) => todo!("account for rotation"),
|
||||||
EntityMeta::GlowSquid(_) => [0.8, 0.8, 0.8],
|
EntityMeta::GlowSquid(_) => [0.8, 0.8, 0.8],
|
||||||
EntityMeta::Goat(e) => [1.3, 0.9, 1.3], // TODO: baby size?
|
EntityMeta::Goat(_) => [1.3, 0.9, 1.3], // TODO: baby size?
|
||||||
EntityMeta::Guardian(_) => [0.85, 0.85, 0.85],
|
EntityMeta::Guardian(_) => [0.85, 0.85, 0.85],
|
||||||
EntityMeta::Hoglin(_) => [1.39648, 1.4, 1.39648], // TODO: baby size?
|
EntityMeta::Hoglin(_) => [1.39648, 1.4, 1.39648], // TODO: baby size?
|
||||||
EntityMeta::Horse(_) => [1.39648, 1.6, 1.39648], // TODO: baby size?
|
EntityMeta::Horse(_) => [1.39648, 1.6, 1.39648], // TODO: baby size?
|
||||||
|
|
|
@ -13,6 +13,7 @@ mod block_pos;
|
||||||
mod bvh;
|
mod bvh;
|
||||||
mod byte_angle;
|
mod byte_angle;
|
||||||
pub mod chunk;
|
pub mod chunk;
|
||||||
|
mod chunk_pos;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
mod codec;
|
mod codec;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
@ -20,6 +21,7 @@ pub mod dimension;
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
pub mod ident;
|
pub mod ident;
|
||||||
mod packets;
|
mod packets;
|
||||||
|
mod player_list;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
mod slotmap;
|
mod slotmap;
|
||||||
|
@ -34,7 +36,8 @@ pub use async_trait::async_trait;
|
||||||
pub use biome::{Biome, BiomeId};
|
pub use biome::{Biome, BiomeId};
|
||||||
pub use block::BlockState;
|
pub use block::BlockState;
|
||||||
pub use block_pos::BlockPos;
|
pub use block_pos::BlockPos;
|
||||||
pub use chunk::{Chunk, ChunkPos, Chunks, ChunksMut};
|
pub use chunk::{Chunk, Chunks, ChunksMut};
|
||||||
|
pub use chunk_pos::ChunkPos;
|
||||||
pub use client::{Client, ClientMut, Clients, ClientsMut};
|
pub use client::{Client, ClientMut, Clients, ClientsMut};
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub use dimension::{Dimension, DimensionId};
|
pub use dimension::{Dimension, DimensionId};
|
||||||
|
|
|
@ -1131,6 +1131,13 @@ pub mod play {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def_struct! {
|
||||||
|
PlayerListHeaderFooter 0x60 {
|
||||||
|
header: Text,
|
||||||
|
footer: Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def_struct! {
|
def_struct! {
|
||||||
EntityTeleport 0x63 {
|
EntityTeleport 0x63 {
|
||||||
entity_id: VarInt,
|
entity_id: VarInt,
|
||||||
|
@ -1219,6 +1226,8 @@ pub mod play {
|
||||||
EntityPosition,
|
EntityPosition,
|
||||||
EntityPositionAndRotation,
|
EntityPositionAndRotation,
|
||||||
EntityRotation,
|
EntityRotation,
|
||||||
|
ChatMessage,
|
||||||
|
PlayerInfo,
|
||||||
PlayerPositionAndLook,
|
PlayerPositionAndLook,
|
||||||
DestroyEntities,
|
DestroyEntities,
|
||||||
EntityHeadLook,
|
EntityHeadLook,
|
||||||
|
@ -1230,6 +1239,7 @@ pub mod play {
|
||||||
EntityMetadata,
|
EntityMetadata,
|
||||||
EntityVelocity,
|
EntityVelocity,
|
||||||
TimeUpdate,
|
TimeUpdate,
|
||||||
|
PlayerListHeaderFooter,
|
||||||
EntityTeleport,
|
EntityTeleport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
385
src/player_list.rs
Normal file
385
src/player_list.rs
Normal file
|
@ -0,0 +1,385 @@
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use bitfield_struct::bitfield;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::client::GameMode;
|
||||||
|
use crate::packets::login::s2c::Property;
|
||||||
|
use crate::packets::play::s2c::{
|
||||||
|
PlayerInfo, PlayerInfoAddPlayer, PlayerListHeaderFooter, S2cPlayPacket,
|
||||||
|
};
|
||||||
|
use crate::var_int::VarInt;
|
||||||
|
use crate::Text;
|
||||||
|
|
||||||
|
pub struct PlayerList {
|
||||||
|
entries: HashMap<Uuid, PlayerListEntry>,
|
||||||
|
removed: HashSet<Uuid>,
|
||||||
|
header: Text,
|
||||||
|
footer: Text,
|
||||||
|
modified_header_or_footer: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerList {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
entries: HashMap::new(),
|
||||||
|
removed: HashSet::new(),
|
||||||
|
header: Text::default(),
|
||||||
|
footer: Text::default(),
|
||||||
|
modified_header_or_footer: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(&self) -> &Text {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn footer(&self) -> &Text {
|
||||||
|
&self.footer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> impl Iterator<Item = (Uuid, &PlayerListEntry)> + '_ {
|
||||||
|
self.entries.iter().map(|(k, v)| (*k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn initial_packets(&self, mut packet: impl FnMut(S2cPlayPacket)) {
|
||||||
|
let add_player: Vec<_> = self
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|(&uuid, e)| PlayerInfoAddPlayer {
|
||||||
|
uuid,
|
||||||
|
username: e.username.clone().into(),
|
||||||
|
properties: {
|
||||||
|
let mut properties = Vec::new();
|
||||||
|
if e.skin().is_some() || e.cape().is_some() {
|
||||||
|
let textures = PlayerTextures {
|
||||||
|
skin: e.skin().cloned().map(TextureUrl::new),
|
||||||
|
cape: e.cape().cloned().map(TextureUrl::new),
|
||||||
|
};
|
||||||
|
|
||||||
|
properties.push(Property {
|
||||||
|
name: "textures".into(),
|
||||||
|
value: base64::encode(serde_json::to_string(&textures).unwrap()),
|
||||||
|
signature: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
properties
|
||||||
|
},
|
||||||
|
game_mode: e.game_mode,
|
||||||
|
ping: VarInt(e.ping),
|
||||||
|
display_name: e.display_name.clone(),
|
||||||
|
sig_data: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !add_player.is_empty() {
|
||||||
|
packet(PlayerInfo::AddPlayer(add_player).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.header != Text::default() || self.footer != Text::default() {
|
||||||
|
packet(
|
||||||
|
PlayerListHeaderFooter {
|
||||||
|
header: self.header.clone(),
|
||||||
|
footer: self.footer.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn packets(&self, mut packet: impl FnMut(S2cPlayPacket)) {
|
||||||
|
if !self.removed.is_empty() {
|
||||||
|
packet(PlayerInfo::RemovePlayer(self.removed.iter().cloned().collect()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut add_player = Vec::new();
|
||||||
|
let mut game_mode = Vec::new();
|
||||||
|
let mut ping = Vec::new();
|
||||||
|
let mut display_name = Vec::new();
|
||||||
|
|
||||||
|
for (&uuid, e) in self.entries.iter() {
|
||||||
|
if e.flags.created_this_tick() {
|
||||||
|
let mut properties = Vec::new();
|
||||||
|
if e.skin().is_some() || e.cape().is_some() {
|
||||||
|
let textures = PlayerTextures {
|
||||||
|
skin: e.skin().cloned().map(TextureUrl::new),
|
||||||
|
cape: e.cape().cloned().map(TextureUrl::new),
|
||||||
|
};
|
||||||
|
|
||||||
|
properties.push(Property {
|
||||||
|
name: "textures".into(),
|
||||||
|
value: base64::encode(serde_json::to_string(&textures).unwrap()),
|
||||||
|
signature: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_player.push(PlayerInfoAddPlayer {
|
||||||
|
uuid,
|
||||||
|
username: e.username.clone().into(),
|
||||||
|
properties,
|
||||||
|
game_mode: e.game_mode,
|
||||||
|
ping: VarInt(e.ping),
|
||||||
|
display_name: e.display_name.clone(),
|
||||||
|
sig_data: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.flags.modified_game_mode() {
|
||||||
|
game_mode.push((uuid, e.game_mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.flags.modified_ping() {
|
||||||
|
ping.push((uuid, VarInt(e.ping)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.flags.modified_display_name() {
|
||||||
|
display_name.push((uuid, e.display_name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !add_player.is_empty() {
|
||||||
|
packet(PlayerInfo::AddPlayer(add_player).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !game_mode.is_empty() {
|
||||||
|
packet(PlayerInfo::UpdateGameMode(game_mode).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ping.is_empty() {
|
||||||
|
packet(PlayerInfo::UpdateLatency(ping).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !display_name.is_empty() {
|
||||||
|
packet(PlayerInfo::UpdateDisplayName(display_name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.modified_header_or_footer {
|
||||||
|
packet(
|
||||||
|
PlayerListHeaderFooter {
|
||||||
|
header: self.header.clone(),
|
||||||
|
footer: self.footer.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PlayerListMut<'a>(pub(crate) &'a mut PlayerList);
|
||||||
|
|
||||||
|
impl<'a> Deref for PlayerListMut<'a> {
|
||||||
|
type Target = PlayerList;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PlayerListMut<'a> {
|
||||||
|
pub fn reborrow(&mut self) -> PlayerListMut {
|
||||||
|
PlayerListMut(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_player(
|
||||||
|
&mut self,
|
||||||
|
uuid: Uuid,
|
||||||
|
username: impl Into<String>,
|
||||||
|
skin: Option<Url>,
|
||||||
|
cape: Option<Url>,
|
||||||
|
game_mode: GameMode,
|
||||||
|
ping: i32,
|
||||||
|
display_name: impl Into<Option<Text>>,
|
||||||
|
) {
|
||||||
|
match self.0.entries.entry(uuid) {
|
||||||
|
Entry::Occupied(mut oe) => {
|
||||||
|
let mut entry = PlayerListEntryMut(oe.get_mut());
|
||||||
|
let username = username.into();
|
||||||
|
|
||||||
|
if entry.username() != username
|
||||||
|
|| entry.skin() != skin.as_ref()
|
||||||
|
|| entry.cape() != cape.as_ref()
|
||||||
|
{
|
||||||
|
self.0.removed.insert(*oe.key());
|
||||||
|
|
||||||
|
oe.insert(PlayerListEntry {
|
||||||
|
username,
|
||||||
|
textures: PlayerTextures {
|
||||||
|
skin: skin.map(TextureUrl::new),
|
||||||
|
cape: cape.map(TextureUrl::new),
|
||||||
|
},
|
||||||
|
game_mode,
|
||||||
|
ping,
|
||||||
|
display_name: display_name.into(),
|
||||||
|
flags: EntryFlags::new().with_created_this_tick(true),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
entry.set_game_mode(game_mode);
|
||||||
|
entry.set_ping(ping);
|
||||||
|
entry.set_display_name(display_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(ve) => {
|
||||||
|
ve.insert(PlayerListEntry {
|
||||||
|
username: username.into(),
|
||||||
|
textures: PlayerTextures {
|
||||||
|
skin: skin.map(TextureUrl::new),
|
||||||
|
cape: cape.map(TextureUrl::new),
|
||||||
|
},
|
||||||
|
game_mode,
|
||||||
|
ping,
|
||||||
|
display_name: display_name.into(),
|
||||||
|
flags: EntryFlags::new().with_created_this_tick(true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_player(&mut self, uuid: Uuid) -> bool {
|
||||||
|
if self.0.entries.remove(&uuid).is_some() {
|
||||||
|
self.0.removed.insert(uuid);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_header(&mut self, header: impl Into<Text>) {
|
||||||
|
let header = header.into();
|
||||||
|
if self.0.header != header {
|
||||||
|
self.0.header = header;
|
||||||
|
self.0.modified_header_or_footer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_footer(&mut self, footer: impl Into<Text>) {
|
||||||
|
let footer = footer.into();
|
||||||
|
if self.0.footer != footer {
|
||||||
|
self.0.footer = footer;
|
||||||
|
self.0.modified_header_or_footer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entries_mut(&mut self) -> impl Iterator<Item = (Uuid, PlayerListEntryMut)> + '_ {
|
||||||
|
self.0
|
||||||
|
.entries
|
||||||
|
.iter_mut()
|
||||||
|
.map(|(k, v)| (*k, PlayerListEntryMut(v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update(&mut self) {
|
||||||
|
for e in self.0.entries.values_mut() {
|
||||||
|
e.flags = EntryFlags(0);
|
||||||
|
}
|
||||||
|
self.0.removed.clear();
|
||||||
|
self.0.modified_header_or_footer = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PlayerListEntry {
|
||||||
|
username: String,
|
||||||
|
textures: PlayerTextures,
|
||||||
|
game_mode: GameMode,
|
||||||
|
ping: i32,
|
||||||
|
display_name: Option<Text>,
|
||||||
|
flags: EntryFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerListEntry {
|
||||||
|
pub fn username(&self) -> &str {
|
||||||
|
&self.username
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skin(&self) -> Option<&Url> {
|
||||||
|
self.textures.skin.as_ref().map(|t| &t.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cape(&self) -> Option<&Url> {
|
||||||
|
self.textures.cape.as_ref().map(|t| &t.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn game_mode(&self) -> GameMode {
|
||||||
|
self.game_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ping(&self) -> i32 {
|
||||||
|
self.ping
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_name(&self) -> Option<&Text> {
|
||||||
|
self.display_name.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PlayerListEntryMut<'a>(&'a mut PlayerListEntry);
|
||||||
|
|
||||||
|
impl<'a> Deref for PlayerListEntryMut<'a> {
|
||||||
|
type Target = PlayerListEntry;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PlayerListEntryMut<'a> {
|
||||||
|
pub fn reborrow(&mut self) -> PlayerListEntryMut {
|
||||||
|
PlayerListEntryMut(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_game_mode(&mut self, game_mode: GameMode) {
|
||||||
|
if self.0.game_mode != game_mode {
|
||||||
|
self.0.game_mode = game_mode;
|
||||||
|
self.0.flags.set_modified_game_mode(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ping(&mut self, ping: i32) {
|
||||||
|
if self.0.ping != ping {
|
||||||
|
self.0.ping = ping;
|
||||||
|
self.0.flags.set_modified_ping(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_display_name(&mut self, display_name: impl Into<Option<Text>>) {
|
||||||
|
let display_name = display_name.into();
|
||||||
|
if self.0.display_name != display_name {
|
||||||
|
self.0.display_name = display_name;
|
||||||
|
self.0.flags.set_modified_display_name(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
struct EntryFlags {
|
||||||
|
created_this_tick: bool,
|
||||||
|
modified_game_mode: bool,
|
||||||
|
modified_ping: bool,
|
||||||
|
modified_display_name: bool,
|
||||||
|
#[bits(4)]
|
||||||
|
_pad: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub(crate) struct PlayerTextures {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
skin: Option<TextureUrl>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
cape: Option<TextureUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
struct TextureUrl {
|
||||||
|
url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureUrl {
|
||||||
|
fn new(url: Url) -> Self {
|
||||||
|
Self { url }
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ use crate::packets::play::c2s::C2sPlayPacket;
|
||||||
use crate::packets::play::s2c::S2cPlayPacket;
|
use crate::packets::play::s2c::S2cPlayPacket;
|
||||||
use crate::packets::status::c2s::{Ping, Request};
|
use crate::packets::status::c2s::{Ping, Request};
|
||||||
use crate::packets::status::s2c::{Pong, Response};
|
use crate::packets::status::s2c::{Pong, Response};
|
||||||
|
use crate::player_list::PlayerTextures;
|
||||||
use crate::protocol::{BoundedArray, BoundedString};
|
use crate::protocol::{BoundedArray, BoundedString};
|
||||||
use crate::util::valid_username;
|
use crate::util::valid_username;
|
||||||
use crate::var_int::VarInt;
|
use crate::var_int::VarInt;
|
||||||
|
@ -396,6 +397,8 @@ fn do_update_loop(server: Server, mut worlds: WorldsMut) -> ShutdownResult {
|
||||||
world.chunks.par_iter_mut().for_each(|(_, mut chunk)| {
|
world.chunks.par_iter_mut().for_each(|(_, mut chunk)| {
|
||||||
chunk.apply_modifications();
|
chunk.apply_modifications();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
world.meta.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sleep for the remainder of the tick.
|
// Sleep for the remainder of the tick.
|
||||||
|
@ -585,7 +588,7 @@ async fn handle_login(
|
||||||
|
|
||||||
ensure!(valid_username(&username), "invalid username '{username}'");
|
ensure!(valid_username(&username), "invalid username '{username}'");
|
||||||
|
|
||||||
let (uuid, _skin_blob) = if server.0.online_mode {
|
let (uuid, textures) = if server.0.online_mode {
|
||||||
let my_verify_token: [u8; 16] = rand::random();
|
let my_verify_token: [u8; 16] = rand::random();
|
||||||
|
|
||||||
c.0.write_packet(&EncryptionRequest {
|
c.0.write_packet(&EncryptionRequest {
|
||||||
|
@ -666,12 +669,12 @@ async fn handle_login(
|
||||||
|
|
||||||
let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?;
|
let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?;
|
||||||
|
|
||||||
let skin_blob = match data.properties.iter().find(|p| p.name == "textures") {
|
let textures = match data.properties.into_iter().find(|p| p.name == "textures") {
|
||||||
Some(p) => base64::decode(&p.value).context("failed to parse skin blob")?,
|
Some(p) => decode_textures(p.value).context("failed to decode skin blob")?,
|
||||||
None => bail!("failed to find skin blob in auth response"),
|
None => bail!("failed to find skin blob in auth response"),
|
||||||
};
|
};
|
||||||
|
|
||||||
(uuid, Some(skin_blob))
|
(uuid, Some(textures))
|
||||||
} else {
|
} else {
|
||||||
// Derive the player's UUID from a hash of their username.
|
// Derive the player's UUID from a hash of their username.
|
||||||
let uuid = Uuid::from_slice(&Sha256::digest(&username)[..16]).unwrap();
|
let uuid = Uuid::from_slice(&Sha256::digest(&username)[..16]).unwrap();
|
||||||
|
@ -710,6 +713,17 @@ async fn handle_login(
|
||||||
Ok(Some(npd))
|
Ok(Some(npd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_textures(encoded: String) -> anyhow::Result<PlayerTextures> {
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Textures {
|
||||||
|
textures: PlayerTextures,
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = base64::decode(encoded)?;
|
||||||
|
let textures: Textures = serde_json::from_slice(&bytes)?;
|
||||||
|
Ok(textures.textures)
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::Result<()> {
|
async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::Result<()> {
|
||||||
let (reply_tx, reply_rx) = oneshot::channel();
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
|
15
src/world.rs
15
src/world.rs
|
@ -3,6 +3,7 @@ use std::ops::Deref;
|
||||||
|
|
||||||
use rayon::iter::ParallelIterator;
|
use rayon::iter::ParallelIterator;
|
||||||
|
|
||||||
|
use crate::player_list::{PlayerList, PlayerListMut};
|
||||||
use crate::slotmap::{Key, SlotMap};
|
use crate::slotmap::{Key, SlotMap};
|
||||||
use crate::{
|
use crate::{
|
||||||
Chunks, ChunksMut, Clients, ClientsMut, DimensionId, Entities, EntitiesMut, Server,
|
Chunks, ChunksMut, Clients, ClientsMut, DimensionId, Entities, EntitiesMut, Server,
|
||||||
|
@ -66,6 +67,7 @@ impl<'a> WorldsMut<'a> {
|
||||||
meta: WorldMeta {
|
meta: WorldMeta {
|
||||||
dimension: dim,
|
dimension: dim,
|
||||||
is_flat: false,
|
is_flat: false,
|
||||||
|
player_list: PlayerList::new(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,6 +185,7 @@ impl<'a> WorldMut<'a> {
|
||||||
pub struct WorldMeta {
|
pub struct WorldMeta {
|
||||||
dimension: DimensionId,
|
dimension: DimensionId,
|
||||||
is_flat: bool,
|
is_flat: bool,
|
||||||
|
player_list: PlayerList,
|
||||||
// TODO: time, weather
|
// TODO: time, weather
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +197,10 @@ impl WorldMeta {
|
||||||
pub fn is_flat(&self) -> bool {
|
pub fn is_flat(&self) -> bool {
|
||||||
self.is_flat
|
self.is_flat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn player_list(&self) -> &PlayerList {
|
||||||
|
&self.player_list
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WorldMetaMut<'a>(&'a mut WorldMeta);
|
pub struct WorldMetaMut<'a>(&'a mut WorldMeta);
|
||||||
|
@ -210,4 +217,12 @@ impl<'a> WorldMetaMut<'a> {
|
||||||
pub fn set_flat(&mut self, flat: bool) {
|
pub fn set_flat(&mut self, flat: bool) {
|
||||||
self.0.is_flat = flat;
|
self.0.is_flat = flat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn player_list_mut(&mut self) -> PlayerListMut {
|
||||||
|
PlayerListMut(&mut self.0.player_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update(&mut self) {
|
||||||
|
self.player_list_mut().update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue