diff --git a/protocol/src/version/mod.rs b/protocol/src/version/mod.rs
index 68404a9..d3dc8ac 100644
--- a/protocol/src/version/mod.rs
+++ b/protocol/src/version/mod.rs
@@ -1,2 +1,19 @@
 pub mod v1_14_4;
 pub mod v1_17_1;
+
+/// Trait to obtain packet ID from packet data.
+pub trait PacketId {
+    /// Get protcol packet ID.
+    fn packet_id(&self) -> u8;
+}
+
+#[macro_export]
+macro_rules! trait_packet_id (
+    ($type: ident, $id: expr) => (
+        impl PacketId for $type {
+            fn packet_id(&self) -> u8 {
+                $id
+            }
+        }
+    )
+);
diff --git a/protocol/src/version/v1_14_4/game.rs b/protocol/src/version/v1_14_4/game.rs
index 4400831..1ebf584 100644
--- a/protocol/src/version/v1_14_4/game.rs
+++ b/protocol/src/version/v1_14_4/game.rs
@@ -3,6 +3,7 @@ use crate::decoder::Decoder;
 use crate::decoder::DecoderReadExt;
 use crate::encoder::EncoderWriteExt;
 use crate::error::DecodeError;
+use crate::{trait_packet_id, version::PacketId};
 use byteorder::{ReadBytesExt, WriteBytesExt};
 use minecraft_protocol_derive::{Decoder, Encoder};
 use nbt::CompoundTag;
@@ -732,3 +733,15 @@ mod tests {
         assert!(abilities.creative_mode);
     }
 }
+
+trait_packet_id!(ServerBoundChatMessage, 0x03);
+trait_packet_id!(ServerBoundKeepAlive, 0x0F);
+trait_packet_id!(ServerBoundAbilities, 0x19);
+
+trait_packet_id!(ClientBoundChatMessage, 0x0E);
+trait_packet_id!(GameDisconnect, 0x1A);
+trait_packet_id!(ClientBoundKeepAlive, 0x20);
+trait_packet_id!(ChunkData, 0x21);
+trait_packet_id!(JoinGame, 0x25);
+trait_packet_id!(BossBar, 0x0D);
+trait_packet_id!(EntityAction, 0x1B);
diff --git a/protocol/src/version/v1_14_4/handshake.rs b/protocol/src/version/v1_14_4/handshake.rs
index f678043..557987b 100644
--- a/protocol/src/version/v1_14_4/handshake.rs
+++ b/protocol/src/version/v1_14_4/handshake.rs
@@ -1,5 +1,6 @@
 use crate::decoder::Decoder;
 use crate::error::DecodeError;
+use crate::{trait_packet_id, version::PacketId};
 use minecraft_protocol_derive::{Decoder, Encoder};
 use std::io::Read;
 
@@ -53,3 +54,5 @@ impl Handshake {
         HandshakeServerBoundPacket::Handshake(handshake)
     }
 }
+
+trait_packet_id!(Handshake, 0x00);
diff --git a/protocol/src/version/v1_14_4/login.rs b/protocol/src/version/v1_14_4/login.rs
index 14248a3..84afb8f 100644
--- a/protocol/src/version/v1_14_4/login.rs
+++ b/protocol/src/version/v1_14_4/login.rs
@@ -4,6 +4,7 @@ use uuid::Uuid;
 use crate::data::chat::Message;
 use crate::decoder::Decoder;
 use crate::error::DecodeError;
+use crate::{trait_packet_id, version::PacketId};
 use minecraft_protocol_derive::{Decoder, Encoder};
 
 pub enum LoginServerBoundPacket {
@@ -480,3 +481,13 @@ mod tests {
         );
     }
 }
