packages feed

gore-and-ash-sync-1.2.0.1: src/Game/GoreAndAsh/Sync/Module.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Module      : Game.GoreAndAsh.Sync.Module
Description : Monad transformer and game module instance
Copyright   : (c) Anton Gushcha, 2015-2016
License     : BSD3
Maintainer  : ncrashed@gmail.com
Stability   : experimental
Portability : POSIX
-}
module Game.GoreAndAsh.Sync.Module(
    SyncT(..)
  , registerSyncIdInternal
  , addSyncTypeRepInternal
  , syncRequestIdInternal
  , getServiceChannel
  , syncLog
  ) where

import Control.Monad.Catch
import Control.Monad.Fix
import Control.Monad.State.Strict
import Data.Maybe
import Data.Monoid
import Data.Proxy
import Data.Serialize
import Data.Text (Text, pack)
import Data.Word
import qualified Data.ByteString as BS
import qualified Data.HashMap.Strict as H
import qualified Data.Sequence as S

import Game.GoreAndAsh
import Game.GoreAndAsh.Actor
import Game.GoreAndAsh.Actor.TypeRep
import Game.GoreAndAsh.Logging
import Game.GoreAndAsh.Network
import Game.GoreAndAsh.Sync.State

-- | Monad transformer of sync core module.
--
-- [@s@] - State of next core module in modules chain;
--
-- [@m@] - Next monad in modules monad stack;
--
-- [@a@] - Type of result value;
--
-- How to embed module:
--
-- @
-- type AppStack = ModuleStack [LoggingT, NetworkT, ActorT, SyncT, ... other modules ... ] IO
--
-- -- | Current GHC (7.10.3) isn't able to derive this
-- instance SyncMonad AppMonad where
--   getSyncIdM = AppMonad . getSyncIdM
--   getSyncTypeRepM = AppMonad . getSyncTypeRepM
--   registerSyncIdM = AppMonad . registerSyncIdM
--   addSyncTypeRepM a b = AppMonad $ addSyncTypeRepM a b
--   syncScheduleMessageM peer ch i mt msg  = AppMonad $ syncScheduleMessageM peer ch i mt msg
--   syncSetLoggingM = AppMonad . syncSetLoggingM
--   syncSetRoleM = AppMonad . syncSetRoleM
--   syncGetRoleM = AppMonad syncGetRoleM
--   syncRequestIdM a b = AppMonad $ syncRequestIdM a b
--
-- newtype AppMonad a = AppMonad (AppStack a)
--   deriving (Functor, Applicative, Monad, MonadFix, MonadIO, LoggingMonad, MonadThrow, MonadCatch, NetworkMonad, ActorMonad)
-- @
--
-- The module is NOT pure within first phase (see 'ModuleStack' docs), therefore currently only 'IO' end monad can handler the module.
newtype SyncT s m a = SyncT { runSyncT :: StateT (SyncState s) m a }
  deriving (Functor, Applicative, Monad, MonadState (SyncState s), MonadFix, MonadTrans, MonadIO, MonadThrow, MonadCatch, MonadMask)

