From 47fe7d0387e6612bb5ca744a297bff86e02f67f0 Mon Sep 17 00:00:00 2001
From: timvisee <tim@visee.me>
Date: Tue, 16 Nov 2021 17:57:34 +0100
Subject: [PATCH] Extract all packet writing logic to single function

---
 Cargo.lock          |   4 +-
 Cargo.toml          |   2 +-
 src/lobby.rs        | 394 ++++++++++++++++++++------------------------
 src/monitor.rs      |  52 ++----
 src/proto/action.rs |  57 +++----
 src/proto/packet.rs |  21 ++-
 6 files changed, 236 insertions(+), 294 deletions(-)

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::<Vec<_>>().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<u64, ()> {
     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(())
+}