fix: some bedrock fixes
also added join and leave translated messages
This commit is contained in:
parent
a2f0122503
commit
f19eee2d70
@ -106,11 +106,12 @@ impl Skin {
|
|||||||
Self {
|
Self {
|
||||||
skin_id: "Standard_Custom".to_string(),
|
skin_id: "Standard_Custom".to_string(),
|
||||||
play_fab_id: String::new(),
|
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_width: 64,
|
||||||
image_height: 64,
|
image_height: 64,
|
||||||
// 64 * 64 * 4 = 16384 bytes of raw RGBA data
|
// 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(),
|
animations: Vec::new(),
|
||||||
cape_width: 0,
|
cape_width: 0,
|
||||||
cape_height: 0,
|
cape_height: 0,
|
||||||
|
|||||||
@ -211,7 +211,7 @@ mod tests {
|
|||||||
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
|
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
|
||||||
let payload = vec![0x2a; PAYLOAD_LEN];
|
let payload = vec![0x2a; PAYLOAD_LEN];
|
||||||
let mut wire_buf = Vec::new();
|
let mut wire_buf = Vec::new();
|
||||||
let mut network_encoder = UDPNetworkEncoder::new();
|
let network_encoder = UDPNetworkEncoder::new();
|
||||||
network_encoder
|
network_encoder
|
||||||
.write_game_packet(
|
.write_game_packet(
|
||||||
0x01,
|
0x01,
|
||||||
|
|||||||
@ -111,7 +111,7 @@ impl UDPNetworkEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_game_packet(
|
pub fn write_game_packet(
|
||||||
&mut self,
|
&self,
|
||||||
packet_id: u16,
|
packet_id: u16,
|
||||||
sub_client_sender: SubClient,
|
sub_client_sender: SubClient,
|
||||||
sub_client_target: SubClient,
|
sub_client_target: SubClient,
|
||||||
@ -161,7 +161,7 @@ impl UDPNetworkEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write_packet(
|
pub async fn write_packet(
|
||||||
&mut self,
|
&self,
|
||||||
packet_data: &[u8],
|
packet_data: &[u8],
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
|
|||||||
17
pumpkin-protocol/src/bedrock/server/emote.rs
Normal file
17
pumpkin-protocol/src/bedrock/server/emote.rs
Normal 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,
|
||||||
|
}
|
||||||
64
pumpkin-protocol/src/bedrock/server/emote_list.rs
Normal file
64
pumpkin-protocol/src/bedrock/server/emote_list.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ pub mod animate;
|
|||||||
pub mod client_cache_status;
|
pub mod client_cache_status;
|
||||||
pub mod command_request;
|
pub mod command_request;
|
||||||
pub mod container_close;
|
pub mod container_close;
|
||||||
|
pub mod emote;
|
||||||
|
pub mod emote_list;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod inventory_transaction;
|
pub mod inventory_transaction;
|
||||||
pub mod loading_screen;
|
pub mod loading_screen;
|
||||||
|
|||||||
@ -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 {
|
const fn get_category(&self) -> u8 {
|
||||||
match self.r#type {
|
match self.r#type {
|
||||||
TextPacketType::Raw
|
TextPacketType::Raw
|
||||||
|
|||||||
@ -150,6 +150,36 @@ impl TextComponentBase {
|
|||||||
text
|
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]
|
#[must_use]
|
||||||
pub fn to_bedrock_legacy(&self, locale: Locale) -> String {
|
pub fn to_bedrock_legacy(&self, locale: Locale) -> String {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
|
|||||||
@ -674,6 +674,58 @@ impl Entity {
|
|||||||
self.world.store(world);
|
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.
|
/// Sets the entity's age in ticks.
|
||||||
/// Negative values indicate that the entity is a baby.
|
/// Negative values indicate that the entity is a baby.
|
||||||
pub fn set_age(&self, age: i32) {
|
pub fn set_age(&self, age: i32) {
|
||||||
|
|||||||
@ -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::play_status::CPlayStatus;
|
||||||
use pumpkin_protocol::bedrock::client::set_time::CSetTime;
|
use pumpkin_protocol::bedrock::client::set_time::CSetTime;
|
||||||
use pumpkin_protocol::bedrock::client::update_abilities::{Ability, CUpdateAbilities};
|
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::bedrock::server::text::SText;
|
||||||
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
|
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
|
||||||
use pumpkin_util::translation::Locale;
|
use pumpkin_util::translation::Locale;
|
||||||
@ -181,6 +180,11 @@ impl ChunkManager {
|
|||||||
&self.world
|
&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 {
|
fn should_enqueue_chunk(&mut self, position: Vector2<i32>, chunk: &SyncChunk) -> bool {
|
||||||
self.chunk_sent
|
self.chunk_sent
|
||||||
.insert(position, Arc::downgrade(chunk))
|
.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;
|
let mut chunk_manager = self.chunk_manager.lock().await;
|
||||||
chunk_manager.pull_new_chunks();
|
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.
|
// 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.
|
// If we have sent too many chunks without receiving an ack, we stop sending chunks.
|
||||||
chunk_manager
|
chunk_manager
|
||||||
@ -1816,7 +1820,8 @@ impl Player {
|
|||||||
.then(|| chunk_manager.next_chunk())
|
.then(|| chunk_manager.next_chunk())
|
||||||
} else {
|
} else {
|
||||||
Some(chunk_manager.next_chunk())
|
Some(chunk_manager.next_chunk())
|
||||||
}
|
};
|
||||||
|
(chunks, chunk_manager.sent_chunks_count())
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(chunk_of_chunks) = chunk_of_chunks {
|
if let Some(chunk_of_chunks) = chunk_of_chunks {
|
||||||
@ -1837,7 +1842,7 @@ impl Player {
|
|||||||
ClientPlatform::Bedrock(bedrock_client) => {
|
ClientPlatform::Bedrock(bedrock_client) => {
|
||||||
for chunk in chunk_of_chunks {
|
for chunk in chunk_of_chunks {
|
||||||
bedrock_client
|
bedrock_client
|
||||||
.send_game_packet(&CLevelChunk {
|
.enqueue_packet(&CLevelChunk {
|
||||||
dimension: 0,
|
dimension: 0,
|
||||||
cache_enabled: false,
|
cache_enabled: false,
|
||||||
chunk: &chunk,
|
chunk: &chunk,
|
||||||
@ -1845,13 +1850,10 @@ impl Player {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.bedrock_spawned.load(Ordering::Relaxed) && chunk_count > 4 {
|
if !self.bedrock_spawned.load(Ordering::Relaxed) && total_sent_chunks > 4 {
|
||||||
let mut frame_set = FrameSet::default();
|
|
||||||
|
|
||||||
bedrock_client
|
bedrock_client
|
||||||
.write_game_packet_to_set(&CPlayStatus::PlayerSpawn, &mut frame_set)
|
.enqueue_packet(&CPlayStatus::PlayerSpawn)
|
||||||
.await;
|
.await;
|
||||||
bedrock_client.send_frame_set(frame_set, 0x84).await;
|
|
||||||
self.bedrock_spawned.store(true, Ordering::Relaxed);
|
self.bedrock_spawned.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2866,11 +2868,23 @@ impl Player {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
ClientPlatform::Bedrock(client) => {
|
ClientPlatform::Bedrock(client) => {
|
||||||
client
|
let locale = Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs);
|
||||||
.send_game_packet(&SText::system_message(text.clone().0.to_bedrock_legacy(
|
let packet = match &*text.0.content {
|
||||||
Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs),
|
pumpkin_util::text::TextContent::Translate {
|
||||||
)))
|
translate,
|
||||||
.await;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ use pumpkin_protocol::{
|
|||||||
client_cache_status::SClientCacheStatus,
|
client_cache_status::SClientCacheStatus,
|
||||||
command_request::SCommandRequest,
|
command_request::SCommandRequest,
|
||||||
container_close::SContainerClose,
|
container_close::SContainerClose,
|
||||||
|
emote::SEmote,
|
||||||
interaction::SInteraction,
|
interaction::SInteraction,
|
||||||
inventory_transaction::SInventoryTransaction,
|
inventory_transaction::SInventoryTransaction,
|
||||||
loading_screen::SLoadingScreen,
|
loading_screen::SLoadingScreen,
|
||||||
@ -59,9 +60,10 @@ use std::net::SocketAddr;
|
|||||||
use tokio::{
|
use tokio::{
|
||||||
net::UdpSocket,
|
net::UdpSocket,
|
||||||
sync::mpsc::{Receiver, Sender},
|
sync::mpsc::{Receiver, Sender},
|
||||||
sync::{Mutex, oneshot},
|
sync::{Mutex, RwLock, oneshot},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||||
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
@ -118,7 +120,7 @@ pub struct BedrockClient {
|
|||||||
outgoing_packet_priority_recv: Mutex<Option<Receiver<OutgoingPacket>>>,
|
outgoing_packet_priority_recv: Mutex<Option<Receiver<OutgoingPacket>>>,
|
||||||
|
|
||||||
/// The packet encoder for outgoing packets.
|
/// The packet encoder for outgoing packets.
|
||||||
network_writer: Arc<Mutex<UDPNetworkEncoder>>,
|
network_writer: Arc<RwLock<UDPNetworkEncoder>>,
|
||||||
/// The packet decoder for incoming packets.
|
/// The packet decoder for incoming packets.
|
||||||
network_reader: Mutex<UDPNetworkDecoder>,
|
network_reader: Mutex<UDPNetworkDecoder>,
|
||||||
|
|
||||||
@ -154,9 +156,9 @@ impl BedrockClient {
|
|||||||
address: SocketAddr,
|
address: SocketAddr,
|
||||||
be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
|
be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (send, recv) = tokio::sync::mpsc::channel(128);
|
let (send, recv) = tokio::sync::mpsc::channel(4096);
|
||||||
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(128);
|
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096);
|
||||||
let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(128);
|
let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(4096);
|
||||||
Self {
|
Self {
|
||||||
socket,
|
socket,
|
||||||
player: Mutex::new(None),
|
player: Mutex::new(None),
|
||||||
@ -164,7 +166,7 @@ impl BedrockClient {
|
|||||||
version: AtomicCell::new(BedrockMinecraftVersion::Unknown),
|
version: AtomicCell::new(BedrockMinecraftVersion::Unknown),
|
||||||
client_data: ArcSwap::new(Arc::new(None)),
|
client_data: ArcSwap::new(Arc::new(None)),
|
||||||
be_clients,
|
be_clients,
|
||||||
network_writer: Arc::new(Mutex::new(UDPNetworkEncoder::new())),
|
network_writer: Arc::new(RwLock::new(UDPNetworkEncoder::new())),
|
||||||
network_reader: Mutex::new(UDPNetworkDecoder::new()),
|
network_reader: Mutex::new(UDPNetworkDecoder::new()),
|
||||||
tasks: TaskTracker::new(),
|
tasks: TaskTracker::new(),
|
||||||
outgoing_packet_queue_send: send,
|
outgoing_packet_queue_send: send,
|
||||||
@ -220,16 +222,19 @@ impl BedrockClient {
|
|||||||
let mut interval = tokio::time::interval(std::time::Duration::from_millis(100));
|
let mut interval = tokio::time::interval(std::time::Duration::from_millis(100));
|
||||||
|
|
||||||
while !client.close_token.is_cancelled() {
|
while !client.close_token.is_cancelled() {
|
||||||
let recv_result = tokio::select! {
|
let packet = tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
() = client.close_token.cancelled() => None,
|
() = client.close_token.cancelled() => break,
|
||||||
res = priority_packet_receiver.recv() => res,
|
res = priority_packet_receiver.recv() => match res {
|
||||||
|
Some(p) => p,
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
_ = interval.tick() => {
|
_ = interval.tick() => {
|
||||||
// Check for timeout (10 seconds)
|
// Check for timeout (10 seconds)
|
||||||
if client.last_seen.load().elapsed() > std::time::Duration::from_secs(10) {
|
if client.last_seen.load().elapsed() > std::time::Duration::from_secs(10) {
|
||||||
debug!("Bedrock client {} timed out", client.address);
|
debug!("Bedrock client {} timed out", client.address);
|
||||||
client.close().await;
|
client.close().await;
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush ACKs
|
// Flush ACKs
|
||||||
@ -244,31 +249,35 @@ impl BedrockClient {
|
|||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
let mut resend = Vec::new();
|
let mut resend = Vec::new();
|
||||||
{
|
{
|
||||||
let unacked = client.unacked_outgoing_frames.lock().await;
|
let mut unacked = client.unacked_outgoing_frames.lock().await;
|
||||||
for (seq, (id, data, timestamp)) in unacked.iter() {
|
for (seq, (id, data, timestamp)) in unacked.iter_mut() {
|
||||||
if now.duration_since(*timestamp) > std::time::Duration::from_secs(1) {
|
if now.duration_since(*timestamp) > std::time::Duration::from_secs(1) {
|
||||||
resend.push((*seq, *id, data.clone()));
|
resend.push((*seq, *id, data.clone()));
|
||||||
|
// Update timestamp
|
||||||
|
*timestamp = now;
|
||||||
|
// Limit resends per tick to avoid starvation
|
||||||
|
if resend.len() >= 50 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (seq, id, data) in resend {
|
if !resend.is_empty() {
|
||||||
debug!("Resending reliable sequence {} (ID: {})", seq, id);
|
let encoder = client.network_writer.read().await;
|
||||||
// Update timestamp
|
for (seq, id, data) in resend {
|
||||||
if let Some(entry) = client.unacked_outgoing_frames.lock().await.get_mut(&seq) {
|
debug!("Resending reliable sequence {} (ID: {})", seq, id);
|
||||||
entry.2 = now;
|
if let Err(err) = encoder.write_packet(&data, client.address, &client.socket).await {
|
||||||
}
|
warn!("Failed to resend packet for sequence {}: {}", seq, err);
|
||||||
if let Err(err) = client.network_writer.lock().await.write_packet(&data, client.address, &client.socket).await {
|
}
|
||||||
warn!("Failed to resend packet for sequence {}: {}", seq, err);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
res = packet_receiver.recv() => res,
|
res = packet_receiver.recv() => match res {
|
||||||
};
|
Some(p) => p,
|
||||||
|
None => break,
|
||||||
let Some(packet) = recv_result else {
|
},
|
||||||
break;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client
|
||||||
@ -301,7 +310,7 @@ impl BedrockClient {
|
|||||||
.set_compression(compression.threshold as usize);
|
.set_compression(compression.threshold as usize);
|
||||||
|
|
||||||
self.network_writer
|
self.network_writer
|
||||||
.lock()
|
.write()
|
||||||
.await
|
.await
|
||||||
.set_compression((compression.threshold as usize, compression.level));
|
.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");
|
debug!("Failed to lock network writer for try_enqueue_packet");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -406,7 +415,7 @@ impl BedrockClient {
|
|||||||
let mut packet_payload = Vec::new();
|
let mut packet_payload = Vec::new();
|
||||||
packet.write_packet(&mut packet_payload)?;
|
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(
|
encoder.write_game_packet(
|
||||||
P::PACKET_ID as u16,
|
P::PACKET_ID as u16,
|
||||||
SubClient::Main,
|
SubClient::Main,
|
||||||
@ -500,7 +509,7 @@ impl BedrockClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..count {
|
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
|
packet_buf.len() % MTU
|
||||||
} else {
|
} else {
|
||||||
MTU
|
MTU
|
||||||
@ -558,7 +567,7 @@ impl BedrockClient {
|
|||||||
|
|
||||||
if let Err(err) = self
|
if let Err(err) = self
|
||||||
.network_writer
|
.network_writer
|
||||||
.lock()
|
.read()
|
||||||
.await
|
.await
|
||||||
.write_packet(&frame_set_buf, self.address, &self.socket)
|
.write_packet(&frame_set_buf, self.address, &self.socket)
|
||||||
.await
|
.await
|
||||||
@ -593,7 +602,7 @@ impl BedrockClient {
|
|||||||
|
|
||||||
if let Err(err) = self
|
if let Err(err) = self
|
||||||
.network_writer
|
.network_writer
|
||||||
.lock()
|
.read()
|
||||||
.await
|
.await
|
||||||
.write_packet(&packet_buf, self.address, &self.socket)
|
.write_packet(&packet_buf, self.address, &self.socket)
|
||||||
.await
|
.await
|
||||||
@ -639,17 +648,25 @@ impl BedrockClient {
|
|||||||
|
|
||||||
async fn handle_nack(&self, nack: &Acknowledge) {
|
async fn handle_nack(&self, nack: &Acknowledge) {
|
||||||
debug!("Received NACK for sequences: {:?}", nack.sequences);
|
debug!("Received NACK for sequences: {:?}", nack.sequences);
|
||||||
let unacked = self.unacked_outgoing_frames.lock().await;
|
let mut resend_data = Vec::new();
|
||||||
for seq in &nack.sequences {
|
{
|
||||||
if let Some((_id, data, _timestamp)) = unacked.get(seq)
|
let unacked = self.unacked_outgoing_frames.lock().await;
|
||||||
&& let Err(err) = self
|
for seq in &nack.sequences {
|
||||||
.network_writer
|
if let Some((_id, data, _timestamp)) = unacked.get(seq) {
|
||||||
.lock()
|
resend_data.push(data.clone());
|
||||||
.await
|
}
|
||||||
.write_packet(data, self.address, &self.socket)
|
}
|
||||||
.await
|
}
|
||||||
|
|
||||||
|
for data in resend_data {
|
||||||
|
if let Err(err) = self
|
||||||
|
.network_writer
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.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 {
|
match self.handle_login(packet, server).await {
|
||||||
Ok(result) => return result,
|
Ok(result) => return result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("Bedrock login failed: {err}");
|
self.kick(DisconnectReason::Unknown, err.to_string()).await;
|
||||||
return PacketHandlerResult::Stop;
|
return PacketHandlerResult::Stop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -941,6 +958,12 @@ impl BedrockClient {
|
|||||||
SAnimate::PACKET_ID => {
|
SAnimate::PACKET_ID => {
|
||||||
self.handle_animate(player, server, &SAnimate::read(reader)?).await;
|
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 => {
|
pumpkin_protocol::bedrock::server::modal_form_response::SModalFormResponse::PACKET_ID => {
|
||||||
self.handle_modal_form_response(
|
self.handle_modal_form_response(
|
||||||
player,
|
player,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use pumpkin_protocol::{
|
|||||||
animate::{AnimateAction, SAnimate},
|
animate::{AnimateAction, SAnimate},
|
||||||
command_request::SCommandRequest,
|
command_request::SCommandRequest,
|
||||||
container_close::SContainerClose,
|
container_close::SContainerClose,
|
||||||
|
emote::SEmote,
|
||||||
interaction::{Action, SInteraction},
|
interaction::{Action, SInteraction},
|
||||||
inventory_transaction::{SInventoryTransaction, TransactionData},
|
inventory_transaction::{SInventoryTransaction, TransactionData},
|
||||||
player_action::{Action as PlayerAction, SPlayerAction},
|
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(
|
pub async fn handle_inventory_action(
|
||||||
&self,
|
&self,
|
||||||
player: &Arc<Player>,
|
player: &Arc<Player>,
|
||||||
|
|||||||
@ -142,8 +142,8 @@ impl JavaClient {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self {
|
pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self {
|
||||||
let (read, write) = tcp_stream.into_split();
|
let (read, write) = tcp_stream.into_split();
|
||||||
let (send, recv) = tokio::sync::mpsc::channel(128);
|
let (send, recv) = tokio::sync::mpsc::channel(4096);
|
||||||
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(128);
|
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096);
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
gameprofile: Mutex::new(None),
|
gameprofile: Mutex::new(None),
|
||||||
|
|||||||
@ -305,7 +305,8 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
|
|||||||
let msg = self.get_text_provider(&message)?;
|
let msg = self.get_text_provider(&message)?;
|
||||||
self.get_world_res(&world)?
|
self.get_world_res(&world)?
|
||||||
.provider
|
.provider
|
||||||
.broadcast_system_message(&msg, overlay);
|
.broadcast_system_message(&msg, overlay)
|
||||||
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,10 +69,7 @@ use pumpkin_data::{BlockDirection, BlockState, translation};
|
|||||||
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
|
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
|
||||||
use pumpkin_inventory::screen_handler::InventoryPlayer;
|
use pumpkin_inventory::screen_handler::InventoryPlayer;
|
||||||
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
|
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
|
||||||
use pumpkin_protocol::bedrock::client::set_actor_data::{
|
use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData};
|
||||||
CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag,
|
|
||||||
entity_data_key,
|
|
||||||
};
|
|
||||||
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
|
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
|
||||||
use pumpkin_protocol::bedrock::frame_set::FrameSet;
|
use pumpkin_protocol::bedrock::frame_set::FrameSet;
|
||||||
use pumpkin_protocol::java::client::play::{
|
use pumpkin_protocol::java::client::play::{
|
||||||
@ -527,9 +524,32 @@ impl World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) {
|
pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) {
|
||||||
let packet = CSystemChatMessage::new(message, overlay);
|
let je_packet = CSystemChatMessage::new(message, overlay);
|
||||||
self.broadcast_packet_all(&packet);
|
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(
|
pub async fn broadcast_message(
|
||||||
@ -565,7 +585,7 @@ impl World {
|
|||||||
Self::broadcast_java_grouped(je_packet, je_recipients_by_version);
|
Self::broadcast_java_grouped(je_packet, je_recipients_by_version);
|
||||||
|
|
||||||
for recipient in be_recipients {
|
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());
|
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;
|
let entity = &player.living_entity.entity;
|
||||||
entity.bedrock_flags.fetch_or(
|
let metadata = entity.bedrock_metadata();
|
||||||
(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 actor_data = CSetActorData {
|
let actor_data = CSetActorData {
|
||||||
actor_runtime_id: VarULong(runtime_id),
|
actor_runtime_id: VarULong(runtime_id),
|
||||||
@ -2008,7 +2008,7 @@ impl World {
|
|||||||
GameMode::Adventure => 2,
|
GameMode::Adventure => 2,
|
||||||
GameMode::Spectator => 6,
|
GameMode::Spectator => 6,
|
||||||
}),
|
}),
|
||||||
metadata: EntityMetadata::default(),
|
metadata: entity.bedrock_metadata(),
|
||||||
properties: EntityProperties::default(),
|
properties: EntityProperties::default(),
|
||||||
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
||||||
entity_unique_id: runtime_id as i64,
|
entity_unique_id: runtime_id as i64,
|
||||||
@ -2088,7 +2088,7 @@ impl World {
|
|||||||
GameMode::Adventure => 2,
|
GameMode::Adventure => 2,
|
||||||
GameMode::Spectator => 6,
|
GameMode::Spectator => 6,
|
||||||
}),
|
}),
|
||||||
metadata: EntityMetadata::default(),
|
metadata: ex_entity.bedrock_metadata(),
|
||||||
properties: EntityProperties::default(),
|
properties: EntityProperties::default(),
|
||||||
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
||||||
entity_unique_id: existing_player.entity_id() as i64,
|
entity_unique_id: existing_player.entity_id() as i64,
|
||||||
@ -2131,7 +2131,8 @@ impl World {
|
|||||||
let event = server.plugin_manager.fire(event).await;
|
let event = server.plugin_manager.fire(event).await;
|
||||||
|
|
||||||
if !event.cancelled {
|
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());
|
info!("{}", event.join_message.to_pretty_console());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2385,7 +2386,7 @@ impl World {
|
|||||||
GameMode::Adventure => 2,
|
GameMode::Adventure => 2,
|
||||||
GameMode::Spectator => 6,
|
GameMode::Spectator => 6,
|
||||||
}),
|
}),
|
||||||
metadata: EntityMetadata::default(),
|
metadata: player.get_entity().bedrock_metadata(),
|
||||||
properties: EntityProperties::default(),
|
properties: EntityProperties::default(),
|
||||||
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
||||||
entity_unique_id: entity_id as i64,
|
entity_unique_id: entity_id as i64,
|
||||||
@ -2493,7 +2494,7 @@ impl World {
|
|||||||
GameMode::Adventure => 2,
|
GameMode::Adventure => 2,
|
||||||
GameMode::Spectator => 6,
|
GameMode::Spectator => 6,
|
||||||
}),
|
}),
|
||||||
metadata: EntityMetadata::default(),
|
metadata: entity.bedrock_metadata(),
|
||||||
properties: EntityProperties::default(),
|
properties: EntityProperties::default(),
|
||||||
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData {
|
||||||
entity_unique_id: existing_player.entity_id() as i64,
|
entity_unique_id: existing_player.entity_id() as i64,
|
||||||
@ -2722,7 +2723,8 @@ impl World {
|
|||||||
let event = server.plugin_manager.fire(event).await;
|
let event = server.plugin_manager.fire(event).await;
|
||||||
|
|
||||||
if !event.cancelled {
|
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")
|
// TODO: Switch to structured logging, e.g. info!(player = %name, "connected")
|
||||||
info!("{}", event.join_message.to_pretty_console());
|
info!("{}", event.join_message.to_pretty_console());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user