aztecs-0.4.0.0: src/Data/Aztecs/System.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Data.Aztecs.System
( -- * Systems
System,
SystemT (..),
forever,
run,
queue,
-- ** Queries
-- *** Reading
all,
filter,
single,
-- *** Writing
map,
map_,
mapSingle,
filterMap,
-- ** Running
runSystem,
runSystem_,
runSystemWithWorld,
-- * Dynamic systems
DynamicSystem,
DynamicSystemT (..),
allDyn,
singleDyn,
mapDyn,
mapDyn_,
queueDyn,
runDyn,
)
where
import Control.Arrow (Arrow (..), ArrowLoop (..))
import Control.Category (Category (..))
import Control.Concurrent (forkIO)
import Data.Aztecs.Access (Access, runAccess)
import Data.Aztecs.Query (DynamicQuery, DynamicQueryFilter (..), Query (..), QueryFilter (..), ReadsWrites)
import qualified Data.Aztecs.Query as Q
import qualified Data.Aztecs.View as V
import Data.Aztecs.World (World (..))
import qualified Data.Aztecs.World as W
import qualified Data.Aztecs.World.Archetype as A
import Data.Aztecs.World.Archetypes (Node (nodeArchetype))
import Data.Aztecs.World.Components (ComponentID, Components)
import qualified Data.Foldable as F
import Data.Set (Set)
import GHC.Conc (TVar, atomically, newTVarIO, readTVarIO, writeTVar)
import Prelude hiding (all, filter, id, map, (.))
type System = SystemT IO
-- | System that can access and alter a `World`.
--
-- Systems can be composed either in sequence or parallel with `Category` and `Arrow` combinators.
-- Using the arrow combinator `&&&`, systems will automatically run in parallel
-- as long as their queries don't intersect.
newtype SystemT m i o = SystemT
{ -- | Initialize a system, producing a `DynamicSystem`.
runSystem' :: Components -> (Components, ReadsWrites, DynamicSystemT m i o)
}
deriving (Functor)
instance (Monad m) => Applicative (SystemT m i) where
pure a = SystemT (,mempty,pure a)
f <*> a =
SystemT $ \w ->
let (w', cIds, f') = runSystem' f w
(w'', cIds', a') = runSystem' a w'
in (w'', cIds <> cIds', f' <*> a')
instance Category System where
id = SystemT (,mempty,id)
f . g = SystemT $ \cs ->
let (cs', cIds, g') = runSystem' g cs
(cs'', cIds', f') = runSystem' f cs'
in (cs'', cIds <> cIds', f' . g')
instance Arrow System where
arr f = SystemT (,mempty,arr f)
first s = SystemT $ \w -> let (w', cIds, dynS) = runSystem' s w in (w', cIds, first dynS)
a &&& b = SystemT $ \w ->
let (w', aRws, dynA) = runSystem' a w
(w'', bRws, dynB) = runSystem' b w'
f = if Q.disjoint aRws bRws then (&&&) else joinDyn
in (w'', aRws <> bRws, f dynA dynB)
instance ArrowLoop System where
loop s = SystemT $ \w -> let (w', cIds, dynS) = runSystem' s w in (w', cIds, loop dynS)
-- | Run a system forever.
forever :: System () () -> System () ()
forever s = SystemT $ \w -> let (w', cIds, dynS) = runSystem' s w in (w', cIds, foreverDyn dynS)
runSystem_ :: System () () -> IO ()
runSystem_ s = runSystem s >> pure ()
runSystem :: System () () -> IO World
runSystem s = runSystemWithWorld s W.empty
runSystemWithWorld :: System () () -> World -> IO World
runSystemWithWorld s w = do
let (cs, _, dynS) = runSystem' s (components w)
w' = w {components = cs}
wVar <- newTVarIO w'
((), access) <- runSystemDyn dynS () wVar
w'' <- readTVarIO wVar
((), w''') <- runAccess access w''
return w'''
-- | Query all matching entities.
all :: Query IO () a -> System () [a]
all q = SystemT $ \cs ->
let (rws, cs', dynQ) = runQuery q cs
in (cs', rws, allDyn (Q.reads rws <> Q.writes rws) dynQ)
-- | Query all matching entities with a `QueryFilter`.
filter :: Query IO () a -> QueryFilter -> System () [a]
filter q qf = SystemT $ \cs ->
let (rws, cs', dynQ) = runQuery q cs
(dynQf, cs'') = runQueryFilter qf cs'
qf' n =
F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynQf)
&& F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynQf)
in (cs'', rws, filterDyn (Q.reads rws <> Q.writes rws) dynQ qf')
-- | Query a single matching entity.
-- If there are zero or multiple matching entities, an error will be thrown.
single :: Query IO () a -> System () a
single q =
fmap
( \as -> case as of
[a] -> a
_ -> error "TODO"
)
(all q)
-- | Map all matching entities, storing the updated entities.
map :: Query IO i a -> System i [a]
map q = SystemT $ \cs ->
let (rws, cs', dynS) = runQuery q cs in (cs', rws, mapDyn (Q.reads rws <> Q.writes rws) dynS)
-- | Map all matching entities and ignore the output, storing the updated entities.
map_ :: Query IO i a -> System i ()
map_ q = const () <$> map q
-- | Map all matching entities with a `QueryFilter`, storing the updated entities.
filterMap :: Query IO i a -> QueryFilter -> System i [a]
filterMap q qf = SystemT $ \cs ->
let (rws, cs', dynQ) = runQuery q cs
(dynQf, cs'') = runQueryFilter qf cs'
f' n =
F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynQf)
&& F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynQf)
in (cs'', rws, filterMapDyn (Q.reads rws <> Q.writes rws) dynQ f')
-- | Map a single matching entity, storing the updated components.
-- If there are zero or multiple matching entities, an error will be thrown.
mapSingle :: Query IO i a -> System i a
mapSingle q =
fmap
( \as -> case as of
[a] -> a
_ -> error "TODO"
)
(map q)
-- | Queue an `Access` to alter the world after this system is complete.
queue :: (Monad m) => (i -> Access m ()) -> SystemT m i ()
queue f = SystemT (,mempty,queueDyn f)
-- | Run a monadic task.
run :: (Monad m) => (i -> m o) -> SystemT m i o
run f = SystemT (,mempty,runDyn f)
type DynamicSystem = DynamicSystemT IO
-- | Dynamic system that can access and alter a `World`.
newtype DynamicSystemT m i o = DynamicSystemT
{runSystemDyn :: i -> TVar World -> m (o, Access m ())}
deriving (Functor)
instance (Monad m) => Applicative (DynamicSystemT m i) where
pure a = DynamicSystemT $ \_ _ -> pure (a, pure ())
f <*> a =
DynamicSystemT $ \i w -> do
(f'', access) <- runSystemDyn f i w
(a'', access') <- runSystemDyn a i w
return (f'' a'', access >> access')
instance Category DynamicSystem where
id = DynamicSystemT $ \i _ -> pure (i, pure ())
f . g = DynamicSystemT $ \i wVar -> do
(a, access) <- runSystemDyn g i wVar
w <- readTVarIO wVar
((), w') <- runAccess access w
atomically $ writeTVar wVar w'
(b, access') <- runSystemDyn f a wVar
return (b, access')
instance Arrow DynamicSystem where
arr f = DynamicSystemT $ \i _ -> pure (f i, pure ())
first s =
DynamicSystemT $ \(i, x) w -> do
(o, access) <- runSystemDyn s i w
return ((o, x), access)
instance ArrowLoop DynamicSystem where
loop s = DynamicSystemT $ \b w -> mdo
((c, d), access) <- runSystemDyn s (b, d) w
return (c, access)
-- | Combine two dynamic systems in parallel.
joinDyn :: DynamicSystem i a -> DynamicSystem i b -> DynamicSystem i (a, b)
joinDyn f g = DynamicSystemT $ \i w -> do
fVar <- newTVarIO Nothing
gVar <- newTVarIO Nothing
_ <- forkIO $ do
result <- runSystemDyn f i w
atomically $ writeTVar fVar (Just result)
_ <- forkIO $ do
result <- runSystemDyn g i w
atomically $ writeTVar gVar (Just result)
let go = do
maybeA <- readTVarIO fVar
maybeB <- readTVarIO gVar
case (maybeA, maybeB) of
(Just (a, accessA), Just (b, accessB)) -> return ((a, b), accessA >> accessB)
_ -> go
go
-- | Run a dynamic system forever.
foreverDyn :: DynamicSystem () () -> DynamicSystem () ()
foreverDyn s = DynamicSystemT $ \_ w -> do
let go w' = do
((), access) <- runSystemDyn s () w'
wAcc' <- readTVarIO w'
((), wAcc'') <- runAccess access wAcc'
atomically $ writeTVar w' wAcc''
go w'
go w
-- | Query all matching entities.
allDyn :: Set ComponentID -> DynamicQuery IO () a -> DynamicSystem () [a]
allDyn cIds q = DynamicSystemT $ \_ w -> do
w' <- readTVarIO w
let v = V.view cIds (archetypes w')
fmap (\(a, _) -> (a, pure ())) (V.allDyn () q v)
filterDyn :: Set ComponentID -> DynamicQuery IO i a -> (Node -> Bool) -> DynamicSystemT IO i [a]
filterDyn cIds q f = DynamicSystemT $ \i wVar -> do
w <- readTVarIO wVar
let v = V.filterView cIds f (archetypes w)
(as, _) <- V.allDyn i q v
return (as, pure ())
-- | Query a single matching entity.
-- If there are zero or multiple matching entities, an error will be thrown.
singleDyn :: Set ComponentID -> DynamicQuery IO () a -> DynamicSystem () a
singleDyn cIds q =
fmap
( \as -> case as of
[a] -> a
_ -> error "TODO"
)
(allDyn cIds q)
-- | Map all matching entities, storing the updated entities.
mapDyn :: Set ComponentID -> DynamicQuery IO i a -> DynamicSystem i [a]
mapDyn cIds q = DynamicSystemT $ \i wVar -> do
w <- readTVarIO wVar
let v = V.view cIds (archetypes w)
(as, v') <- V.allDyn i q v
atomically $ writeTVar wVar $ V.unview v' w
return (as, pure ())
filterMapDyn :: Set ComponentID -> DynamicQuery IO i a -> (Node -> Bool) -> DynamicSystemT IO i [a]
filterMapDyn cIds q f = DynamicSystemT $ \i wVar -> do
w <- readTVarIO wVar
let v = V.filterView cIds f (archetypes w)
(as, v') <- V.allDyn i q v
atomically $ writeTVar wVar $ V.unview v' w
return (as, pure ())
-- | Map all matching entities and ignore the output, storing the updated entities.
mapDyn_ :: Set ComponentID -> DynamicQuery IO () a -> DynamicSystem () ()
mapDyn_ cIds q = const () <$> mapDyn cIds q
-- | Queue an `Access` to alter the world after this system is complete.
queueDyn :: (Applicative m) => (i -> Access m ()) -> DynamicSystemT m i ()
queueDyn f = DynamicSystemT $ \i _ -> pure ((), f i)
-- | Run a monadic task.
runDyn :: (Monad m) => (i -> m o) -> DynamicSystemT m i o
runDyn f = DynamicSystemT $ \i _ -> fmap (,pure ()) (f i)