aztecs-0.3.0.0: src/Data/Aztecs/Query.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.Aztecs.Query
( Query (..),
(<?>),
fetch,
fetchId,
all,
queryAll,
queryAll',
map,
Queryable (..),
lookup,
lookupQuery,
alter,
QueryState (..),
IsEq,
Map (..),
)
where
import Data.Aztecs.Component
import Data.Aztecs.Entity (ConcatT, Difference, DifferenceT, Entity (..), EntityID, EntityT, FromEntity (..), Intersect, IntersectT, Sort, ToEntity (..))
import qualified Data.Aztecs.Entity as E
import Data.Aztecs.World (World (..))
import Data.Aztecs.World.Archetype (Archetype)
import qualified Data.Aztecs.World.Archetype as A
import Data.Aztecs.World.Archetypes (Archetypes, Node (nodeArchetype))
import qualified Data.Aztecs.World.Archetypes as AS
import Data.Aztecs.World.Components (Components)
import qualified Data.Aztecs.World.Components as CS
import Data.Data (Typeable)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Prelude hiding (all, lookup, map)
data QueryState a = QueryState
{ queryStateComponentIds :: Set ComponentID,
queryStateAll :: Archetype -> ([Entity a], [Entity a] -> Archetype -> Archetype),
queryStateLookup :: EntityID -> Archetype -> Maybe (Entity a),
queryStateInsert :: EntityID -> Archetype -> Entity a -> Archetype
}
-- | Query into the `World`.
newtype Query a = Query {runQuery' :: Components -> QueryState a}
(<?>) ::
(E.Split a (ConcatT a b), E.SplitT a (ConcatT a b) ~ b) =>
Query a ->
Query b ->
Query (ConcatT a b)
(Query a) <?> (Query b) = Query $ \cs ->
let aQS = a cs
bQS = b cs
in QueryState
{ queryStateComponentIds = queryStateComponentIds aQS <> queryStateComponentIds bQS,
queryStateAll = \arch ->
let (a'', aF) = queryStateAll aQS arch
(b'', bF) = queryStateAll bQS arch
in ( uncurry E.concat <$> zip a'' b'',
\new newArch -> let (as, bs) = unzip $ fmap E.split new in bF bs $ aF as newArch
),
queryStateLookup = \eId arch -> do
aE <- queryStateLookup aQS eId arch
bE <- queryStateLookup bQS eId arch
return $ E.concat aE bE,
queryStateInsert = \eId arch e ->
let (aE, bE) = E.split e
arch' = queryStateInsert aQS eId arch aE
in queryStateInsert bQS eId arch' bE
}
-- | Fetch a `Component` by its type.
fetch :: forall a. (Component a, Typeable (StorageT a)) => Query '[a]
fetch = Query $ \cs ->
let cId = fromMaybe (error "TODO") (CS.lookup @a cs)
in QueryState
{ queryStateComponentIds = Set.singleton cId,
queryStateAll = \arch ->
let as = A.all cId arch
in ( fmap (\x -> ECons (snd x) ENil) as,
A.insertAscList cId . fmap (\((e, _), ECons a ENil) -> (e, a)) . zip as
),
queryStateLookup = \eId arch -> do
a <- A.lookup eId cId arch
return $ ECons a ENil,
queryStateInsert = \eId arch e -> let (_, arch', _) = A.insert eId e arch cs in arch'
}
-- | Fetch a `Component` by its `ComponentID`.
fetchId :: forall a. (Component a, Typeable (StorageT a)) => ComponentID -> Query '[a]
fetchId cId = fetch' @a (const cId)
fetch' :: forall a. (Component a, Typeable (StorageT a)) => (Components -> ComponentID) -> Query '[a]
fetch' f = Query $ \cs ->
let cId = f cs
in QueryState
{ queryStateComponentIds = Set.singleton cId,
queryStateAll = \arch ->
let as = A.all cId arch
in ( fmap (\x -> ECons (snd x) ENil) as,
A.insertAscList cId . fmap (\((e, _), ECons a ENil) -> (e, a)) . zip as
),
queryStateLookup = \eId arch -> do
a <- A.lookup eId cId arch
return $ ECons a ENil,
queryStateInsert = \eId arch e -> let (_, arch', _) = A.insert eId e arch cs in arch'
}
all :: forall a. (ToEntity a, FromEntity a, Queryable (EntityT a)) => World -> [a]
all = fmap fromEntity . queryAll (query @(EntityT a))
queryAll :: Query a -> World -> [Entity a]
queryAll q w = fromMaybe [] $ do
let qS = runQuery' q (components w)
return $ concatMap (fst . queryStateAll qS) (AS.lookup (queryStateComponentIds qS) (archetypes w))
queryAll' :: Query a -> Archetypes -> Components -> [Entity a]
queryAll' q as cs = fromMaybe [] $ do
let qS = runQuery' q cs
return $ concatMap (fst . queryStateAll qS) (AS.lookup (queryStateComponentIds qS) as)
class Queryable a where
query :: Query a
instance {-# OVERLAPPING #-} (Component a, Typeable (StorageT a)) => Queryable '[a] where
query = fetch @a
instance (Component a, Typeable (StorageT a), Queryable as) => Queryable (a ': as) where
query = fetch @a <?> query @as
-- | Map over all entities that match this query,
-- storing the resulting components in the @World@.
map ::
forall i o.
(Map (IsEq (Entity (EntityT i)) (Entity (EntityT o))) i o) =>
(i -> o) ->
World ->
([o], World)
map f w =
let (o, arches) = map' @(IsEq (Entity (EntityT i)) (Entity (EntityT o))) f (components w) AS.map (archetypes w)
in (o, w {archetypes = arches})
-- Returns @True@ if @a@ and @b@ are the same type.
type family IsEq a b :: Bool where
IsEq a a = 'True
IsEq a b = 'False
-- Map over an entity's components.
--
-- @flag@ indicates if the input and output entities are the same,
-- in order to specialize at compile-time.
--
-- If the input and output entities are different, a reader and writer query are combined into one.
-- Otherwise, if the input and output entities are the same, we can use a more efficient implementation.
class Map (flag :: Bool) i o where
map' :: (i -> o) -> Components -> (Set ComponentID -> (Archetype -> ([o], Archetype)) -> a -> ([[o]], a)) -> a -> ([o], a)
instance (FromEntity i, ToEntity o, EntityT i ~ a, EntityT o ~ a, Queryable a) => Map 'True i o where
map' f cs mapArches arches = fromMaybe ([], arches) $ do
let qS = runQuery' (query @a) cs
go arch =
let (as, h) = queryStateAll qS arch
as' = fmap (f . fromEntity) as
arch' = h (fmap toEntity as') arch
in (as', arch')
let (es, arches') = mapArches (queryStateComponentIds qS) go arches'
return (concat es, arches')
instance
( FromEntity i,
ToEntity o,
Intersect (EntityT i) (EntityT o),
Queryable (IntersectT (EntityT i) (EntityT o)),
Difference (EntityT i) (EntityT o),
Queryable (DifferenceT (EntityT i) (EntityT o)),
Sort (ConcatT (DifferenceT (EntityT i) (EntityT o)) (IntersectT (EntityT i) (EntityT o))) (EntityT i),
Sort (EntityT o) (IntersectT (EntityT i) (EntityT o))
) =>
Map 'False i o
where
map' f cs mapArches arches = fromMaybe ([], arches) $ do
let i = query @(IntersectT (EntityT i) (EntityT o))
o = query @(DifferenceT (EntityT i) (EntityT o))
aQS = runQuery' i cs
bQS = runQuery' o cs
g arch =
let (as, aH) = queryStateAll aQS arch
(bs, _) = queryStateAll bQS arch
es = fmap (\(aE, bE) -> f $ fromEntity @i (E.sort $ E.concat bE aE)) (zip as bs)
arch' = aH (fmap (\x -> let e = E.sort $ toEntity x in e) es) arch
in (es, arch')
(es', arches') = mapArches (queryStateComponentIds aQS <> queryStateComponentIds bQS) g arches
return (concat es', arches')
lookup :: forall a. (FromEntity a, Queryable (EntityT a)) => EntityID -> World -> Maybe a
lookup eId w = fromEntity <$> lookupQuery eId (query @(EntityT a)) w
lookupQuery :: EntityID -> Query a -> World -> Maybe (Entity a)
lookupQuery eId q w = do
let qS = runQuery' q (components w)
aId <- Map.lookup eId (entities w)
node <- AS.lookupNode aId (archetypes w)
queryStateLookup qS eId (nodeArchetype node)
alter :: forall a. (FromEntity a, ToEntity a, Queryable (EntityT a)) => EntityID -> (a -> a) -> World -> World
alter eId f w = fromMaybe w $ do
let qS = runQuery' (query @(EntityT a)) (components w)
aId <- Map.lookup eId (entities w)
node <- AS.lookupNode aId (archetypes w)
e <- queryStateLookup qS eId (nodeArchetype node)
let e' = f (fromEntity e)
arch' = queryStateInsert qS eId (nodeArchetype node) (toEntity e')
return $
w
{ archetypes =
(archetypes w) {AS.nodes = Map.insert aId node {nodeArchetype = arch'} (AS.nodes $ archetypes w)}
}