packages feed

aztecs 0.16.1 → 0.17.0

raw patch · 15 files changed

+1423/−1454 lines, 15 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Aztecs.Hierarchy: ChildState :: Set EntityID -> ChildState
- Aztecs.Hierarchy: ParentState :: EntityID -> ParentState
- Aztecs.Hierarchy: [unChildState] :: ChildState -> Set EntityID
- Aztecs.Hierarchy: [unParentState] :: ParentState -> EntityID
- Aztecs.Hierarchy: instance GHC.Base.Monad m => Aztecs.ECS.Component.Component m Aztecs.Hierarchy.ChildState
- Aztecs.Hierarchy: instance GHC.Base.Monad m => Aztecs.ECS.Component.Component m Aztecs.Hierarchy.ParentState
- Aztecs.Hierarchy: instance GHC.Generics.Generic Aztecs.Hierarchy.ChildState
- Aztecs.Hierarchy: instance GHC.Generics.Generic Aztecs.Hierarchy.ParentState
- Aztecs.Hierarchy: instance GHC.Show.Show Aztecs.Hierarchy.ChildState
- Aztecs.Hierarchy: instance GHC.Show.Show Aztecs.Hierarchy.ParentState
- Aztecs.Hierarchy: newtype ChildState
- Aztecs.Hierarchy: newtype ParentState
- Aztecs.Hierarchy: update :: forall (m :: Type -> Type). Monad m => Access m ()

Files

aztecs.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4
 name:          aztecs
-version:       0.16.1
+version:       0.17.0
 license:       BSD-3-Clause
 license-file:  LICENSE
 maintainer:    matt@hunzinger.me
