fix: some bedrock fixes

also added join and leave translated messages
This commit is contained in:
Alexander Medvedev 2026-06-04 20:54:48 +02:00
parent a2f0122503
commit f19eee2d70
15 changed files with 357 additions and 101 deletions

View File

@ -106,11 +106,12 @@ impl Skin {
Self {
skin_id: "Standard_Custom".to_string(),
play_fab_id: String::new(),
resource_patch: r#"{"geometry":{"default":"geometry.humanoid.custom"}}"#.into(),
resource_patch: r#"{"geometry":{"default":"geometry.humanoid"}}"#.into(),
image_width: 64,
image_height: 64,
// 64 * 64 * 4 = 16384 bytes of raw RGBA data
skin_data: vec![0; 16384],
// Fill with 255 so the skin is visible (solid white) instead of invisible (transparent)
skin_data: vec![255; 16384],
animations: Vec::new(),
cape_width: 0,
cape_height: 0,

View File

@ -211,7 +211,7 @@ mod tests {
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
let payload = vec![0x2a; PAYLOAD_LEN];
let mut wire_buf = Vec::new();
let mut network_encoder = UDPNetworkEncoder::new();
let network_encoder = UDPNetworkEncoder::new();
network_encoder
.write_game_packet(
0x01,

View File

@ -111,7 +111,7 @@ impl UDPNetworkEncoder {
}
pub fn write_game_packet(
&mut self,
&self,
packet_id: u16,
sub_client_sender: SubClient,
sub_client_target: SubClient,
@ -161,7 +161,7 @@ impl UDPNetworkEncoder {
}
pub async fn write_packet(
&mut self,
&self,
packet_data: &[u8],
addr: SocketAddr,
socket: &UdpSocket,

View File

@ -0,0 +1,17 @@
use crate::codec::var_ulong::VarULong;
use crate::serial::{PacketRead, PacketWrite};
use pumpkin_macros::packet;
pub const EMOTE_FLAG_SERVER_SIDE: u8 = 1 << 0;
pub const EMOTE_FLAG_MUTE_CHAT: u8 = 1 << 1;
#[derive(PacketRead, PacketWrite, Debug)]
#[packet(138)]
pub struct SEmote {
pub runtime_entity_id: VarULong,
pub emote_id: String,
pub emote_length: u32,
pub xuid: String,
pub platform_id: String,
pub flags: u8,
}

View File

@ -0,0 +1,64 @@
use std::io::{Error, Read, Write};
use uuid::Uuid;
use crate::{
codec::{var_uint::VarUInt, var_ulong::VarULong},
serial::{PacketRead, PacketWrite},
};
use pumpkin_macros::packet;
#[derive(Debug)]
#[packet(152)]
pub struct SEmoteList {
pub runtime_entity_id: VarULong,
pub emote_pieces: Vec<Uuid>,
}
impl PacketRead for SEmoteList {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let runtime_entity_id = VarULong::read(reader)?;
let len = VarUInt::read(reader)?.0 as usize;
let mut emote_pieces = Vec::with_capacity(len);
for _ in 0..len {
emote_pieces.push(Uuid::read(reader)?);
}
Ok(Self {
runtime_entity_id,
emote_pieces,
})
}
}
impl PacketWrite for SEmoteList {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.runtime_entity_id.write(writer)?;
VarUInt(self.emote_pieces.len() as u32).write(writer)?;
for piece in &self.emote_pieces {
piece.write(writer)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn emote_list_serialization() {
let packet = SEmoteList {
runtime_entity_id: VarULong(123),
emote_pieces: vec![Uuid::new_v4(), Uuid::new_v4()],
};
let mut buf = Vec::new();
packet.write(&mut buf).unwrap();
let mut reader = Cursor::new(buf);
let decoded = SEmoteList::read(&mut reader).unwrap();
assert_eq!(packet.runtime_entity_id.0, decoded.runtime_entity_id.0);
assert_eq!(packet.emote_pieces, decoded.emote_pieces);
}
}

View File

@ -3,6 +3,8 @@ pub mod animate;
pub mod client_cache_status;
pub mod command_request;
pub mod container_close;
pub mod emote;
pub mod emote_list;
pub mod interaction;
pub mod inventory_transaction;
pub mod loading_screen;

View File

@ -69,6 +69,20 @@ impl SText {
}
}
#[must_use]
pub const fn json(message: String) -> Self {
Self {
needs_translation: false,
r#type: TextPacketType::Json,
source_name: String::new(),
message,
parameters: Vec::new(),
xuid: String::new(),
platform_chat_id: String::new(),
filtered_message: None,
}
}
const fn get_category(&self) -> u8 {
match self.r#type {
TextPacketType::Raw

View File

@ -150,6 +150,36 @@ impl TextComponentBase {
text
}
/// Converts this component into a raw Bedrock string, specifically for translation parameters.
/// Translations are emitted as `%translation.key` so Bedrock evaluates them natively.
#[must_use]
pub fn to_bedrock_string(&self) -> String {
let mut text = String::new();
match &*self.content {
TextContent::Text { text: t } => text.push_str(t),
TextContent::Translate {
translate,
bedrock_translate,
with: _,
} => {
let key = bedrock_translate.as_deref().unwrap_or(translate.as_ref());
let _ = write!(text, "%{key}");
}
TextContent::EntityNames { selector, .. } => text.push_str(selector),
TextContent::Keybind { keybind } => text.push_str(keybind),
TextContent::Custom { key, .. } => {
let _ = write!(text, "%{key}");
}
}
for child in &self.extra {
text.push_str(&child.to_bedrock_string());
}
text
}
#[must_use]
pub fn to_bedrock_legacy(&self, locale: Locale) -> String {
let mut text = String::new();

View File

@ -674,6 +674,58 @@ impl Entity {
self.world.store(world);
}
pub fn bedrock_metadata(&self) -> EntityMetadata {
if self.bedrock_flags.load(Ordering::Relaxed) == 0 {
self.bedrock_flags.fetch_or(
(1i64 << entity_data_flag::HAS_GRAVITY)
| (1i64 << entity_data_flag::CLIMB)
| (1i64 << entity_data_flag::HAS_COLLISION)
| (1i64 << entity_data_flag::BREATHING),
Ordering::Relaxed,
);
}
let mut metadata = EntityMetadata::new();
metadata.set(
entity_data_key::WIDTH,
MetadataValue::Float(self.entity_type.dimension[0]),
);
metadata.set(
entity_data_key::HEIGHT,
MetadataValue::Float(self.entity_type.dimension[1]),
);
metadata.set(entity_data_key::SCALE, MetadataValue::Float(1.0));
metadata.set(
entity_data_key::FLAGS,
MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)),
);
metadata.set(
entity_data_key::FLAGS_TWO,
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
);
if let Some(name) = &**self.custom_name.load() {
metadata.set(
entity_data_key::NAME,
MetadataValue::String(name.clone().get_text()),
);
if self.custom_name_visible.load(Ordering::Relaxed) {
metadata.set_flag(
entity_data_key::FLAGS,
entity_data_flag::SHOW_NAME as u8,
true,
);
metadata.set_flag(
entity_data_key::FLAGS,
entity_data_flag::ALWAYS_SHOW_NAME as u8,
true,
);
}
}
metadata
}
/// Sets the entity's age in ticks.
/// Negative values indicate that the entity is a baby.
pub fn set_age(&self, age: i32) {

View File

@ -20,7 +20,6 @@ use pumpkin_protocol::bedrock::client::level_chunk::CLevelChunk;
use pumpkin_protocol::bedrock::client::play_status::CPlayStatus;
use pumpkin_protocol::bedrock::client::set_time::CSetTime;
use pumpkin_protocol::bedrock::client::update_abilities::{Ability, CUpdateAbilities};
use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::bedrock::server::text::SText;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_util::translation::Locale;
@ -181,6 +180,11 @@ impl ChunkManager {
&self.world
}
#[must_use]
pub fn sent_chunks_count(&self) -> usize {
self.chunk_sent.len()
}
fn should_enqueue_chunk(&mut self, position: Vector2<i32>, chunk: &SyncChunk) -> bool {
self.chunk_sent
.insert(position, Arc::downgrade(chunk))
@ -1805,10 +1809,10 @@ impl Player {
}
}
let chunk_of_chunks = {
let (chunk_of_chunks, total_sent_chunks) = {
let mut chunk_manager = self.chunk_manager.lock().await;
chunk_manager.pull_new_chunks();
if let ClientPlatform::Java(_) = self.client {
let chunks = if let ClientPlatform::Java(_) = self.client {
// Java clients can only send a limited amount of chunks per tick.
// If we have sent too many chunks without receiving an ack, we stop sending chunks.
chunk_manager
@ -1816,7 +1820,8 @@ impl Player {
.then(|| chunk_manager.next_chunk())
} else {
Some(chunk_manager.next_chunk())
}
};
(chunks, chunk_manager.sent_chunks_count())
};
if let Some(chunk_of_chunks) = chunk_of_chunks {
@ -1837,7 +1842,7 @@ impl Player {
ClientPlatform::Bedrock(bedrock_client) => {
for chunk in chunk_of_chunks {
bedrock_client
.send_game_packet(&CLevelChunk {
.enqueue_packet(&CLevelChunk {
dimension: 0,
cache_enabled: false,
chunk: &chunk,
@ -1845,13 +1850,10 @@ impl Player {
.await;
}
if !self.bedrock_spawned.load(Ordering::Relaxed) && chunk_count > 4 {
let mut frame_set = FrameSet::default();
if !self.bedrock_spawned.load(Ordering::Relaxed) && total_sent_chunks > 4 {
bedrock_client
.write_game_packet_to_set(&CPlayStatus::PlayerSpawn, &mut frame_set)
.enqueue_packet(&CPlayStatus::PlayerSpawn)
.await;
bedrock_client.send_frame_set(frame_set, 0x84).await;
self.bedrock_spawned.store(true, Ordering::Relaxed);
}
}
@ -2866,11 +2868,23 @@ impl Player {
.await;
}
ClientPlatform::Bedrock(client) => {
client
.send_game_packet(&SText::system_message(text.clone().0.to_bedrock_legacy(
Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs),
)))
.await;
let locale = Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs);
let packet = match &*text.0.content {
pumpkin_util::text::TextContent::Translate {
translate,
bedrock_translate,
with,
} => {
let key = bedrock_translate.as_deref().unwrap_or(translate.as_ref());
let parameters = with
.iter()
.map(pumpkin_util::text::TextComponentBase::to_bedrock_string)
.collect();
SText::translation(key.to_string(), parameters)
}
_ => SText::system_message(text.0.to_bedrock_legacy(locale)),
};
client.enqueue_packet(&packet).await;
}
}
}

View File

@ -31,6 +31,7 @@ use pumpkin_protocol::{
client_cache_status::SClientCacheStatus,
command_request::SCommandRequest,
container_close::SContainerClose,
emote::SEmote,
interaction::SInteraction,
inventory_transaction::SInventoryTransaction,
loading_screen::SLoadingScreen,
@ -59,9 +60,10 @@ use std::net::SocketAddr;
use tokio::{
net::UdpSocket,
sync::mpsc::{Receiver, Sender},
sync::{Mutex, oneshot},
sync::{Mutex, RwLock, oneshot},
task::JoinHandle,
};
use tokio_util::{sync::CancellationToken, task::TaskTracker};
pub mod connection;
@ -118,7 +120,7 @@ pub struct BedrockClient {
outgoing_packet_priority_recv: Mutex<Option<Receiver<OutgoingPacket>>>,
/// The packet encoder for outgoing packets.
network_writer: Arc<Mutex<UDPNetworkEncoder>>,
network_writer: Arc<RwLock<UDPNetworkEncoder>>,
/// The packet decoder for incoming packets.
network_reader: Mutex<UDPNetworkDecoder>,
@ -154,9 +156,9 @@ impl BedrockClient {
address: SocketAddr,
be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
) -> Self {
let (send, recv) = tokio::sync::mpsc::channel(128);
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(128);
let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(128);
let (send, recv) = tokio::sync::mpsc::channel(4096);
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096);
let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(4096);
Self {
socket,
player: Mutex::new(None),
@ -164,7 +166,7 @@ impl BedrockClient {
version: AtomicCell::new(BedrockMinecraftVersion::Unknown),
client_data: ArcSwap::new(Arc::new(None)),
be_clients,
network_writer: Arc::new(Mutex::new(UDPNetworkEncoder::new())),
network_writer: Arc::new(RwLock::new(UDPNetworkEncoder::new())),
network_reader: Mutex::new(UDPNetworkDecoder::new()),
tasks: TaskTracker::new(),
outgoing_packet_queue_send: send,
@ -220,16 +222,19 @@ impl BedrockClient {
let mut interval = tokio::time::interval(std::time::Duration::from_millis(100));
while !client.close_token.is_cancelled() {
let recv_result = tokio::select! {
let packet = tokio::select! {
biased;
() = client.close_token.cancelled() => None,
res = priority_packet_receiver.recv() => res,
() = client.close_token.cancelled() => break,
res = priority_packet_receiver.recv() => match res {
Some(p) => p,
None => break,
},
_ = interval.tick() => {
// Check for timeout (10 seconds)
if client.last_seen.load().elapsed() > std::time::Duration::from_secs(10) {
debug!("Bedrock client {} timed out", client.address);
client.close().await;
return;
break;
}
// Flush ACKs
@ -244,31 +249,35 @@ impl BedrockClient {
let now = std::time::Instant::now();
let mut resend = Vec::new();
{
let unacked = client.unacked_outgoing_frames.lock().await;
for (seq, (id, data, timestamp)) in unacked.iter() {
let mut unacked = client.unacked_outgoing_frames.lock().await;
for (seq, (id, data, timestamp)) in unacked.iter_mut() {
if now.duration_since(*timestamp) > std::time::Duration::from_secs(1) {
resend.push((*seq, *id, data.clone()));
// Update timestamp
*timestamp = now;
// Limit resends per tick to avoid starvation
if resend.len() >= 50 {
break;
}
}
}
}
if !resend.is_empty() {
let encoder = client.network_writer.read().await;
for (seq, id, data) in resend {
debug!("Resending reliable sequence {} (ID: {})", seq, id);
// Update timestamp
if let Some(entry) = client.unacked_outgoing_frames.lock().await.get_mut(&seq) {
entry.2 = now;
}
if let Err(err) = client.network_writer.lock().await.write_packet(&data, client.address, &client.socket).await {
if let Err(err) = encoder.write_packet(&data, client.address, &client.socket).await {
warn!("Failed to resend packet for sequence {}: {}", seq, err);
}
}
}
continue;
}
res = packet_receiver.recv() => res,
};
let Some(packet) = recv_result else {
break;
res = packet_receiver.recv() => match res {
Some(p) => p,
None => break,
},
};
client
@ -301,7 +310,7 @@ impl BedrockClient {
.set_compression(compression.threshold as usize);
self.network_writer
.lock()
.write()
.await
.set_compression((compression.threshold as usize, compression.level));
}
@ -329,7 +338,7 @@ impl BedrockClient {
}
{
let Ok(mut network_writer) = self.network_writer.try_lock() else {
let Ok(network_writer) = self.network_writer.try_read() else {
debug!("Failed to lock network writer for try_enqueue_packet");
return;
};
@ -406,7 +415,7 @@ impl BedrockClient {
let mut packet_payload = Vec::new();
packet.write_packet(&mut packet_payload)?;
let mut encoder = self.network_writer.lock().await;
let encoder = self.network_writer.read().await;
encoder.write_game_packet(
P::PACKET_ID as u16,
SubClient::Main,
@ -500,7 +509,7 @@ impl BedrockClient {
}
for i in 0..count {
let end = if i + 1 == count {
let end = if i + 1 == count && !packet_buf.len().is_multiple_of(MTU) {
packet_buf.len() % MTU
} else {
MTU
@ -558,7 +567,7 @@ impl BedrockClient {
if let Err(err) = self
.network_writer
.lock()
.read()
.await
.write_packet(&frame_set_buf, self.address, &self.socket)
.await
@ -593,7 +602,7 @@ impl BedrockClient {
if let Err(err) = self
.network_writer
.lock()
.read()
.await
.write_packet(&packet_buf, self.address, &self.socket)
.await
@ -639,17 +648,25 @@ impl BedrockClient {
async fn handle_nack(&self, nack: &Acknowledge) {
debug!("Received NACK for sequences: {:?}", nack.sequences);
let mut resend_data = Vec::new();
{
let unacked = self.unacked_outgoing_frames.lock().await;
for seq in &nack.sequences {
if let Some((_id, data, _timestamp)) = unacked.get(seq)
&& let Err(err) = self
if let Some((_id, data, _timestamp)) = unacked.get(seq) {
resend_data.push(data.clone());
}
}
}
for data in resend_data {
if let Err(err) = self
.network_writer
.lock()
.read()
.await
.write_packet(data, self.address, &self.socket)
.write_packet(&data, self.address, &self.socket)
.await
{
warn!("Failed to resend packet for sequence {}: {}", seq, err);
warn!("Failed to resend packet from NACK: {}", err);
}
}
}
@ -857,7 +874,7 @@ impl BedrockClient {
match self.handle_login(packet, server).await {
Ok(result) => return result,
Err(err) => {
warn!("Bedrock login failed: {err}");
self.kick(DisconnectReason::Unknown, err.to_string()).await;
return PacketHandlerResult::Stop;
}
}
@ -941,6 +958,12 @@ impl BedrockClient {
SAnimate::PACKET_ID => {
self.handle_animate(player, server, &SAnimate::read(reader)?).await;
}
SEmote::PACKET_ID => {
self.handle_emote(player, server, SEmote::read(reader)?).await;
}
// SEmoteList::PACKET_ID => {
// self.handle_emote_list(player, server, SEmoteList::read(reader)?);
// }
pumpkin_protocol::bedrock::server::modal_form_response::SModalFormResponse::PACKET_ID => {
self.handle_modal_form_response(
player,

View File

@ -11,6 +11,7 @@ use pumpkin_protocol::{
animate::{AnimateAction, SAnimate},
command_request::SCommandRequest,
container_close::SContainerClose,
emote::SEmote,
interaction::{Action, SInteraction},
inventory_transaction::{SInventoryTransaction, TransactionData},
player_action::{Action as PlayerAction, SPlayerAction},
@ -338,6 +339,41 @@ impl BedrockClient {
}
}
pub async fn handle_emote(&self, player: &Arc<Player>, _server: &Server, packet: SEmote) {
if !player.has_client_loaded() {
return;
}
let entity = &player.living_entity.entity;
let world = entity.world.load();
let mut broadcast_packet = packet;
broadcast_packet.flags |= pumpkin_protocol::bedrock::server::emote::EMOTE_FLAG_SERVER_SIDE;
world
.broadcast_packet_except_editioned(
&[player.gameprofile.id],
&CEntityAnimation::new(
VarInt(entity.entity_id),
Animation::SwingMainArm, // Fallback for Java? Or just ignore
),
&broadcast_packet,
)
.await;
}
// pub fn handle_emote_list(
// &self,
// player: &Arc<Player>,
// _server: &Server,
// packet: &SEmoteList,
// ) {
// debug!(
// "Player {} sent emote list: {:?}",
// player.gameprofile.name, packet.emote_pieces
// );
// }
pub async fn handle_inventory_action(
&self,
player: &Arc<Player>,

View File

@ -142,8 +142,8 @@ impl JavaClient {
#[must_use]
pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self {
let (read, write) = tcp_stream.into_split();
let (send, recv) = tokio::sync::mpsc::channel(128);
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(128);
let (send, recv) = tokio::sync::mpsc::channel(4096);
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096);
Self {
id,
gameprofile: Mutex::new(None),

View File

@ -305,7 +305,8 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
let msg = self.get_text_provider(&message)?;
self.get_world_res(&world)?
.provider
.broadcast_system_message(&msg, overlay);
.broadcast_system_message(&msg, overlay)
.await;
Ok(())
}

View File

@ -69,10 +69,7 @@ use pumpkin_data::{BlockDirection, BlockState, translation};
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
use pumpkin_protocol::bedrock::client::set_actor_data::{
CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag,
entity_data_key,
};
use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData};
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::java::client::play::{
@ -527,9 +524,32 @@ impl World {
}
}
pub fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) {
let packet = CSystemChatMessage::new(message, overlay);
self.broadcast_packet_all(&packet);
pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) {
let je_packet = CSystemChatMessage::new(message, overlay);
let be_packet = Self::component_to_bedrock_text(message);
self.broadcast_editioned(&je_packet, &be_packet).await;
}
fn component_to_bedrock_text(message: &TextComponent) -> SText {
match &*message.0.content {
pumpkin_util::text::TextContent::Translate {
translate,
bedrock_translate,
with,
} => {
let key = bedrock_translate.as_deref().unwrap_or(translate.as_ref());
let parameters = with
.iter()
.map(pumpkin_util::text::TextComponentBase::to_bedrock_string)
.collect();
SText::translation(key.to_string(), parameters)
}
_ => SText::system_message(
message
.0
.to_bedrock_legacy(pumpkin_util::translation::Locale::EnUs),
),
}
}
pub async fn broadcast_message(
@ -565,7 +585,7 @@ impl World {
Self::broadcast_java_grouped(je_packet, je_recipients_by_version);
for recipient in be_recipients {
recipient.send_game_packet(be_packet).await;
recipient.enqueue_packet(be_packet).await;
}
}
@ -1838,28 +1858,8 @@ impl World {
abilities.set_for_gamemode(player.gamemode.load());
};
let mut metadata = EntityMetadata::new();
metadata.set(entity_data_key::WIDTH, MetadataValue::Float(0.6));
metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8));
// This is super important, otherwise the client will float by default
let entity = &player.living_entity.entity;
entity.bedrock_flags.fetch_or(
(1i64 << entity_data_flag::HAS_GRAVITY)
| (1i64 << entity_data_flag::CLIMB)
| (1i64 << entity_data_flag::HAS_COLLISION)
| (1i64 << entity_data_flag::BREATHING),
Ordering::Relaxed,
);
metadata.set(
entity_data_key::FLAGS,
MetadataValue::Long(entity.bedrock_flags.load(Ordering::Relaxed)),
);
metadata.set(
entity_data_key::FLAGS_TWO,
MetadataValue::Long(entity.bedrock_flags_two.load(Ordering::Relaxed)),
);
let metadata = entity.bedrock_metadata();
let actor_data = CSetActorData {
actor_runtime_id: VarULong(runtime_id),
@ -2008,7 +2008,7 @@ impl World {
GameMode::Adventure => 2,
GameMode::Spectator => 6,
}),
metadata: EntityMetadata::default(),
metadata: entity.bedrock_metadata(),
properties: EntityProperties::default(),
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
entity_unique_id: runtime_id as i64,
@ -2088,7 +2088,7 @@ impl World {
GameMode::Adventure => 2,
GameMode::Spectator => 6,
}),
metadata: EntityMetadata::default(),
metadata: ex_entity.bedrock_metadata(),
properties: EntityProperties::default(),
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
entity_unique_id: existing_player.entity_id() as i64,
@ -2131,7 +2131,8 @@ impl World {
let event = server.plugin_manager.fire(event).await;
if !event.cancelled {
self.broadcast_system_message(&event.join_message, false);
self.broadcast_system_message(&event.join_message, false)
.await;
info!("{}", event.join_message.to_pretty_console());
}
}
@ -2385,7 +2386,7 @@ impl World {
GameMode::Adventure => 2,
GameMode::Spectator => 6,
}),
metadata: EntityMetadata::default(),
metadata: player.get_entity().bedrock_metadata(),
properties: EntityProperties::default(),
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
entity_unique_id: entity_id as i64,
@ -2493,7 +2494,7 @@ impl World {
GameMode::Adventure => 2,
GameMode::Spectator => 6,
}),
metadata: EntityMetadata::default(),
metadata: entity.bedrock_metadata(),
properties: EntityProperties::default(),
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
entity_unique_id: existing_player.entity_id() as i64,
@ -2722,7 +2723,8 @@ impl World {
let event = server.plugin_manager.fire(event).await;
if !event.cancelled {
self.broadcast_system_message(&event.join_message, false);
self.broadcast_system_message(&event.join_message, false)
.await;
// TODO: Switch to structured logging, e.g. info!(player = %name, "connected")
info!("{}", event.join_message.to_pretty_console());
}