aztecs-0.16.0: src/Aztecs/ECS/Query.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Module : Aztecs.ECS.Query
-- 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)
--
-- Query for matching entities.
module Aztecs.ECS.Query
( -- * Queries
Query (..),
DynamicQueryF (..),
-- ** Operations
query,
queryMaybe,
queryMap,
queryMap_,
queryMapM,
queryMapWith,
queryMapWith_,
queryMapWithM,
queryMapWithAccum,
queryMapWithAccumM,
-- ** Running
readQuery,
readQuery',
readQuerySingle,
readQuerySingle',
readQuerySingleMaybe,
readQuerySingleMaybe',
runQuery,
runQuerySingle,
runQuerySingleMaybe,
-- * Filters
QueryFilter (..),
with,
without,
-- * Reads and writes
ReadsWrites (..),
disjoint,
)
where
import Aztecs.ECS.Access.Internal (Access)
import Aztecs.ECS.Component
import Aztecs.ECS.Query.Dynamic
import Aztecs.ECS.World.Components (Components)
import qualified Aztecs.ECS.World.Components as CS
import Aztecs.ECS.World.Entities (Entities (..))
import qualified Aztecs.ECS.World.Entities as E
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Vector (Vector)
import GHC.Stack
import Prelude hiding (reads)
-- | Query for matching entities.
newtype Query m a = Query
{ -- | Run a query, producing a `DynamicQuery`.
--
-- @since 0.10
runQuery' :: Components -> (ReadsWrites, Components, DynamicQuery m a)
}
deriving (Functor)
instance (Monad m) => Applicative (Query m) where
pure a = Query (mempty,,pure a)
{-# INLINE pure #-}
(Query f) <*> (Query g) = Query $ \cs ->
let !(cIdsG, cs', aQS) = g cs
!(cIdsF, cs'', bQS) = f cs'
in (cIdsG <> cIdsF, cs'', bQS <*> aQS)
{-# INLINE (<*>) #-}
instance (Monad m) => DynamicQueryF m (Query m) where
entity = Query (mempty,,entity)
{-# INLINE entity #-}
queryDyn = dynQueryReader queryDyn
{-# INLINE queryDyn #-}
queryMaybeDyn = dynQueryReader queryMaybeDyn
{-# INLINE queryMaybeDyn #-}
queryMapDyn f = dynQueryWriter' $ queryMapDyn f
{-# INLINE queryMapDyn #-}
queryMapDyn_ f = dynQueryWriter' $ queryMapDyn_ f
{-# INLINE queryMapDyn_ #-}
queryMapDynM f = dynQueryWriter' $ queryMapDynM f
{-# INLINE queryMapDynM #-}
queryMapDynWith f = dynQueryWriter $ queryMapDynWith f
{-# INLINE queryMapDynWith #-}
queryMapDynWith_ f = dynQueryWriter $ queryMapDynWith_ f
{-# INLINE queryMapDynWith_ #-}
queryMapDynWithM f = dynQueryWriter $ queryMapDynWithM f
{-# INLINE queryMapDynWithM #-}
queryMapDynWithAccum f = dynQueryWriter $ queryMapDynWithAccum f
{-# INLINE queryMapDynWithAccum #-}
queryUntracked (Query q) = Query $ \cs ->
let !(rws, cs', dynQ) = q cs
in (rws, cs', queryUntracked dynQ)
{-# INLINE queryUntracked #-}
queryMapDynWithAccumM f = dynQueryWriter $ queryMapDynWithAccumM f
{-# INLINE queryMapDynWithAccumM #-}
queryFilterMap p (Query q) = Query $ \cs ->
let !(rws, cs', dynQ) = q cs
in (rws, cs', queryFilterMap p dynQ)
{-# INLINE queryFilterMap #-}
-- | Query a component.
query :: forall m a. (Monad m, Component m a) => Query m a
query = queryReader @m @a queryDyn
{-# INLINE query #-}
-- | Optionally query a component, returning @Nothing@ if it does not exist.
queryMaybe :: forall m a. (Monad m, Component m a) => Query m (Maybe a)
queryMaybe = queryReader @m @a queryMaybeDyn
{-# INLINE queryMaybe #-}
-- | Query a component and update it.
queryMap :: forall m a. (Monad m, Component m a) => (a -> a) -> Query m a
queryMap f = queryWriter' @m @a $ queryMapDyn f
{-# INLINE queryMap #-}
-- | Query a component and update it, ignoring any output.
queryMap_ :: forall m a. (Monad m, Component m a) => (a -> a) -> Query m ()
queryMap_ f = queryWriter' @m @a $ queryMapDyn_ f
{-# INLINE queryMap_ #-}
-- | Query a component and update it with a monadic action.
queryMapM :: forall m a. (Monad m, Component m a) => (a -> m a) -> Query m a
queryMapM f = queryWriter' @m @a $ queryMapDynM f
{-# INLINE queryMapM #-}
-- | Query a component with input and update it.
queryMapWith :: forall m a b. (Monad m, Component m b) => (a -> b -> b) -> Query m a -> Query m b
queryMapWith f = queryWriter @m @b $ queryMapDynWith f
{-# INLINE queryMapWith #-}
-- | Query a component with input and update it, ignoring any output.
queryMapWith_ :: forall m a b. (Monad m, Component m b) => (a -> b -> b) -> Query m a -> Query m ()
queryMapWith_ f = queryWriter @m @b $ queryMapDynWith_ f
{-# INLINE queryMapWith_ #-}
-- | Query a component with input and update it with a monadic action.
queryMapWithM ::
forall m a b.
(Monad m, Component m b) =>
(a -> b -> m b) ->
Query m a ->
Query m b
queryMapWithM f = queryWriter @m @b $ queryMapDynWithM f
{-# INLINE queryMapWithM #-}
-- | Query a component with input, returning a tuple of the result and the updated component.
queryMapWithAccum ::
forall m a b c.
(Monad m, Component m c) =>
(b -> c -> (a, c)) ->
Query m b ->
Query m (a, c)
queryMapWithAccum f = queryWriter @m @c $ queryMapDynWithAccum f
{-# INLINE queryMapWithAccum #-}
-- | Query a component with input and update it with a monadic action, returning a tuple.
queryMapWithAccumM ::
forall m a b c.
(Monad m, Component m c) =>
(b -> c -> m (a, c)) ->
Query m b ->
Query m (a, c)
queryMapWithAccumM f = queryWriter @m @c $ queryMapDynWithAccumM f
{-# INLINE queryMapWithAccumM #-}
dynQueryReader :: (ComponentID -> DynamicQuery m a) -> ComponentID -> Query m a
dynQueryReader f cId = Query (ReadsWrites {reads = Set.singleton cId, writes = Set.empty},,f cId)
{-# INLINE dynQueryReader #-}
dynQueryWriter ::
( ComponentID ->
DynamicQuery m a ->
DynamicQuery m b
) ->
ComponentID ->
Query m a ->
Query m b
dynQueryWriter f cId q = Query $ \cs ->
let !(rws, cs', dynQ) = runQuery' q cs
in (rws <> ReadsWrites Set.empty (Set.singleton cId), cs', f cId dynQ)
{-# INLINE dynQueryWriter #-}
dynQueryWriter' :: (ComponentID -> DynamicQuery m a) -> ComponentID -> Query m a
dynQueryWriter' f cId = Query (ReadsWrites {reads = Set.empty, writes = Set.singleton cId},,f cId)
{-# INLINE dynQueryWriter' #-}
queryReader :: forall m a b. (Component m a) => (ComponentID -> DynamicQuery m b) -> Query m b
queryReader f = Query $ \cs ->
let !(cId, cs') = CS.insert @a @m cs
in (ReadsWrites {reads = Set.singleton cId, writes = Set.empty}, cs', f cId)
{-# INLINE queryReader #-}
queryWriter ::
forall m a b c.
(Component m a) =>
( ComponentID ->
DynamicQuery m b ->
DynamicQuery m c
) ->
Query m b ->
Query m c
queryWriter f (Query g) = Query $ \cs ->
let !(rws, cs', dynQ) = g cs
!(cId, cs'') = CS.insert @a @m cs'
in (rws <> ReadsWrites Set.empty (Set.singleton cId), cs'', f cId dynQ)
{-# INLINE queryWriter #-}
queryWriter' :: forall m a b. (Component m a) => (ComponentID -> DynamicQuery m b) -> Query m b
queryWriter' f = Query $ \cs ->
let !(cId, cs') = CS.insert @a @m cs
in (ReadsWrites {reads = Set.empty, writes = Set.singleton cId}, cs', f cId)
{-# INLINE queryWriter' #-}
-- | Reads and writes of a `Query`.
data ReadsWrites = ReadsWrites
{ -- | Component IDs being read.
reads :: !(Set ComponentID),
-- | Component IDs being written.
writes :: !(Set ComponentID)
}
deriving (Show)
instance Semigroup ReadsWrites where
ReadsWrites r1 w1 <> ReadsWrites r2 w2 = ReadsWrites (r1 <> r2) (w1 <> w2)
{-# INLINE (<>) #-}
instance Monoid ReadsWrites where
mempty = ReadsWrites mempty mempty
{-# INLINE mempty #-}
-- | `True` if the reads and writes of two `Query`s overlap.
disjoint :: ReadsWrites -> ReadsWrites -> Bool
disjoint a b =
Set.disjoint (reads a) (writes b)
|| Set.disjoint (reads b) (writes a)
|| Set.disjoint (writes b) (writes a)
-- | Match all entities.
readQuery :: (Monad m) => Query m a -> Entities m -> m (Vector a, Entities m)
readQuery q es = do
(as, cs) <- readQuery' q es
return (as, es {E.components = cs})
{-# INLINE readQuery #-}
-- | Match all entities.
readQuery' :: (Monad m) => Query m a -> Entities m -> m (Vector a, Components)
readQuery' q es = do
let !(rws, cs', dynQ) = runQuery' q (E.components es)
!cIds = reads rws <> writes rws
as <- readQueryDyn cIds dynQ es
return (as, cs')
{-# INLINE readQuery' #-}
-- | Match a single entity.
readQuerySingle :: (HasCallStack, Monad m) => Query m a -> Entities m -> m (a, Entities m)
readQuerySingle q es = do
(a, cs) <- readQuerySingle' q es
return (a, es {E.components = cs})
{-# INLINE readQuerySingle #-}
-- | Match a single entity.
readQuerySingle' :: (HasCallStack, Monad m) => Query m a -> Entities m -> m (a, Components)
readQuerySingle' q es = do
let !(rws, cs', dynQ) = runQuery' q (E.components es)
!cIds = reads rws <> writes rws
a <- readQuerySingleDyn cIds dynQ es
return (a, cs')
{-# INLINE readQuerySingle' #-}
-- | Match a single entity.
readQuerySingleMaybe :: (Monad m) => Query m a -> Entities m -> m (Maybe a, Entities m)
readQuerySingleMaybe q es = do
(a, cs) <- readQuerySingleMaybe' q es
return (a, es {E.components = cs})
{-# INLINE readQuerySingleMaybe #-}
-- | Match a single entity.
readQuerySingleMaybe' :: (Monad m) => Query m a -> Entities m -> m (Maybe a, Components)
readQuerySingleMaybe' q es = do
let !(rws, cs', dynQ) = runQuery' q (E.components es)
!cIds = reads rws <> writes rws
a <- readQuerySingleMaybeDyn cIds dynQ es
return (a, cs')
{-# INLINE readQuerySingleMaybe' #-}
-- | Map all matched entities.
runQuery :: (Monad m) => Query m o -> Entities m -> m (Vector o, Entities m, Access m ())
runQuery q es = do
let !(rws, cs', dynQ) = runQuery' q $ components es
!cIds = reads rws <> writes rws
(as, es', hook) <- runQueryDyn cIds dynQ es
return (as, es' {components = cs'}, hook)
{-# INLINE runQuery #-}
-- | Map a single matched entity.
runQuerySingle ::
(HasCallStack, Monad m) =>
Query m a ->
Entities m ->
m (a, Entities m, Access m ())
runQuerySingle q es = do
let !(rws, cs', dynQ) = runQuery' q $ components es
!cIds = reads rws <> writes rws
(as, es', hook) <- runQuerySingleDyn cIds dynQ es
return (as, es' {components = cs'}, hook)
{-# INLINE runQuerySingle #-}
-- | Map a single matched entity, or `Nothing`.
runQuerySingleMaybe ::
(Monad m) =>
Query m a ->
Entities m ->
m (Maybe a, Entities m, Access m ())
runQuerySingleMaybe q es = do
let !(rws, cs', dynQ) = runQuery' q $ components es
!cIds = reads rws <> writes rws
(as, es', hook) <- runQuerySingleMaybeDyn cIds dynQ es
return (as, es' {components = cs'}, hook)
{-# INLINE runQuerySingleMaybe #-}
-- | Filter for a `Query`.
newtype QueryFilter = QueryFilter
{ -- | Run a query filter.
runQueryFilter :: Components -> (DynamicQueryFilter, Components)
}
instance Semigroup QueryFilter where
a <> b =
QueryFilter
( \cs ->
let !(withA', cs') = runQueryFilter a cs
!(withB', cs'') = runQueryFilter b cs'
in (withA' <> withB', cs'')
)
instance Monoid QueryFilter where
mempty = QueryFilter (mempty,)
-- | Filter for entities containing this component.
with :: forall m a. (Component m a) => QueryFilter
with = QueryFilter $ \cs ->
let !(cId, cs') = CS.insert @a @m cs in (mempty {filterWith = Set.singleton cId}, cs')
-- | Filter out entities containing this component.
without :: forall m a. (Component m a) => QueryFilter
without = QueryFilter $ \cs ->
let !(cId, cs') = CS.insert @a @m cs in (mempty {filterWithout = Set.singleton cId}, cs')