instance (NetworkMonad m, LoggingMonad m, ActorMonad m, GameModule m s) => GameModule (SyncT s m) (SyncState s) where
  type ModuleState (SyncT s m) = SyncState s
  runModule (SyncT m) s = do
    ((a, s'), nextState) <- runModule runCurrentModule (syncNextState s)
    return (a, s' {
        syncNextState = nextState
      })
    where
    runCurrentModule = do
      (a, s') <- runStateT m s
      s'' <- processServiceMessages s'
      return (a, s'')

  newModuleState = emptySyncState <$> newModuleState
  withModule _ = withModule (Proxy :: Proxy m)
  cleanupModule _ = return ()

-- | Detect service messages arrived to the machine and process them
--
-- Note: service channel had id 1 by default, but if there is no such
-- channel, it fallbacks to 0. Make shure that client and server has corresponding
-- count of channels.
processServiceMessages :: forall m s . (ActorMonad m, NetworkMonad m, LoggingMonad m)
  => SyncState s -> m (SyncState s)
processServiceMessages sstate = do
  serviceChan <- getServiceChannel
  peers <- networkPeersM
  foldM (process serviceChan) sstate peers
  where
  -- | Process one peer
  process :: ChannelID -> SyncState s -> Peer -> m (SyncState s)
  process serviceChan s peer = do
    bss <- peerMessagesM peer serviceChan
    let serviceMsgs = catMaybesSeq . fmap decodeService $ bss
    foldM (processService serviceChan peer) s serviceMsgs

  -- | Decode service message
  decodeService bs = case decode bs of
    Left _ -> Nothing
    Right (w64 :: Word64, mbs :: BS.ByteString) -> if w64 == 0
      then case decode mbs of
        Left _ -> Nothing
        Right !msg -> Just msg
      else Nothing

  -- | Service one service message for given peer
  processService :: ChannelID -> Peer -> SyncState s -> SyncServiceMsg -> m (SyncState s)
  processService serviceChan peer s serviceMsg = case serviceMsg of
    SyncServiceRequestId aname -> do
      syncLog s $ "Received request for network id for actor " <> pack aname
      marep <- findActorTypeRepM aname
      case marep of
        Nothing -> do
          syncLog s "Such actor isn't known"
          sendService peer serviceChan $ SyncServiceResponseNotRegistered aname
          return s
        Just arep -> case H.lookup arep . syncIdMap $ s of
          Nothing -> do
            syncLog s "Registering actor network id, sending"
            let (w64, s') = registerSyncIdInternal arep s
            sendService peer serviceChan $ SyncServiceResponseId aname w64
            return s'
          Just w64 -> do
            syncLog s "Known actor id, sending"
            sendService peer serviceChan $ SyncServiceResponseId aname w64
            return s
    SyncServiceResponseId aname w64 -> do
      syncLog s $ "Received response for network id for actor " <> pack aname <> " and id " <> pack (show w64)
      marep <- findActorTypeRepM aname
      case marep of
        Nothing -> do
          syncLog s "Not known actor, ignoring"
          return s
        Just arep -> do
          syncLog s "Sending all scheduled messages"
          let s' = addSyncTypeRepInternal arep w64 s
              msgs = fromMaybe S.empty . H.lookup peer . syncScheduledMessages $! s'
          sheduled <- fmap catMaybesSeq . forM msgs $ \(aname', chan, msg) -> if aname == aname'
            then do
              peerSendM peer chan . msg $! w64
              return Nothing
            else return $! Just (aname', chan, msg)

          let sended = S.length msgs - S.length sheduled
          syncLog s $ "Sended: " <> pack (show sended)

          return $! s' {
              syncScheduledMessages = H.insert peer sheduled . syncScheduledMessages $! s'
            }
    SyncServiceResponseNotRegistered aname -> do
      putMsgLnM LogError $ "Sync module: Failed to resolve actor id with name " <> (pack aname)
      return s

-- | Helper for sending service messages
sendService :: (NetworkMonad m, LoggingMonad m) => Peer -> ChannelID -> SyncServiceMsg -> m ()
sendService peer chanid msg = do
  let msg' = encode (0 :: Word64, encode msg)
  peerSendM peer chanid . Message ReliableMessage $ msg'

-- | catMaybes for sequences
catMaybesSeq :: S.Seq (Maybe a) -> S.Seq a
catMaybesSeq = fmap fromJust . S.filter isJust

-- | Internal implementation of actor registrarion when monadic context isn't in scope
registerSyncIdInternal :: HashableTypeRep -> SyncState s -> (Word64, SyncState s)
registerSyncIdInternal tr sstate = case H.lookup tr . syncIdMap $! sstate of
  Just !i -> (i, sstate)
  Nothing -> (i, sstate {
        syncIdMap = H.insert tr i . syncIdMap $! sstate
      , syncIdMapRev = H.insert i tr . syncIdMapRev $! sstate
      , syncNextId = i+1
      })
    where
      !i = findNextEmptyId sstate $ syncNextId sstate
  where
    findNextEmptyId ss i = case H.lookup i .syncIdMapRev $! ss of
      Nothing -> i
      Just _ -> findNextEmptyId ss (i+1)

-- | Internal implementation of actor registrarion when monadic context isn't in scope
addSyncTypeRepInternal :: HashableTypeRep -> Word64 -> SyncState s -> SyncState s
addSyncTypeRepInternal !tr !i sstate = case H.lookup i . syncIdMapRev $! sstate of
  Just _ -> sstate
  Nothing -> sstate {
      syncIdMap = H.insert tr i . syncIdMap $! sstate
    , syncIdMapRev = H.insert i tr . syncIdMapRev $! sstate
    }

-- | Internal implementation of sending service request for actor net id
syncRequestIdInternal :: forall proxy i m s . (ActorMonad m, NetworkMonad m, LoggingMonad m, NetworkMessage i)
    => Peer -> proxy i -> SyncState s -> m (SyncState s)
syncRequestIdInternal peer p s = do
  chan <- getServiceChannel
  registerActorTypeRepM p
  sendService peer chan $ SyncServiceRequestId $ show $ actorFingerprint p
  return s

-- | Return channel id 1 if network module has more than 1 channel, either fallback to 0
--
-- Note: If you open more than one channel,
-- the module would use chanel id 1 as service channel, therefore count of channels
-- on client and server should match (server won't response on channel 1 if it doesn't
-- have it).
getServiceChannel :: NetworkMonad m => m ChannelID
getServiceChannel = do
  maxi <- networkChannels
  return . ChannelID $! if maxi > 1 then 1 else 0

-- | Log only when flag is turned on
syncLog :: LoggingMonad m => SyncState s -> Text -> m ()
syncLog SyncState{..} = when syncLogging . putMsgLnM LogInfo . ("Sync module: " <>)