src/Aztecs.hs view
@@ -1,7 +1,8 @@--- | Aztecs is a type-safe and friendly ECS for games and more.------ An ECS is a modern approach to organizing your application state as a database,--- providing patterns for data-oriented design and parallel processing.-module Aztecs (module Aztecs.ECS) where--import Aztecs.ECS+-- | Aztecs is a type-safe and friendly ECS for games and more.
+--
+-- An ECS is a modern approach to organizing your application state as a database,
+-- providing patterns for data-oriented design and parallel processing.
+module Aztecs (module Aztecs.ECS, module Aztecs.Hierarchy) where
+
+import Aztecs.ECS
+import Aztecs.Hierarchy
src/Aztecs/ECS/Access.hs view
@@ -1,110 +1,110 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeSynonymInstances #-}---- |--- 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 (..),-    runAccess,-    runAccess_,-    spawn,-    spawn_,-    insert,-    insertUntracked,-    lookup,-    remove,-    despawn,-    system,-    triggerEvent,-    triggerEntityEvent,-  )-where--import Aztecs.ECS.Access.Internal-import Aztecs.ECS.Component-import Aztecs.ECS.Entity-import Aztecs.ECS.System (System (..))-import qualified Aztecs.ECS.System as S-import Aztecs.ECS.World (World)-import qualified Aztecs.ECS.World as W-import Aztecs.ECS.World.Bundle-import qualified Aztecs.ECS.World.Entities as E-import Control.Monad-import Control.Monad.State-import Prelude hiding (all, filter, lookup, map, mapM)---- | Run an `Access` on a `World`, returning the output and updated `World`.-runAccess :: (Functor m) => Access m a -> World m -> m (a, World m)-runAccess a = runStateT $ unAccess a---- | Run an `Access` on an empty `World`.-runAccess_ :: (Monad m) => Access m a -> m a-runAccess_ a = fmap fst . runAccess a $ W.empty--spawn :: (Monad m) => BundleT m -> Access m EntityID-spawn b = Access $ do-  !w <- get-  let (e, w', hook) = W.spawn b w-  put w'-  unAccess hook-  return e--spawn_ :: (Monad m) => BundleT m -> Access m ()-spawn_ = void . spawn--insert :: (Monad m) => EntityID -> BundleT m -> Access m ()-insert e c = Access $ do-  !w <- get-  let (w', hook) = W.insert e c w-  put w'-  unAccess hook---- | Insert a component into an entity without running lifecycle hooks.-insertUntracked :: (Monad m) => EntityID -> BundleT m -> Access m ()-insertUntracked e c = Access $ do-  !w <- get-  let w' = W.insertUntracked e c w-  put w'--lookup :: forall m a. (Monad m, Component m a) => EntityID -> Access m (Maybe a)-lookup e = Access $ do-  !w <- get-  return $ W.lookup @m e w--remove :: forall m a. (Monad m, Component m a) => EntityID -> Access m (Maybe a)-remove e = Access $ do-  !w <- get-  let (a, w', hook) = W.remove @m e w-  put w'-  unAccess hook-  return a--despawn :: (Monad m) => EntityID -> Access m ()-despawn e = Access $ do-  !w <- get-  let !(_, w') = W.despawn e w-  put w'---- | Run a `System` on the `World`.-system :: (Monad m) => System m a -> Access m a-system sys = Access $ do-  !w <- get-  let !es = W.entities w-      !(cs', dynSys) = S.runSystem sys $ E.components es-  (a, es', hook) <- lift $ S.runDynamicSystem dynSys es-  put w {W.entities = es' {E.components = cs'}}-  unAccess hook-  return a-{-# INLINE system #-}+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+-- |
+-- 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 (..),
+    runAccess,
+    runAccess_,
+    spawn,
+    spawn_,
+    insert,
+    insertUntracked,
+    lookup,
+    remove,
+    despawn,
+    system,
+    triggerEvent,
+    triggerEntityEvent,
+  )
+where
+
+import Aztecs.ECS.Access.Internal
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.System (System (..))
+import qualified Aztecs.ECS.System as S
+import Aztecs.ECS.World (World)
+import qualified Aztecs.ECS.World as W
+import Aztecs.ECS.World.Bundle
+import qualified Aztecs.ECS.World.Entities as E
+import Control.Monad
+import Control.Monad.State
+import Prelude hiding (all, filter, lookup, map, mapM)
+
+-- | Run an `Access` on a `World`, returning the output and updated `World`.
+runAccess :: (Functor m) => Access m a -> World m -> m (a, World m)
+runAccess a = runStateT $ unAccess a
+
+-- | Run an `Access` on an empty `World`.
+runAccess_ :: (Monad m) => Access m a -> m a
+runAccess_ a = fmap fst . runAccess a $ W.empty
+
+spawn :: (Monad m) => BundleT m -> Access m EntityID
+spawn b = Access $ do
+  !w <- get
+  let (e, w', hook) = W.spawn b w
+  put w'
+  unAccess hook
+  return e
+
+spawn_ :: (Monad m) => BundleT m -> Access m ()
+spawn_ = void . spawn
+
+insert :: (Monad m) => EntityID -> BundleT m -> Access m ()
+insert e c = Access $ do
+  !w <- get
+  let (w', hook) = W.insert e c w
+  put w'
+  unAccess hook
+
+-- | Insert a component into an entity without running lifecycle hooks.
+insertUntracked :: (Monad m) => EntityID -> BundleT m -> Access m ()
+insertUntracked e c = Access $ do
+  !w <- get
+  let w' = W.insertUntracked e c w
+  put w'
+
+lookup :: forall m a. (Monad m, Component m a) => EntityID -> Access m (Maybe a)
+lookup e = Access $ do
+  !w <- get
+  return $ W.lookup @m e w
+
+remove :: forall m a. (Monad m, Component m a) => EntityID -> Access m (Maybe a)
+remove e = Access $ do
+  !w <- get
+  let (a, w', hook) = W.remove @m e w
+  put w'
+  unAccess hook
+  return a
+
+despawn :: (Monad m) => EntityID -> Access m ()
+despawn e = Access $ do
+  !w <- get
+  let !(_, w') = W.despawn e w
+  put w'
+
+-- | Run a `System` on the `World`.
+system :: (Monad m) => System m a -> Access m a
+system sys = Access $ do
+  !w <- get
+  let !es = W.entities w
+      !(cs', dynSys) = S.runSystem sys $ E.components es
+  (a, es', hook) <- lift $ S.runDynamicSystem dynSys es
+  put w {W.entities = es' {E.components = cs'}}
+  unAccess hook
+  return a
+{-# INLINE system #-}
src/Aztecs/ECS/Access/Internal.hs view
@@ -1,76 +1,76 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}---- |--- Module      : Aztecs.ECS.Access.Internal--- 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.Internal-  ( Access (..),-    runAccessWith,-    evalAccess,-    triggerEvent,-    triggerEntityEvent,-  )-where--import Aztecs.ECS.Entity-import Aztecs.ECS.Event-import Aztecs.ECS.World.Internal-import qualified Aztecs.ECS.World.Observers as O-import Aztecs.ECS.World.Observers.Internal-import Control.Monad-import Control.Monad.Fix-import Control.Monad.State-import Data.Dynamic-import Data.Maybe-import qualified Data.Set as Set-import Data.Typeable---- | Access into a `World`.-newtype Access m a = Access {unAccess :: StateT (World m) m a}-  deriving (Functor, Applicative, Monad, MonadFix, MonadIO)---- | Run an `Access` with a given `World`, returning the result and updated world.-runAccessWith :: Access m a -> World m -> m (a, World m)-runAccessWith a = runStateT (unAccess a)---- | Run an `Access` with a given `World`, returning only the result.-evalAccess :: (Monad m) => Access m a -> World m -> m a-evalAccess a = evalStateT (unAccess a)---- | Trigger an event.-triggerEvent :: forall m e. (Monad m, Event e) => e -> Access m ()-triggerEvent evt = Access $ do-  !w <- get-  let eventTypeRep = typeOf (Proxy @e)-      globalOs = O.lookupGlobalObservers eventTypeRep $ observers w-      callbacks = mapMaybe (\oId -> O.lookupCallback oId (observers w)) $ Set.toList globalOs-      dynEvt = toDyn evt-  forM_ callbacks $ \cb -> case cb of-    DynEventObserver f -> f dynEvt-    DynEntityObserver _ -> pure ()---- | Trigger an event for a specific entity.-triggerEntityEvent ::-  forall m e.-  (Monad m, Event e) =>-  EntityID ->-  e ->-  Access m ()-triggerEntityEvent targetEntity evt = Access $ do-  !w <- get-  let eventTypeRep = typeOf $ Proxy @e-      entityOs = O.lookupEntityObservers eventTypeRep targetEntity $ observers w-      callbacks = mapMaybe (\oId -> O.lookupCallback oId (observers w)) $ Set.toList entityOs-      dynEvt = toDyn evt-  forM_ callbacks $ \cb -> case cb of-    DynEntityObserver f -> f targetEntity dynEvt-    DynEventObserver _ -> pure ()+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- |
+-- Module      : Aztecs.ECS.Access.Internal
+-- 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.Internal
+  ( Access (..),
+    runAccessWith,
+    evalAccess,
+    triggerEvent,
+    triggerEntityEvent,
+  )
+where
+
+import Aztecs.ECS.Entity
+import Aztecs.ECS.Event
+import Aztecs.ECS.World.Internal
+import qualified Aztecs.ECS.World.Observers as O
+import Aztecs.ECS.World.Observers.Internal
+import Control.Monad
+import Control.Monad.Fix
+import Control.Monad.State
+import Data.Dynamic
+import Data.Maybe
+import qualified Data.Set as Set
+import Data.Typeable
+
+-- | Access into a `World`.
+newtype Access m a = Access {unAccess :: StateT (World m) m a}
+  deriving (Functor, Applicative, Monad, MonadFix, MonadIO)
+
+-- | Run an `Access` with a given `World`, returning the result and updated world.
+runAccessWith :: Access m a -> World m -> m (a, World m)
+runAccessWith a = runStateT (unAccess a)
+
+-- | Run an `Access` with a given `World`, returning only the result.
+evalAccess :: (Monad m) => Access m a -> World m -> m a
+evalAccess a = evalStateT (unAccess a)
+
+-- | Trigger an event.
+triggerEvent :: forall m e. (Monad m, Event e) => e -> Access m ()
+triggerEvent evt = Access $ do
+  !w <- get
+  let eventTypeRep = typeOf (Proxy @e)
+      globalOs = O.lookupGlobalObservers eventTypeRep $ observers w
+      callbacks = mapMaybe (\oId -> O.lookupCallback oId (observers w)) $ Set.toList globalOs
+      dynEvt = toDyn evt
+  forM_ callbacks $ \cb -> case cb of
+    DynEventObserver f -> f dynEvt
+    DynEntityObserver _ -> pure ()
+
+-- | Trigger an event for a specific entity.
+triggerEntityEvent ::
+  forall m e.
+  (Monad m, Event e) =>
+  EntityID ->
+  e ->
+  Access m ()
+triggerEntityEvent targetEntity evt = Access $ do
+  !w <- get
+  let eventTypeRep = typeOf $ Proxy @e
+      entityOs = O.lookupEntityObservers eventTypeRep targetEntity $ observers w
+      callbacks = mapMaybe (\oId -> O.lookupCallback oId (observers w)) $ Set.toList entityOs
+      dynEvt = toDyn evt
+  forM_ callbacks $ \cb -> case cb of
+    DynEntityObserver f -> f targetEntity dynEvt
+    DynEventObserver _ -> pure ()
src/Aztecs/ECS/Observer.hs view
@@ -1,102 +1,102 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}---- |--- Module      : Aztecs.ECS.Observer--- 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.Observer-  ( Observer (..),-    ObserverID (..),-    ObserverKind (..),-    observer,-    observerFor,-    observerGlobal,-  )-where--import Aztecs.ECS.Access.Internal-import Aztecs.ECS.Component-import Aztecs.ECS.Entity-import Aztecs.ECS.Event-import qualified Aztecs.ECS.World as W-import Aztecs.ECS.World.Bundle-import Aztecs.ECS.World.Internal (World (..))-import qualified Aztecs.ECS.World.Observers as O-import Aztecs.ECS.World.Observers.Internal-import Control.Monad.State-import Data.Set (Set)-import qualified Data.Set as Set-import Data.Typeable-import Data.Vector (Vector)---- | The kind of observer - either entity-specific or global.-data ObserverKind m e-  = -- | Observe events on specific entities (callback receives EntityID and event).-    EntityObserver !(Set EntityID) !(EntityID -> e -> Access m ())-  | -- | Observe all events of this type globally (callback receives just the event).-    EventObserver !(e -> Access m ())---- | Observer component-data Observer m e = Observer-  { -- | The kind and callback for this observer.-    observerKind :: !(ObserverKind m e),-    -- | The ObserverID assigned after registration (Nothing before registration).-    observerId :: !(Maybe ObserverID)-  }--instance Show (ObserverKind m e) where-  show (EntityObserver targets _) = "EntityObserver " ++ show targets-  show (EventObserver _) = "EventObserver"--instance Show (Observer m e) where-  show o = "Observer { kind = " ++ show (observerKind o) ++ ", id = " ++ show (observerId o) ++ " }"--instance (Monad m, Typeable m, Event e) => Component m (Observer m e) where-  type StorageT (Observer m e) = Vector (Observer m e)--  componentOnInsert ownerEntity o = Access $ do-    !w <- get-    (oId, observers') <- case observerKind o of-      EntityObserver targets f -> do-        let f' eId evt = unAccess $ f eId evt-            (oId, observers') = O.insertEntityObserver @e f' $ observers w-            observers'' = foldr (\e os -> O.addEntityObserver @_ @e e oId os) observers' targets-        return (oId, observers'')-      EventObserver callback -> do-        let wrappedCallback evt = unAccess $ callback evt-            (oId, observers') = O.insertEventObserver @e wrappedCallback (observers w)-            observers'' = O.addGlobalObserver @_ @e oId observers'-        return (oId, observers'')-    let o' = o {observerId = Just oId}-        w' = W.insertUntracked ownerEntity (bundleUntracked o') w {observers = observers'}-    put w'--  componentOnRemove _ownerEntity o = Access $ case observerId o of-    Just oId -> do-      !w <- get-      put w {observers = O.removeObserver oId (observers w)}-    Nothing -> pure ()---- | Create an observer for specific entities.-observerFor :: forall m e. (Event e) => Set EntityID -> (EntityID -> e -> Access m ()) -> Observer m e-observerFor targets callback = Observer {observerKind = EntityObserver targets callback, observerId = Nothing}---- | Create a global observer (observes all events of this type).-observerGlobal :: forall m e. (Event e) => (e -> Access m ()) -> Observer m e-observerGlobal callback = Observer {observerKind = EventObserver callback, observerId = Nothing}---- | Create an observer for a single entity.-observer :: forall m e. (Event e) => EntityID -> (EntityID -> e -> Access m ()) -> Observer m e-observer e = observerFor (Set.singleton e)+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module      : Aztecs.ECS.Observer
+-- 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.Observer
+  ( Observer (..),
+    ObserverID (..),
+    ObserverKind (..),
+    observer,
+    observerFor,
+    observerGlobal,
+  )
+where
+
+import Aztecs.ECS.Access.Internal
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.Event
+import qualified Aztecs.ECS.World as W
+import Aztecs.ECS.World.Bundle
+import Aztecs.ECS.World.Internal (World (..))
+import qualified Aztecs.ECS.World.Observers as O
+import Aztecs.ECS.World.Observers.Internal
+import Control.Monad.State
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Data.Typeable
+import Data.Vector (Vector)
+
+-- | The kind of observer - either entity-specific or global.
+data ObserverKind m e
+  = -- | Observe events on specific entities (callback receives EntityID and event).
+    EntityObserver !(Set EntityID) !(EntityID -> e -> Access m ())
+  | -- | Observe all events of this type globally (callback receives just the event).
+    EventObserver !(e -> Access m ())
+
+-- | Observer component
+data Observer m e = Observer
+  { -- | The kind and callback for this observer.
+    observerKind :: !(ObserverKind m e),
+    -- | The ObserverID assigned after registration (Nothing before registration).
+    observerId :: !(Maybe ObserverID)
+  }
+
+instance Show (ObserverKind m e) where
+  show (EntityObserver targets _) = "EntityObserver " ++ show targets
+  show (EventObserver _) = "EventObserver"
+
+instance Show (Observer m e) where
+  show o = "Observer { kind = " ++ show (observerKind o) ++ ", id = " ++ show (observerId o) ++ " }"
+
+instance (Monad m, Typeable m, Event e) => Component m (Observer m e) where
+  type StorageT (Observer m e) = Vector (Observer m e)
+
+  componentOnInsert ownerEntity o = Access $ do
+    !w <- get
+    (oId, observers') <- case observerKind o of
+      EntityObserver targets f -> do
+        let f' eId evt = unAccess $ f eId evt
+            (oId, observers') = O.insertEntityObserver @e f' $ observers w
+            observers'' = foldr (\e os -> O.addEntityObserver @_ @e e oId os) observers' targets
+        return (oId, observers'')
+      EventObserver callback -> do
+        let wrappedCallback evt = unAccess $ callback evt
+            (oId, observers') = O.insertEventObserver @e wrappedCallback (observers w)
+            observers'' = O.addGlobalObserver @_ @e oId observers'
+        return (oId, observers'')
+    let o' = o {observerId = Just oId}
+        w' = W.insertUntracked ownerEntity (bundleUntracked o') w {observers = observers'}
+    put w'
+
+  componentOnRemove _ownerEntity o = Access $ case observerId o of
+    Just oId -> do
+      !w <- get
+      put w {observers = O.removeObserver oId (observers w)}
+    Nothing -> pure ()
+
+-- | Create an observer for specific entities.
+observerFor :: forall m e. (Event e) => Set EntityID -> (EntityID -> e -> Access m ()) -> Observer m e
+observerFor targets callback = Observer {observerKind = EntityObserver targets callback, observerId = Nothing}
+
+-- | Create a global observer (observes all events of this type).
+observerGlobal :: forall m e. (Event e) => (e -> Access m ()) -> Observer m e
+observerGlobal callback = Observer {observerKind = EventObserver callback, observerId = Nothing}
+
+-- | Create an observer for a single entity.
+observer :: forall m e. (Event e) => EntityID -> (EntityID -> e -> Access m ()) -> Observer m e
+observer e = observerFor (Set.singleton e)
src/Aztecs/ECS/Query/Dynamic.hs view
@@ -1,288 +1,288 @@-{-# LANGUAGE ApplicativeDo #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TypeFamilies #-}---- |--- Module      : Aztecs.ECS.Query.Dynamic--- 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.Query.Dynamic-  ( -- * Dynamic queries-    DynamicQuery (..),-    DynamicQueryF (..),--    -- ** Running-    readQueryDyn,-    readQueryFilteredDyn,-    readQuerySingleDyn,-    readQuerySingleMaybeDyn,-    runQueryDyn,-    runQueryFilteredDyn,-    runQuerySingleDyn,-    runQuerySingleMaybeDyn,--    -- * Dynamic query filters-    DynamicQueryFilter (..),-  )-where--import Aztecs.ECS.Access.Internal (Access)-import Aztecs.ECS.Component-import Aztecs.ECS.Query.Dynamic.Class-import Aztecs.ECS.World.Archetype (Archetype)-import qualified Aztecs.ECS.World.Archetype as A-import Aztecs.ECS.World.Archetypes (Node (..))-import qualified Aztecs.ECS.World.Archetypes as AS-import Aztecs.ECS.World.Entities (Entities (..))-import Aztecs.ECS.World.Storage.Dynamic-import Data.Foldable-import qualified Data.IntMap.Strict as IntMap-import qualified Data.Map as Map-import Data.Set (Set)-import qualified Data.Set as Set-import Data.Vector (Vector)-import qualified Data.Vector as V-import GHC.Stack---- | Dynamic query for components by ID.-newtype DynamicQuery m a-  = DynamicQuery-  { -- | Run a dynamic query.-    ---    -- @since 0.10-    runDynQuery :: Archetype m -> m (Vector a, Archetype m, Access m ())-  }-  deriving (Functor)--instance (Monad m) => Applicative (DynamicQuery m) where-  pure a = DynamicQuery $ \arch -> pure (V.replicate (length $ A.entities arch) a, arch, return ())-  {-# INLINE pure #-}--  f <*> g = DynamicQuery $ \arch -> do-    x <- runDynQuery g arch-    y <- runDynQuery f arch-    return $-      let (as, arch', hook1) = x-          (bs, arch'', hook2) = y-       in (V.zipWith ($) bs as, arch' <> arch'', hook1 >> hook2)-  {-# INLINE (<*>) #-}--instance (Monad m) => DynamicQueryF m (DynamicQuery m) where-  entity = DynamicQuery $ \arch -> pure (V.fromList . Set.toList $ A.entities arch, arch, return ())-  {-# INLINE entity #-}--  queryDyn cId = DynamicQuery $ \arch -> pure (A.lookupComponentsAsc cId arch, arch, return ())-  {-# INLINE queryDyn #-}--  queryMaybeDyn cId = DynamicQuery $ \arch -> case A.lookupComponentsAscMaybe cId arch of-    Just as -> pure (V.map Just as, arch, return ())-    Nothing -> pure (V.replicate (length $ A.entities arch) Nothing, arch, return ())-  {-# INLINE queryMaybeDyn #-}--  queryMapDyn f cId = DynamicQuery $ \arch -> do-    let (cs, arch', hook) = A.zipWith (V.replicate (length $ A.entities arch) ()) (const f) cId arch-    return (cs, arch', hook)-  {-# INLINE queryMapDyn #-}--  queryMapDyn_ f cId = DynamicQuery $ \arch -> do-    let (arch', hook) = A.zipWith_ (V.replicate (length $ A.entities arch) ()) (const f) cId arch-    return (V.replicate (length $ A.entities arch) (), arch', hook)-  {-# INLINE queryMapDyn_ #-}--  queryMapDynM f cId = DynamicQuery $ \arch -> do-    (cs, arch', hook) <- A.zipWithM (V.replicate (length $ A.entities arch) ()) (const f) cId arch-    return (cs, arch', hook)-  {-# INLINE queryMapDynM #-}--  queryMapDynWith f cId q = DynamicQuery $ \arch -> do-    (as, arch', hook1) <- runDynQuery q arch-    let (cs, arch'', hook2) = A.zipWith as f cId arch'-    return (cs, arch'', hook1 >> hook2)-  {-# INLINE queryMapDynWith #-}--  queryMapDynWith_ f cId q = DynamicQuery $ \arch -> do-    (as, arch', hook1) <- runDynQuery q arch-    let (arch'', hook2) = A.zipWith_ as f cId arch'-    return (V.map (const ()) as, arch'', hook1 >> hook2)-  {-# INLINE queryMapDynWith_ #-}--  queryMapDynWithM f cId q = DynamicQuery $ \arch -> do-    (as, arch', hook1) <- runDynQuery q arch-    (cs, arch'', hook2) <- A.zipWithM as f cId arch'-    return (cs, arch'', hook1 >> hook2)-  {-# INLINE queryMapDynWithM #-}--  queryMapDynWithAccum f cId q = DynamicQuery $ \arch -> do-    (bs, arch', hook1) <- runDynQuery q arch-    let (pairs, arch'', hook2) = A.zipWithAccum bs f cId arch'-    return (pairs, arch'', hook1 >> hook2)-  {-# INLINE queryMapDynWithAccum #-}--  queryMapDynWithAccumM f cId q = DynamicQuery $ \arch -> do-    (bs, arch', hook1) <- runDynQuery q arch-    (pairs, arch'', hook2) <- A.zipWithAccumM bs f cId arch'-    return (pairs, arch'', hook1 >> hook2)-  {-# INLINE queryMapDynWithAccumM #-}--  queryUntracked q = DynamicQuery $ \arch -> do-    (as, arch', _hooks) <- runDynQuery q arch-    return (as, arch', return ())-  {-# INLINE queryUntracked #-}--  queryFilterMap p q = DynamicQuery $ \arch -> do-    (as, _, _) <- runDynQuery q arch-    let eIds = V.fromList . Set.toList $ A.entities arch-        mapped = V.map p as-        (filteredEIds, indices, filteredBs) = V.unzip3 . V.imapMaybe (\i (e, mb) -> (\b -> (e, i, b)) <$> mb) $ V.zip eIds mapped-        filteredArch = filterArchetype indices arch-    (_, filteredArch', hook) <- runDynQuery q filteredArch {A.entities = Set.fromList $ V.toList filteredEIds}-    let resultArch = unfilterArchetype indices arch filteredArch'-    return (filteredBs, resultArch, hook)-    where-      filterArchetype indices arch =-        arch {A.storages = IntMap.map (filterStorage indices) $ A.storages arch}-      filterStorage indices s =-        let allVec = toAscVectorDyn s-            filteredVec = V.map (allVec V.!) indices-         in fromAscVectorDyn filteredVec s-      unfilterArchetype indices original filtered =-        original {A.storages = IntMap.mapWithKey go $ A.storages original}-        where-          go cId s = case IntMap.lookup cId (A.storages filtered) of-            Just filteredStorage ->-              let origVec = toAscVectorDyn s-                  filteredVec = toAscVectorDyn filteredStorage-                  mergedVec = V.accum (\_ new -> new) origVec (V.toList $ V.zip indices filteredVec)-               in fromAscVectorDyn mergedVec s-            Nothing -> s-  {-# INLINE queryFilterMap #-}---- | Match all entities.-readQueryDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Vector a)-readQueryDyn cIds q es =-  if Set.null cIds-    then (\(a, _, _) -> a) <$> runDynQuery q A.empty {A.entities = Map.keysSet $ entities es}-    else do-      let go n = (\(a, _, _) -> a) <$> runDynQuery q (AS.nodeArchetype n)-      results <- mapM go . Map.elems $ AS.find cIds $ archetypes es-      return $ V.concat results---- | Match all entities with a filter.-readQueryFilteredDyn :: (Monad m) => Set ComponentID -> (Node m -> Bool) -> DynamicQuery m a -> Entities m -> m (Vector a)-readQueryFilteredDyn cIds f q es =-  if Set.null cIds-    then (\(a, _, _) -> a) <$> runDynQuery q A.empty {A.entities = Map.keysSet $ entities es}-    else do-      let go n = (\(a, _, _) -> a) <$> runDynQuery q (AS.nodeArchetype n)-      results <- mapM go . Map.elems . Map.filter f $ AS.find cIds $ archetypes es-      return $ V.concat results---- | Match a single entity.-readQuerySingleDyn :: (HasCallStack, Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m a-readQuerySingleDyn cIds q es = do-  res <- readQuerySingleMaybeDyn cIds q es-  case res of-    Just a -> return a-    _ -> error "readQuerySingleDyn: expected a single entity"---- | Match a single entity, or `Nothing`.-readQuerySingleMaybeDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Maybe a)-readQuerySingleMaybeDyn cIds q es =-  if Set.null cIds-    then case Map.keys $ entities es of-      [eId] -> do-        (v, _, _) <- runDynQuery q $ A.singleton eId-        return $ if V.length v == 1 then Just (V.head v) else Nothing-      _ -> return Nothing-    else case Map.elems $ AS.find cIds $ archetypes es of-      [n] -> do-        (v, _, _) <- runDynQuery q $ AS.nodeArchetype n-        return $ if V.length v == 1 then Just (V.head v) else Nothing-      _ -> return Nothing---- | Map all matched entities.-runQueryDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Vector a, Entities m, Access m ())-runQueryDyn cIds q es =-  let go = runDynQuery q-   in if Set.null cIds-        then do-          (as, _, hook) <- go A.empty {A.entities = Map.keysSet $ entities es}-          return (as, es, hook)-        else-          let go' (acc, esAcc, hooks) (aId, n) = do-                (as', arch', hook) <- go $ nodeArchetype n-                let !nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes esAcc-                return (as' V.++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}}, hooks >> hook)-           in foldlM go' (V.empty, es, return ()) $ Map.toList . AS.find cIds $ archetypes es-{-# INLINE runQueryDyn #-}---- | Map all matched entities.-runQueryFilteredDyn :: (Monad m) => Set ComponentID -> (Node m -> Bool) -> DynamicQuery m a -> Entities m -> m (Vector a, Entities m, Access m ())-runQueryFilteredDyn cIds f q es =-  let go = runDynQuery q-   in if Set.null cIds-        then do-          (as, _, hook) <- go A.empty {A.entities = Map.keysSet $ entities es}-          return (as, es, hook)-        else-          let go' (acc, esAcc, hooks) (aId, n) = do-                (as', arch', hook) <- go $ nodeArchetype n-                let !nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes esAcc-                return (as' V.++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}}, hooks >> hook)-           in foldlM go' (V.empty, es, return ()) $ Map.toList . Map.filter f . AS.find cIds $ archetypes es-{-# INLINE runQueryFilteredDyn #-}---- | Map a single matched entity.-runQuerySingleDyn :: (HasCallStack, Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (a, Entities m, Access m ())-runQuerySingleDyn cIds q es = do-  res <- runQuerySingleMaybeDyn cIds q es-  return $ case res of-    (Just a, es', hook) -> (a, es', hook)-    _ -> error "querySingleDyn: expected single matching entity"---- | Map a single matched entity, or @Nothing@.-runQuerySingleMaybeDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Maybe a, Entities m, Access m ())-runQuerySingleMaybeDyn cIds q es =-  if Set.null cIds-    then case Map.keys $ entities es of-      [eId] -> do-        res <- runDynQuery q $ A.singleton eId-        return $ case res of-          (v, _, hook) | V.length v == 1 -> (Just (V.head v), es, hook)-          _ -> (Nothing, es, return ())-      _ -> pure (Nothing, es, return ())-    else case Map.toList $ AS.find cIds $ archetypes es of-      [(aId, n)] -> do-        res <- runDynQuery q $ AS.nodeArchetype n-        return $ case res of-          (v, arch', hook)-            | V.length v == 1 ->-                let nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes es-                 in (Just (V.head v), es {archetypes = (archetypes es) {AS.nodes = nodes}}, hook)-          _ -> (Nothing, es, return ())-      _ -> pure (Nothing, es, return ())-{-# INLINE runQuerySingleMaybeDyn #-}---- | Dynamic query filter.-data DynamicQueryFilter = DynamicQueryFilter-  { -- | `ComponentID`s to include.-    filterWith :: !(Set ComponentID),-    -- | `ComponentID`s to exclude.-    filterWithout :: !(Set ComponentID)-  }--instance Semigroup DynamicQueryFilter where-  DynamicQueryFilter withA withoutA <> DynamicQueryFilter withB withoutB =-    DynamicQueryFilter (withA <> withB) (withoutA <> withoutB)-  {-# INLINE (<>) #-}--instance Monoid DynamicQueryFilter where-  mempty = DynamicQueryFilter mempty mempty-  {-# INLINE mempty #-}+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module      : Aztecs.ECS.Query.Dynamic
+-- 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.Query.Dynamic
+  ( -- * Dynamic queries
+    DynamicQuery (..),
+    DynamicQueryF (..),
+
+    -- ** Running
+    readQueryDyn,
+    readQueryFilteredDyn,
+    readQuerySingleDyn,
+    readQuerySingleMaybeDyn,
+    runQueryDyn,
+    runQueryFilteredDyn,
+    runQuerySingleDyn,
+    runQuerySingleMaybeDyn,
+
+    -- * Dynamic query filters
+    DynamicQueryFilter (..),
+  )
+where
+
+import Aztecs.ECS.Access.Internal (Access)
+import Aztecs.ECS.Component
+import Aztecs.ECS.Query.Dynamic.Class
+import Aztecs.ECS.World.Archetype (Archetype)
+import qualified Aztecs.ECS.World.Archetype as A
+import Aztecs.ECS.World.Archetypes (Node (..))
+import qualified Aztecs.ECS.World.Archetypes as AS
+import Aztecs.ECS.World.Entities (Entities (..))
+import Aztecs.ECS.World.Storage.Dynamic
+import Data.Foldable
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.Map as Map
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Data.Vector (Vector)
+import qualified Data.Vector as V
+import GHC.Stack
+
+-- | Dynamic query for components by ID.
+newtype DynamicQuery m a
+  = DynamicQuery
+  { -- | Run a dynamic query.
+    --
+    -- @since 0.10
+    runDynQuery :: Archetype m -> m (Vector a, Archetype m, Access m ())
+  }
+  deriving (Functor)
+
+instance (Monad m) => Applicative (DynamicQuery m) where
+  pure a = DynamicQuery $ \arch -> pure (V.replicate (length $ A.entities arch) a, arch, return ())
+  {-# INLINE pure #-}
+
+  f <*> g = DynamicQuery $ \arch -> do
+    x <- runDynQuery g arch
+    y <- runDynQuery f arch
+    return $
+      let (as, arch', hook1) = x
+          (bs, arch'', hook2) = y
+       in (V.zipWith ($) bs as, arch' <> arch'', hook1 >> hook2)
+  {-# INLINE (<*>) #-}
+
+instance (Monad m) => DynamicQueryF m (DynamicQuery m) where
+  entity = DynamicQuery $ \arch -> pure (V.fromList . Set.toList $ A.entities arch, arch, return ())
+  {-# INLINE entity #-}
+
+  queryDyn cId = DynamicQuery $ \arch -> pure (A.lookupComponentsAsc cId arch, arch, return ())
+  {-# INLINE queryDyn #-}
+
+  queryMaybeDyn cId = DynamicQuery $ \arch -> case A.lookupComponentsAscMaybe cId arch of
+    Just as -> pure (V.map Just as, arch, return ())
+    Nothing -> pure (V.replicate (length $ A.entities arch) Nothing, arch, return ())
+  {-# INLINE queryMaybeDyn #-}
+
+  queryMapDyn f cId = DynamicQuery $ \arch -> do
+    let (cs, arch', hook) = A.zipWith (V.replicate (length $ A.entities arch) ()) (const f) cId arch
+    return (cs, arch', hook)
+  {-# INLINE queryMapDyn #-}
+
+  queryMapDyn_ f cId = DynamicQuery $ \arch -> do
+    let (arch', hook) = A.zipWith_ (V.replicate (length $ A.entities arch) ()) (const f) cId arch
+    return (V.replicate (length $ A.entities arch) (), arch', hook)
+  {-# INLINE queryMapDyn_ #-}
+
+  queryMapDynM f cId = DynamicQuery $ \arch -> do
+    (cs, arch', hook) <- A.zipWithM (V.replicate (length $ A.entities arch) ()) (const f) cId arch
+    return (cs, arch', hook)
+  {-# INLINE queryMapDynM #-}
+
+  queryMapDynWith f cId q = DynamicQuery $ \arch -> do
+    (as, arch', hook1) <- runDynQuery q arch
+    let (cs, arch'', hook2) = A.zipWith as f cId arch'
+    return (cs, arch'', hook1 >> hook2)
+  {-# INLINE queryMapDynWith #-}
+
+  queryMapDynWith_ f cId q = DynamicQuery $ \arch -> do
+    (as, arch', hook1) <- runDynQuery q arch
+    let (arch'', hook2) = A.zipWith_ as f cId arch'
+    return (V.map (const ()) as, arch'', hook1 >> hook2)
+  {-# INLINE queryMapDynWith_ #-}
+
+  queryMapDynWithM f cId q = DynamicQuery $ \arch -> do
+    (as, arch', hook1) <- runDynQuery q arch
+    (cs, arch'', hook2) <- A.zipWithM as f cId arch'
+    return (cs, arch'', hook1 >> hook2)
+  {-# INLINE queryMapDynWithM #-}
+
+  queryMapDynWithAccum f cId q = DynamicQuery $ \arch -> do
+    (bs, arch', hook1) <- runDynQuery q arch
+    let (pairs, arch'', hook2) = A.zipWithAccum bs f cId arch'
+    return (pairs, arch'', hook1 >> hook2)
+  {-# INLINE queryMapDynWithAccum #-}
+
+  queryMapDynWithAccumM f cId q = DynamicQuery $ \arch -> do
+    (bs, arch', hook1) <- runDynQuery q arch
+    (pairs, arch'', hook2) <- A.zipWithAccumM bs f cId arch'
+    return (pairs, arch'', hook1 >> hook2)
+  {-# INLINE queryMapDynWithAccumM #-}
+
+  queryUntracked q = DynamicQuery $ \arch -> do
+    (as, arch', _hooks) <- runDynQuery q arch
+    return (as, arch', return ())
+  {-# INLINE queryUntracked #-}
+
+  queryFilterMap p q = DynamicQuery $ \arch -> do
+    (as, _, _) <- runDynQuery q arch
+    let eIds = V.fromList . Set.toList $ A.entities arch
+        mapped = V.map p as
+        (filteredEIds, indices, filteredBs) = V.unzip3 . V.imapMaybe (\i (e, mb) -> (\b -> (e, i, b)) <$> mb) $ V.zip eIds mapped
+        filteredArch = filterArchetype indices arch
+    (_, filteredArch', hook) <- runDynQuery q filteredArch {A.entities = Set.fromList $ V.toList filteredEIds}
+    let resultArch = unfilterArchetype indices arch filteredArch'
+    return (filteredBs, resultArch, hook)
+    where
+      filterArchetype indices arch =
+        arch {A.storages = IntMap.map (filterStorage indices) $ A.storages arch}
+      filterStorage indices s =
+        let allVec = toAscVectorDyn s
+            filteredVec = V.map (allVec V.!) indices
+         in fromAscVectorDyn filteredVec s
+      unfilterArchetype indices original filtered =
+        original {A.storages = IntMap.mapWithKey go $ A.storages original}
+        where
+          go cId s = case IntMap.lookup cId (A.storages filtered) of
+            Just filteredStorage ->
+              let origVec = toAscVectorDyn s
+                  filteredVec = toAscVectorDyn filteredStorage
+                  mergedVec = V.accum (\_ new -> new) origVec (V.toList $ V.zip indices filteredVec)
+               in fromAscVectorDyn mergedVec s
+            Nothing -> s
+  {-# INLINE queryFilterMap #-}
+
+-- | Match all entities.
+readQueryDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Vector a)
+readQueryDyn cIds q es =
+  if Set.null cIds
+    then (\(a, _, _) -> a) <$> runDynQuery q A.empty {A.entities = Map.keysSet $ entities es}
+    else do
+      let go n = (\(a, _, _) -> a) <$> runDynQuery q (AS.nodeArchetype n)
+      results <- mapM go . Map.elems $ AS.find cIds $ archetypes es
+      return $ V.concat results
+
+-- | Match all entities with a filter.
+readQueryFilteredDyn :: (Monad m) => Set ComponentID -> (Node m -> Bool) -> DynamicQuery m a -> Entities m -> m (Vector a)
+readQueryFilteredDyn cIds f q es =
+  if Set.null cIds
+    then (\(a, _, _) -> a) <$> runDynQuery q A.empty {A.entities = Map.keysSet $ entities es}
+    else do
+      let go n = (\(a, _, _) -> a) <$> runDynQuery q (AS.nodeArchetype n)
+      results <- mapM go . Map.elems . Map.filter f $ AS.find cIds $ archetypes es
+      return $ V.concat results
+
+-- | Match a single entity.
+readQuerySingleDyn :: (HasCallStack, Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m a
+readQuerySingleDyn cIds q es = do
+  res <- readQuerySingleMaybeDyn cIds q es
+  case res of
+    Just a -> return a
+    _ -> error "readQuerySingleDyn: expected a single entity"
+
+-- | Match a single entity, or `Nothing`.
+readQuerySingleMaybeDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Maybe a)
+readQuerySingleMaybeDyn cIds q es =
+  if Set.null cIds
+    then case Map.keys $ entities es of
+      [eId] -> do
+        (v, _, _) <- runDynQuery q $ A.singleton eId
+        return $ if V.length v == 1 then Just (V.head v) else Nothing
+      _ -> return Nothing
+    else case Map.elems $ AS.find cIds $ archetypes es of
+      [n] -> do
+        (v, _, _) <- runDynQuery q $ AS.nodeArchetype n
+        return $ if V.length v == 1 then Just (V.head v) else Nothing
+      _ -> return Nothing
+
+-- | Map all matched entities.
+runQueryDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Vector a, Entities m, Access m ())
+runQueryDyn cIds q es =
+  let go = runDynQuery q
+   in if Set.null cIds
+        then do
+          (as, _, hook) <- go A.empty {A.entities = Map.keysSet $ entities es}
+          return (as, es, hook)
+        else
+          let go' (acc, esAcc, hooks) (aId, n) = do
+                (as', arch', hook) <- go $ nodeArchetype n
+                let !nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes esAcc
+                return (as' V.++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}}, hooks >> hook)
+           in foldlM go' (V.empty, es, return ()) $ Map.toList . AS.find cIds $ archetypes es
+{-# INLINE runQueryDyn #-}
+
+-- | Map all matched entities.
+runQueryFilteredDyn :: (Monad m) => Set ComponentID -> (Node m -> Bool) -> DynamicQuery m a -> Entities m -> m (Vector a, Entities m, Access m ())
+runQueryFilteredDyn cIds f q es =
+  let go = runDynQuery q
+   in if Set.null cIds
+        then do
+          (as, _, hook) <- go A.empty {A.entities = Map.keysSet $ entities es}
+          return (as, es, hook)
+        else
+          let go' (acc, esAcc, hooks) (aId, n) = do
+                (as', arch', hook) <- go $ nodeArchetype n
+                let !nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes esAcc
+                return (as' V.++ acc, esAcc {archetypes = (archetypes esAcc) {AS.nodes = nodes}}, hooks >> hook)
+           in foldlM go' (V.empty, es, return ()) $ Map.toList . Map.filter f . AS.find cIds $ archetypes es
+{-# INLINE runQueryFilteredDyn #-}
+
+-- | Map a single matched entity.
+runQuerySingleDyn :: (HasCallStack, Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (a, Entities m, Access m ())
+runQuerySingleDyn cIds q es = do
+  res <- runQuerySingleMaybeDyn cIds q es
+  return $ case res of
+    (Just a, es', hook) -> (a, es', hook)
+    _ -> error "querySingleDyn: expected single matching entity"
+
+-- | Map a single matched entity, or @Nothing@.
+runQuerySingleMaybeDyn :: (Monad m) => Set ComponentID -> DynamicQuery m a -> Entities m -> m (Maybe a, Entities m, Access m ())
+runQuerySingleMaybeDyn cIds q es =
+  if Set.null cIds
+    then case Map.keys $ entities es of
+      [eId] -> do
+        res <- runDynQuery q $ A.singleton eId
+        return $ case res of
+          (v, _, hook) | V.length v == 1 -> (Just (V.head v), es, hook)
+          _ -> (Nothing, es, return ())
+      _ -> pure (Nothing, es, return ())
+    else case Map.toList $ AS.find cIds $ archetypes es of
+      [(aId, n)] -> do
+        res <- runDynQuery q $ AS.nodeArchetype n
+        return $ case res of
+          (v, arch', hook)
+            | V.length v == 1 ->
+                let nodes = Map.insert aId n {nodeArchetype = arch' <> nodeArchetype n} . AS.nodes $ archetypes es
+                 in (Just (V.head v), es {archetypes = (archetypes es) {AS.nodes = nodes}}, hook)
+          _ -> (Nothing, es, return ())
+      _ -> pure (Nothing, es, return ())
+{-# INLINE runQuerySingleMaybeDyn #-}
+
+-- | Dynamic query filter.
+data DynamicQueryFilter = DynamicQueryFilter
+  { -- | `ComponentID`s to include.
+    filterWith :: !(Set ComponentID),
+    -- | `ComponentID`s to exclude.
+    filterWithout :: !(Set ComponentID)
+  }
+
+instance Semigroup DynamicQueryFilter where
+  DynamicQueryFilter withA withoutA <> DynamicQueryFilter withB withoutB =
+    DynamicQueryFilter (withA <> withB) (withoutA <> withoutB)
+  {-# INLINE (<>) #-}
+
+instance Monoid DynamicQueryFilter where
+  mempty = DynamicQueryFilter mempty mempty
+  {-# INLINE mempty #-}
src/Aztecs/ECS/System/Dynamic.hs view
@@ -1,145 +1,145 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeSynonymInstances #-}---- |--- Module      : Aztecs.ECS.System--- 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.System.Dynamic-  ( -- * Dynamic systems-    DynamicSystem (..),-    runDynamicSystem,--    -- ** Queries-    runQuery,-    runQueryFiltered,-    runQuerySingle,-    runQuerySingleMaybe,-    readQuery,-    readQueryFiltered,-    readQuerySingle,-    readQuerySingleMaybe,-  )-where--import Aztecs.ECS.Access.Internal (Access)-import Aztecs.ECS.Component-import Aztecs.ECS.Query.Dynamic (DynamicQuery)-import qualified Aztecs.ECS.Query.Dynamic as DQ-import Aztecs.ECS.World.Archetypes (Node (..))-import Aztecs.ECS.World.Entities (Entities)-import Data.Set (Set)-import Data.Vector (Vector)-import Prelude hiding (all, filter, map, mapM)---- | Query operation.-data Op m a where-  RunQuery :: DynamicQuery m a -> Op m (Vector a)-  RunFiltered :: (Node m -> Bool) -> DynamicQuery m a -> Op m (Vector a)-  RunQuerySingle :: DynamicQuery m a -> Op m a-  RunQuerySingleMaybe :: DynamicQuery m a -> Op m (Maybe a)-  ReadQuery :: DynamicQuery m a -> Op m (Vector a)-  ReadQueryFiltered :: DynamicQuery m a -> (Node m -> Bool) -> Op m (Vector a)-  ReadQuerySingle :: DynamicQuery m a -> Op m a-  ReadQuerySingleMaybe :: DynamicQuery m a -> Op m (Maybe a)---- | Run a query operation on entities.-runOp :: (Monad m) => Set ComponentID -> Op m a -> Entities m -> m (a, Entities m, Access m ())-runOp cIds (RunQuery q) es = DQ.runQueryDyn cIds q es-runOp cIds (RunFiltered flt q) es = DQ.runQueryFilteredDyn cIds flt q es-runOp cIds (RunQuerySingle q) es = DQ.runQuerySingleDyn cIds q es-runOp cIds (RunQuerySingleMaybe q) es = DQ.runQuerySingleMaybeDyn cIds q es-runOp cIds (ReadQuery q) es = do-  as <- DQ.readQueryDyn cIds q es-  return (as, es, return ())-runOp cIds (ReadQueryFiltered q flt) es = do-  as <- DQ.readQueryFilteredDyn cIds flt q es-  return (as, es, return ())-runOp cIds (ReadQuerySingle q) es = do-  a <- DQ.readQuerySingleDyn cIds q es-  return (a, es, return ())-runOp cIds (ReadQuerySingleMaybe q) es = do-  a <- DQ.readQuerySingleMaybeDyn cIds q es-  return (a, es, return ())-{-# INLINE runOp #-}---- | Dynamic system.-data DynamicSystem m a where-  -- | Pure value.-  Pure :: a -> DynamicSystem m a-  -- | Functor map.-  Map :: (b -> a) -> DynamicSystem m b -> DynamicSystem m a-  -- | Applicative apply.-  Ap :: DynamicSystem m (b -> a) -> DynamicSystem m b -> DynamicSystem m a-  -- | Query operation.-  Op :: Set ComponentID -> Op m a -> DynamicSystem m a--instance Functor (DynamicSystem m) where-  fmap f (Pure a) = Pure (f a)-  fmap f s = Map f s-  {-# INLINE fmap #-}--instance Applicative (DynamicSystem m) where-  pure = Pure-  {-# INLINE pure #-}--  Pure f <*> s = fmap f s-  f <*> Pure a = fmap ($ a) f-  f <*> s = Ap f s-  {-# INLINE (<*>) #-}---- | Run a dynamic system on entities, returning results, updated entities, and hooks to run.-runDynamicSystem :: (Monad m) => DynamicSystem m a -> Entities m -> m (a, Entities m, Access m ())-runDynamicSystem (Pure a) es = return (a, es, return ())-runDynamicSystem (Map f s) es = do-  (b, es', hook) <- runDynamicSystem s es-  return (f b, es', hook)-runDynamicSystem (Ap sf sa) es = do-  (f, es', hook1) <- runDynamicSystem sf es-  (a, es'', hook2) <- runDynamicSystem sa es'-  return (f a, es'', hook1 >> hook2)-runDynamicSystem (Op cIds op) es = runOp cIds op es-{-# INLINE runDynamicSystem #-}--runQuery :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Vector a)-runQuery cIds q = Op cIds (RunQuery q)-{-# INLINE runQuery #-}--runQueryFiltered :: Set ComponentID -> DynamicQuery m a -> (Node m -> Bool) -> DynamicSystem m (Vector a)-runQueryFiltered cIds q flt = Op cIds (RunFiltered flt q)-{-# INLINE runQueryFiltered #-}--runQuerySingle :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m a-runQuerySingle cIds q = Op cIds (RunQuerySingle q)-{-# INLINE runQuerySingle #-}--runQuerySingleMaybe :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Maybe a)-runQuerySingleMaybe cIds q = Op cIds (RunQuerySingleMaybe q)-{-# INLINE runQuerySingleMaybe #-}--readQuery :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Vector a)-readQuery cIds q = Op cIds (ReadQuery q)-{-# INLINE readQuery #-}--readQueryFiltered :: Set ComponentID -> (Node m -> Bool) -> DynamicQuery m a -> DynamicSystem m (Vector a)-readQueryFiltered cIds flt q = Op cIds (ReadQueryFiltered q flt)-{-# INLINE readQueryFiltered #-}--readQuerySingle :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m a-readQuerySingle cIds q = Op cIds (ReadQuerySingle q)--readQuerySingleMaybe :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Maybe a)-readQuerySingleMaybe cIds q = Op cIds (ReadQuerySingleMaybe q)-{-# INLINE readQuerySingleMaybe #-}+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+-- |
+-- Module      : Aztecs.ECS.System
+-- 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.System.Dynamic
+  ( -- * Dynamic systems
+    DynamicSystem (..),
+    runDynamicSystem,
+
+    -- ** Queries
+    runQuery,
+    runQueryFiltered,
+    runQuerySingle,
+    runQuerySingleMaybe,
+    readQuery,
+    readQueryFiltered,
+    readQuerySingle,
+    readQuerySingleMaybe,
+  )
+where
+
+import Aztecs.ECS.Access.Internal (Access)
+import Aztecs.ECS.Component
+import Aztecs.ECS.Query.Dynamic (DynamicQuery)
+import qualified Aztecs.ECS.Query.Dynamic as DQ
+import Aztecs.ECS.World.Archetypes (Node (..))
+import Aztecs.ECS.World.Entities (Entities)
+import Data.Set (Set)
+import Data.Vector (Vector)
+import Prelude hiding (all, filter, map, mapM)
+
+-- | Query operation.
+data Op m a where
+  RunQuery :: DynamicQuery m a -> Op m (Vector a)
+  RunFiltered :: (Node m -> Bool) -> DynamicQuery m a -> Op m (Vector a)
+  RunQuerySingle :: DynamicQuery m a -> Op m a
+  RunQuerySingleMaybe :: DynamicQuery m a -> Op m (Maybe a)
+  ReadQuery :: DynamicQuery m a -> Op m (Vector a)
+  ReadQueryFiltered :: DynamicQuery m a -> (Node m -> Bool) -> Op m (Vector a)
+  ReadQuerySingle :: DynamicQuery m a -> Op m a
+  ReadQuerySingleMaybe :: DynamicQuery m a -> Op m (Maybe a)
+
+-- | Run a query operation on entities.
+runOp :: (Monad m) => Set ComponentID -> Op m a -> Entities m -> m (a, Entities m, Access m ())
+runOp cIds (RunQuery q) es = DQ.runQueryDyn cIds q es
+runOp cIds (RunFiltered flt q) es = DQ.runQueryFilteredDyn cIds flt q es
+runOp cIds (RunQuerySingle q) es = DQ.runQuerySingleDyn cIds q es
+runOp cIds (RunQuerySingleMaybe q) es = DQ.runQuerySingleMaybeDyn cIds q es
+runOp cIds (ReadQuery q) es = do
+  as <- DQ.readQueryDyn cIds q es
+  return (as, es, return ())
+runOp cIds (ReadQueryFiltered q flt) es = do
+  as <- DQ.readQueryFilteredDyn cIds flt q es
+  return (as, es, return ())
+runOp cIds (ReadQuerySingle q) es = do
+  a <- DQ.readQuerySingleDyn cIds q es
+  return (a, es, return ())
+runOp cIds (ReadQuerySingleMaybe q) es = do
+  a <- DQ.readQuerySingleMaybeDyn cIds q es
+  return (a, es, return ())
+{-# INLINE runOp #-}
+
+-- | Dynamic system.
+data DynamicSystem m a where
+  -- | Pure value.
+  Pure :: a -> DynamicSystem m a
+  -- | Functor map.
+  Map :: (b -> a) -> DynamicSystem m b -> DynamicSystem m a
+  -- | Applicative apply.
+  Ap :: DynamicSystem m (b -> a) -> DynamicSystem m b -> DynamicSystem m a
+  -- | Query operation.
+  Op :: Set ComponentID -> Op m a -> DynamicSystem m a
+
+instance Functor (DynamicSystem m) where
+  fmap f (Pure a) = Pure (f a)
+  fmap f s = Map f s
+  {-# INLINE fmap #-}
+
+instance Applicative (DynamicSystem m) where
+  pure = Pure
+  {-# INLINE pure #-}
+
+  Pure f <*> s = fmap f s
+  f <*> Pure a = fmap ($ a) f
+  f <*> s = Ap f s
+  {-# INLINE (<*>) #-}
+
+-- | Run a dynamic system on entities, returning results, updated entities, and hooks to run.
+runDynamicSystem :: (Monad m) => DynamicSystem m a -> Entities m -> m (a, Entities m, Access m ())
+runDynamicSystem (Pure a) es = return (a, es, return ())
+runDynamicSystem (Map f s) es = do
+  (b, es', hook) <- runDynamicSystem s es
+  return (f b, es', hook)
+runDynamicSystem (Ap sf sa) es = do
+  (f, es', hook1) <- runDynamicSystem sf es
+  (a, es'', hook2) <- runDynamicSystem sa es'
+  return (f a, es'', hook1 >> hook2)
+runDynamicSystem (Op cIds op) es = runOp cIds op es
+{-# INLINE runDynamicSystem #-}
+
+runQuery :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Vector a)
+runQuery cIds q = Op cIds (RunQuery q)
+{-# INLINE runQuery #-}
+
+runQueryFiltered :: Set ComponentID -> DynamicQuery m a -> (Node m -> Bool) -> DynamicSystem m (Vector a)
+runQueryFiltered cIds q flt = Op cIds (RunFiltered flt q)
+{-# INLINE runQueryFiltered #-}
+
+runQuerySingle :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m a
+runQuerySingle cIds q = Op cIds (RunQuerySingle q)
+{-# INLINE runQuerySingle #-}
+
+runQuerySingleMaybe :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Maybe a)
+runQuerySingleMaybe cIds q = Op cIds (RunQuerySingleMaybe q)
+{-# INLINE runQuerySingleMaybe #-}
+
+readQuery :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Vector a)
+readQuery cIds q = Op cIds (ReadQuery q)
+{-# INLINE readQuery #-}
+
+readQueryFiltered :: Set ComponentID -> (Node m -> Bool) -> DynamicQuery m a -> DynamicSystem m (Vector a)
+readQueryFiltered cIds flt q = Op cIds (ReadQueryFiltered q flt)
+{-# INLINE readQueryFiltered #-}
+
+readQuerySingle :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m a
+readQuerySingle cIds q = Op cIds (ReadQuerySingle q)
+
+readQuerySingleMaybe :: Set ComponentID -> DynamicQuery m a -> DynamicSystem m (Maybe a)
+readQuerySingleMaybe cIds q = Op cIds (ReadQuerySingleMaybe q)
+{-# INLINE readQuerySingleMaybe #-}
src/Aztecs/ECS/World.hs view
@@ -1,89 +1,89 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeFamilies #-}---- |--- Module      : Aztecs.ECS.World--- 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.World-  ( World (..),-    empty,-    spawn,-    spawnEmpty,-    insert,-    insertUntracked,-    lookup,-    remove,-    removeWithId,-    despawn,-  )-where--import Aztecs.ECS.Access.Internal (Access)-import Aztecs.ECS.Component-import Aztecs.ECS.Entity-import Aztecs.ECS.World.Bundle-import qualified Aztecs.ECS.World.Entities as E-import Aztecs.ECS.World.Internal (World (..))-import qualified Aztecs.ECS.World.Observers as O-import Data.Dynamic-import Data.IntMap (IntMap)-import Prelude hiding (lookup)---- | Empty `World`.-empty :: World m-empty =-  World-    { entities = E.empty,-      nextEntityId = EntityID 0,-      observers = O.empty-    }---- | Spawn a `Bundle` into the `World`. Returns the entity ID, updated world, and onInsert hook.-spawn :: (Monad m) => BundleT m -> World m -> (EntityID, World m, Access m ())-spawn b w =-  let e = nextEntityId w-      (es', hook) = E.spawn e b $ entities w-   in (e, w {entities = es', nextEntityId = EntityID $ unEntityId e + 1}, hook)---- | Spawn an empty entity.-spawnEmpty :: World m -> (EntityID, World m)-spawnEmpty w = let e = nextEntityId w in (e, w {nextEntityId = EntityID $ unEntityId e + 1})---- | Insert a `Bundle` into an entity. Returns updated world and onInsert hook.-insert :: (Monad m) => EntityID -> BundleT m -> World m -> (World m, Access m ())-insert e c w =-  let (es', hook) = E.insert e c (entities w)-   in (w {entities = es'}, hook)---- | Insert a `Bundle` into an entity without running lifecycle hooks.-insertUntracked :: (Monad m) => EntityID -> BundleT m -> World m -> World m-insertUntracked e c w =-  let es' = E.insertUntracked e c (entities w)-   in w {entities = es'}---- | Lookup a component in an entity.-lookup :: forall m a. (Component m a) => EntityID -> World m -> Maybe a-lookup e w = E.lookup e $ entities w---- | Remove a component from an entity. Returns the component (if found), updated world, and onRemove hook.-remove :: forall m a. (Component m a) => EntityID -> World m -> (Maybe a, World m, Access m ())-remove e w =-  let (a, es, hook) = E.remove e (entities w)-   in (a, w {entities = es}, hook)---- | Remove a component from an entity with its `ComponentID`. Returns the component (if found), updated world, and onRemove hook.-removeWithId :: forall m a. (Component m a) => EntityID -> ComponentID -> World m -> (Maybe a, World m, Access m ())-removeWithId e cId w =-  let (a, es, hook) = E.removeWithId e cId (entities w)-   in (a, w {entities = es}, hook)---- | Despawn an entity, returning its components.-despawn :: EntityID -> World m -> (IntMap Dynamic, World m)-despawn e w = let (a, es) = E.despawn e (entities w) in (a, w {entities = es})+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module      : Aztecs.ECS.World
+-- 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.World
+  ( World (..),
+    empty,
+    spawn,
+    spawnEmpty,
+    insert,
+    insertUntracked,
+    lookup,
+    remove,
+    removeWithId,
+    despawn,
+  )
+where
+
+import Aztecs.ECS.Access.Internal (Access)
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Bundle
+import qualified Aztecs.ECS.World.Entities as E
+import Aztecs.ECS.World.Internal (World (..))
+import qualified Aztecs.ECS.World.Observers as O
+import Data.Dynamic
+import Data.IntMap (IntMap)
+import Prelude hiding (lookup)
+
+-- | Empty `World`.
+empty :: World m
+empty =
+  World
+    { entities = E.empty,
+      nextEntityId = EntityID 0,
+      observers = O.empty
+    }
+
+-- | Spawn a `Bundle` into the `World`. Returns the entity ID, updated world, and onInsert hook.
+spawn :: (Monad m) => BundleT m -> World m -> (EntityID, World m, Access m ())
+spawn b w =
+  let e = nextEntityId w
+      (es', hook) = E.spawn e b $ entities w
+   in (e, w {entities = es', nextEntityId = EntityID $ unEntityId e + 1}, hook)
+
+-- | Spawn an empty entity.
+spawnEmpty :: World m -> (EntityID, World m)
+spawnEmpty w = let e = nextEntityId w in (e, w {nextEntityId = EntityID $ unEntityId e + 1})
+
+-- | Insert a `Bundle` into an entity. Returns updated world and onInsert hook.
+insert :: (Monad m) => EntityID -> BundleT m -> World m -> (World m, Access m ())
+insert e c w =
+  let (es', hook) = E.insert e c (entities w)
+   in (w {entities = es'}, hook)
+
+-- | Insert a `Bundle` into an entity without running lifecycle hooks.
+insertUntracked :: (Monad m) => EntityID -> BundleT m -> World m -> World m
+insertUntracked e c w =
+  let es' = E.insertUntracked e c (entities w)
+   in w {entities = es'}
+
+-- | Lookup a component in an entity.
+lookup :: forall m a. (Component m a) => EntityID -> World m -> Maybe a
+lookup e w = E.lookup e $ entities w
+
+-- | Remove a component from an entity. Returns the component (if found), updated world, and onRemove hook.
+remove :: forall m a. (Component m a) => EntityID -> World m -> (Maybe a, World m, Access m ())
+remove e w =
+  let (a, es, hook) = E.remove e (entities w)
+   in (a, w {entities = es}, hook)
+
+-- | Remove a component from an entity with its `ComponentID`. Returns the component (if found), updated world, and onRemove hook.
+removeWithId :: forall m a. (Component m a) => EntityID -> ComponentID -> World m -> (Maybe a, World m, Access m ())
+removeWithId e cId w =
+  let (a, es, hook) = E.removeWithId e cId (entities w)
+   in (a, w {entities = es}, hook)
+
+-- | Despawn an entity, returning its components.
+despawn :: EntityID -> World m -> (IntMap Dynamic, World m)
+despawn e w = let (a, es) = E.despawn e (entities w) in (a, w {entities = es})
src/Aztecs/ECS/World/Archetype/Internal.hs view
@@ -1,41 +1,41 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE KindSignatures #-}---- |--- Module      : Aztecs.ECS.World.Archetype.Internal--- 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.World.Archetype.Internal (Archetype (..), empty) where--import Aztecs.ECS.Entity (EntityID)-import Aztecs.ECS.World.Storage.Dynamic (DynamicStorage)-import Data.IntMap (IntMap)-import qualified Data.IntMap as IntMap-import Data.Kind (Type)-import Data.Set (Set)-import qualified Data.Set as Set-import GHC.Generics---- | Archetype of entities and components.--- An archetype is guranteed to contain one of each stored component per entity.-data Archetype (m :: Type -> Type) = Archetype-  { -- | Component storages.-    storages :: !(IntMap DynamicStorage),-    -- | Entities stored in this archetype.-    entities :: !(Set EntityID)-  }-  deriving (Show, Generic)--instance Semigroup (Archetype m) where-  a <> b = Archetype {storages = storages a <> storages b, entities = entities a <> entities b}--instance Monoid (Archetype m) where-  mempty = empty---- | Empty archetype.-empty :: Archetype m-empty = Archetype {storages = IntMap.empty, entities = Set.empty}+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE KindSignatures #-}
+
+-- |
+-- Module      : Aztecs.ECS.World.Archetype.Internal
+-- 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.World.Archetype.Internal (Archetype (..), empty) where
+
+import Aztecs.ECS.Entity (EntityID)
+import Aztecs.ECS.World.Storage.Dynamic (DynamicStorage)
+import Data.IntMap (IntMap)
+import qualified Data.IntMap as IntMap
+import Data.Kind (Type)
+import Data.Set (Set)
+import qualified Data.Set as Set
+import GHC.Generics
+
+-- | Archetype of entities and components.
+-- An archetype is guranteed to contain one of each stored component per entity.
+data Archetype (m :: Type -> Type) = Archetype
+  { -- | Component storages.
+    storages :: !(IntMap DynamicStorage),
+    -- | Entities stored in this archetype.
+    entities :: !(Set EntityID)
+  }
+  deriving (Show, Generic)
+
+instance Semigroup (Archetype m) where
+  a <> b = Archetype {storages = storages a <> storages b, entities = entities a <> entities b}
+
+instance Monoid (Archetype m) where
+  mempty = empty
+
+-- | Empty archetype.
+empty :: Archetype m
+empty = Archetype {storages = IntMap.empty, entities = Set.empty}
src/Aztecs/ECS/World/Archetypes.hs view
@@ -1,204 +1,204 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}---- |--- Module      : Aztecs.ECS.World.Archetypes--- 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.World.Archetypes-  ( ArchetypeID (..),-    Node (..),-    Archetypes (..),-    empty,-    insertArchetype,-    lookupArchetypeId,-    findArchetypeIds,-    lookup,-    find,-    map,-    adjustArchetype,-    insert,-    remove,-  )-where--import Aztecs.ECS.Access.Internal-import Aztecs.ECS.Component-import Aztecs.ECS.Entity-import Aztecs.ECS.Event-import Aztecs.ECS.World.Archetype (Archetype (..))-import qualified Aztecs.ECS.World.Archetype as A-import Aztecs.ECS.World.Archetypes.Internal-import Aztecs.ECS.World.Bundle.Dynamic-import Aztecs.ECS.World.Storage.Dynamic-import Data.Dynamic-import Data.Foldable hiding (find)-import qualified Data.IntMap.Strict as IntMap-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as Map-import Data.Maybe-import Data.Set (Set)-import qualified Data.Set as Set-import qualified Data.Vector as V-import Prelude hiding (all, lookup, map)---- | Empty `Archetypes`.-empty :: Archetypes m-empty =-  Archetypes-    { nodes = mempty,-      archetypeIds = mempty,-      nextArchetypeId = ArchetypeID 0,-      componentIds = mempty-    }---- | Insert an archetype by its set of `ComponentID`s.-insertArchetype :: Set ComponentID -> Node m -> Archetypes m -> (ArchetypeID, Archetypes m)-insertArchetype cIds n arches =-  let aId = nextArchetypeId arches-   in ( aId,-        arches-          { nodes = Map.insert aId n (nodes arches),-            archetypeIds = Map.insert cIds aId (archetypeIds arches),-            nextArchetypeId = ArchetypeID (unArchetypeId aId + 1),-            componentIds = Map.unionWith (<>) (Map.fromSet (const (Set.singleton aId)) cIds) (componentIds arches)-          }-      )---- | Adjust an `Archetype` by its `ArchetypeID`.-adjustArchetype :: ArchetypeID -> (Archetype m -> Archetype m) -> Archetypes m -> Archetypes m-adjustArchetype aId f arches =-  arches {nodes = Map.adjust (\node -> node {nodeArchetype = f (nodeArchetype node)}) aId (nodes arches)}---- | Find `ArchetypeID`s containing a set of `ComponentID`s.-findArchetypeIds :: Set ComponentID -> Archetypes m -> Set ArchetypeID-findArchetypeIds cIds arches = case mapMaybe (\cId -> Map.lookup cId (componentIds arches)) (Set.elems cIds) of-  (aId : aIds') -> foldl' Set.intersection aId aIds'-  [] -> Set.empty---- | Lookup `Archetype`s containing a set of `ComponentID`s.-find :: Set ComponentID -> Archetypes m -> Map ArchetypeID (Node m)-find cIds arches = Map.fromSet (\aId -> nodes arches Map.! aId) (findArchetypeIds cIds arches)---- | Map over `Archetype`s containing a set of `ComponentID`s.-map :: Set ComponentID -> (Archetype m -> (a, Archetype m)) -> Archetypes m -> ([a], Archetypes m)-map cIds f arches =-  let go (acc, archAcc) aId =-        let !node = nodes archAcc Map.! aId-            !(a, arch') = f (nodeArchetype node)-            nodes' = Map.insert aId (node {nodeArchetype = arch'}) (nodes archAcc)-         in (a : acc, archAcc {nodes = nodes'})-   in foldl' go ([], arches) $ findArchetypeIds cIds arches---- | Lookup an `ArchetypeID` by its set of `ComponentID`s.-lookupArchetypeId :: Set ComponentID -> Archetypes m -> Maybe ArchetypeID-lookupArchetypeId cIds arches = Map.lookup cIds (archetypeIds arches)---- | Lookup an `Archetype` by its `ArchetypeID`.-lookup :: ArchetypeID -> Archetypes m -> Maybe (Node m)-lookup aId arches = Map.lookup aId (nodes arches)---- | Insert a component into an entity with its `ComponentID`.-insert ::-  (Monad m) =>-  EntityID ->-  ArchetypeID ->-  Set ComponentID ->-  DynamicBundle m ->-  Archetypes m ->-  (Maybe ArchetypeID, Archetypes m, Access m ())-insert e aId cIds b arches = case lookup aId arches of-  Just node ->-    if Set.isSubsetOf cIds $ nodeComponentIds node-      then-        let go n =-              let (arch', hook) = runDynamicBundle b e $ nodeArchetype n-               in (n {nodeArchetype = arch'}, hook)-            (hooks, nodes') =-              Map.alterF-                ( \maybeN -> case maybeN of-                    Just n -> let (n', hook) = go n in (hook, Just n')-                    Nothing -> (return (), Nothing)-                )-                aId-                $ nodes arches-         in (Nothing, arches {nodes = nodes'}, hooks)-      else-        let cIds' = cIds <> nodeComponentIds node-         in case lookupArchetypeId cIds' arches of-              Just nextAId ->-                let !(cs, arch) = A.remove e $ nodeArchetype node-                    node' = node {nodeArchetype = arch}-                    !nodes' = Map.insert aId node' $ nodes arches-                    adjustNode nextNode =-                      let nextArch = nodeArchetype nextNode-                          nextArch' = nextArch {A.entities = Set.insert e $ A.entities nextArch}-                          !nextArch'' = A.insertComponents e cs nextArch'-                          (finalArch, hook) = runDynamicBundle b e nextArch''-                       in (nextNode {nodeArchetype = finalArch}, hook)-                    (hooks, nodes'') =-                      Map.alterF-                        ( \maybeN -> case maybeN of-                            Just n -> let (n', hook) = adjustNode n in (hook, Just n')-                            Nothing -> (return (), Nothing)-                        )-                        nextAId-                        nodes'-                 in (Just nextAId, arches {nodes = nodes''}, hooks)-              Nothing ->-                let !(s, arch) = A.removeStorages e $ nodeArchetype node-                    nodes' = Map.insert aId node {nodeArchetype = arch} $ nodes arches-                    (nextArch, hook) = runDynamicBundle b e Archetype {storages = s, entities = Set.singleton e}-                    !n = Node {nodeComponentIds = cIds', nodeArchetype = nextArch}-                    !(nextAId, arches') = insertArchetype cIds' n arches {nodes = nodes'}-                 in (Just nextAId, arches', hook)-  Nothing -> (Nothing, arches, return ())---- | Remove a component from an entity with its `ComponentID`.-remove ::-  (Component m a) =>-  EntityID ->-  ArchetypeID ->-  ComponentID ->-  Archetypes m ->-  (Maybe (a, ArchetypeID), Archetypes m, Access m ())-remove e aId cId arches = case lookup aId arches of-  Just node -> case lookupArchetypeId (Set.delete cId (nodeComponentIds node)) arches of-    Just nextAId ->-      let !(cs, arch') = A.remove e (nodeArchetype node)-          !arches' = arches {nodes = Map.insert aId node {nodeArchetype = arch'} (nodes arches)}-          (a, cs') = IntMap.updateLookupWithKey (\_ _ -> Nothing) (unComponentId cId) cs-          go' archAcc (itemCId, dyn) =-            let adjustStorage s = fromAscVectorDyn (V.fromList . Map.elems . Map.insert e dyn . Map.fromAscList . zip (Set.toList $ entities archAcc) . V.toList $ toAscVectorDyn s) s-             in archAcc {storages = IntMap.adjust adjustStorage itemCId (storages archAcc)}-          go nextNode =-            nextNode {nodeArchetype = foldl' go' (nodeArchetype nextNode) (IntMap.toList cs')}-          maybeA = a >>= fromDynamic-          hook = maybe (return ()) (\comp -> componentOnRemove e comp >> triggerEntityEvent e (OnRemove comp)) maybeA-       in ( (,nextAId) <$> maybeA,-            arches' {nodes = Map.adjust go nextAId (nodes arches')},-            hook-          )-    Nothing ->-      let !(cs, arch') = A.removeStorages e (nodeArchetype node)-          (a, cs') = IntMap.updateLookupWithKey (\_ _ -> Nothing) (unComponentId cId) cs-          destCIds = Set.delete cId (nodeComponentIds node)-          !n =-            Node-              { nodeComponentIds = destCIds,-                nodeArchetype = Archetype {storages = cs', entities = Set.singleton e}-              }-          !(nextAId, arches') = insertArchetype destCIds n arches-          node' = node {nodeArchetype = arch'}-          maybeA = a >>= (\dynS -> V.headM (toAscVectorDyn dynS) >>= fromDynamic)-          hook = maybe (return ()) (\comp -> componentOnRemove e comp >> triggerEntityEvent e (OnRemove comp)) maybeA-       in ( (,nextAId) <$> maybeA,-            arches' {nodes = Map.insert aId node' (nodes arches')},-            hook-          )-  Nothing -> (Nothing, arches, return ())+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+
+-- |
+-- Module      : Aztecs.ECS.World.Archetypes
+-- 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.World.Archetypes
+  ( ArchetypeID (..),
+    Node (..),
+    Archetypes (..),
+    empty,
+    insertArchetype,
+    lookupArchetypeId,
+    findArchetypeIds,
+    lookup,
+    find,
+    map,
+    adjustArchetype,
+    insert,
+    remove,
+  )
+where
+
+import Aztecs.ECS.Access.Internal
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import Aztecs.ECS.Event
+import Aztecs.ECS.World.Archetype (Archetype (..))
+import qualified Aztecs.ECS.World.Archetype as A
+import Aztecs.ECS.World.Archetypes.Internal
+import Aztecs.ECS.World.Bundle.Dynamic
+import Aztecs.ECS.World.Storage.Dynamic
+import Data.Dynamic
+import Data.Foldable hiding (find)
+import qualified Data.IntMap.Strict as IntMap
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Maybe
+import Data.Set (Set)
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Prelude hiding (all, lookup, map)
+
+-- | Empty `Archetypes`.
+empty :: Archetypes m
+empty =
+  Archetypes
+    { nodes = mempty,
+      archetypeIds = mempty,
+      nextArchetypeId = ArchetypeID 0,
+      componentIds = mempty
+    }
+
+-- | Insert an archetype by its set of `ComponentID`s.
+insertArchetype :: Set ComponentID -> Node m -> Archetypes m -> (ArchetypeID, Archetypes m)
+insertArchetype cIds n arches =
+  let aId = nextArchetypeId arches
+   in ( aId,
+        arches
+          { nodes = Map.insert aId n (nodes arches),
+            archetypeIds = Map.insert cIds aId (archetypeIds arches),
+            nextArchetypeId = ArchetypeID (unArchetypeId aId + 1),
+            componentIds = Map.unionWith (<>) (Map.fromSet (const (Set.singleton aId)) cIds) (componentIds arches)
+          }
+      )
+
+-- | Adjust an `Archetype` by its `ArchetypeID`.
+adjustArchetype :: ArchetypeID -> (Archetype m -> Archetype m) -> Archetypes m -> Archetypes m
+adjustArchetype aId f arches =
+  arches {nodes = Map.adjust (\node -> node {nodeArchetype = f (nodeArchetype node)}) aId (nodes arches)}
+
+-- | Find `ArchetypeID`s containing a set of `ComponentID`s.
+findArchetypeIds :: Set ComponentID -> Archetypes m -> Set ArchetypeID
+findArchetypeIds cIds arches = case mapMaybe (\cId -> Map.lookup cId (componentIds arches)) (Set.elems cIds) of
+  (aId : aIds') -> foldl' Set.intersection aId aIds'
+  [] -> Set.empty
+
+-- | Lookup `Archetype`s containing a set of `ComponentID`s.
+find :: Set ComponentID -> Archetypes m -> Map ArchetypeID (Node m)
+find cIds arches = Map.fromSet (\aId -> nodes arches Map.! aId) (findArchetypeIds cIds arches)
+
+-- | Map over `Archetype`s containing a set of `ComponentID`s.
+map :: Set ComponentID -> (Archetype m -> (a, Archetype m)) -> Archetypes m -> ([a], Archetypes m)
+map cIds f arches =
+  let go (acc, archAcc) aId =
+        let !node = nodes archAcc Map.! aId
+            !(a, arch') = f (nodeArchetype node)
+            nodes' = Map.insert aId (node {nodeArchetype = arch'}) (nodes archAcc)
+         in (a : acc, archAcc {nodes = nodes'})
+   in foldl' go ([], arches) $ findArchetypeIds cIds arches
+
+-- | Lookup an `ArchetypeID` by its set of `ComponentID`s.
+lookupArchetypeId :: Set ComponentID -> Archetypes m -> Maybe ArchetypeID
+lookupArchetypeId cIds arches = Map.lookup cIds (archetypeIds arches)
+
+-- | Lookup an `Archetype` by its `ArchetypeID`.
+lookup :: ArchetypeID -> Archetypes m -> Maybe (Node m)
+lookup aId arches = Map.lookup aId (nodes arches)
+
+-- | Insert a component into an entity with its `ComponentID`.
+insert ::
+  (Monad m) =>
+  EntityID ->
+  ArchetypeID ->
+  Set ComponentID ->
+  DynamicBundle m ->
+  Archetypes m ->
+  (Maybe ArchetypeID, Archetypes m, Access m ())
+insert e aId cIds b arches = case lookup aId arches of
+  Just node ->
+    if Set.isSubsetOf cIds $ nodeComponentIds node
+      then
+        let go n =
+              let (arch', hook) = runDynamicBundle b e $ nodeArchetype n
+               in (n {nodeArchetype = arch'}, hook)
+            (hooks, nodes') =
+              Map.alterF
+                ( \maybeN -> case maybeN of
+                    Just n -> let (n', hook) = go n in (hook, Just n')
+                    Nothing -> (return (), Nothing)
+                )
+                aId
+                $ nodes arches
+         in (Nothing, arches {nodes = nodes'}, hooks)
+      else
+        let cIds' = cIds <> nodeComponentIds node
+         in case lookupArchetypeId cIds' arches of
+              Just nextAId ->
+                let !(cs, arch) = A.remove e $ nodeArchetype node
+                    node' = node {nodeArchetype = arch}
+                    !nodes' = Map.insert aId node' $ nodes arches
+                    adjustNode nextNode =
+                      let nextArch = nodeArchetype nextNode
+                          nextArch' = nextArch {A.entities = Set.insert e $ A.entities nextArch}
+                          !nextArch'' = A.insertComponents e cs nextArch'
+                          (finalArch, hook) = runDynamicBundle b e nextArch''
+                       in (nextNode {nodeArchetype = finalArch}, hook)
+                    (hooks, nodes'') =
+                      Map.alterF
+                        ( \maybeN -> case maybeN of
+                            Just n -> let (n', hook) = adjustNode n in (hook, Just n')
+                            Nothing -> (return (), Nothing)
+                        )
+                        nextAId
+                        nodes'
+                 in (Just nextAId, arches {nodes = nodes''}, hooks)
+              Nothing ->
+                let !(s, arch) = A.removeStorages e $ nodeArchetype node
+                    nodes' = Map.insert aId node {nodeArchetype = arch} $ nodes arches
+                    (nextArch, hook) = runDynamicBundle b e Archetype {storages = s, entities = Set.singleton e}
+                    !n = Node {nodeComponentIds = cIds', nodeArchetype = nextArch}
+                    !(nextAId, arches') = insertArchetype cIds' n arches {nodes = nodes'}
+                 in (Just nextAId, arches', hook)
+  Nothing -> (Nothing, arches, return ())
+
+-- | Remove a component from an entity with its `ComponentID`.
+remove ::
+  (Component m a) =>
+  EntityID ->
+  ArchetypeID ->
+  ComponentID ->
+  Archetypes m ->
+  (Maybe (a, ArchetypeID), Archetypes m, Access m ())
+remove e aId cId arches = case lookup aId arches of
+  Just node -> case lookupArchetypeId (Set.delete cId (nodeComponentIds node)) arches of
+    Just nextAId ->
+      let !(cs, arch') = A.remove e (nodeArchetype node)
+          !arches' = arches {nodes = Map.insert aId node {nodeArchetype = arch'} (nodes arches)}
+          (a, cs') = IntMap.updateLookupWithKey (\_ _ -> Nothing) (unComponentId cId) cs
+          go' archAcc (itemCId, dyn) =
+            let adjustStorage s = fromAscVectorDyn (V.fromList . Map.elems . Map.insert e dyn . Map.fromAscList . zip (Set.toList $ entities archAcc) . V.toList $ toAscVectorDyn s) s
+             in archAcc {storages = IntMap.adjust adjustStorage itemCId (storages archAcc)}
+          go nextNode =
+            nextNode {nodeArchetype = foldl' go' (nodeArchetype nextNode) (IntMap.toList cs')}
+          maybeA = a >>= fromDynamic
+          hook = maybe (return ()) (\comp -> componentOnRemove e comp >> triggerEntityEvent e (OnRemove comp)) maybeA
+       in ( (,nextAId) <$> maybeA,
+            arches' {nodes = Map.adjust go nextAId (nodes arches')},
+            hook
+          )
+    Nothing ->
+      let !(cs, arch') = A.removeStorages e (nodeArchetype node)
+          (a, cs') = IntMap.updateLookupWithKey (\_ _ -> Nothing) (unComponentId cId) cs
+          destCIds = Set.delete cId (nodeComponentIds node)
+          !n =
+            Node
+              { nodeComponentIds = destCIds,
+                nodeArchetype = Archetype {storages = cs', entities = Set.singleton e}
+              }
+          !(nextAId, arches') = insertArchetype destCIds n arches
+          node' = node {nodeArchetype = arch'}
+          maybeA = a >>= (\dynS -> V.headM (toAscVectorDyn dynS) >>= fromDynamic)
+          hook = maybe (return ()) (\comp -> componentOnRemove e comp >> triggerEntityEvent e (OnRemove comp)) maybeA
+       in ( (,nextAId) <$> maybeA,
+            arches' {nodes = Map.insert aId node' (nodes arches')},
+            hook
+          )
+  Nothing -> (Nothing, arches, return ())
src/Aztecs/ECS/World/Bundle/Dynamic.hs view
@@ -1,39 +1,39 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}---- |--- Module      : Aztecs.ECS.World.Bundle.Dynamic--- 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.World.Bundle.Dynamic (DynamicBundle (..), MonoidDynamicBundle (..)) where--import Aztecs.ECS.Access.Internal (Access)-import Aztecs.ECS.Entity-import Aztecs.ECS.World.Archetype-import Aztecs.ECS.World.Bundle.Dynamic.Class---- | Dynamic bundle of components.-newtype DynamicBundle m = DynamicBundle-  { -- | Insert components into an archetype.-    runDynamicBundle :: EntityID -> Archetype m -> (Archetype m, Access m ())-  }--instance (Monad m) => Semigroup (DynamicBundle m) where-  DynamicBundle d1 <> DynamicBundle d2 = DynamicBundle go-    where-      go eId arch =-        let (arch', hook1) = d1 eId arch-            (arch'', hook2) = d2 eId arch'-         in (arch'', hook1 >> hook2)--instance (Monad m) => Monoid (DynamicBundle m) where-  mempty = DynamicBundle (\_ arch -> (arch, return ()))--instance (Monad m) => MonoidDynamicBundle m (DynamicBundle m) where-  dynBundle cId a = DynamicBundle (\eId arch -> insertComponent eId cId a arch)-  dynBundleUntracked cId a = DynamicBundle (\eId arch -> (insertComponentUntracked eId cId a arch, return ()))+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+-- |
+-- Module      : Aztecs.ECS.World.Bundle.Dynamic
+-- 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.World.Bundle.Dynamic (DynamicBundle (..), MonoidDynamicBundle (..)) where
+
+import Aztecs.ECS.Access.Internal (Access)
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Archetype
+import Aztecs.ECS.World.Bundle.Dynamic.Class
+
+-- | Dynamic bundle of components.
+newtype DynamicBundle m = DynamicBundle
+  { -- | Insert components into an archetype.
+    runDynamicBundle :: EntityID -> Archetype m -> (Archetype m, Access m ())
+  }
+
+instance (Monad m) => Semigroup (DynamicBundle m) where
+  DynamicBundle d1 <> DynamicBundle d2 = DynamicBundle go
+    where
+      go eId arch =
+        let (arch', hook1) = d1 eId arch
+            (arch'', hook2) = d2 eId arch'
+         in (arch'', hook1 >> hook2)
+
+instance (Monad m) => Monoid (DynamicBundle m) where
+  mempty = DynamicBundle (\_ arch -> (arch, return ()))
+
+instance (Monad m) => MonoidDynamicBundle m (DynamicBundle m) where
+  dynBundle cId a = DynamicBundle (\eId arch -> insertComponent eId cId a arch)
+  dynBundleUntracked cId a = DynamicBundle (\eId arch -> (insertComponentUntracked eId cId a arch, return ()))
src/Aztecs/ECS/World/Entities.hs view
@@ -1,187 +1,187 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}---- |--- Module      : Aztecs.ECS.World.Entities--- 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.World.Entities-  ( Entities (..),-    empty,-    spawn,-    spawnWithArchetypeId,-    insert,-    insertDyn,-    insertUntracked,-    lookup,-    remove,-    removeWithId,-    despawn,-  )-where--import Aztecs.ECS.Access.Internal (Access)-import Aztecs.ECS.Component-import Aztecs.ECS.Entity-import qualified Aztecs.ECS.World.Archetype as A-import Aztecs.ECS.World.Archetypes (ArchetypeID, Node (..))-import qualified Aztecs.ECS.World.Archetypes as AS-import Aztecs.ECS.World.Bundle-import Aztecs.ECS.World.Bundle.Dynamic-import qualified Aztecs.ECS.World.Components as CS-import Aztecs.ECS.World.Entities.Internal (Entities (..))-import Data.Dynamic-import Data.IntMap (IntMap)-import qualified Data.IntMap as IntMap-import qualified Data.Map as Map-import Data.Maybe-import Data.Set (Set)-import qualified Data.Set as Set-import Prelude hiding (lookup)---- | Empty `World`.-empty :: Entities m-empty =-  Entities-    { archetypes = AS.empty,-      components = CS.empty,-      entities = mempty-    }---- | Spawn a `Bundle`. Returns the updated entities and the onInsert hook to run.-spawn :: (Monad m) => EntityID -> BundleT m -> Entities m -> (Entities m, Access m ())-spawn eId b w =-  let (cIds, components', dynB) = unBundle b (components w)-   in case AS.lookupArchetypeId cIds (archetypes w) of-        Just aId -> case AS.lookup aId $ archetypes w of-          Just node ->-            let (arch', hook) =-                  runDynamicBundle-                    dynB-                    eId-                    ( (nodeArchetype node)-                        { A.entities = Set.insert eId . A.entities $ nodeArchetype node-                        }-                    )-             in ( w-                    { archetypes = (archetypes w) {AS.nodes = Map.insert aId node {nodeArchetype = arch'} (AS.nodes $ archetypes w)},-                      components = components',-                      entities = Map.insert eId aId (entities w)-                    },-                  hook-                )-          Nothing -> (w, return ())-        Nothing ->-          let (arch', hook) = runDynamicBundle dynB eId $ A.singleton eId-              node' = Node {nodeComponentIds = cIds, nodeArchetype = arch'}-              (aId, arches) = AS.insertArchetype cIds node' $ archetypes w-           in ( w-                  { archetypes = arches,-                    entities = Map.insert eId aId (entities w),-                    components = components'-                  },-                hook-              )---- | Spawn a `DynamicBundle` with a specified `ArchetypeID`. Returns the updated entities and the onInsert hook.-spawnWithArchetypeId ::-  (Monad m) =>-  EntityID ->-  ArchetypeID ->-  DynamicBundle m ->-  Entities m ->-  (Entities m, Access m ())-spawnWithArchetypeId e aId b w =-  let f n =-        let (arch', hook) = runDynamicBundle b e ((nodeArchetype n) {A.entities = Set.insert e . A.entities $ nodeArchetype n})-         in (n {nodeArchetype = arch'}, hook)-      (hooks, nodes') =-        Map.alterF-          ( \maybeN -> case maybeN of-              Just n -> let (n', hook) = f n in (hook, Just n')-              Nothing -> (return (), Nothing)-          )-          aId-          (AS.nodes $ archetypes w)-   in ( w-          { archetypes = (archetypes w) {AS.nodes = nodes'},-            entities = Map.insert e aId (entities w)-          },-        hooks-      )---- | Insert a component into an entity. Returns the updated entities and the onInsert hook.-insert :: (Monad m) => EntityID -> BundleT m -> Entities m -> (Entities m, Access m ())-insert e b w =-  let !(cIds, components', dynB) = unBundle b (components w)-   in insertDyn e cIds dynB w {components = components'}---- | Insert a component into an entity with its `ComponentID`. Returns the updated entities and the onInsert hook.-insertDyn :: (Monad m) => EntityID -> Set ComponentID -> DynamicBundle m -> Entities m -> (Entities m, Access m ())-insertDyn e cIds b w = case Map.lookup e $ entities w of-  Just aId ->-    let (maybeNextAId, arches, hook) = AS.insert e aId cIds b $ archetypes w-        es = case maybeNextAId of-          Just nextAId -> Map.insert e nextAId $ entities w-          Nothing -> entities w-     in (w {archetypes = arches, entities = es}, hook)-  Nothing -> case AS.lookupArchetypeId cIds $ archetypes w of-    Just aId -> spawnWithArchetypeId e aId b w-    Nothing ->-      let (arch, hook) = runDynamicBundle b e $ A.singleton e-          node = Node {nodeComponentIds = cIds, nodeArchetype = arch}-          (aId, arches) = AS.insertArchetype cIds node $ archetypes w-       in (w {archetypes = arches, entities = Map.insert e aId (entities w)}, hook)---- | Insert a component into an entity without running lifecycle hooks.-insertUntracked :: (Monad m) => EntityID -> BundleT m -> Entities m -> Entities m-insertUntracked e b w = fst $ insert e b w---- | Lookup a component in an entity.-lookup :: forall m a. (Component m a) => EntityID -> Entities m -> Maybe a-lookup e w = do-  !cId <- CS.lookup @a $ components w-  !aId <- Map.lookup e $ entities w-  !node <- AS.lookup aId $ archetypes w-  A.lookupComponent e cId $ nodeArchetype node---- | Remove a component from an entity. Returns the component (if found), updated entities, and the onRemove hook.-remove :: forall m a. (Component m a) => EntityID -> Entities m -> (Maybe a, Entities m, Access m ())-remove e w =-  let !(cId, components') = CS.insert @a @m (components w)-   in removeWithId e cId w {components = components'}---- | Remove a component from an entity with its `ComponentID`. Returns the component (if found), updated entities, and the onRemove hook.-removeWithId :: forall m a. (Component m a) => EntityID -> ComponentID -> Entities m -> (Maybe a, Entities m, Access m ())-removeWithId e cId w = case Map.lookup e (entities w) of-  Just aId ->-    let (res, as, hook) = AS.remove @m @a e aId cId $ archetypes w-        (maybeA, es) = case res of-          Just (a, nextAId) -> (Just a, Map.insert e nextAId (entities w))-          Nothing -> (Nothing, entities w)-     in (maybeA, w {archetypes = as, entities = es}, hook)-  Nothing -> (Nothing, w, return ())---- | Despawn an entity, returning its components.-despawn :: EntityID -> Entities m -> (IntMap Dynamic, Entities m)-despawn e w =-  let res = do-        !aId <- Map.lookup e $ entities w-        !node <- AS.lookup aId $ archetypes w-        return (aId, node)-   in case res of-        Just (aId, node) ->-          let !(dynAcc, arch') = A.remove e (nodeArchetype node)-           in ( dynAcc,-                w-                  { archetypes = (archetypes w) {AS.nodes = Map.insert aId node {nodeArchetype = arch'} (AS.nodes $ archetypes w)},-                    entities = Map.delete e (entities w)-                  }-              )-        Nothing -> (IntMap.empty, w)+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module      : Aztecs.ECS.World.Entities
+-- 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.World.Entities
+  ( Entities (..),
+    empty,
+    spawn,
+    spawnWithArchetypeId,
+    insert,
+    insertDyn,
+    insertUntracked,
+    lookup,
+    remove,
+    removeWithId,
+    despawn,
+  )
+where
+
+import Aztecs.ECS.Access.Internal (Access)
+import Aztecs.ECS.Component
+import Aztecs.ECS.Entity
+import qualified Aztecs.ECS.World.Archetype as A
+import Aztecs.ECS.World.Archetypes (ArchetypeID, Node (..))
+import qualified Aztecs.ECS.World.Archetypes as AS
+import Aztecs.ECS.World.Bundle
+import Aztecs.ECS.World.Bundle.Dynamic
+import qualified Aztecs.ECS.World.Components as CS
+import Aztecs.ECS.World.Entities.Internal (Entities (..))
+import Data.Dynamic
+import Data.IntMap (IntMap)
+import qualified Data.IntMap as IntMap
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Prelude hiding (lookup)
+
+-- | Empty `World`.
+empty :: Entities m
+empty =
+  Entities
+    { archetypes = AS.empty,
+      components = CS.empty,
+      entities = mempty
+    }
+
+-- | Spawn a `Bundle`. Returns the updated entities and the onInsert hook to run.
+spawn :: (Monad m) => EntityID -> BundleT m -> Entities m -> (Entities m, Access m ())
+spawn eId b w =
+  let (cIds, components', dynB) = unBundle b (components w)
+   in case AS.lookupArchetypeId cIds (archetypes w) of
+        Just aId -> case AS.lookup aId $ archetypes w of
+          Just node ->
+            let (arch', hook) =
+                  runDynamicBundle
+                    dynB
+                    eId
+                    ( (nodeArchetype node)
+                        { A.entities = Set.insert eId . A.entities $ nodeArchetype node
+                        }
+                    )
+             in ( w
+                    { archetypes = (archetypes w) {AS.nodes = Map.insert aId node {nodeArchetype = arch'} (AS.nodes $ archetypes w)},
+                      components = components',
+                      entities = Map.insert eId aId (entities w)
+                    },
+                  hook
+                )
+          Nothing -> (w, return ())
+        Nothing ->
+          let (arch', hook) = runDynamicBundle dynB eId $ A.singleton eId
+              node' = Node {nodeComponentIds = cIds, nodeArchetype = arch'}
+              (aId, arches) = AS.insertArchetype cIds node' $ archetypes w
+           in ( w
+                  { archetypes = arches,
+                    entities = Map.insert eId aId (entities w),
+                    components = components'
+                  },
+                hook
+              )
+
+-- | Spawn a `DynamicBundle` with a specified `ArchetypeID`. Returns the updated entities and the onInsert hook.
+spawnWithArchetypeId ::
+  (Monad m) =>
+  EntityID ->
+  ArchetypeID ->
+  DynamicBundle m ->
+  Entities m ->
+  (Entities m, Access m ())
+spawnWithArchetypeId e aId b w =
+  let f n =
+        let (arch', hook) = runDynamicBundle b e ((nodeArchetype n) {A.entities = Set.insert e . A.entities $ nodeArchetype n})
+         in (n {nodeArchetype = arch'}, hook)
+      (hooks, nodes') =
+        Map.alterF
+          ( \maybeN -> case maybeN of
+              Just n -> let (n', hook) = f n in (hook, Just n')
+              Nothing -> (return (), Nothing)
+          )
+          aId
+          (AS.nodes $ archetypes w)
+   in ( w
+          { archetypes = (archetypes w) {AS.nodes = nodes'},
+            entities = Map.insert e aId (entities w)
+          },
+        hooks
+      )
+
+-- | Insert a component into an entity. Returns the updated entities and the onInsert hook.
+insert :: (Monad m) => EntityID -> BundleT m -> Entities m -> (Entities m, Access m ())
+insert e b w =
+  let !(cIds, components', dynB) = unBundle b (components w)
+   in insertDyn e cIds dynB w {components = components'}
+
+-- | Insert a component into an entity with its `ComponentID`. Returns the updated entities and the onInsert hook.
+insertDyn :: (Monad m) => EntityID -> Set ComponentID -> DynamicBundle m -> Entities m -> (Entities m, Access m ())
+insertDyn e cIds b w = case Map.lookup e $ entities w of
+  Just aId ->
+    let (maybeNextAId, arches, hook) = AS.insert e aId cIds b $ archetypes w
+        es = case maybeNextAId of
+          Just nextAId -> Map.insert e nextAId $ entities w
+          Nothing -> entities w
+     in (w {archetypes = arches, entities = es}, hook)
+  Nothing -> case AS.lookupArchetypeId cIds $ archetypes w of
+    Just aId -> spawnWithArchetypeId e aId b w
+    Nothing ->
+      let (arch, hook) = runDynamicBundle b e $ A.singleton e
+          node = Node {nodeComponentIds = cIds, nodeArchetype = arch}
+          (aId, arches) = AS.insertArchetype cIds node $ archetypes w
+       in (w {archetypes = arches, entities = Map.insert e aId (entities w)}, hook)
+
+-- | Insert a component into an entity without running lifecycle hooks.
+insertUntracked :: (Monad m) => EntityID -> BundleT m -> Entities m -> Entities m
+insertUntracked e b w = fst $ insert e b w
+
+-- | Lookup a component in an entity.
+lookup :: forall m a. (Component m a) => EntityID -> Entities m -> Maybe a
+lookup e w = do
+  !cId <- CS.lookup @a $ components w
+  !aId <- Map.lookup e $ entities w
+  !node <- AS.lookup aId $ archetypes w
+  A.lookupComponent e cId $ nodeArchetype node
+
+-- | Remove a component from an entity. Returns the component (if found), updated entities, and the onRemove hook.
+remove :: forall m a. (Component m a) => EntityID -> Entities m -> (Maybe a, Entities m, Access m ())
+remove e w =
+  let !(cId, components') = CS.insert @a @m (components w)
+   in removeWithId e cId w {components = components'}
+
+-- | Remove a component from an entity with its `ComponentID`. Returns the component (if found), updated entities, and the onRemove hook.
+removeWithId :: forall m a. (Component m a) => EntityID -> ComponentID -> Entities m -> (Maybe a, Entities m, Access m ())
+removeWithId e cId w = case Map.lookup e (entities w) of
+  Just aId ->
+    let (res, as, hook) = AS.remove @m @a e aId cId $ archetypes w
+        (maybeA, es) = case res of
+          Just (a, nextAId) -> (Just a, Map.insert e nextAId (entities w))
+          Nothing -> (Nothing, entities w)
+     in (maybeA, w {archetypes = as, entities = es}, hook)
+  Nothing -> (Nothing, w, return ())
+
+-- | Despawn an entity, returning its components.
+despawn :: EntityID -> Entities m -> (IntMap Dynamic, Entities m)
+despawn e w =
+  let res = do
+        !aId <- Map.lookup e $ entities w
+        !node <- AS.lookup aId $ archetypes w
+        return (aId, node)
+   in case res of
+        Just (aId, node) ->
+          let !(dynAcc, arch') = A.remove e (nodeArchetype node)
+           in ( dynAcc,
+                w
+                  { archetypes = (archetypes w) {AS.nodes = Map.insert aId node {nodeArchetype = arch'} (AS.nodes $ archetypes w)},
+                    entities = Map.delete e (entities w)
+                  }
+              )
+        Nothing -> (IntMap.empty, w)
src/Aztecs/ECS/World/Internal.hs view
@@ -1,26 +1,26 @@-{-# LANGUAGE DeriveGeneric #-}---- |--- Module      : Aztecs.ECS.World.Internal--- 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.World.Internal (World (..)) where--import Aztecs.ECS.Entity-import Aztecs.ECS.World.Entities.Internal-import Aztecs.ECS.World.Observers.Internal-import Control.Monad.State---- | World of entities and their components.-data World m = World-  { -- | Entities and their components.-    entities :: !(Entities m),-    -- | Next unique entity identifier.-    nextEntityId :: !EntityID,-    -- | Observers for events.-    observers :: !(Observers (StateT (World m) m))-  }+{-# LANGUAGE DeriveGeneric #-}
+
+-- |
+-- Module      : Aztecs.ECS.World.Internal
+-- 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.World.Internal (World (..)) where
+
+import Aztecs.ECS.Entity
+import Aztecs.ECS.World.Entities.Internal
+import Aztecs.ECS.World.Observers.Internal
+import Control.Monad.State
+
+-- | World of entities and their components.
+data World m = World
+  { -- | Entities and their components.
+    entities :: !(Entities m),
+    -- | Next unique entity identifier.
+    nextEntityId :: !EntityID,
+    -- | Observers for events.
+    observers :: !(Observers (StateT (World m) m))
+  }
src/Aztecs/ECS/World/Storage/Dynamic.hs view
@@ -1,70 +1,70 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}---- |--- Module      : Aztecs.ECS.World.Storage.Dynamic--- 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.World.Storage.Dynamic-  ( DynamicStorage (..),-    dynStorage,-    singletonDyn,-    fromAscVectorDyn,-    toAscVectorDyn,-  )-where--import qualified Aztecs.ECS.World.Storage as S-import Data.Dynamic-import Data.Maybe-import Data.Vector (Vector)-import qualified Data.Vector as V---- | Dynamic storage of components.-data DynamicStorage = DynamicStorage-  { -- | Dynamic storage.-    storageDyn :: !Dynamic,-    -- | Singleton storage.-    singletonDyn' :: !(Dynamic -> Dynamic),-    -- | Convert this storage to an ascending vector.-    toAscVectorDyn' :: !(Dynamic -> Vector Dynamic),-    -- | Convert from an ascending vector.-    fromAscVectorDyn' :: !(Vector Dynamic -> Dynamic)-  }--instance Show DynamicStorage where-  show s = "DynamicStorage " ++ show (storageDyn s)---- | Create a dynamic storage from a storage.-dynStorage :: forall a s. (S.Storage a s) => s -> DynamicStorage-dynStorage s =-  DynamicStorage-    { storageDyn = toDyn s,-      singletonDyn' = toDyn . S.singleton @a @s . fromMaybe (error "TODO") . fromDynamic,-      toAscVectorDyn' = \d -> V.map toDyn (S.toAscVector @a @s (fromMaybe (error "TODO") $ fromDynamic d)),-      fromAscVectorDyn' = toDyn . S.fromAscVector @a @s . V.map (fromMaybe (error "TODO") . fromDynamic)-    }-{-# INLINE dynStorage #-}---- | Singleton dynamic storage.-singletonDyn :: Dynamic -> DynamicStorage -> DynamicStorage-singletonDyn dyn s = s {storageDyn = singletonDyn' s dyn}---- | Convert from an ascending vector.-fromAscVectorDyn :: Vector Dynamic -> DynamicStorage -> DynamicStorage-fromAscVectorDyn dyns s = s {storageDyn = fromAscVectorDyn' s dyns}---- | Convert this storage to an ascending vector.-toAscVectorDyn :: DynamicStorage -> Vector Dynamic-toAscVectorDyn = toAscVectorDyn' <*> storageDyn+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module      : Aztecs.ECS.World.Storage.Dynamic
+-- 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.World.Storage.Dynamic
+  ( DynamicStorage (..),
+    dynStorage,
+    singletonDyn,
+    fromAscVectorDyn,
+    toAscVectorDyn,
+  )
+where
+
+import qualified Aztecs.ECS.World.Storage as S
+import Data.Dynamic
+import Data.Maybe
+import Data.Vector (Vector)
+import qualified Data.Vector as V
+
+-- | Dynamic storage of components.
+data DynamicStorage = DynamicStorage
+  { -- | Dynamic storage.
+    storageDyn :: !Dynamic,
+    -- | Singleton storage.
+    singletonDyn' :: !(Dynamic -> Dynamic),
+    -- | Convert this storage to an ascending vector.
+    toAscVectorDyn' :: !(Dynamic -> Vector Dynamic),
+    -- | Convert from an ascending vector.
+    fromAscVectorDyn' :: !(Vector Dynamic -> Dynamic)
+  }
+
+instance Show DynamicStorage where
+  show s = "DynamicStorage " ++ show (storageDyn s)
+
+-- | Create a dynamic storage from a storage.
+dynStorage :: forall a s. (S.Storage a s) => s -> DynamicStorage
+dynStorage s =
+  DynamicStorage
+    { storageDyn = toDyn s,
+      singletonDyn' = toDyn . S.singleton @a @s . fromMaybe (error "TODO") . fromDynamic,
+      toAscVectorDyn' = \d -> V.map toDyn (S.toAscVector @a @s (fromMaybe (error "TODO") $ fromDynamic d)),
+      fromAscVectorDyn' = toDyn . S.fromAscVector @a @s . V.map (fromMaybe (error "TODO") . fromDynamic)
+    }
+{-# INLINE dynStorage #-}
+
+-- | Singleton dynamic storage.
+singletonDyn :: Dynamic -> DynamicStorage -> DynamicStorage
+singletonDyn dyn s = s {storageDyn = singletonDyn' s dyn}
+
+-- | Convert from an ascending vector.
+fromAscVectorDyn :: Vector Dynamic -> DynamicStorage -> DynamicStorage
+fromAscVectorDyn dyns s = s {storageDyn = fromAscVectorDyn' s dyns}
+
+-- | Convert this storage to an ascending vector.
+toAscVectorDyn :: DynamicStorage -> Vector Dynamic
+toAscVectorDyn = toAscVectorDyn' <*> storageDyn
src/Aztecs/Hierarchy.hs view
@@ -24,7 +24,6 @@ module Aztecs.Hierarchy
   ( Parent (..),
     Children (..),
-    update,
     Hierarchy (..),
     toList,
     foldWithKey,
@@ -32,8 +31,6 @@     mapWithAccum,
     hierarchy,
     hierarchies,
-    ParentState (..),
-    ChildState (..),
   )
 where
 
@@ -41,7 +38,6 @@ import qualified Aztecs.ECS.Access as A
 import qualified Aztecs.ECS.Query as Q
 import qualified Aztecs.ECS.System as S
-import Control.Monad
 import Data.Map (Map)
 import qualified Data.Map as Map
 import Data.Maybe
@@ -58,80 +54,52 @@   }
   deriving (Eq, Ord, Show, Generic)
 
-instance (Monad m) => Component m Parent
+instance (Monad m) => Component m Parent where
+  componentOnInsert e (Parent parent) = do
+    -- Add this entity to the parent's children.
+    maybeChildren <- A.lookup parent
+    let parentChildren = maybe mempty unChildren maybeChildren
+    A.insertUntracked parent . bundle . Children $ Set.insert e parentChildren
 
--- | Parent internal state component.
-newtype ParentState = ParentState {unParentState :: EntityID}
-  deriving (Show, Generic)
+  componentOnChange e (Parent oldParent) (Parent newParent) = do
+    -- Remove this entity from the old parent's children.
+    maybeOldChildren <- A.lookup oldParent
+    let oldChildren = maybe mempty unChildren maybeOldChildren
+    let oldChildren' = Set.filter (/= e) oldChildren
+    A.insertUntracked oldParent . bundle . Children $ oldChildren'
 
-instance (Monad m) => Component m ParentState
+    -- Add this entity to the new parent's children.
+    maybeNewChildren <- A.lookup newParent
+    let newChildren = maybe mempty unChildren maybeNewChildren
+    A.insertUntracked newParent . bundle . Children $ Set.insert e newChildren
 
+  componentOnRemove e (Parent parent) = do
+    -- Remove this entity from the parent's children.
+    maybeChildren <- A.lookup parent
+    let parentChildren = maybe mempty unChildren maybeChildren
+    let parentChildren' = Set.filter (/= e) parentChildren
+    A.insertUntracked parent . bundle . Children $ parentChildren'
+
 -- | Children component.
 newtype Children = Children {unChildren :: Set EntityID}
   deriving (Eq, Ord, Show, Semigroup, Monoid, Generic)
 
-instance (Monad m) => Component m Children
-
--- | Child internal state component.
-newtype ChildState = ChildState {unChildState :: Set EntityID}
-  deriving (Show, Generic)
-
-instance (Monad m) => Component m ChildState
-
--- | Update the parent-child relationships.
-update :: (Monad m) => Access m ()
-update = do
-  parents <- A.system . S.readQuery $ do
-    e <- Q.entity
-    parent <- Q.query
-    maybeParentState <- Q.queryMaybe @_ @ParentState
-    return (e, unParent parent, maybeParentState)
-
-  children <- A.system . S.readQuery $ do
-    e <- Q.entity
-    cs <- Q.query
-    maybeChildState <- Q.queryMaybe @_ @ChildState
-    return (e, unChildren cs, maybeChildState)
-
-  let go = do
-        mapM_
-          ( \(e, parent, maybeParentState) -> case maybeParentState of
-              Just (ParentState parentState) -> do
-                when (parent /= parentState) $ do
-                  A.insert parent . bundle $ ParentState parent
+instance (Monad m) => Component m Children where
+  componentOnInsert e (Children cs) = do
+    -- Set parent on all children.
+    mapM_ (\child -> A.insertUntracked child . bundle $ Parent e) cs
 
-                  -- Remove this entity from the previous parent's children.
-                  maybeLastChildren <- A.lookup parentState
-                  let lastChildren = maybe mempty unChildren maybeLastChildren
-                  let lastChildren' = Set.filter (/= e) lastChildren
-                  A.insert parentState . bundle . Children $ lastChildren'
+  componentOnChange e (Children oldCs) (Children newCs) = do
+    let added = Set.difference newCs oldCs
+        removed = Set.difference oldCs newCs
+    -- Set parent on added children.
+    mapM_ (\child -> A.insertUntracked child . bundle $ Parent e) added
+    -- Remove parent from removed children.
+    mapM_ (\child -> A.remove @_ @Parent child) removed
 
-                  -- Add this entity to the new parent's children.
-                  maybeChildren <- A.lookup parent
-                  let parentChildren = maybe mempty unChildren maybeChildren
-                  A.insert parent . bundle . Children $ Set.insert e parentChildren
-              Nothing -> do
-                A.spawn_ . bundle $ ParentState parent
-                maybeChildren <- A.lookup parent
-                let parentChildren = maybe mempty unChildren maybeChildren
-                A.insert parent . bundle . Children $ Set.insert e parentChildren
-          )
-          parents
-        mapM_
-          ( \(e, children', maybeChildState) -> case maybeChildState of
-              Just (ChildState childState) -> do
-                when (children' /= childState) $ do
-                  A.insert e . bundle $ ChildState children'
-                  let added = Set.difference children' childState
-                      removed = Set.difference childState children'
-                  mapM_ (\e' -> A.insert e' . bundle . Parent $ e') added
-                  mapM_ (A.remove @_ @Parent) removed
-              Nothing -> do
-                A.insert e . bundle $ ChildState children'
-                mapM_ (\e' -> A.insert e' . bundle . Parent $ e') children'
-          )
-          children
-  go
+  componentOnRemove _ (Children cs) = do
+    -- Remove parent from all children.
+    mapM_ (\child -> A.remove @_ @Parent child) cs
 
 -- | Hierarchy of entities.
 data Hierarchy a = Node