diff --git a/Cargo.lock b/Cargo.lock index 0d41237..5b61c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,7 +685,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "minecraft-protocol" version = "0.1.0" -source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=d26a525#d26a525c7b29b61d2db64805181fb5471ea4317a" +source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=4e6a1f9#4e6a1f93807f35671943630c9bdc0d0c5da67eb8" dependencies = [ "byteorder", "minecraft-protocol-derive", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "minecraft-protocol-derive" version = "0.0.0" -source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=d26a525#d26a525c7b29b61d2db64805181fb5471ea4317a" +source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=4e6a1f9#4e6a1f93807f35671943630c9bdc0d0c5da67eb8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e931492..34531cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ dotenv = "0.15" flate2 = { version = "1.0", default-features = false, features = ["default"] } futures = { version = "0.3", default-features = false } log = "0.4" -minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "d26a525" } +minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "4e6a1f9" } pretty_env_logger = "0.4" rand = "0.8" serde = "1.0" diff --git a/src/lobby.rs b/src/lobby.rs index c7a681f..b2c97a1 100644 --- a/src/lobby.rs +++ b/src/lobby.rs @@ -8,15 +8,13 @@ use bytes::BytesMut; use futures::FutureExt; use minecraft_protocol::data::chat::{Message, Payload}; use minecraft_protocol::decoder::Decoder; -use minecraft_protocol::encoder::Encoder; use minecraft_protocol::version::v1_14_4::handshake::Handshake; use minecraft_protocol::version::v1_14_4::login::{LoginStart, LoginSuccess, SetCompression}; use minecraft_protocol::version::v1_17_1::game::{ - ClientBoundKeepAlive, JoinGame, NamedSoundEffect, PlayerPositionAndLook, PluginMessage, - Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate, + ClientBoundKeepAlive, ClientBoundPluginMessage, JoinGame, NamedSoundEffect, + PlayerPositionAndLook, Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate, }; use nbt::CompoundTag; -use tokio::io::AsyncWriteExt; use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::TcpStream; use tokio::select; @@ -28,8 +26,7 @@ use crate::mc; use crate::net; use crate::proto; use crate::proto::client::{Client, ClientInfo, ClientState}; -use crate::proto::packet::{self, RawPacket}; -use crate::proto::packets; +use crate::proto::{packet, packets}; use crate::proxy; use crate::server::{Server, State}; @@ -185,15 +182,7 @@ async fn respond_set_compression( writer: &mut WriteHalf<'_>, threshold: i32, ) -> Result<(), ()> { - let packet = SetCompression { threshold }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::login::CLIENT_SET_COMPRESSION, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + packet::write_packet(SetCompression { threshold }, client, writer).await } /// Respond to client with login success packet @@ -203,21 +192,18 @@ async fn respond_login_success( writer: &mut WriteHalf<'_>, login_start: &LoginStart, ) -> Result<(), ()> { - let packet = LoginSuccess { - uuid: Uuid::new_v3( - &Uuid::new_v3(&Uuid::nil(), b"OfflinePlayer"), - login_start.name.as_bytes(), - ), - username: login_start.name.clone(), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::login::CLIENT_LOGIN_SUCCESS, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + packet::write_packet( + LoginSuccess { + uuid: Uuid::new_v3( + &Uuid::new_v3(&Uuid::nil(), b"OfflinePlayer"), + login_start.name.as_bytes(), + ), + username: login_start.name.clone(), + }, + client, + writer, + ) + .await } /// Play lobby ready sound effect if configured. @@ -271,84 +257,75 @@ async fn send_lobby_join_game( server: &Server, ) -> Result<(), ()> { // Send Minecrafts default states, slightly customised for lobby world - let packet = { - let status = server.status().await; + 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: snbt_to_compound_tag(include_str!("../res/dimension_codec.snbt")), - dimension: snbt_to_compound_tag(include_str!("../res/dimension.snbt")), - 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, - } - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_JOIN_GAME, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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: snbt_to_compound_tag(include_str!("../res/dimension_codec.snbt")), + dimension: snbt_to_compound_tag(include_str!("../res/dimension.snbt")), + 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<(), ()> { - let packet = PluginMessage { - channel: "minecraft:brand".into(), - data: SERVER_BRAND.into(), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_PLUGIN_MESSAGE, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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 - let 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, - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_PLAYER_POS_LOOK, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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. @@ -356,38 +333,32 @@ async fn send_lobby_time_update(client: &Client, writer: &mut WriteHalf<'_>) -> const MC_TIME_NOON: i64 = 6000; // Send time update, required once for keep-alive packets - let packet = TimeUpdate { - world_age: MC_TIME_NOON, - time_of_day: MC_TIME_NOON, - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_TIME_UPDATE, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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<(), ()> { - let packet = ClientBoundKeepAlive { - // Keep sending new IDs - id: KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_KEEP_ALIVE, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; + 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 - - Ok(()) } /// Send lobby title packets to client. @@ -405,50 +376,45 @@ async fn send_lobby_title( let subtitle = text.lines().skip(1).collect::>().join("\n"); // Set title - let packet = SetTitleText { - text: Message::new(Payload::text(title)), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_SET_TITLE_TEXT, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; + packet::write_packet( + SetTitleText { + text: Message::new(Payload::text(title)), + }, + client, + writer, + ) + .await?; // Set subtitle - let packet = SetTitleSubtitle { - text: Message::new(Payload::text(&subtitle)), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_SET_TITLE_SUBTITLE, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; + packet::write_packet( + SetTitleSubtitle { + text: Message::new(Payload::text(&subtitle)), + }, + client, + writer, + ) + .await?; // Set title times - let 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, - } - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_SET_TITLE_TIMES, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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. @@ -457,23 +423,20 @@ async fn send_lobby_sound_effect( writer: &mut WriteHalf<'_>, sound_name: &str, ) -> Result<(), ()> { - let 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, - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_NAMED_SOUND_EFFECT, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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. @@ -484,24 +447,21 @@ async fn send_respawn_from_join( writer: &mut WriteHalf<'_>, join_game: JoinGame, ) -> Result<(), ()> { - let 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, - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_RESPAWN, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ())?; - - Ok(()) + 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. @@ -634,29 +594,27 @@ async fn connect_to_server_no_timeout( tmp_client.set_state(ClientState::Login); // Handshake packet - let packet = Handshake { - protocol_version: client_info.protocol_version.unwrap(), - server_addr: config.server.address.ip().to_string(), - server_port: config.server.address.port(), - next_state: ClientState::Login.to_id(), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let request = RawPacket::new(packets::handshake::SERVER_HANDSHAKE, data).encode(&tmp_client)?; - writer.write_all(&request).await.map_err(|_| ())?; + packet::write_packet( + Handshake { + protocol_version: client_info.protocol_version.unwrap(), + server_addr: config.server.address.ip().to_string(), + server_port: config.server.address.port(), + next_state: ClientState::Login.to_id(), + }, + &tmp_client, + &mut writer, + ) + .await?; // Request login start - let packet = LoginStart { - name: client_info.username.ok_or(())?, - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let request = RawPacket::new(packets::login::SERVER_LOGIN_START, data).encode(&tmp_client)?; - writer.write_all(&request).await.map_err(|_| ())?; + packet::write_packet( + LoginStart { + name: client_info.username.ok_or(())?, + }, + &tmp_client, + &mut writer, + ) + .await?; // Incoming buffer let mut buf = BytesMut::new(); diff --git a/src/monitor.rs b/src/monitor.rs index d0d8849..39dd270 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -7,18 +7,17 @@ use std::time::Duration; use bytes::BytesMut; use minecraft_protocol::data::server_status::ServerStatus; use minecraft_protocol::decoder::Decoder; -use minecraft_protocol::encoder::Encoder; use minecraft_protocol::version::v1_14_4::handshake::Handshake; -use minecraft_protocol::version::v1_14_4::status::{PingRequest, PingResponse, StatusResponse}; +use minecraft_protocol::version::v1_14_4::status::{ + PingRequest, PingResponse, StatusRequest, StatusResponse, +}; use rand::Rng; -use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; use tokio::time; use crate::config::Config; use crate::proto::client::{Client, ClientState}; -use crate::proto::packet::{self, RawPacket}; -use crate::proto::packets; +use crate::proto::{packet, packets}; use crate::server::{Server, State}; /// Monitor ping inverval in seconds. @@ -125,45 +124,28 @@ async fn send_handshake( config: &Config, addr: SocketAddr, ) -> Result<(), ()> { - let handshake = Handshake { - protocol_version: config.public.protocol as i32, - server_addr: addr.ip().to_string(), - server_port: addr.port(), - next_state: ClientState::Status.to_id(), - }; - - let mut packet = Vec::new(); - handshake.encode(&mut packet).map_err(|_| ())?; - - let raw = RawPacket::new(packets::handshake::SERVER_HANDSHAKE, packet) - .encode(client) - .map_err(|_| ())?; - stream.write_all(&raw).await.map_err(|_| ())?; - - Ok(()) + packet::write_packet( + Handshake { + protocol_version: config.public.protocol as i32, + server_addr: addr.ip().to_string(), + server_port: addr.port(), + next_state: ClientState::Status.to_id(), + }, + client, + &mut stream.split().1, + ) + .await } /// Send status request. async fn request_status(client: &Client, stream: &mut TcpStream) -> Result<(), ()> { - let raw = RawPacket::new(packets::status::SERVER_STATUS, vec![]) - .encode(client) - .map_err(|_| ())?; - stream.write_all(&raw).await.map_err(|_| ())?; - Ok(()) + packet::write_packet(StatusRequest {}, client, &mut stream.split().1).await } /// Send status request. async fn send_ping(client: &Client, stream: &mut TcpStream) -> Result { let token = rand::thread_rng().gen(); - let ping = PingRequest { time: token }; - - let mut packet = Vec::new(); - ping.encode(&mut packet).map_err(|_| ())?; - - let raw = RawPacket::new(packets::status::SERVER_PING, packet) - .encode(client) - .map_err(|_| ())?; - stream.write_all(&raw).await.map_err(|_| ())?; + packet::write_packet(PingRequest { time: token }, client, &mut stream.split().1).await?; Ok(token) } diff --git a/src/proto/action.rs b/src/proto/action.rs index 3253ac4..9456f72 100644 --- a/src/proto/action.rs +++ b/src/proto/action.rs @@ -1,51 +1,36 @@ use minecraft_protocol::data::chat::{Message, Payload}; -use minecraft_protocol::encoder::Encoder; use minecraft_protocol::version::v1_14_4::game::GameDisconnect; use minecraft_protocol::version::v1_14_4::login::LoginDisconnect; -use tokio::io::AsyncWriteExt; use tokio::net::tcp::WriteHalf; use crate::proto::client::{Client, ClientState}; -use crate::proto::packet::RawPacket; -use crate::proto::packets; +use crate::proto::packet; /// Kick client with a message. /// /// Should close connection afterwards. pub async fn kick(client: &Client, msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> { match client.state() { - ClientState::Login => login_kick(client, msg, writer).await, - ClientState::Play => play_kick(client, msg, writer).await, + ClientState::Login => { + packet::write_packet( + LoginDisconnect { + reason: Message::new(Payload::text(msg)), + }, + client, + writer, + ) + .await + } + ClientState::Play => { + packet::write_packet( + GameDisconnect { + reason: Message::new(Payload::text(msg)), + }, + client, + writer, + ) + .await + } _ => Err(()), } } - -/// Kick client with a message in login state. -/// -/// Should close connection afterwards. -async fn login_kick(client: &Client, msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> { - let packet = LoginDisconnect { - reason: Message::new(Payload::text(msg)), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::login::CLIENT_DISCONNECT, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ()) -} - -/// Kick client with a message in play state. -/// -/// Should close connection afterwards. -async fn play_kick(client: &Client, msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> { - let packet = GameDisconnect { - reason: Message::new(Payload::text(msg)), - }; - - let mut data = Vec::new(); - packet.encode(&mut data).map_err(|_| ())?; - - let response = RawPacket::new(packets::play::CLIENT_DISCONNECT, data).encode(client)?; - writer.write_all(&response).await.map_err(|_| ()) -} diff --git a/src/proto/packet.rs b/src/proto/packet.rs index 7505bd4..ba5b1ad 100644 --- a/src/proto/packet.rs +++ b/src/proto/packet.rs @@ -4,9 +4,11 @@ use bytes::BytesMut; use flate2::read::ZlibDecoder; use flate2::write::ZlibEncoder; use flate2::Compression; +use minecraft_protocol::encoder::Encoder; +use minecraft_protocol::version::PacketId; use tokio::io; -use tokio::io::AsyncReadExt; -use tokio::net::tcp::ReadHalf; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::tcp::{ReadHalf, WriteHalf}; use crate::proto::client::Client; use crate::proto::BUF_SIZE; @@ -197,3 +199,18 @@ pub async fn read_packet( Ok(Some((packet, raw.to_vec()))) } + +/// Write packet to stream writer. +pub async fn write_packet( + packet: impl PacketId + Encoder, + client: &Client, + writer: &mut WriteHalf<'_>, +) -> Result<(), ()> { + let mut data = Vec::new(); + packet.encode(&mut data).map_err(|_| ())?; + + let response = RawPacket::new(packet.packet_id(), data).encode(client)?; + writer.write_all(&response).await.map_err(|_| ())?; + + Ok(()) +}