alpaca-netcode-0.1.0.0: src/Alpaca/NetCode/Internal/Server.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
-- | Rollback and replay based game networking
module Alpaca.NetCode.Internal.Server
( runServerWith'
, ServerConfig (..)
, defaultServerConfig
) where
import Control.Applicative
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Concurrent.STM as STM
import Control.Monad (forM_, forever, join, when, forM)
import Data.Coerce (coerce)
import Data.IntMap (IntMap)
import qualified Data.IntMap as IM
import Data.List (dropWhileEnd, foldl')
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
import Data.Time (getCurrentTime)
import Flat
import Prelude
import Alpaca.NetCode.Internal.Common
-- | Configuration options specific to the server.
data ServerConfig = ServerConfig
{
-- | Tick rate (ticks per second). Typically @30@ or @60@. Must be the same
-- across all clients and the server. Packet rate and hence network bandwidth
-- will scale linearly with this the tick rate.
scTickRate :: Int
-- | Seconds of not receiving packets from a client before disconnecting that
-- client.
, scClientTimeout :: Float
} deriving (Show, Read, Eq, Ord)
-- | Sensible defaults for @ServerConfig@ based on the tick rate.
defaultServerConfig ::
-- | Tick rate (ticks per second). Typically @30@ or @60@. Must be the same
-- across all clients and the server. Packet rate and hence network bandwidth
-- will scale linearly with this the tick rate.
Int
-> ServerConfig
defaultServerConfig tickRate = ServerConfig
{ scTickRate = tickRate
, scClientTimeout = 5
}
-- | Run a server for a single game. This will block until the game ends,
-- specifically when all players have disconnected.
runServerWith' ::
forall input clientAddress.
( Eq input
, Flat input
, Show clientAddress
, Ord clientAddress
) =>
-- | Function to send messages to clients. The underlying communication
-- protocol need only guarantee data integrity but is otherwise free to drop
-- and reorder packets. Typically this is backed by a UDP socket.
(NetMsg input -> clientAddress -> IO ()) ->
-- | Blocking function to receive messages from the clients. Has the same
-- reliability requirements as the send function.
(IO (NetMsg input, clientAddress)) ->
-- | Optional simulation of network conditions. In production this should be
-- `Nothing`. May differ between clients.
Maybe SimNetConditions ->
-- | The 'defaultServerConfig' works well for most cases.
ServerConfig ->
-- | Initial input for new players. Must be the same across all clients and
-- the server. See 'Alpaca.NetCode.runClient'.
input ->
IO ()
runServerWith' sendToClient' recvFromClient' simNetConditionsMay serverConfig input0 = playCommon (scTickRate serverConfig) $ \tickTime getTime resetTime -> forever $ do
(sendToClient'', recvFromClient) <- simulateNetConditions
(uncurry sendToClient')
recvFromClient'
simNetConditionsMay
let sendToClient = curry sendToClient''
debugStrLn "Waiting for clients"
-- Authoritative Map from tick and PlayerId to inputs. The inner map is
-- always complete (e.g. if we have the IntMap for tick i, then it contains
-- the inputs for *all* known players)
authInputsTVar :: TVar (IntMap (M.Map PlayerId input)) <- newTVarIO (IM.singleton 0 M.empty)
-- The next Tick i.e. the first non-frozen tick. All ticks before this
-- one have been frozen (w.r.t authInputsTVar).
nextTickTVar :: TVar Tick <- newTVarIO 1
-- Known players as of now. Nothing means the host (me).
playersTVar :: TVar (M.Map clientAddress PlayerData) <- newTVarIO M.empty
-- Known Players (
-- , last time for which a message was received
-- )
-- Next available PlayerId
nextPlayerIdTVar :: TVar PlayerId <- newTVarIO 0
-- As the host we're authoritative and always simulating significantly
-- behind clients. This allows for ample time to receive inputs even
-- with large ping and jitter. Although the authoritative simulation is
-- significantly behind clients, we send input hints eagerly, and that
-- allows clients to make accurate predictions and hence they don't
-- perceive the lag in authoritative inputs.
-- Main message processing loop
msgProcessingTID <- forkIO $
forever $ do
(msg, sender) <- recvFromClient
-- Handle the message
serverReceiveTimeMay <- case msg of
Msg_Connected{} -> do
debugStrLn $ "Server received unexpected Msg_Connected from " ++ show sender ++ ". Ignoring."
return Nothing
Msg_AuthInput{} -> do
debugStrLn $ "Server received unexpected Msg_AuthInput from " ++ show sender ++ ". Ignoring."
return Nothing
Msg_HeartbeatResponse{} -> do
debugStrLn $ "Server received unexpected Msg_HeartbeatResponse from " ++ show sender ++ ". Ignoring."
return Nothing
Msg_HintInput{} -> do
debugStrLn $ "Server received unexpected Msg_HintInput from " ++ show sender ++ ". Perhaps you meant to send a Msg_SubmitInput. Ignoring."
return Nothing
Msg_Connect clientSendTime -> do
-- new client connection
currentTimeUTC <- getCurrentTime
currentTime <- getTime
join $
atomically $ do
playerMay <- M.lookup sender <$> readTVar playersTVar
(pid, debugMsg, serverReceiveTime) <- case playerMay of
Nothing -> do
-- New player
pid <- readTVar nextPlayerIdTVar
writeTVar nextPlayerIdTVar (pid + 1)
players <- readTVar playersTVar
let isFirstConnection = M.null players
-- We only start the game on first connection, so must reset the timer
serverReceiveTime <-
if isFirstConnection
then do
resetTime currentTimeUTC
return 0
else return currentTime
writeTVar playersTVar (M.insert sender (PlayerData{playerId = pid, maxAuthTick = 0, lastMesgRcvTime = serverReceiveTime}) players)
return (pid, Just ("Connected " ++ show sender ++ " as " ++ show pid), serverReceiveTime)
Just PlayerData{..} -> do
-- Existing player
return (playerId, Nothing, currentTime)
return $ do
sendToClient (Msg_Connected pid) sender
sendToClient (Msg_HeartbeatResponse clientSendTime serverReceiveTime) sender
mapM_ debugStrLn debugMsg
return (Just serverReceiveTime)
Msg_Heartbeat clientSendTime -> do
serverReceiveTime <- getTime
isConnected <- atomically (isJust . M.lookup sender <$> readTVar playersTVar)
when isConnected $ sendToClient (Msg_HeartbeatResponse clientSendTime serverReceiveTime) sender
return (Just serverReceiveTime)
Msg_Ack clientMaxAuthTick -> do
atomically $ modifyTVar playersTVar (M.update (\pd -> Just $ pd{maxAuthTick = clientMaxAuthTick}) sender)
Just <$> getTime
Msg_SubmitInput submittedInputs -> do
msgMay <- atomically $ do
-- Check that the sender is connected.
playerMay <- M.lookup sender <$> readTVar playersTVar
case playerMay of
Nothing -> return [Just $ "Got Msg_SubmitInput from client that is not yet connected " ++ show sender]
Just PlayerData{..} -> forM submittedInputs $ \(tick, input) -> do
-- Check that the tick time has not already been simulated.
nextTick <- readTVar nextTickTVar
-- TODO upper bound on allowed tick time.
if tick < nextTick
then
return $
Just $
"Late Msg_Input from " ++ show playerId
++ " for "
++ show tick
++ " but already simulated up to "
++ show (nextTick - 1)
++ ". Ignoring."
else do
inputs <- readTVar authInputsTVar
let inptsAtTick = fromMaybe M.empty (inputs IM.!? fromIntegral tick)
case inptsAtTick M.!? playerId of
Just existingInput
-- Duplicate message. Silently ignore
| existingInput == input -> return Nothing
-- Different input for the same tick!
| otherwise ->
return $
Just $
"Received inputs from " ++ show playerId ++ " for " ++ show tick
++ " but already have inputs for that time with a DIFFERENT value! Ignoring."
-- First time we're hearing of this input. Store it.
Nothing -> do
writeTVar authInputsTVar $
IM.insert
(fromIntegral tick)
(M.insert playerId input inptsAtTick)
inputs
return Nothing
mapM_ debugStrLn (catMaybes msgMay)
Just <$> getTime
-- set receive time for players
forM_ serverReceiveTimeMay $ \serverReceiveTime ->
atomically $
modifyTVar
playersTVar
( M.update
(\player -> Just player{lastMesgRcvTime = serverReceiveTime})
sender
)
-- Wait for a connection
atomically $ do
players <- readTVar playersTVar
STM.check $ not $ M.null players
debugStrLn "Client connected. Starting game."
-- Disconnect players after a timeout
disconnectTID <- forkIO $
forever $ do
-- Find next possilbe time to disconnect a player
oldestMsgRcvTime <- atomically (minimum . fmap lastMesgRcvTime . M.elems <$> readTVar playersTVar)
let disconnectTime = oldestMsgRcvTime + scClientTimeout serverConfig
-- Wait till the disconnect time (plus a bit to really make sure we pass the threshold)
t <- getTime
when (t < disconnectTime) $
threadDelay (round (((disconnectTime - t) + 0.01) * 1000000))
-- Kick players as needed
currentTime <- getTime
kickedPlayers <- atomically $ do
players <- readTVar playersTVar
let (retainedPlayers, kickedPlayers) = M.partition (\PlayerData{..} -> lastMesgRcvTime + scClientTimeout serverConfig > currentTime) players
writeTVar playersTVar retainedPlayers
return kickedPlayers
when (not (M.null kickedPlayers)) $ debugStrLn $ "Disconnect players due to timeout: " ++ show [pid | PlayerData{playerId = PlayerId pid} <- M.elems kickedPlayers]
-- Main "simulation" loop
simTID <- forkIO $
forever $ do
-- Calculate target tick according to current time
currTime <- getTime
let targetTick = floor $ currTime / tickTime
-- Fill auth inputs
atomically $ do
nextAuthTick <- readTVar nextTickTVar
-- Freeze ticks.
writeTVar nextTickTVar (targetTick + 1)
-- Advance auth inputs up to target tick.
knownPlayers <- readTVar playersTVar
authInputs <- readTVar authInputsTVar
let nextAuthTickInputs = authInputs IM.! fromIntegral (nextAuthTick - 1)
writeTVar authInputsTVar $
fst $
foldl'
( \(authInputs', prevInputs) currTick ->
let -- Fill inputs for the current tick.
currInputsRaw = fromMaybe M.empty (IM.lookup (fromIntegral currTick) authInputs)
currInputs =
M.fromList
[ ( pidInt
, fromMaybe
input0
( currInputsRaw M.!? pid
<|> prevInputs M.!? pid
)
)
| pid <- playerId <$> M.elems knownPlayers
, let pidInt = coerce pid
]
in (IM.insert (fromIntegral currTick) currInputs authInputs', currInputs)
)
(authInputs, nextAuthTickInputs)
[nextAuthTick .. targetTick]
-- broadcast some auth inputs
knownPlayers <- atomically $ readTVar playersTVar
(authInputs, nextAuthTick) <- atomically $ do
authInputs <- readTVar authInputsTVar
nextAuthTick <- readTVar nextTickTVar
return (authInputs, nextAuthTick)
forM_ (M.assocs knownPlayers) $ \(sock, playerData) -> do
let lastAuthTick = maxAuthTick playerData
(_, _, inputsToSendIntMap') = IM.splitLookup (fromIntegral lastAuthTick) authInputs
(inputsToSendIntMap, firstHint, _) = IM.splitLookup (fromIntegral nextAuthTick) inputsToSendIntMap'
inputsToSend = take maxRequestAuthInputs $ IM.elems inputsToSendIntMap
hintsToSendCount = maxRequestAuthInputs - IM.size inputsToSendIntMap
hintsToSend =
fmap (fromMaybe M.empty) $
dropWhileEnd isNothing $
take hintsToSendCount $
firstHint :
[ authInputs IM.!? fromIntegral hintTick
| hintTick <- [succ nextAuthTick ..]
]
when (not $ null inputsToSend) $
sendToClient
( Msg_AuthInput
(lastAuthTick + 1)
(toCompactMaps inputsToSend)
(toCompactMaps hintsToSend)
)
sock
-- Sleep thread till the next tick.
currTime' <- getTime
let nextTick = targetTick + 1
nextTickTime = fromIntegral nextTick * tickTime
timeTillNextTick = nextTickTime - currTime'
threadDelay $ round $ 1000000 * timeTillNextTick
-- Wait till all players quit
atomically $ do
players <- readTVar playersTVar
STM.check $ M.null players
debugStrLn "No more clients, Stopping game!"
mapM_ killThread [msgProcessingTID, disconnectTID, simTID]
-- | Per player info stored by the server
data PlayerData = PlayerData
{ -- | last tick for which auth inputs were sent from the server
playerId :: PlayerId
, -- | Client's max known auth inputs tick such that there are no missing
-- ticks before it.
maxAuthTick :: Tick
, -- | Last server time at which a message was received from this player.
lastMesgRcvTime :: Float
}