Support multiple Minecraft protocol versions in lobby and probe logic

This commit is contained in:
timvisee 2021-11-23 01:12:50 +01:00
parent cf6bd526d9
commit b404ab0a87
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
20 changed files with 752 additions and 354 deletions

4
Cargo.lock generated
View File

@ -869,7 +869,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]] [[package]]
name = "minecraft-protocol" name = "minecraft-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=6d1ef0b#6d1ef0b27d7d49ee25109256e2c6b7a095ef255d" source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=5b80bc2#5b80bc2df31e07e2e6e203e652a301bd8a29a610"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"minecraft-protocol-derive", "minecraft-protocol-derive",
@ -882,7 +882,7 @@ dependencies = [
[[package]] [[package]]
name = "minecraft-protocol-derive" name = "minecraft-protocol-derive"
version = "0.0.0" version = "0.0.0"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=6d1ef0b#6d1ef0b27d7d49ee25109256e2c6b7a095ef255d" source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=5b80bc2#5b80bc2df31e07e2e6e203e652a301bd8a29a610"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -28,7 +28,7 @@ rcon = ["rust_rcon", "async-std"]
# Lobby support # Lobby support
# Add lobby join method, keeps client in fake lobby world until server is ready. # Add lobby join method, keeps client in fake lobby world until server is ready.
lobby = ["md-5", "named-binary-tag", "quartz_nbt", "uuid"] lobby = ["md-5", "uuid"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
@ -42,10 +42,12 @@ dotenv = "0.15"
flate2 = { version = "1.0", default-features = false, features = ["default"] } flate2 = { version = "1.0", default-features = false, features = ["default"] }
futures = { version = "0.3", default-features = false, features = ["executor"] } futures = { version = "0.3", default-features = false, features = ["executor"] }
log = "0.4" log = "0.4"
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "6d1ef0b" } minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "5b80bc2" }
named-binary-tag = "0.6"
notify = "4.0" notify = "4.0"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
proxy-protocol = "0.5" proxy-protocol = "0.5"
quartz_nbt = "0.2"
rand = "0.8" rand = "0.8"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
@ -61,8 +63,6 @@ async-std = { version = "1.9.0", default-features = false, optional = true }
# Feature: lobby # Feature: lobby
md-5 = { version = "0.9", optional = true } md-5 = { version = "0.9", optional = true }
named-binary-tag = { version = "0.6", optional = true }
quartz_nbt = { version = "0.2", optional = true }
uuid = { version = "0.7", optional = true, features = ["v3"] } uuid = { version = "0.7", optional = true, features = ["v3"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]

View File

@ -1,21 +1,32 @@
#[cfg(feature = "lobby")]
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "lobby")]
use std::time::Duration; use std::time::Duration;
#[cfg(feature = "lobby")]
use bytes::BytesMut; use bytes::BytesMut;
use minecraft_protocol::decoder::Decoder; use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::encoder::Encoder; use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::forge_v1_13::login::{Acknowledgement, LoginWrapper, ModList}; use minecraft_protocol::version::forge_v1_13::login::{Acknowledgement, LoginWrapper, ModList};
use minecraft_protocol::version::v1_14_4::login::{LoginPluginRequest, LoginPluginResponse}; use minecraft_protocol::version::v1_14_4::login::{LoginPluginRequest, LoginPluginResponse};
use minecraft_protocol::version::PacketId; use minecraft_protocol::version::PacketId;
#[cfg(feature = "lobby")]
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::tcp::WriteHalf; use tokio::net::tcp::WriteHalf;
#[cfg(feature = "lobby")]
use tokio::net::TcpStream; use tokio::net::TcpStream;
#[cfg(feature = "lobby")]
use tokio::time; use tokio::time;
use crate::forge; use crate::forge;
use crate::proto::client::{Client, ClientState}; use crate::proto::client::Client;
#[cfg(feature = "lobby")]
use crate::proto::client::ClientState;
use crate::proto::packet;
use crate::proto::packet::RawPacket; use crate::proto::packet::RawPacket;
use crate::proto::{packet, packets}; #[cfg(feature = "lobby")]
use crate::proto::packets;
#[cfg(feature = "lobby")]
use crate::server::Server; use crate::server::Server;
/// Forge status magic. /// Forge status magic.
@ -76,7 +87,7 @@ pub async fn respond_login_plugin_request(
// Determine whether we received the mod list // Determine whether we received the mod list
let is_unknown_header = login_wrapper.channel != forge::CHANNEL_HANDSHAKE; let is_unknown_header = login_wrapper.channel != forge::CHANNEL_HANDSHAKE;
let is_mod_list = !is_unknown_header && packet.id == packets::forge::login::CLIENT_MOD_LIST; let is_mod_list = !is_unknown_header && packet.id == ModList::PACKET_ID;
// If not the mod list, just acknowledge // If not the mod list, just acknowledge
if !is_mod_list { if !is_mod_list {
@ -144,6 +155,7 @@ pub async fn decode_forge_login_packet(
} }
/// Replay the Forge login payload for a client. /// Replay the Forge login payload for a client.
#[cfg(feature = "lobby")]
pub async fn replay_login_payload( pub async fn replay_login_payload(
client: &Client, client: &Client,
inbound: &mut TcpStream, inbound: &mut TcpStream,
@ -169,6 +181,7 @@ pub async fn replay_login_payload(
} }
/// Drain Forge login plugin response packets from stream. /// Drain Forge login plugin response packets from stream.
#[cfg(feature = "lobby")]
async fn drain_forge_responses( async fn drain_forge_responses(
client: &Client, client: &Client,
inbound: &mut TcpStream, inbound: &mut TcpStream,

View File

@ -1,27 +1,14 @@
use std::io::ErrorKind; use std::io::ErrorKind;
use std::ops::Deref; use std::ops::Deref;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use bytes::BytesMut; use bytes::BytesMut;
use futures::FutureExt; use futures::FutureExt;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::decoder::Decoder; use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::version::forge_v1_13::login::LoginWrapper;
use minecraft_protocol::version::v1_14_4::login::{ use minecraft_protocol::version::v1_14_4::login::{
LoginDisconnect, LoginPluginRequest, LoginPluginResponse, LoginStart, LoginSuccess, LoginPluginRequest, LoginStart, LoginSuccess, SetCompression,
SetCompression,
}; };
use minecraft_protocol::version::v1_16_5::game::{Title, TitleAction};
use minecraft_protocol::version::v1_17_1::game::{
ClientBoundKeepAlive, ClientBoundPluginMessage, JoinGame, NamedSoundEffect,
PlayerPositionAndLook, Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate,
};
use minecraft_protocol::version::PacketId;
use nbt::CompoundTag;
use proto::packet::RawPacket;
use rand::Rng;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
@ -30,19 +17,17 @@ use tokio::time;
use crate::config::*; use crate::config::*;
use crate::forge; use crate::forge;
use crate::mc::{self, dimension, uuid}; use crate::mc::uuid;
use crate::net; use crate::net;
use crate::proto; use crate::proto;
use crate::proto::client::{Client, ClientInfo, ClientState}; use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::packets::play::join_game::JoinGameData;
use crate::proto::{packet, packets}; use crate::proto::{packet, packets};
use crate::proxy; use crate::proxy;
use crate::server::{Server, State}; use crate::server::{Server, State};
/// Interval to send keep-alive packets at. /// Interval to send keep-alive packets at.
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10); pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
/// Auto incrementing ID source for keep alive packets.
static KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0);
/// Timeout for creating new server connection for lobby client. /// Timeout for creating new server connection for lobby client.
const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60); const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60);
@ -60,11 +45,6 @@ const SERVER_JOIN_GAME_TIMEOUT: Duration = Duration::from_secs(20);
/// See warning at: https://wiki.vg/Protocol#Login_Success /// See warning at: https://wiki.vg/Protocol#Login_Success
const SERVER_WARMUP: Duration = Duration::from_secs(1); const SERVER_WARMUP: Duration = Duration::from_secs(1);
/// Server brand to send to client in lobby world.
///
/// Shown in F3 menu. Updated once client is relayed to real server.
const SERVER_BRAND: &[u8] = b"lazymc";
/// Serve lobby service for given client connection. /// Serve lobby service for given client connection.
/// ///
/// The client must be in the login state, or this will error. /// The client must be in the login state, or this will error.
@ -121,8 +101,7 @@ pub async fn serve(
if config.server.forge { if config.server.forge {
forge::replay_login_payload(client, &mut inbound, server.clone(), &mut inbound_buf) forge::replay_login_payload(client, &mut inbound, server.clone(), &mut inbound_buf)
.await?; .await?;
let (returned_reader, returned_writer) = inbound.split(); let (_returned_reader, returned_writer) = inbound.split();
reader = returned_reader;
writer = returned_writer; writer = returned_writer;
} }
@ -140,25 +119,33 @@ pub async fn serve(
trace!(target: "lazymc::lobby", "Client login success, sending required play packets for lobby world"); trace!(target: "lazymc::lobby", "Client login success, sending required play packets for lobby world");
// Send packets to client required to get into workable play state for lobby world // Send packets to client required to get into workable play state for lobby world
send_lobby_play_packets(client, &mut writer, &server).await?; send_lobby_play_packets(client, &client_info, &mut writer, &server).await?;
// Wait for server to come online, then set up new connection to it // Wait for server to come online
stage_wait(client, &server, &config, &mut writer).await?; stage_wait(client, &client_info, &server, &config, &mut writer).await?;
// Start new connection to server
let server_client_info = client_info.clone();
let (server_client, mut outbound, mut server_buf) = let (server_client, mut outbound, mut server_buf) =
connect_to_server(client_info, &inbound, &config).await?; connect_to_server(&server_client_info, &inbound, &config).await?;
let (returned_reader, returned_writer) = inbound.split(); let (returned_reader, returned_writer) = inbound.split();
reader = returned_reader; reader = returned_reader;
writer = returned_writer; writer = returned_writer;
// Grab join game packet from server // Grab join game packet from server
let join_game = let join_game_data = wait_for_server_join_game(
wait_for_server_join_game(&server_client, &mut outbound, &mut server_buf).await?; &server_client,
&server_client_info,
&mut outbound,
&mut server_buf,
)
.await?;
// Reset lobby title // Reset lobby title
send_lobby_title(client, &mut writer, "").await?; packets::play::title::send(client, &client_info, &mut writer, "").await?;
// Play ready sound if configured // Play ready sound if configured
play_lobby_ready_sound(client, &mut writer, &config).await?; play_lobby_ready_sound(client, &client_info, &mut writer, &config).await?;
// Wait a second because Notchian servers are slow // Wait a second because Notchian servers are slow
// See: https://wiki.vg/Protocol#Login_Success // See: https://wiki.vg/Protocol#Login_Success
@ -166,7 +153,8 @@ pub async fn serve(
time::sleep(SERVER_WARMUP).await; time::sleep(SERVER_WARMUP).await;
// Send respawn packet, initiates teleport to real server world // Send respawn packet, initiates teleport to real server world
send_respawn_from_join(client, &mut writer, join_game).await?; packets::play::respawn::lobby_send(client, &client_info, &mut writer, join_game_data)
.await?;
// Drain inbound connection so we don't confuse the server // Drain inbound connection so we don't confuse the server
// TODO: can we void everything? we might need to forward everything to server except // TODO: can we void everything? we might need to forward everything to server except
@ -226,6 +214,7 @@ async fn respond_login_success(
/// Play lobby ready sound effect if configured. /// Play lobby ready sound effect if configured.
async fn play_lobby_ready_sound( async fn play_lobby_ready_sound(
client: &Client, client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
config: &Config, config: &Config,
) -> Result<(), ()> { ) -> Result<(), ()> {
@ -237,8 +226,8 @@ async fn play_lobby_ready_sound(
} }
// Play sound effect // Play sound effect
send_lobby_player_pos(client, writer).await?; packets::play::player_pos::send(client, client_info, writer).await?;
send_lobby_sound_effect(client, writer, sound_name).await?; packets::play::sound::send(client, client_info, writer, sound_name).await?;
} }
Ok(()) Ok(())
@ -247,253 +236,33 @@ async fn play_lobby_ready_sound(
/// Send packets to client to get workable play state for lobby world. /// Send packets to client to get workable play state for lobby world.
async fn send_lobby_play_packets( async fn send_lobby_play_packets(
client: &Client, client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
server: &Server, server: &Server,
) -> Result<(), ()> { ) -> Result<(), ()> {
// See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F // See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F
// Send initial game join // Send initial game join
send_lobby_join_game(client, writer, server).await?; packets::play::join_game::lobby_send(client, client_info, writer, server).await?;
// Send server brand // Send server brand
send_lobby_brand(client, writer).await?; packets::play::server_brand::send(client, client_info, writer).await?;
// Send spawn and player position, disables 'download terrain' screen // Send spawn and player position, disables 'download terrain' screen
send_lobby_player_pos(client, writer).await?; packets::play::player_pos::send(client, client_info, writer).await?;
// Notify client of world time, required once before keep-alive packets // Notify client of world time, required once before keep-alive packets
send_lobby_time_update(client, writer).await?; packets::play::time_update::send(client, client_info, writer).await?;
Ok(()) Ok(())
} }
/// Send initial join game packet to client for lobby.
async fn send_lobby_join_game(
client: &Client,
writer: &mut WriteHalf<'_>,
server: &Server,
) -> Result<(), ()> {
// Get dimension codec and build lobby dimension
let dimension_codec: CompoundTag =
if let Some(ref join_game) = server.probed_join_game.lock().await.as_ref() {
join_game.dimension_codec.clone()
} else {
dimension::lobby_default_dimension_codec()
};
let dimension: CompoundTag = dimension::lobby_dimension(&dimension_codec);
// Send Minecrafts default states, slightly customised for lobby world
packet::write_packet(
{
let status = server.status().await;
JoinGame {
// Player ID must be unique, if it collides with another server entity ID the player gets
// in a weird state and cannot move
entity_id: 0,
// TODO: use real server value
hardcore: false,
game_mode: 3,
previous_game_mode: -1i8 as u8,
world_names: vec![
"minecraft:overworld".into(),
"minecraft:the_nether".into(),
"minecraft:the_end".into(),
],
dimension_codec,
dimension,
world_name: "lazymc:lobby".into(),
hashed_seed: 0,
max_players: status.as_ref().map(|s| s.players.max as i32).unwrap_or(20),
// TODO: use real server value
view_distance: 10,
// TODO: use real server value
reduced_debug_info: false,
// TODO: use real server value
enable_respawn_screen: true,
is_debug: true,
is_flat: false,
}
},
client,
writer,
)
.await
}
/// Send lobby brand to client.
async fn send_lobby_brand(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
packet::write_packet(
ClientBoundPluginMessage {
channel: "minecraft:brand".into(),
data: SERVER_BRAND.into(),
},
client,
writer,
)
.await
}
/// Send lobby player position to client.
async fn send_lobby_player_pos(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send player location, disables download terrain screen
packet::write_packet(
PlayerPositionAndLook {
x: 0.0,
y: 0.0,
z: 0.0,
yaw: 0.0,
pitch: 90.0,
flags: 0b00000000,
teleport_id: 0,
dismount_vehicle: true,
},
client,
writer,
)
.await
}
/// Send lobby time update to client.
async fn send_lobby_time_update(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
const MC_TIME_NOON: i64 = 6000;
// Send time update, required once for keep-alive packets
packet::write_packet(
TimeUpdate {
world_age: MC_TIME_NOON,
time_of_day: MC_TIME_NOON,
},
client,
writer,
)
.await
}
/// Send keep alive packet to client.
///
/// Required periodically in play mode to prevent client timeout.
async fn send_keep_alive(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
packet::write_packet(
ClientBoundKeepAlive {
// Keep sending new IDs
id: KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed),
},
client,
writer,
)
.await
// TODO: verify we receive keep alive response with same ID from client
}
/// Send lobby title packets to client.
///
/// This will show the given text for two keep-alive periods. Use a newline for the subtitle.
///
/// If an empty string is given, the title times will be reset to default.
async fn send_lobby_title(
client: &Client,
writer: &mut WriteHalf<'_>,
text: &str,
) -> Result<(), ()> {
// Grab title and subtitle bits
let title = text.lines().next().unwrap_or("");
let subtitle = text.lines().skip(1).collect::<Vec<_>>().join("\n");
// Set title
packet::write_packet(
SetTitleText {
text: Message::new(Payload::text(title)),
},
client,
writer,
)
.await?;
// Set subtitle
packet::write_packet(
SetTitleSubtitle {
text: Message::new(Payload::text(&subtitle)),
},
client,
writer,
)
.await?;
// Set title times
packet::write_packet(
if title.is_empty() && subtitle.is_empty() {
// Defaults: https://minecraft.fandom.com/wiki/Commands/title#Detail
SetTitleTimes {
fade_in: 10,
stay: 70,
fade_out: 20,
}
} else {
SetTitleTimes {
fade_in: 0,
stay: KEEP_ALIVE_INTERVAL.as_secs() as i32 * mc::TICKS_PER_SECOND as i32 * 2,
fade_out: 0,
}
},
client,
writer,
)
.await
}
/// Send lobby ready sound effect to client.
async fn send_lobby_sound_effect(
client: &Client,
writer: &mut WriteHalf<'_>,
sound_name: &str,
) -> Result<(), ()> {
packet::write_packet(
NamedSoundEffect {
sound_name: sound_name.into(),
sound_category: 0,
effect_pos_x: 0,
effect_pos_y: 0,
effect_pos_z: 0,
volume: 1.0,
pitch: 1.0,
},
client,
writer,
)
.await
}
/// Send respawn packet to client to jump from lobby into now loaded server.
///
/// The required details will be fetched from the `join_game` packet as provided by the server.
async fn send_respawn_from_join(
client: &Client,
writer: &mut WriteHalf<'_>,
join_game: JoinGame,
) -> Result<(), ()> {
packet::write_packet(
Respawn {
dimension: join_game.dimension,
world_name: join_game.world_name,
hashed_seed: join_game.hashed_seed,
game_mode: join_game.game_mode,
previous_game_mode: join_game.previous_game_mode,
is_debug: join_game.is_debug,
is_flat: join_game.is_flat,
copy_metadata: false,
},
client,
writer,
)
.await
}
/// An infinite keep-alive loop. /// An infinite keep-alive loop.
/// ///
/// This will keep sending keep-alive and title packets to the client until it is dropped. /// This will keep sending keep-alive and title packets to the client until it is dropped.
async fn keep_alive_loop( async fn keep_alive_loop(
client: &Client, client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
config: &Config, config: &Config,
) -> Result<(), ()> { ) -> Result<(), ()> {
@ -505,8 +274,10 @@ async fn keep_alive_loop(
trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client"); trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client");
// Send keep alive and title packets // Send keep alive and title packets
send_keep_alive(client, writer).await?; packets::play::keep_alive::send(client, client_info, writer).await?;
send_lobby_title(client, writer, &config.join.lobby.message).await?; packets::play::title::send(client, client_info, writer, &config.join.lobby.message).await?;
// TODO: verify we receive correct keep alive response
} }
} }
@ -517,12 +288,13 @@ async fn keep_alive_loop(
/// During this stage we keep sending keep-alive and title packets to the client to keep it active. /// During this stage we keep sending keep-alive and title packets to the client to keep it active.
async fn stage_wait( async fn stage_wait(
client: &Client, client: &Client,
client_info: &ClientInfo,
server: &Server, server: &Server,
config: &Config, config: &Config,
writer: &mut WriteHalf<'_>, writer: &mut WriteHalf<'_>,
) -> Result<(), ()> { ) -> Result<(), ()> {
select! { select! {
a = keep_alive_loop(client, writer, config) => a, a = keep_alive_loop(client, client_info, writer, config) => a,
b = wait_for_server(server, config) => b, b = wait_for_server(server, config) => b,
} }
} }
@ -586,7 +358,7 @@ async fn wait_for_server(server: &Server, config: &Config) -> Result<(), ()> {
/// ///
/// This will initialize the connection to the play state. Client details are used. /// This will initialize the connection to the play state. Client details are used.
async fn connect_to_server( async fn connect_to_server(
client_info: ClientInfo, client_info: &ClientInfo,
inbound: &TcpStream, inbound: &TcpStream,
config: &Config, config: &Config,
) -> Result<(Client, TcpStream, BytesMut), ()> { ) -> Result<(Client, TcpStream, BytesMut), ()> {
@ -605,7 +377,7 @@ async fn connect_to_server(
/// This will initialize the connection to the play state. Client details are used. /// This will initialize the connection to the play state. Client details are used.
// TODO: clean this up // TODO: clean this up
async fn connect_to_server_no_timeout( async fn connect_to_server_no_timeout(
client_info: ClientInfo, client_info: &ClientInfo,
inbound: &TcpStream, inbound: &TcpStream,
config: &Config, config: &Config,
) -> Result<(Client, TcpStream, BytesMut), ()> { ) -> Result<(Client, TcpStream, BytesMut), ()> {
@ -639,12 +411,17 @@ async fn connect_to_server_no_timeout(
ClientState::Login.to_id(), ClientState::Login.to_id(),
"Client handshake should have login as next state" "Client handshake should have login as next state"
); );
packet::write_packet(client_info.handshake.unwrap(), &tmp_client, &mut writer).await?; packet::write_packet(
client_info.handshake.clone().unwrap(),
&tmp_client,
&mut writer,
)
.await?;
// Request login start // Request login start
packet::write_packet( packet::write_packet(
LoginStart { LoginStart {
name: client_info.username.ok_or(())?, name: client_info.username.clone().ok_or(())?,
}, },
&tmp_client, &tmp_client,
&mut writer, &mut writer,
@ -768,12 +545,13 @@ async fn connect_to_server_no_timeout(
/// This parses, consumes and returns the packet. /// This parses, consumes and returns the packet.
async fn wait_for_server_join_game( async fn wait_for_server_join_game(
client: &Client, client: &Client,
client_info: &ClientInfo,
outbound: &mut TcpStream, outbound: &mut TcpStream,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<JoinGame, ()> { ) -> Result<JoinGameData, ()> {
time::timeout( time::timeout(
SERVER_JOIN_GAME_TIMEOUT, SERVER_JOIN_GAME_TIMEOUT,
wait_for_server_join_game_no_timeout(client, outbound, buf), wait_for_server_join_game_no_timeout(client, client_info, outbound, buf),
) )
.await .await
.map_err(|_| { .map_err(|_| {
@ -788,9 +566,10 @@ async fn wait_for_server_join_game(
// TODO: do not drop error here, return Box<dyn Error> // TODO: do not drop error here, return Box<dyn Error>
async fn wait_for_server_join_game_no_timeout( async fn wait_for_server_join_game_no_timeout(
client: &Client, client: &Client,
client_info: &ClientInfo,
outbound: &mut TcpStream, outbound: &mut TcpStream,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<JoinGame, ()> { ) -> Result<JoinGameData, ()> {
let (mut reader, mut _writer) = outbound.split(); let (mut reader, mut _writer) = outbound.split();
loop { loop {
@ -805,12 +584,13 @@ async fn wait_for_server_join_game_no_timeout(
}; };
// Catch join game // Catch join game
if packet.id == packets::play::CLIENT_JOIN_GAME { if packets::play::join_game::is_packet(client_info, packet.id) {
let join_game = JoinGame::decode(&mut packet.data.as_slice()).map_err(|err| { // Parse join game data
dbg!(err); let join_game_data = JoinGameData::from_packet(client_info, packet).map_err(|err| {
warn!(target: "lazymc::lobby", "Failed to parse join game packet: {:?}", err);
})?; })?;
return Ok(join_game); return Ok(join_game_data);
} }
// Show unhandled packet warning // Show unhandled packet warning

View File

@ -76,7 +76,7 @@ fn lobby_base_dimension(dimension_types: &CompoundTag) -> CompoundTag {
/// ///
/// This likely breaks if the Minecraft version doesn't match exactly. /// This likely breaks if the Minecraft version doesn't match exactly.
/// Please use an up-to-date coded from the server instead. /// Please use an up-to-date coded from the server instead.
pub fn lobby_default_dimension_codec() -> CompoundTag { pub fn default_dimension_codec() -> CompoundTag {
snbt_to_compound_tag(include_str!("../../res/dimension_codec.snbt")) snbt_to_compound_tag(include_str!("../../res/dimension_codec.snbt"))
} }

View File

@ -8,14 +8,14 @@ use minecraft_protocol::version::v1_14_4::handshake::Handshake;
use minecraft_protocol::version::v1_14_4::login::{ use minecraft_protocol::version::v1_14_4::login::{
LoginPluginRequest, LoginPluginResponse, LoginStart, SetCompression, LoginPluginRequest, LoginPluginResponse, LoginStart, SetCompression,
}; };
use minecraft_protocol::version::v1_17_1::game::JoinGame;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::time; use tokio::time;
use crate::config::Config; use crate::config::Config;
use crate::forge; use crate::forge;
use crate::net; use crate::net;
use crate::proto::client::{Client, ClientState}; use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::packets::play::join_game::JoinGameData;
use crate::proto::{self, packet, packets}; use crate::proto::{self, packet, packets};
use crate::server::{Server, State}; use crate::server::{Server, State};
@ -149,6 +149,10 @@ async fn connect_to_server_no_timeout(
}; };
tmp_client.set_state(ClientState::Login); tmp_client.set_state(ClientState::Login);
// Construct client info
let mut tmp_client_info = ClientInfo::empty();
tmp_client_info.protocol.replace(config.public.protocol);
let (mut reader, mut writer) = outbound.split(); let (mut reader, mut writer) = outbound.split();
// Select server address to use, add magic if Forge // Select server address to use, add magic if Forge
@ -270,8 +274,10 @@ async fn connect_to_server_no_timeout(
tmp_client.set_state(ClientState::Play); tmp_client.set_state(ClientState::Play);
// Wait to catch join game packet // Wait to catch join game packet
let join_game = wait_for_server_join_game(&tmp_client, &mut outbound, &mut buf).await?; let join_game_data =
server.probed_join_game.lock().await.replace(join_game); wait_for_server_join_game(&tmp_client, &tmp_client_info, &mut outbound, &mut buf)
.await?;
server.probed_join_game.lock().await.replace(join_game_data);
// Gracefully close connection // Gracefully close connection
let _ = net::close_tcp_stream(outbound).await; let _ = net::close_tcp_stream(outbound).await;
@ -296,12 +302,13 @@ async fn connect_to_server_no_timeout(
/// This parses, consumes and returns the packet. /// This parses, consumes and returns the packet.
async fn wait_for_server_join_game( async fn wait_for_server_join_game(
client: &Client, client: &Client,
client_info: &ClientInfo,
outbound: &mut TcpStream, outbound: &mut TcpStream,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<JoinGame, ()> { ) -> Result<JoinGameData, ()> {
time::timeout( time::timeout(
PROBE_JOIN_GAME_TIMEOUT, PROBE_JOIN_GAME_TIMEOUT,
wait_for_server_join_game_no_timeout(client, outbound, buf), wait_for_server_join_game_no_timeout(client, client_info, outbound, buf),
) )
.await .await
.map_err(|_| { .map_err(|_| {
@ -316,9 +323,10 @@ async fn wait_for_server_join_game(
// TODO: do not drop error here, return Box<dyn Error> // TODO: do not drop error here, return Box<dyn Error>
async fn wait_for_server_join_game_no_timeout( async fn wait_for_server_join_game_no_timeout(
client: &Client, client: &Client,
client_info: &ClientInfo,
outbound: &mut TcpStream, outbound: &mut TcpStream,
buf: &mut BytesMut, buf: &mut BytesMut,
) -> Result<JoinGame, ()> { ) -> Result<JoinGameData, ()> {
let (mut reader, mut _writer) = outbound.split(); let (mut reader, mut _writer) = outbound.split();
loop { loop {
@ -333,12 +341,13 @@ async fn wait_for_server_join_game_no_timeout(
}; };
// Catch join game // Catch join game
if packet.id == packets::play::CLIENT_JOIN_GAME { if packets::play::join_game::is_packet(client_info, packet.id) {
let join_game = JoinGame::decode(&mut packet.data.as_slice()).map_err(|err| { // Parse join game data
error!(target: "lazymc::probe", "Failed to decode join game packet, ignoring: {:?}", err); let join_game_data = JoinGameData::from_packet(client_info, packet).map_err(|err| {
warn!(target: "lazymc::lobby", "Failed to parse join game packet: {:?}", err);
})?; })?;
return Ok(join_game); return Ok(join_game_data);
} }
// Show unhandled packet warning // Show unhandled packet warning

View File

@ -115,6 +115,9 @@ impl Default for ClientState {
/// Client info, useful during connection handling. /// Client info, useful during connection handling.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ClientInfo { pub struct ClientInfo {
/// Used protocol version.
pub protocol: Option<u32>,
/// Handshake as received from client. /// Handshake as received from client.
pub handshake: Option<Handshake>, pub handshake: Option<Handshake>,
@ -126,4 +129,10 @@ impl ClientInfo {
pub fn empty() -> Self { pub fn empty() -> Self {
Self::default() Self::default()
} }
/// Get protocol version.
pub fn protocol(&self) -> Option<u32> {
self.protocol
.or_else(|| self.handshake.as_ref().map(|h| h.protocol_version as u32))
}
} }

View File

@ -1,54 +0,0 @@
//! Minecraft protocol packet IDs.
#![allow(unused)]
pub mod handshake {
pub const SERVER_HANDSHAKE: u8 = 0x00;
}
pub mod status {
pub const CLIENT_STATUS: u8 = 0x0;
pub const CLIENT_PING: u8 = 0x01;
pub const SERVER_STATUS: u8 = 0x00;
pub const SERVER_PING: u8 = 0x01;
}
pub mod login {
pub const CLIENT_DISCONNECT: u8 = 0x00;
pub const CLIENT_LOGIN_SUCCESS: u8 = 0x02;
pub const CLIENT_SET_COMPRESSION: u8 = 0x03;
pub const CLIENT_LOGIN_PLUGIN_REQUEST: u8 = 0x04;
pub const SERVER_LOGIN_START: u8 = 0x00;
pub const SERVER_LOGIN_PLUGIN_RESPONSE: u8 = 0x02;
}
pub mod play {
pub const CLIENT_CHAT_MSG: u8 = 0x0F;
pub const CLIENT_PLUGIN_MESSAGE: u8 = 0x18;
pub const CLIENT_NAMED_SOUND_EFFECT: u8 = 0x19;
pub const CLIENT_DISCONNECT: u8 = 0x1A;
pub const CLIENT_KEEP_ALIVE: u8 = 0x21;
pub const CLIENT_JOIN_GAME: u8 = 0x26;
pub const CLIENT_PLAYER_POS_LOOK: u8 = 0x38;
pub const CLIENT_RESPAWN: u8 = 0x3D;
pub const CLIENT_SPAWN_POS: u8 = 0x4B;
pub const CLIENT_SET_TITLE_SUBTITLE: u8 = 0x57;
pub const CLIENT_TIME_UPDATE: u8 = 0x58;
pub const CLIENT_SET_TITLE_TEXT: u8 = 0x59;
pub const CLIENT_SET_TITLE_TIMES: u8 = 0x5A;
pub const SERVER_CLIENT_SETTINGS: u8 = 0x05;
// TODO: update
pub const SERVER_PLUGIN_MESSAGE: u8 = 0x0B; //0A
pub const SERVER_PLAYER_POS: u8 = 0x11;
pub const SERVER_PLAYER_POS_ROT: u8 = 0x12;
}
pub mod forge {
pub mod login {
pub const CLIENT_MOD_LIST: u8 = 1;
pub const CLIENT_SERVER_REGISTRY: u8 = 3;
pub const CLIENT_CONFIG_DATA: u8 = 4;
pub const SERVER_MOD_LIST_REPLY: u8 = 2;
pub const SERVER_ACKNOWLEDGEMENT: u8 = 99;
}
}

31
src/proto/packets/mod.rs Normal file
View File

@ -0,0 +1,31 @@
//! Minecraft protocol packet IDs.
pub mod play;
pub mod handshake {
use minecraft_protocol::version::v1_14_4::handshake::*;
pub const SERVER_HANDSHAKE: u8 = Handshake::PACKET_ID;
}
pub mod status {
use minecraft_protocol::version::v1_14_4::status::*;
pub const CLIENT_STATUS: u8 = StatusResponse::PACKET_ID;
pub const CLIENT_PING: u8 = PingResponse::PACKET_ID;
pub const SERVER_STATUS: u8 = StatusRequest::PACKET_ID;
pub const SERVER_PING: u8 = PingRequest::PACKET_ID;
}
pub mod login {
use minecraft_protocol::version::v1_14_4::login::*;
#[cfg(feature = "lobby")]
pub const CLIENT_DISCONNECT: u8 = LoginDisconnect::PACKET_ID;
pub const CLIENT_LOGIN_SUCCESS: u8 = LoginSuccess::PACKET_ID;
pub const CLIENT_SET_COMPRESSION: u8 = SetCompression::PACKET_ID;
pub const CLIENT_LOGIN_PLUGIN_REQUEST: u8 = LoginPluginRequest::PACKET_ID;
pub const SERVER_LOGIN_START: u8 = LoginStart::PACKET_ID;
#[cfg(feature = "lobby")]
pub const SERVER_LOGIN_PLUGIN_RESPONSE: u8 = LoginPluginResponse::PACKET_ID;
}

View File

@ -0,0 +1,174 @@
use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::error::DecodeError;
use minecraft_protocol::version::{v1_16_5, v1_17};
use nbt::CompoundTag;
#[cfg(feature = "lobby")]
use tokio::net::tcp::WriteHalf;
#[cfg(feature = "lobby")]
use crate::mc::dimension;
#[cfg(feature = "lobby")]
use crate::proto::client::Client;
use crate::proto::client::ClientInfo;
#[cfg(feature = "lobby")]
use crate::proto::packet;
use crate::proto::packet::RawPacket;
#[cfg(feature = "lobby")]
use crate::server::Server;
/// Data extracted from `JoinGame` packet.
#[derive(Debug, Clone)]
pub struct JoinGameData {
pub dimension: Option<CompoundTag>,
pub dimension_codec: Option<CompoundTag>,
pub world_name: Option<String>,
pub hashed_seed: Option<i64>,
pub game_mode: Option<u8>,
pub previous_game_mode: Option<u8>,
pub is_debug: Option<bool>,
pub is_flat: Option<bool>,
}
impl JoinGameData {
/// Extract join game data from given packet.
pub fn from_packet(client_info: &ClientInfo, packet: RawPacket) -> Result<Self, DecodeError> {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
Ok(v1_16_5::game::JoinGame::decode(&mut packet.data.as_slice())?.into())
}
_ => Ok(v1_17::game::JoinGame::decode(&mut packet.data.as_slice())?.into()),
}
}
}
impl From<v1_16_5::game::JoinGame> for JoinGameData {
fn from(join_game: v1_16_5::game::JoinGame) -> Self {
Self {
dimension: Some(join_game.dimension),
dimension_codec: Some(join_game.dimension_codec),
world_name: Some(join_game.world_name),
hashed_seed: Some(join_game.hashed_seed),
game_mode: Some(join_game.game_mode),
previous_game_mode: Some(join_game.previous_game_mode),
is_debug: Some(join_game.is_debug),
is_flat: Some(join_game.is_flat),
}
}
}
impl From<v1_17::game::JoinGame> for JoinGameData {
fn from(join_game: v1_17::game::JoinGame) -> Self {
Self {
dimension: Some(join_game.dimension),
dimension_codec: Some(join_game.dimension_codec),
world_name: Some(join_game.world_name),
hashed_seed: Some(join_game.hashed_seed),
game_mode: Some(join_game.game_mode),
previous_game_mode: Some(join_game.previous_game_mode),
is_debug: Some(join_game.is_debug),
is_flat: Some(join_game.is_flat),
}
}
}
/// Check whether the packet ID matches.
pub fn is_packet(client_info: &ClientInfo, packet_id: u8) -> bool {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => packet_id == v1_16_5::game::JoinGame::PACKET_ID,
_ => packet_id == v1_17::game::JoinGame::PACKET_ID,
}
}
/// Send initial join game packet to client for lobby.
#[cfg(feature = "lobby")]
pub async fn lobby_send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
server: &Server,
) -> Result<(), ()> {
// Get dimension codec and build lobby dimension
let dimension_codec: CompoundTag =
if let Some(ref join_game) = server.probed_join_game.lock().await.as_ref() {
join_game
.dimension_codec
.clone()
.unwrap_or_else(|| dimension::default_dimension_codec())
} else {
dimension::default_dimension_codec()
};
let dimension: CompoundTag = dimension::lobby_dimension(&dimension_codec);
let status = server.status().await;
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(
v1_16_5::game::JoinGame {
// Player ID must be unique, if it collides with another server entity ID the player gets
// in a weird state and cannot move
entity_id: 0,
// TODO: use real server value
hardcore: false,
game_mode: 3,
previous_game_mode: -1i8 as u8,
world_names: vec![
"minecraft:overworld".into(),
"minecraft:the_nether".into(),
"minecraft:the_end".into(),
],
dimension_codec,
dimension,
world_name: "lazymc:lobby".into(),
hashed_seed: 0,
max_players: status.as_ref().map(|s| s.players.max as i32).unwrap_or(20),
// TODO: use real server value
view_distance: 10,
// TODO: use real server value
reduced_debug_info: false,
// TODO: use real server value
enable_respawn_screen: true,
is_debug: true,
is_flat: false,
},
client,
writer,
)
.await
}
_ => {
packet::write_packet(
v1_17::game::JoinGame {
// Player ID must be unique, if it collides with another server entity ID the player gets
// in a weird state and cannot move
entity_id: 0,
// TODO: use real server value
hardcore: false,
game_mode: 3,
previous_game_mode: -1i8 as u8,
world_names: vec![
"minecraft:overworld".into(),
"minecraft:the_nether".into(),
"minecraft:the_end".into(),
],
dimension_codec,
dimension,
world_name: "lazymc:lobby".into(),
hashed_seed: 0,
max_players: status.as_ref().map(|s| s.players.max as i32).unwrap_or(20),
// TODO: use real server value
view_distance: 10,
// TODO: use real server value
reduced_debug_info: false,
// TODO: use real server value
enable_respawn_screen: true,
is_debug: true,
is_flat: false,
},
client,
writer,
)
.await
}
}
}

View File

@ -0,0 +1,29 @@
use std::sync::atomic::{AtomicU64, Ordering};
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
/// Auto incrementing ID source for keep alive packets.
static KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0);
/// Send keep alive packet to client.
///
/// Required periodically in play mode to prevent client timeout.
pub async fn send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
// Keep sending new IDs
let id = KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed);
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(v1_16_5::game::ClientBoundKeepAlive { id }, client, writer).await
}
_ => packet::write_packet(v1_17::game::ClientBoundKeepAlive { id }, client, writer).await,
}
}

View File

@ -0,0 +1,15 @@
pub mod join_game;
#[cfg(feature = "lobby")]
pub mod keep_alive;
#[cfg(feature = "lobby")]
pub mod player_pos;
#[cfg(feature = "lobby")]
pub mod respawn;
#[cfg(feature = "lobby")]
pub mod server_brand;
#[cfg(feature = "lobby")]
pub mod sound;
#[cfg(feature = "lobby")]
pub mod time_update;
#[cfg(feature = "lobby")]
pub mod title;

View File

@ -0,0 +1,48 @@
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
/// Move player to world origin.
pub async fn send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(
v1_16_5::game::PlayerPositionAndLook {
x: 0.0,
y: 0.0,
z: 0.0,
yaw: 0.0,
pitch: 90.0,
flags: 0b00000000,
teleport_id: 0,
},
client,
writer,
)
.await
}
_ => {
packet::write_packet(
v1_17::game::PlayerPositionAndLook {
x: 0.0,
y: 0.0,
z: 0.0,
yaw: 0.0,
pitch: 90.0,
flags: 0b00000000,
teleport_id: 0,
dismount_vehicle: true,
},
client,
writer,
)
.await
}
}
}

View File

@ -0,0 +1,66 @@
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
use super::join_game::JoinGameData;
use crate::mc::dimension;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
/// Send respawn packet to client to jump from lobby into now loaded server.
///
/// The required details will be fetched from the `join_game` packet as provided by the server.
pub async fn lobby_send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
data: JoinGameData,
) -> Result<(), ()> {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(
v1_16_5::game::Respawn {
dimension: data.dimension.unwrap_or_else(|| {
dimension::lobby_dimension(
&data
.dimension_codec
.unwrap_or_else(|| dimension::default_dimension_codec()),
)
}),
world_name: data.world_name.unwrap_or_else(|| "world".into()),
hashed_seed: data.hashed_seed.unwrap_or(0),
game_mode: data.game_mode.unwrap_or(0),
previous_game_mode: data.previous_game_mode.unwrap_or(-1i8 as u8),
is_debug: data.is_debug.unwrap_or(false),
is_flat: data.is_flat.unwrap_or(false),
copy_metadata: false,
},
client,
writer,
)
.await
}
_ => {
packet::write_packet(
v1_17::game::Respawn {
dimension: data.dimension.unwrap_or_else(|| {
dimension::lobby_dimension(
&data
.dimension_codec
.unwrap_or_else(|| dimension::default_dimension_codec()),
)
}),
world_name: data.world_name.unwrap_or_else(|| "world".into()),
hashed_seed: data.hashed_seed.unwrap_or(0),
game_mode: data.game_mode.unwrap_or(0),
previous_game_mode: data.previous_game_mode.unwrap_or(-1i8 as u8),
is_debug: data.is_debug.unwrap_or(false),
is_flat: data.is_flat.unwrap_or(false),
copy_metadata: false,
},
client,
writer,
)
.await
}
}
}

View File

@ -0,0 +1,45 @@
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
/// Minecraft channel to set brand.
const CHANNEL: &str = "minecraft:brand";
/// Server brand to send to client in lobby world.
///
/// Shown in F3 menu. Updated once client is relayed to real server.
const SERVER_BRAND: &[u8] = b"lazymc";
/// Send lobby brand to client.
pub async fn send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(
v1_16_5::game::ClientBoundPluginMessage {
channel: CHANNEL.into(),
data: SERVER_BRAND.into(),
},
client,
writer,
)
.await
}
_ => {
packet::write_packet(
v1_17::game::ClientBoundPluginMessage {
channel: CHANNEL.into(),
data: SERVER_BRAND.into(),
},
client,
writer,
)
.await
}
}
}

View File

@ -0,0 +1,48 @@
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
/// Play a sound effect at world origin.
pub async fn send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
sound_name: &str,
) -> Result<(), ()> {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(
v1_16_5::game::NamedSoundEffect {
sound_name: sound_name.into(),
sound_category: 0,
effect_pos_x: 0,
effect_pos_y: 0,
effect_pos_z: 0,
volume: 1.0,
pitch: 1.0,
},
client,
writer,
)
.await
}
_ => {
packet::write_packet(
v1_17::game::NamedSoundEffect {
sound_name: sound_name.into(),
sound_category: 0,
effect_pos_x: 0,
effect_pos_y: 0,
effect_pos_z: 0,
volume: 1.0,
pitch: 1.0,
},
client,
writer,
)
.await
}
}
}

View File

@ -0,0 +1,41 @@
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
/// Send lobby time update to client.
///
/// Sets world time to 0.
///
/// Required once for keep-alive packets.
pub async fn send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => {
packet::write_packet(
v1_16_5::game::TimeUpdate {
world_age: 0,
time_of_day: 0,
},
client,
writer,
)
.await
}
_ => {
packet::write_packet(
v1_17::game::TimeUpdate {
world_age: 0,
time_of_day: 0,
},
client,
writer,
)
.await
}
}
}

View File

@ -0,0 +1,141 @@
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::version::{v1_16_5, v1_17};
use tokio::net::tcp::WriteHalf;
#[cfg(feature = "lobby")]
use crate::lobby::KEEP_ALIVE_INTERVAL;
use crate::mc;
use crate::proto::client::{Client, ClientInfo};
use crate::proto::packet;
#[cfg(feature = "lobby")]
const DISPLAY_TIME: i32 = KEEP_ALIVE_INTERVAL.as_secs() as i32 * mc::TICKS_PER_SECOND as i32 * 2;
#[cfg(not(feature = "lobby"))]
const DISPLAY_TIME: i32 = 10 * mc::TICKS_PER_SECOND as i32 * 2;
/// Send lobby title packets to client.
///
/// This will show the given text for two keep-alive periods. Use a newline for the subtitle.
///
/// If an empty string is given, the title times will be reset to default.
pub async fn send(
client: &Client,
client_info: &ClientInfo,
writer: &mut WriteHalf<'_>,
text: &str,
) -> Result<(), ()> {
// Grab title and subtitle bits
let title = text.lines().next().unwrap_or("");
let subtitle = text.lines().skip(1).collect::<Vec<_>>().join("\n");
match client_info.protocol() {
Some(p) if p <= v1_16_5::PROTOCOL => send_v1_16_5(client, writer, title, &subtitle).await,
_ => send_v1_17(client, writer, title, &subtitle).await,
}
}
async fn send_v1_16_5(
client: &Client,
writer: &mut WriteHalf<'_>,
title: &str,
subtitle: &str,
) -> Result<(), ()> {
use v1_16_5::game::{Title, TitleAction};
// Set title
packet::write_packet(
Title {
action: TitleAction::SetTitle {
text: Message::new(Payload::text(title)),
},
},
client,
writer,
)
.await?;
// Set subtitle
packet::write_packet(
Title {
action: TitleAction::SetSubtitle {
text: Message::new(Payload::text(&subtitle)),
},
},
client,
writer,
)
.await?;
// Set title times
packet::write_packet(
Title {
action: if title.is_empty() && subtitle.is_empty() {
// Defaults: https://minecraft.fandom.com/wiki/Commands/title#Detail
TitleAction::SetTimesAndDisplay {
fade_in: 10,
stay: 70,
fade_out: 20,
}
} else {
TitleAction::SetTimesAndDisplay {
fade_in: 0,
stay: DISPLAY_TIME,
fade_out: 0,
}
},
},
client,
writer,
)
.await
}
async fn send_v1_17(
client: &Client,
writer: &mut WriteHalf<'_>,
title: &str,
subtitle: &str,
) -> Result<(), ()> {
use v1_17::game::{SetTitleSubtitle, SetTitleText, SetTitleTimes};
// Set title
packet::write_packet(
SetTitleText {
text: Message::new(Payload::text(title)),
},
client,
writer,
)
.await?;
// Set subtitle
packet::write_packet(
SetTitleSubtitle {
text: Message::new(Payload::text(&subtitle)),
},
client,
writer,
)
.await?;
// Set title times
packet::write_packet(
if title.is_empty() && subtitle.is_empty() {
// Defaults: https://minecraft.fandom.com/wiki/Commands/title#Detail
SetTitleTimes {
fade_in: 10,
stay: 70,
fade_out: 20,
}
} else {
SetTitleTimes {
fade_in: 0,
stay: DISPLAY_TIME,
fade_out: 0,
}
},
client,
writer,
)
.await
}

View File

@ -5,7 +5,6 @@ use std::time::{Duration, Instant};
use futures::FutureExt; use futures::FutureExt;
use minecraft_protocol::data::server_status::ServerStatus; use minecraft_protocol::data::server_status::ServerStatus;
use minecraft_protocol::version::v1_17_1::game::JoinGame;
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::watch; use tokio::sync::watch;
#[cfg(feature = "rcon")] #[cfg(feature = "rcon")]
@ -16,6 +15,7 @@ use tokio::time;
use crate::config::{Config, Server as ConfigServer}; use crate::config::{Config, Server as ConfigServer};
use crate::mc::ban::{BannedIp, BannedIps}; use crate::mc::ban::{BannedIp, BannedIps};
use crate::os; use crate::os;
use crate::proto::packets::play::join_game::JoinGameData;
/// Server cooldown after the process quit. /// Server cooldown after the process quit.
/// Used to give it some more time to quit forgotten threads, such as for RCON. /// Used to give it some more time to quit forgotten threads, such as for RCON.
@ -84,7 +84,7 @@ pub struct Server {
// TODO: dont use mutex, do not make public, dont use bytesmut // TODO: dont use mutex, do not make public, dont use bytesmut
pub forge_payload: Mutex<Vec<Vec<u8>>>, pub forge_payload: Mutex<Vec<Vec<u8>>>,
// TODO: dont use mutex, do not make public, dont use bytesmut // TODO: dont use mutex, do not make public, dont use bytesmut
pub probed_join_game: Mutex<Option<JoinGame>>, pub probed_join_game: Mutex<Option<JoinGameData>>,
} }
impl Server { impl Server {

View File

@ -84,6 +84,9 @@ pub async fn serve(
}; };
// Update client info and client state // Update client info and client state
client_info
.protocol
.replace(handshake.protocol_version as u32);
client_info.handshake.replace(handshake); client_info.handshake.replace(handshake);
client.set_state(new_state); client.set_state(new_state);