gbnet-hs-0.1.0.0: src/GBNet/Packet.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : GBNet.Packet
-- Description : Packet types and header definitions for the wire protocol
--
-- Defines 'PacketType' and 'PacketHeader' for the gbnet wire format.
-- Header is 68 bits: 4-bit type + 16-bit sequence + 16-bit ack + 32-bit ack bitfield.
module GBNet.Packet
( -- * Types
PacketType (..),
PacketHeader (..),
Packet (..),
-- * Constants
packetHeaderBitSize,
packetHeaderByteSize,
-- * Serialization
serializePacket,
deserializePacket,
serializeHeader,
deserializeHeader,
)
where
import Control.DeepSeq (NFData (..), rwhnf)
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import qualified Data.ByteString as BS
import Data.ByteString.Internal (unsafeCreate)
import qualified Data.ByteString.Unsafe as BSU
import Data.Word (Word16, Word32, Word8)
import Foreign.Storable (pokeByteOff)
import GBNet.Types (SequenceNum (..))
import Optics.TH (makeFieldLabelsNoPrefix)
-- | Header size in bits (4 + 16 + 16 + 32 = 68).
packetHeaderBitSize :: Int
packetHeaderBitSize = 68
-- | Packet type tag (4 bits on wire).
-- Additional fields are serialized after the header.
data PacketType
= -- | 0: Client initiates connection
ConnectionRequest
| -- | 1: Server accepts
ConnectionAccepted
| -- | 2: Server rejects (+ 8-bit reason in payload)
ConnectionDenied
| -- | 3: Normal game data (+ 3-bit channel, 1-bit is_fragment in payload)
Payload
| -- | 4: Graceful disconnect (+ 8-bit reason in payload)
Disconnect
| -- | 5: Keep connection alive
Keepalive
| -- | 6: Server challenge (+ 64-bit server_salt in payload)
ConnectionChallenge
| -- | 7: Client response (+ 64-bit client_salt in payload)
ConnectionResponse
deriving (Eq, Show, Enum, Bounded)
instance NFData PacketType where rnf = rwhnf
-- | Packet header (68 bits on wire).
data PacketHeader = PacketHeader
{ -- | 4 bits
packetType :: !PacketType,
-- | 16 bits
sequenceNum :: !SequenceNum,
-- | 16 bits - most recent received sequence
ack :: !SequenceNum,
-- | 32 bits - preceding 32 acks
ackBitfield :: !Word32
}
deriving (Eq, Show)
instance NFData PacketHeader where
rnf (PacketHeader pt sn ak abf) = rnf pt `seq` rnf sn `seq` rnf ak `seq` rnf abf
-- | Header size in bytes (68 bits = 9 bytes, rounded up).
packetHeaderByteSize :: Int
packetHeaderByteSize = (packetHeaderBitSize + 7) `div` 8
-- | A complete packet with header and payload.
data Packet = Packet
{ pktHeader :: !PacketHeader,
pktPayload :: !BS.ByteString
}
deriving (Eq, Show)
makeFieldLabelsNoPrefix ''PacketHeader
makeFieldLabelsNoPrefix ''Packet
-- | Bit shift for packet type in first byte.
packetTypeBitShift :: Int
packetTypeBitShift = 4
-- | Serialize a packet header to bytes.
-- Uses optimized direct memory writes.
--
-- Wire format (68 bits, MSB-first):
-- Byte 0: [type:4][seq_hi:4]
-- Byte 1: [seq_mid:8]
-- Byte 2: [seq_lo:4][ack_hi:4]
-- Byte 3: [ack_mid:8]
-- Byte 4: [ack_lo:4][abf_hi:4]
-- Bytes 5-7: [abf:24]
-- Byte 8: [abf_lo:4][pad:4]
serializeHeader :: PacketHeader -> BS.ByteString
serializeHeader !hdr = unsafeCreate packetHeaderByteSize $ \ptr -> do
let !pt = fromIntegral (fromEnum (packetType hdr)) :: Word8
!(SequenceNum !sn) = sequenceNum hdr
!(SequenceNum !ak) = ack hdr
!abf = ackBitfield hdr
!byte0 = (pt `shiftL` packetTypeBitShift) .|. fromIntegral (sn `shiftR` 12)
!byte1 = fromIntegral (sn `shiftR` 4) :: Word8
!byte2 = ((fromIntegral (sn .&. 0x0F) :: Word8) `shiftL` 4) .|. fromIntegral (ak `shiftR` 12)
!byte3 = fromIntegral (ak `shiftR` 4) :: Word8
!byte4 = ((fromIntegral (ak .&. 0x0F) :: Word8) `shiftL` 4) .|. fromIntegral (abf `shiftR` 28)
!byte5 = fromIntegral (abf `shiftR` 20) :: Word8
!byte6 = fromIntegral (abf `shiftR` 12) :: Word8
!byte7 = fromIntegral (abf `shiftR` 4) :: Word8
!byte8 = fromIntegral (abf .&. 0x0F) `shiftL` 4 :: Word8
pokeByteOff ptr 0 byte0
pokeByteOff ptr 1 byte1
pokeByteOff ptr 2 byte2
pokeByteOff ptr 3 byte3
pokeByteOff ptr 4 byte4
pokeByteOff ptr 5 byte5
pokeByteOff ptr 6 byte6
pokeByteOff ptr 7 byte7
pokeByteOff ptr 8 byte8
{-# INLINE serializeHeader #-}
-- | Deserialize a packet header from bytes.
-- Uses optimized direct memory access.
deserializeHeader :: BS.ByteString -> Either String PacketHeader
deserializeHeader !bs
| BS.length bs < packetHeaderByteSize = Left "Header too short"
| otherwise =
let !b0 = BSU.unsafeIndex bs 0
!b1 = BSU.unsafeIndex bs 1
!b2 = BSU.unsafeIndex bs 2
!b3 = BSU.unsafeIndex bs 3
!b4 = BSU.unsafeIndex bs 4
!b5 = BSU.unsafeIndex bs 5
!b6 = BSU.unsafeIndex bs 6
!b7 = BSU.unsafeIndex bs 7
!b8 = BSU.unsafeIndex bs 8
!ptVal = fromIntegral (b0 `shiftR` packetTypeBitShift) :: Int
!snHi = fromIntegral (b0 .&. 0x0F) :: Word16
!snMid = fromIntegral b1 :: Word16
!snLo = fromIntegral (b2 `shiftR` 4) :: Word16
!sn = (snHi `shiftL` 12) .|. (snMid `shiftL` 4) .|. snLo
!akHi = fromIntegral (b2 .&. 0x0F) :: Word16
!akMid = fromIntegral b3 :: Word16
!akLo = fromIntegral (b4 `shiftR` 4) :: Word16
!ak = (akHi `shiftL` 12) .|. (akMid `shiftL` 4) .|. akLo
!abf0 = fromIntegral (b4 .&. 0x0F) :: Word32
!abf1 = fromIntegral b5 :: Word32
!abf2 = fromIntegral b6 :: Word32
!abf3 = fromIntegral b7 :: Word32
!abf4 = fromIntegral (b8 `shiftR` 4) :: Word32
!abf = (abf0 `shiftL` 28) .|. (abf1 `shiftL` 20) .|. (abf2 `shiftL` 12) .|. (abf3 `shiftL` 4) .|. abf4
in if ptVal > fromEnum (maxBound :: PacketType)
then Left $ "Invalid packet type: " ++ show ptVal
else
Right
PacketHeader
{ packetType = toEnum ptVal,
sequenceNum = SequenceNum sn,
ack = SequenceNum ak,
ackBitfield = abf
}
{-# INLINE deserializeHeader #-}
-- | Serialize a complete packet (header + payload) to bytes.
serializePacket :: Packet -> BS.ByteString
serializePacket pkt =
let headerBytes = serializeHeader (pktHeader pkt)
in headerBytes <> pktPayload pkt
-- | Deserialize a complete packet from bytes.
deserializePacket :: BS.ByteString -> Either String Packet
deserializePacket bs = do
header <- deserializeHeader bs
let payload = BS.drop packetHeaderByteSize bs
Right Packet {pktHeader = header, pktPayload = payload}