packages feed

aztecs-0.15.0: src/Aztecs/ECS/Access.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

-- |
-- Module      : Aztecs.ECS.Access
-- Copyright   : (c) Matt Hunzinger, 2025
-- License     : BSD-style (see the LICENSE file in the distribution)
--
-- Maintainer  : matt@hunzinger.me
-- Stability   : provisional
-- Portability : non-portable (GHC extensions)
module Aztecs.ECS.Access
  ( Access,
    AccessT (..),
    MonadAccess (..),
    runAccessT,
    runAccessT_,
    system,
  )
where

import Aztecs.ECS.Access.Class
import Aztecs.ECS.Query (QueryT (..))
import qualified Aztecs.ECS.Query as Q
import Aztecs.ECS.Query.Dynamic (DynamicQueryT)
import qualified Aztecs.ECS.Query.Dynamic as Q
import Aztecs.ECS.Query.Dynamic.Reader (DynamicQueryReader)
import qualified Aztecs.ECS.Query.Dynamic.Reader as Q
import Aztecs.ECS.Query.Reader
import Aztecs.ECS.System
import Aztecs.ECS.World (World (..))
import qualified Aztecs.ECS.World as W
import qualified Aztecs.ECS.World.Archetype as A
import Aztecs.ECS.World.Archetypes (Node (..))
import Aztecs.ECS.World.Bundle
import qualified Aztecs.ECS.World.Entities as E
import Control.Concurrent.STM
import Control.Monad.Fix
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.State.Strict
import qualified Data.Foldable as F

-- | @since 0.9
type Access = AccessT Identity

-- | Access into the `World`.
--
-- @since 0.9
newtype AccessT m a = AccessT {unAccessT :: StateT World m a}
  deriving (Functor, Applicative, MonadFix, MonadIO)

-- | @since 0.9
instance (Monad m) => Monad (AccessT m) where
  a >>= f = AccessT $ do
    !w <- get
    (a', w') <- lift $ runAccessT a w
    put w'
    unAccessT $ f a'

-- | Run an `Access` on a `World`, returning the output and updated `World`.
--
-- @since 0.9
runAccessT :: (Functor m) => AccessT m a -> World -> m (a, World)
runAccessT a = runStateT $ unAccessT a

-- | Run an `Access` on an empty `World`.
--
-- @since 0.9
runAccessT_ :: (Functor m) => AccessT m a -> m a
runAccessT_ a = fmap fst . runAccessT a $ W.empty

-- | @since 0.9
instance (Monad m) => MonadAccess Bundle (AccessT m) where
  spawn b = AccessT $ do
    !w <- get
    let !(e, w') = W.spawn b w
    put w'
    return e
  insert e c = AccessT $ do
    !w <- get
    let !w' = W.insert e c w
    put w'
  lookup e = AccessT $ do
    !w <- get
    return $ W.lookup e w
  remove e = AccessT $ do
    !w <- get
    let !(a, w') = W.remove e w
    put w'
    return a
  despawn e = AccessT $ do
    !w <- get
    let !(_, w') = W.despawn e w
    put w'

-- | @since 0.9
instance (Monad m) => MonadReaderSystem QueryReader (AccessT m) where
  all q = AccessT $ do
    w <- get
    let (cIds, cs, dynQ) = runQueryReader q . E.components $ entities w
    put w {entities = (entities w) {E.components = cs}}
    unAccessT $ allDyn cIds dynQ
  filter q f = AccessT $ do
    w <- get
    let (cIds, cs, dynQ) = runQueryReader q . E.components $ entities w
        (dynF, cs') = runQueryFilter f cs
    put w {entities = (entities w) {E.components = cs'}}
    let f' n =
          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynF)
            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynF)
    unAccessT $ filterDyn cIds dynQ f'

-- | @since 0.9
instance (Monad m) => MonadSystem (QueryT m) (AccessT m) where
  map q = AccessT $ do
    !w <- get
    let (rws, cs, dynQ) = runQuery q . E.components $ entities w
    put w {entities = (entities w) {E.components = cs}}
    unAccessT $ mapDyn (Q.reads rws <> Q.writes rws) dynQ
  mapSingleMaybe q = AccessT $ do
    !w <- get
    let (rws, cs, dynQ) = runQuery q . E.components $ entities w
    put w {entities = (entities w) {E.components = cs}}
    unAccessT $ mapSingleMaybeDyn (Q.reads rws <> Q.writes rws) dynQ
  filterMap q f = AccessT $ do
    !w <- get
    let (rws, cs, dynQ) = runQuery q . E.components $ entities w
        (dynF, cs') = runQueryFilter f cs
    put w {entities = (entities w) {E.components = cs'}}
    let f' n =
          F.all (\cId -> A.member cId $ nodeArchetype n) (filterWith dynF)
            && F.all (\cId -> not (A.member cId $ nodeArchetype n)) (filterWithout dynF)
    unAccessT $ filterMapDyn (Q.reads rws <> Q.writes rws) f' dynQ

-- | @since 0.9
instance (Monad m) => MonadDynamicReaderSystem DynamicQueryReader (AccessT m) where
  allDyn cIds q = AccessT $ do
    !w <- get
    return . Q.allDyn cIds q $ entities w
  filterDyn cIds q f = AccessT $ do
    !w <- get
    return . Q.filterDyn cIds f q $ entities w

-- | @since 0.9
instance (Monad m) => MonadDynamicSystem (DynamicQueryT m) (AccessT m) where
  mapDyn cIds q = AccessT $ do
    !w <- get
    (as, es) <- lift . Q.mapDyn cIds q $ entities w
    put w {entities = es}
    return as
  mapSingleMaybeDyn cIds q = AccessT $ do
    !w <- get
    (res, es) <- lift . Q.mapSingleMaybeDyn cIds q $ entities w
    put w {entities = es}
    return res
  filterMapDyn cIds f q = AccessT $ do
    !w <- get
    (as, es) <- lift . Q.filterMapDyn cIds f q $ entities w
    put w {entities = es}
    return as

-- | Run a `System`.
--
-- @since 0.9
system :: System a -> AccessT IO a
system s = AccessT $ do
  !w <- get
  esVar <- lift . newTVarIO $ entities w
  a <- lift . atomically $ runReaderT (runSystemT s) esVar
  es <- lift $ readTVarIO esVar
  put w {entities = es}
  return a