mischief-ecs-0.2.0.0: src/Mischief/ECS/World.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
module Mischief.ECS.World
( -- * World
World (..),
newWorld,
setSystemId,
setDeferred,
Frame (..),
SystemTools (..),
worldGet,
worldGetRAny,
worldSet,
worldSpawnByInsert,
-- * Prefs
PrefType,
setPref,
getPref,
SupressEventsPref,
-- * Systems
System (..),
SystemId (..),
runWhen,
runSystem,
MonadSystem,
unsafeGetWorld,
sysUnliftIO,
SystemAny (..),
-- * Parallel
ParSystem (..),
ParWorld (..),
parLift,
)
where
import Colog qualified
import Control.Concurrent.STM (TVar, newTVarIO)
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Primitive (PrimMonad (..), RealWorld)
import Control.Monad.Reader.Class (MonadReader (..), asks)
import Control.Monad.Trans (MonadTrans (..))
import Control.Monad.Trans.Reader (ReaderT (runReaderT))
import Data.Data
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.Map (Map)
import Data.Map qualified as Map
import Mischief.ECS.Archetypes (Archetypes, emptyArchetypes)
import Mischief.ECS.Collectable
import Mischief.ECS.Components
( Component (IsExclusiveRel),
Components,
Rel,
Tick (Tick),
emptyComponents,
)
import Mischief.ECS.Components.Bundle
import Mischief.ECS.Entities
( Entities,
Entity (Entity),
emptyEntities,
)
import Mischief.ECS.Hidden
import Mischief.ECS.Tables (Tables, emptyTables)
import Mischief.ECS.Utils (ErasedValue (..), eraseValue, unsafeUneraseValue)
-- | @The World@ is the main data structure storing the entities, components, archetypes, and everything else that lives in our app.
data World = World
{ -- | Archetype storage.
archetypes :: Archetypes,
-- | Components storage.
components :: Components,
-- | Entities storage.
entities :: Entities,
-- | Archetype tables. Storage for the actual data of components.
tables :: Tables,
-- | List of deferred system to be ran at the next sync point. They can also be flushed manually using 'flush'.
deferred :: IORef [System ()],
-- | Secondary list of deferred systems stored in a TVar, meant to be used when running systems asynchronously.
-- They are ran at the first available sync point, and can also be flushed manually with 'flushAsync'.
deferredAsync :: TVar [System ()],
-- | Systems to be tried at the next sync point. If the first system returns a Just, it is removed from the list and the second
-- system is ran with the returned value.
trySystems :: IORef [(System (Maybe ErasedValue), ErasedValue -> System ())],
-- | The current tick, incremented each time a system is ran, used for change detection.
tick :: IORef Tick,
-- | Id of the current system.
systemId :: SystemId,
-- | The current frame.
frame :: IORef Frame,
-- | Certain toggleable settings.
prefs :: Prefs,
logger :: Colog.LogAction IO Colog.Message,
loggerErr :: Colog.LogAction IO Colog.Message,
tools :: SystemTools
}
newtype Prefs = Prefs (Map TypeRep Pref)
data Pref where
Pref :: (Typeable a) => a -> Pref
class (Typeable a, Typeable b) => PrefType a b | a -> b
data SupressEventsPref
instance PrefType SupressEventsPref Bool
worldSetPref :: forall a b. (PrefType a b) => b -> World -> World
worldSetPref !value !world =
let Prefs !prefs = world.prefs
in world {prefs = Prefs $ Map.insert (typeRep $ Proxy @a) (Pref value) prefs}
worldGetPref :: forall a b. (PrefType a b) => World -> Maybe b
worldGetPref World {prefs = Prefs !prefs} = worldGetPref' @a =<< Map.lookup (typeRep $ Proxy @a) prefs
worldGetPref' :: forall a b. (PrefType a b) => Pref -> Maybe b
worldGetPref' (Pref (!s :: a')) =
case eqT @a' @b of
Just Refl -> Just s
Nothing -> Nothing
setPref :: forall a b c. (PrefType a b) => b -> System c -> System c
setPref !value !s = do
!world <- unsafeGetWorld
let world' = worldSetPref @a value world
liftIO $ runSystem s world'
getPref :: forall a m b w. (PrefType a b, MonadSystem w m) => m (Maybe b)
getPref = worldGetPref @a <$> unsafeGetWorld
newtype Frame = Frame Int deriving (Show, Eq, Ord)
-- | Unique id assigned to each system that's added to a schedule. Just a wrapper around Entity.
newtype SystemId = SystemId {id :: Entity} deriving (Show, Eq, Ord)
-- | Create a new World in IO.
newWorld :: SystemTools -> IO World
newWorld !tools = do
!archetypes <- emptyArchetypes
!components <- emptyComponents
!entities <- emptyEntities
!tables <- emptyTables
!deferred <- newIORef []
!deferredAsync <- newTVarIO []
!tick <- newIORef (Tick 0)
!frame <- newIORef (Frame 0)
!trySystems <- newIORef []
let !prefs = Prefs Map.empty
let !logger = Colog.cmap Colog.fmtMessage Colog.logTextStdout
let !loggerErr = Colog.cmap Colog.fmtMessage Colog.logTextStderr
pure
World
{ archetypes,
components,
entities,
tables,
deferred,
deferredAsync,
tick,
systemId = SystemId (Entity (# 0##, 0## #)),
frame,
prefs,
logger,
loggerErr,
trySystems,
tools
}
-- | Change the current SystemId of the World.
setSystemId :: SystemId -> World -> World
setSystemId !systemId !world = world {systemId}
-- | Set the list of deferred systems of the World.
setDeferred :: IORef [System ()] -> World -> World
setDeferred !deferred !world = world {deferred}
-- | A System is a monad that mutates the World.
--
-- It can be added to a certain schedule and ordered with other systems.
-- It also offers IO access via 'liftIO'.
--
-- Read the [tutorial]("Mischief.ECS.Tutorial.Systems") on systems for more details.
newtype System a = System (ReaderT (Hidden World) IO a)
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader (Hidden World), MonadFail, MonadThrow)
-- | Run a 'System' with the given 'World' inside 'IO'
runSystem :: System a -> World -> IO a
runSystem (System !r) !w = runReaderT r (hide w)
-- runSystem' :: (MonadSystem w m) => m a -> World -> IO a
-- runSystem' (System !r) w =
instance PrimMonad System where
type PrimState System = PrimState IO
primitive = System . lift . primitive
-- | Special wrapper around World given to 'ParSystem's.
data ParWorld = ParWorld
{ -- | @Hidden@ ensures users cannot access and mutate the World.
world :: Hidden World,
-- | Deferred systems will be collected in this dedicated list and then
-- merged back into the main deferred list once the parallel systems are joined.
parDeferred :: IORef [System ()]
}
-- | A variant of 'System' that contains a 'ParWorld' instead of 'World'.
--
-- @Parallel systems@ will only be able to run systems that are either specifically intended for them or
-- are made to work with any @'MonadSystem'@ (such as queries).
--
-- To run a normal @System ()@, you need to 'Mischief.ECS.World.Defer.defer'!
newtype ParSystem a = ParSystem (ReaderT ParWorld IO a)
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader ParWorld, MonadFail, PrimMonad, MonadThrow)
class GetWorld a where
getWorld :: a -> Hidden World
instance (GetWorld (Hidden World)) where
getWorld = id
instance (GetWorld ParWorld) where
getWorld !x = x.world
class SystemAny m where
systemAny :: System a -> ParSystem a -> m a
instance SystemAny ParSystem where
systemAny _ a = a
instance SystemAny System where
systemAny a _ = a
-- | Typeclass that can be used to generalize systems to both @'System'@ and @'ParSystem'@.
--
-- Used mainly by queries and other operations which don't mutate the World.
class (GetWorld w, MonadReader w a, Applicative a, MonadFail a, Functor a, Monad a, MonadIO a, PrimMonad a, PrimState a ~ RealWorld, MonadThrow a, SystemAny a) => MonadSystem w a
instance MonadSystem (Hidden World) System
instance MonadSystem ParWorld ParSystem
-- Grab the World. DO NOT USE THIS.
unsafeGetWorld :: (MonadSystem w m) => m World
unsafeGetWorld = do
asks (unhide . getWorld)
instance EraseIntoStorage (System ()) [System ()] where
erase :: System () -> [System ()]
erase !x = [x]
data SystemTools = SystemTools
{ get :: forall c m w. (MonadSystem w m, Component c) => Proxy c -> Entity -> m (Maybe c),
getRAny :: forall c m w. (Component c, MonadSystem w m, IsExclusiveRel c ~ False) => Proxy c -> Entity -> m (Maybe [Rel c]),
set :: forall c. (Bundle c) => c -> Entity -> System (),
spawnByInsert :: forall b. (Bundle b) => Entity -> b -> System ()
}
worldGet :: forall c m w. (MonadSystem w m, Component c) => Proxy c -> Entity -> m (Maybe c)
worldGet !p !e = do
World {tools = SystemTools {get = !get}} <- unsafeGetWorld
get p e
worldSet :: forall c. (Bundle c) => c -> Entity -> System ()
worldSet !c !e = do
World {tools = SystemTools {set = !set}} <- unsafeGetWorld
set c e
worldGetRAny :: forall c m w. (Component c, MonadSystem w m, IsExclusiveRel c ~ False) => Proxy c -> Entity -> m (Maybe [Rel c])
worldGetRAny !p !e = do
World {tools = SystemTools {getRAny = !getRAny}} <- unsafeGetWorld
getRAny p e
worldSpawnByInsert :: forall b. (Bundle b) => Entity -> b -> System ()
worldSpawnByInsert !e !b = do
World {tools = SystemTools {spawnByInsert = !spawnByInsert}} <- unsafeGetWorld
spawnByInsert e b
sysUnliftIO :: System (System a -> IO a)
sysUnliftIO = do
!world <- hide <$> unsafeGetWorld
pure $ \(System !a) -> runReaderT a world
-- | Primitive that allows you to set a system to run at every sync point until it returns a @Just a@, at which point
-- it will run the second system with the obtained @a@ and stop running.
--
-- It will run once on the spot, meaning it has no overhead if the first system immediately succeed.
--
-- You should only use this in one-time logic, such as Events or the Startup schedule. You should not use it
-- in logic that runs continuously, such as in Update, as it will keep queueing up a new system to check without removing the old one.
runWhen :: (Typeable a) => System (Maybe a) -> (a -> System b) -> System ()
runWhen !s1 !s2 = do
b <- s1
case b of
Just a -> void $ s2 a
Nothing -> do
World {trySystems = !trySystems} <- unsafeGetWorld
liftIO $ modifyIORef' trySystems (++ [(fmap eraseValue <$> s1, void . s2 . unsafeUneraseValue)])
-- | Lift a ParSystem into a System. Doesn't flush the deferred queue!
parLift :: ParSystem a -> System a
parLift (ParSystem !p) = do
world <- unsafeGetWorld
systems <- liftIO $ newIORef []
a <- liftIO $ runReaderT p ParWorld {world = Hidden world, parDeferred = systems}
systems <- liftIO $ readIORef systems
liftIO $ modifyIORef' world.deferred (++ systems)
pure a