Support multiple Minecraft protocol versions in lobby and probe logic
This commit is contained in:
parent
cf6bd526d9
commit
b404ab0a87
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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]
|
||||||
|
19
src/forge.rs
19
src/forge.rs
@ -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,
|
||||||
|
334
src/lobby.rs
334
src/lobby.rs
@ -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
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
src/probe.rs
31
src/probe.rs
@ -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
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
31
src/proto/packets/mod.rs
Normal 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;
|
||||||
|
}
|
174
src/proto/packets/play/join_game.rs
Normal file
174
src/proto/packets/play/join_game.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/proto/packets/play/keep_alive.rs
Normal file
29
src/proto/packets/play/keep_alive.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
15
src/proto/packets/play/mod.rs
Normal file
15
src/proto/packets/play/mod.rs
Normal 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;
|
48
src/proto/packets/play/player_pos.rs
Normal file
48
src/proto/packets/play/player_pos.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
src/proto/packets/play/respawn.rs
Normal file
66
src/proto/packets/play/respawn.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/proto/packets/play/server_brand.rs
Normal file
45
src/proto/packets/play/server_brand.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/proto/packets/play/sound.rs
Normal file
48
src/proto/packets/play/sound.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/proto/packets/play/time_update.rs
Normal file
41
src/proto/packets/play/time_update.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
src/proto/packets/play/title.rs
Normal file
141
src/proto/packets/play/title.rs
Normal 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
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user