+
+trait_packet_id!(LoginStart, 0x00);
+trait_packet_id!(EncryptionResponse, 0x01);
+trait_packet_id!(LoginPluginResponse, 0x02);
+
+trait_packet_id!(LoginDisconnect, 0x00);
+trait_packet_id!(EncryptionRequest, 0x01);
+trait_packet_id!(LoginSuccess, 0x02);
+trait_packet_id!(SetCompression, 0x03);
+trait_packet_id!(LoginPluginRequest, 0x04);
diff --git a/protocol/src/version/v1_14_4/status.rs b/protocol/src/version/v1_14_4/status.rs
index 57c281a..2c5cfb2 100644
--- a/protocol/src/version/v1_14_4/status.rs
+++ b/protocol/src/version/v1_14_4/status.rs
@@ -1,11 +1,12 @@
 use crate::data::server_status::*;
 use crate::decoder::Decoder;
 use crate::error::DecodeError;
+use crate::{trait_packet_id, version::PacketId};
 use minecraft_protocol_derive::{Decoder, Encoder};
 use std::io::Read;
 
 pub enum StatusServerBoundPacket {
-    StatusRequest,
+    StatusRequest(StatusRequest),
     PingRequest(PingRequest),
 }
 
@@ -17,14 +18,14 @@ pub enum StatusClientBoundPacket {
 impl StatusServerBoundPacket {
     pub fn get_type_id(&self) -> u8 {
         match self {
-            StatusServerBoundPacket::StatusRequest => 0x00,
+            StatusServerBoundPacket::StatusRequest(_) => 0x00,
             StatusServerBoundPacket::PingRequest(_) => 0x01,
         }
     }
 
     pub fn decode<R: Read>(type_id: u8, reader: &mut R) -> Result<Self, DecodeError> {
         match type_id {
-            0x00 => Ok(StatusServerBoundPacket::StatusRequest),
+            0x00 => Ok(StatusServerBoundPacket::StatusRequest(StatusRequest {})),
             0x01 => {
                 let ping_request = PingRequest::decode(reader)?;
 
@@ -70,6 +71,15 @@ impl PingResponse {
     }
 }
 
+#[derive(Encoder, Decoder, Debug)]
+pub struct StatusRequest {}
+
+impl StatusRequest {
+    pub fn new() -> StatusServerBoundPacket {
+        StatusServerBoundPacket::StatusRequest(StatusRequest {})
+    }
+}
+
 #[derive(Encoder, Decoder, Debug)]
 pub struct StatusResponse {
     pub server_status: ServerStatus,
@@ -198,3 +208,9 @@ mod tests {
         );
     }
 }
+
+trait_packet_id!(StatusRequest, 0x00);
+trait_packet_id!(PingRequest, 0x01);
+
+trait_packet_id!(StatusResponse, 0x00);
+trait_packet_id!(PingResponse, 0x01);
diff --git a/protocol/src/version/v1_17_1/game.rs b/protocol/src/version/v1_17_1/game.rs
index 7543160..e052c37 100644
--- a/protocol/src/version/v1_17_1/game.rs
+++ b/protocol/src/version/v1_17_1/game.rs
@@ -1,6 +1,7 @@
 use crate::data::chat::Message;
 use crate::decoder::Decoder;
 use crate::error::DecodeError;
+use crate::{trait_packet_id, version::PacketId};
 use minecraft_protocol_derive::Decoder;
 use minecraft_protocol_derive::Encoder;
 use nbt::CompoundTag;
@@ -14,7 +15,7 @@ pub use super::super::v1_14_4::game::{
 
 pub enum GameServerBoundPacket {
     ServerBoundChatMessage(ServerBoundChatMessage),
-    PluginMessage(PluginMessage),
+    ServerBoundPluginMessage(ServerBoundPluginMessage),
     ServerBoundKeepAlive(ServerBoundKeepAlive),
     ServerBoundAbilities(ServerBoundAbilities),
 }
@@ -28,7 +29,7 @@ pub enum GameClientBoundPacket {
     BossBar(BossBar),
     EntityAction(EntityAction),
 
-    PluginMessage(PluginMessage),
+    ClientBoundPluginMessage(ClientBoundPluginMessage),
     NamedSoundEffect(NamedSoundEffect),
     Respawn(Respawn),
     PlayerPositionAndLook(PlayerPositionAndLook),
@@ -43,7 +44,7 @@ impl GameServerBoundPacket {
     pub fn get_type_id(&self) -> u8 {
         match self {
             GameServerBoundPacket::ServerBoundChatMessage(_) => 0x03,
-            GameServerBoundPacket::PluginMessage(_) => 0x0A,
+            GameServerBoundPacket::ServerBoundPluginMessage(_) => 0x0A,
             GameServerBoundPacket::ServerBoundKeepAlive(_) => 0x0F,
             GameServerBoundPacket::ServerBoundAbilities(_) => 0x19,
         }
@@ -57,9 +58,11 @@ impl GameServerBoundPacket {
                 Ok(GameServerBoundPacket::ServerBoundChatMessage(chat_message))
             }
             0x0A => {
-                let plugin_message = PluginMessage::decode(reader)?;
+                let plugin_message = ServerBoundPluginMessage::decode(reader)?;
 
-                Ok(GameServerBoundPacket::PluginMessage(plugin_message))
+                Ok(GameServerBoundPacket::ServerBoundPluginMessage(
+                    plugin_message,
+                ))
             }
             0x0F => {
                 let keep_alive = ServerBoundKeepAlive::decode(reader)?;
@@ -80,7 +83,7 @@ impl GameClientBoundPacket {
     pub fn get_type_id(&self) -> u8 {
         match self {
             GameClientBoundPacket::ClientBoundChatMessage(_) => 0x0E,
-            GameClientBoundPacket::PluginMessage(_) => 0x18,
+            GameClientBoundPacket::ClientBoundPluginMessage(_) => 0x18,
             GameClientBoundPacket::NamedSoundEffect(_) => 0x19,
             GameClientBoundPacket::GameDisconnect(_) => 0x1A,
             GameClientBoundPacket::ClientBoundKeepAlive(_) => 0x20,
@@ -106,9 +109,11 @@ impl GameClientBoundPacket {
                 Ok(GameClientBoundPacket::ClientBoundChatMessage(chat_message))
             }
             0x18 => {
-                let plugin_message = PluginMessage::decode(reader)?;
+                let plugin_message = ClientBoundPluginMessage::decode(reader)?;
 
-                Ok(GameClientBoundPacket::PluginMessage(plugin_message))
+                Ok(GameClientBoundPacket::ClientBoundPluginMessage(
+                    plugin_message,
+                ))
             }
             0x19 => {
                 let named_sound_effect = NamedSoundEffect::decode(reader)?;
@@ -179,7 +184,15 @@ impl GameClientBoundPacket {
 
 // TODO(timvisee): implement new()
 #[derive(Encoder, Decoder, Debug)]
-pub struct PluginMessage {
+pub struct ServerBoundPluginMessage {
+    #[data_type(max_length = 32767)]
+    pub channel: String,
+    pub data: Vec<u8>,
+}
+
+// TODO(timvisee): implement new()
+#[derive(Encoder, Decoder, Debug)]
+pub struct ClientBoundPluginMessage {
     #[data_type(max_length = 32767)]
     pub channel: String,
     pub data: Vec<u8>,
@@ -285,3 +298,16 @@ pub struct SpawnPosition {
     pub position: u64,
     pub angle: f32,
 }
+
+trait_packet_id!(ServerBoundPluginMessage, 0x0A);
+
+trait_packet_id!(ClientBoundPluginMessage, 0x18);
+trait_packet_id!(NamedSoundEffect, 0x19);
+trait_packet_id!(JoinGame, 0x25);
+trait_packet_id!(PlayerPositionAndLook, 0x38);
+trait_packet_id!(Respawn, 0x3D);
+trait_packet_id!(SpawnPosition, 0x4B);
+trait_packet_id!(SetTitleSubtitle, 0x57);
+trait_packet_id!(TimeUpdate, 0x58);
+trait_packet_id!(SetTitleText, 0x59);
+trait_packet_id!(SetTitleTimes, 0x5A);