mischief-ecs-0.1.0.0: src/Mischief/ECS/World/Insert.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
module Mischief.ECS.World.Insert where
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Reader (MonadReader (..))
import Data.Data
import Data.Foldable (for_)
import Data.IORef
import Data.List hiding (insert)
import Data.Map qualified as Map
import Data.Maybe (fromMaybe)
import Data.Set qualified as Set
import Data.Text qualified as Text
import GHC.Base (Int (..))
import GHC.Stack
import Mischief.ECS.Archetypes
import Mischief.ECS.Archetypes.Graph
( getArchetypeOnInsert,
)
import Mischief.ECS.Components
import Mischief.ECS.Components.Bundle
import Mischief.ECS.Entities
import Mischief.ECS.EntityDef
import Mischief.ECS.EventDef
import Mischief.ECS.Events
import Mischief.ECS.Log
import Mischief.ECS.Tables
import Mischief.ECS.Vec qualified as Vec
import Mischief.ECS.World
import Mischief.ECS.World.Change
import Mischief.ECS.World.Prefs
import Mischief.ECS.World.Query
import Mischief.ECS.World.Query.Markers
import Mischief.ECS.World.Query.Queryable
import Mischief.ECS.World.Utils
data Exception' = Exception' deriving (Show)
instance Exception Exception'
-- | Insert a bundle of components on an Entity.
--
-- If the entity already contains these components, their values will be
-- updated in-place instead of causing an archetype change.
insert :: (HasCallStack) => forall b. (Bundle b) => b -> Entity -> System ()
insert bundle entity =
do
world <- unsafeGetWorld
pointer <- liftIO $ getPointer entity world.entities
case pointer of
Nothing -> warn $ "Insertion failed: Entity " <> text entity <> " is not alive."
Just currentPointer -> do
let BundleData {elements} = bundleData bundle
currentTick <- liftIO $ readIORef world.tick
bundleData <- liftIO $ processBundleElements world ComponentTicks {changed = currentTick, added = currentTick} elements
let newComponents = sort $ map (\x -> x.id) bundleData.elements
(EntityPointer (# archetypeId, rowIndex #)) <- liftIO $ readIORef currentPointer
currentTable <- Vec.read world.tables.inner (I# archetypeId)
-- Simple case, no archetype change.
if newComponents `isSubsequenceOf` currentTable.components
then
liftIO $ replaceComponentsIntoTable bundleData (Just currentTick) (EntityPointer (# archetypeId, rowIndex #)) currentTable
-- Complex case, archetype change.
else do
newArchetype <- getArchetypeOnInsert (ArchetypeId $ I# archetypeId) newComponents
ChangeResult {requiredComponentsAdded} <- changeArchetype entity newArchetype (Just bundleData)
unless world.prefs.supressEvents $
triggerInsertEvent (ProcessedBundleData requiredComponentsAdded) entity
unless world.prefs.supressEvents $
triggerInsertEvent bundleData entity
getOrInsert :: forall qd. (Updateable (Result qd), Bundle qd) => qd -> Entity -> System (Result qd)
getOrInsert val entity = do
g <- update (Result (val, entity))
case g of
Just g -> return g
Nothing -> do
insert val entity
return $ Result (val, entity)
-- | Insert a bundle of components on an Entity.
--
-- Only the components that the entity doesn't already have will be inserted, and the rest ignored.
insertNew :: forall b. (Bundle b) => b -> Entity -> System ()
insertNew bundle entity =
do
world <- unsafeGetWorld
pointer <- liftIO $ getPointer entity world.entities
case pointer of
Nothing -> warn $ "Insertion failed: Entity " <> text entity <> " is not alive."
Just currentPointer -> do
let BundleData {elements} = bundleData bundle
currentTick <- liftIO $ readIORef world.tick
bundleData <- liftIO $ processBundleElements world ComponentTicks {changed = currentTick, added = currentTick} elements
(EntityPointer (# archetypeId, _ #)) <- liftIO $ readIORef currentPointer
currentTable <- Vec.read world.tables.inner (I# archetypeId)
let newComponents = ProcessedBundleData $ filter (\c -> c.id `notElem` currentTable.components) bundleData.elements
unless (null newComponents.elements) $ do
newArchetype <- getArchetypeOnInsert (ArchetypeId $ I# archetypeId) $ map (\x -> x.id) newComponents.elements
ChangeResult {requiredComponentsAdded} <- changeArchetype entity newArchetype (Just bundleData)
unless world.prefs.supressEvents $
triggerInsertEvent (ProcessedBundleData requiredComponentsAdded) entity
unless world.prefs.supressEvents $
triggerInsertEvent newComponents entity
insertIfNeq :: (BundleEq b) => b -> Entity -> System ()
insertIfNeq b entity = do
let BundleData {elements} = bundleDataEq b
comps <- flip filterM (Set.toList elements) $ \BundleElement {rep, component = ErasedComponentEq (val :: c)} -> do
val' <- case rep of
PairRep (_, target) -> fmap (\x -> x.comp) <$> get (R @c target) entity
_ -> fmap value <$> get (C @c) entity
case val' of
Nothing -> return True
Just val' -> return (val /= val')
insert (bundleEqToSimple $ BundleData (Set.fromList comps)) entity
class Settable c i | c -> i where
setInner :: c -> i -> System ()
setIfNeqInner :: (Eq i) => c -> i -> System ()
class Settable' isRel c i | isRel c -> i where
setInner' :: c -> i -> System ()
setIfNeqInner' :: (Eq i) => c -> i -> System ()
instance (Component c) => Settable' False (Result (Rel c)) c where
setInner' :: Result (Rel c) -> c -> System ()
setInner' !result !newValue = Mischief.ECS.World.Insert.insert (Rel newValue result.target) (entityOf result)
setIfNeqInner' :: (Component c, Eq c) => Result (Rel c) -> c -> System ()
setIfNeqInner' !result !newValue = do
curr <- get (R @c result.target) (entityOf result)
case curr of
Nothing -> warn $ "SetIfNeq failed: Entity " <> text (entityOf result) <> " is not alive."
Just curr ->
when (curr.comp /= newValue) $
setInner' @False result newValue
instance (Component c, IsComponentC c ~ HTrue) => Settable' True (Result c) c where
setInner' :: Result c -> c -> System ()
setInner' !result !newValue = Mischief.ECS.World.Insert.insert newValue (entityOf result)
setIfNeqInner' :: (Component c, Eq c) => Result c -> c -> System ()
setIfNeqInner' !result !newValue = do
curr <- get (C @c) (entityOf result)
case curr of
Nothing -> warn $ "SetIfNeq failed: Entity " <> text (entityOf result) <> " is not alive."
Just curr ->
when (value curr /= newValue) $
setInner' @True result newValue
instance (Settable' (IsComp c) (Result c) i) => Settable (Result c) i where
setInner = setInner' @(IsComp c)
setIfNeqInner = setIfNeqInner' @(IsComp c)
-- | Set the value of a component obtained as query result.
--
-- Note that the local 'Result' won't be mutated.
-- You'll need to query the component again or use 'update' to update the current result.
set :: (Settable c i) => c -> i -> System ()
set = setInner
setIfNeq :: (Eq i, Settable c i) => c -> i -> System ()
setIfNeq = setIfNeqInner
class Updateable' flag r where
updateInner' :: r -> System (Maybe r)
instance (Component c, IsComponentC c ~ HTrue) => Updateable' True (Result c) where
updateInner' r = get (C @c) (entityOf r)
instance (Component c) => Updateable' False (Result (Rel c)) where
updateInner' r = get (R @c r.target) (entityOf r)
class Updateable r where
updateInner :: r -> System (Maybe r)
instance (Updateable' (IsComp c) (Result c)) => Updateable (Result c) where
updateInner = updateInner' @(IsComp c)
-- | Update the value of a 'Result'.
--
-- Useful if you've done changed to the component and want to grab the live value
-- without re-querying.
update :: forall c. (Updateable (Result c)) => Result c -> System (Maybe (Result c))
update = updateInner
triggerInsertEvent :: ProcessedBundleData -> Entity -> System ()
triggerInsertEvent bundle entity =
for_ bundle.elements $ \x -> do
let !(ComponentId (# id, target #)) = x.id
case target of
Nothing ->
triggerInsertEventC x.component.value entity
Just target ->
triggerInsertEventR x.component.value target entity
triggerInsertEventC :: ErasedComponent -> Entity -> System ()
triggerInsertEventC (ErasedComponent (_ :: c)) entity =
runEvent $ eraseEvent $ OnInsert @c entity
triggerInsertEventR :: ErasedComponent -> Entity -> Entity -> System ()
triggerInsertEventR (ErasedComponent (_ :: c)) target entity = do
runEvent $ eraseEvent $ OnInsertRel @c entity target