packages feed

ohhecs-0.0.1: src/Games/ECS/MessageQueue.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE Trustworthy #-}
-- For Empty and :<
{-# OPTIONS_GHC -Wno-incomplete-patterns #-}

-- |
-- Module      :  Games.ECS.MessageQueue
-- Description : A simple message queue implementation.
-- Copyright   :  (C) 2020 Sophie Taylor
-- License     :  AGPL-3.0-or-later
-- Maintainer  :  Sophie Taylor <sophie@spacekitteh.moe>
-- Stability   :  experimental
-- Portability: GHC
--
-- Implements a simple message queuing system.
module Games.ECS.MessageQueue
  ( MessageQueue,
    --    queue,
    newMessageQueue,
    MonadAtomicMessageQueue (..),
    queueMessage,
    queueImmediateMessage,
    queueMessages,
    readMessage,
    processMessageQueue,
    MessagesProcessed (..),
  )
where

import Control.Lens
import Control.Monad
import Data.Hashable
import Data.Sequence (Seq, singleton)
import Data.Tuple (swap)
import GHC.Generics
import GHC.Types (SPEC (..))
import Games.ECS.Util.Misc
import Games.ECS.World

data DelayedOrNot
  = NotDelayed
  | Delayed
  deriving stock (Eq, Generic)
  deriving anyclass (Hashable)

-- | The core message queue type.
newtype MessageQueue a = MessageQueue {_queue :: Seq (DelayedOrNot, a)} deriving stock (Eq, Generic)

makeLenses ''MessageQueue

-- | Construct a new, empty t'MessageQueue'.
{-# INLINE newMessageQueue #-}
newMessageQueue :: MessageQueue a
newMessageQueue = let q = Empty in MessageQueue q

instance (Hashable a) => Hashable (MessageQueue a) where
  {-# INLINE hashWithSalt #-}
  hashWithSalt salt (MessageQueue q) = hashWithSalt salt (HashableSeq q)

-- | Monads which can atomically modify a t'MessageQueue'.
class (Monad m) => MonadAtomicMessageQueue a m where
  -- | Monadically perform a given state transition atomically on the t'MessageQueue', and return the result.
  stateMessageQueue :: (MessageQueue a -> (MessageQueue a, val)) -> m val

  -- | Atomically modify a t'MessageQueue'.
  modifyMessageQueue :: (MessageQueue a -> MessageQueue a) -> m ()

{-# INLINEABLE queueMessage #-}

-- | Queue message
queueMessage :: forall a m. (MonadAtomicMessageQueue a m) => a -> m ()
queueMessage msg = do
  modifyMessageQueue (\q -> q & queue %~ (|> (NotDelayed, msg)))

{-# INLINEABLE queueImmediateMessage #-}

-- | Push a message on to the front of the queue.
queueImmediateMessage :: forall a m. (MonadAtomicMessageQueue a m) => a -> m ()
queueImmediateMessage msg = do
  modifyMessageQueue (\q -> q & queue %~ ((NotDelayed, msg) <|))

{-# INLINEABLE queueMessages #-}

-- | Queue a series of messages
queueMessages :: (Foldable f, MonadAtomicMessageQueue a m) => f a -> m ()
queueMessages msgs = forM_ msgs queueMessage

{-# INLINEABLE readMessage #-}

-- | Pops the message queue
readMessage :: (MonadAtomicMessageQueue a m) => (a -> Bool) -> m (Maybe a)
readMessage shouldSkip =
  stateMessageQueue
    ( \(MessageQueue msgs) -> swap . fmap (MessageQueue . fmap clearDelayed) . go $ msgs
    )
  where
    clearDelayed (Delayed, a) = (NotDelayed, a)
    clearDelayed x = x
    go Empty = (Nothing, Empty)
    go ((NotDelayed, a) :< rest)
      | shouldSkip a = fmap ((Delayed, a) <|) (go rest)
      | otherwise = (Just a, rest)
    go ((Delayed, a) :< rest) = fmap ((Delayed, a) <|) (go rest)

-- | Which messages were processed.
data MessagesProcessed a
  = AllMessagesProcessed
  | NewMessagesProcessed !(Seq a)
  deriving stock (Eq, Generic, Show)

instance (Hashable a) => Hashable (MessagesProcessed a) where
  {-# INLINE hashWithSalt #-}
  hashWithSalt salt AllMessagesProcessed = hashWithSalt salt (0 :: Int)
  hashWithSalt salt (NewMessagesProcessed msgs) = (salt `hashWithSalt` (1 :: Int)) `hashWithSalt` HashableSeq msgs

instance Semigroup (MessagesProcessed a) where
  {-# INLINE (<>) #-}
  AllMessagesProcessed <> (NewMessagesProcessed Empty) = AllMessagesProcessed
  AllMessagesProcessed <> b = b
  (NewMessagesProcessed Empty) <> AllMessagesProcessed = AllMessagesProcessed
  a <> AllMessagesProcessed = a
  (NewMessagesProcessed a) <> (NewMessagesProcessed b) = NewMessagesProcessed (a <> b)

instance Monoid (MessagesProcessed a) where
  {-# INLINEABLE mempty #-}
  mempty = AllMessagesProcessed

{-# INLINEABLE processMessageQueue #-}

-- | Processes a message queue until it's empty.
processMessageQueue ::
  forall world a m.
  (MonadAtomicMessageQueue a m) =>
  -- | Predicate on whether a message should be skipped this time round, and left in the queue.
  (world Storing -> a -> Bool) ->
  -- | How to process an individual message.
  (world Storing -> a -> m (world Storing)) ->
  -- | The world.
  world Storing ->
  m (MessagesProcessed a, world Storing)
processMessageQueue shouldSkip processMsg oldWorld = processNextMessage SPEC mempty oldWorld
  where
    -- Process the message queue one at a time. Because it only ever locks the message queue long enough to
    -- pop it, messages can safely enqueue other messages.
    processNextMessage !sPEC doneMessages world = do
      nextMessage <- readMessage (shouldSkip world)
      case nextMessage of
        -- The queue is empty or has no messages available to process; return the list of effects we processed and the updated world
        Nothing -> pure (doneMessages, world)
        -- We've popped another effect off of the queue. Process it, then process the rest of the queue
        Just msg -> do
          processedNewWorld <- processMsg world msg
          processNextMessage sPEC (doneMessages <> (NewMessagesProcessed (singleton msg))) processedNewWorld