packages feed

mischief-ecs-0.2.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 Data.Foldable (for_)
import Data.IORef
import Data.List hiding (insert)
import Data.Maybe
import Data.Set qualified as Set
import Data.Text qualified as T
import GHC.Base (Int (..))
import GHC.Stack
import Mischief.ECS.Archetypes.Graph
  ( getArchetypeOnInsert,
  )
import Mischief.ECS.Components
import Mischief.ECS.Components.Bundle
import Mischief.ECS.Components.HooksDef (HookContext (..), HookContextRel (..))
import Mischief.ECS.Components.Spawn (ComponentAddHooks (ComponentAddHooks), ComponentAddHooksRel (ComponentAddHooksRel), ComponentSetHooks (ComponentSetHooks), ComponentSetHooksRel (ComponentSetHooksRel), meta)
import Mischief.ECS.Entities
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.Query
import Mischief.ECS.World.Query.Markers
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 " <> T.show entity <> " is not alive."
      Just !currentPointer -> do
        let BundleData {elements = !elements, resources = !resources, external = !external} = bundleData bundle

        for_ external $ \(!e, !x) -> do
          insert x e

        for_ resources $ \BundleElement {component = ErasedComponent (!val :: c)} -> do
          !m <- meta @c
          insert val m

        !currentTick <- liftIO $ readIORef world.tick

        !bundleData <- liftIO $ processBundleElements world elements
        let !newComponents = sort $ map (\(!x) -> x.id) bundleData.elements

        (EntityPointer (# !archetypeId, !rowIndex #)) <- liftIO $ readIORef currentPointer

        !currentTable <- Vec.read world.tables.inner (I# archetypeId)

        ChangeResult {requiredComponentsAdded = !requiredComponentsAdded, newComponents = !newComponents} <-
          if newComponents `isSubsequenceOf` currentTable.components
            -- Simple case, no archetype change.
            then do
              liftIO $ replaceComponentsIntoTable bundleData (Just currentTick) (EntityPointer (# archetypeId, rowIndex #)) currentTable
              pure $ ChangeResult [] []
            -- Complex case, archetype change.
            else do
              !newArchetype <- getArchetypeOnInsert (ArchetypeId $ I# archetypeId) newComponents
              changeArchetype entity newArchetype (Just bundleData)

        !supressEvents <- fromMaybe False <$> getPref @SupressEventsPref
        unless supressEvents $ do
          triggerAddEvent (ProcessedBundleData newComponents) entity
          triggerAddEvent (ProcessedBundleData requiredComponentsAdded) entity
          triggerSetEvent bundleData entity
          triggerSetEvent (ProcessedBundleData requiredComponentsAdded) 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
    let BundleData {elements = !elements, resources = !resources, external = !external} = bundleData bundle
    for_ external $ \(!e, !s) -> do
      insertNew s e

    !world <- unsafeGetWorld
    !pointer <- liftIO $ getPointer entity world.entities

    case pointer of
      Nothing -> warn $ "Insertion failed: Entity " <> T.show entity <> " is not alive."
      Just !currentPointer -> do
        for_ resources $ \BundleElement {component = ErasedComponent (!val :: c)} -> do
          !m <- meta @c
          !r <- single $ mkGet m (C @c)
          case r of
            Just _ -> pure ()
            Nothing -> insert val m

        !bundleData <- liftIO $ processBundleElements world 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)

          !supressEvents <- fromMaybe False <$> getPref @SupressEventsPref
          unless supressEvents $ do
            triggerAddEvent (ProcessedBundleData requiredComponentsAdded) entity
            triggerAddEvent newComponents entity
            triggerSetEvent (ProcessedBundleData requiredComponentsAdded) entity
            triggerSetEvent newComponents entity

insertIfNeq :: forall b. (BundleEq b) => b -> Entity -> System ()
insertIfNeq !b !entity = do
  let BundleData {elements = !elements, resources = !resources, external = !external} = 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) <$> single (mkGet entity (R @c target))
      _ -> single (mkGet entity (C @c))

    case val' of
      Nothing -> pure True
      Just !val' -> pure (val /= val')

  resources <- flip filterM (Set.toList resources) $ \BundleElement {rep, component = ErasedComponentEq (!val :: c)} -> do
    !e <- meta @c
    val' <- case rep of
      ComponentRep _ -> single $ mkGet e (C @c)
      _ -> undefined

    case val' of
      Nothing -> pure True
      Just !val' -> pure (val /= val')

  for_ external $ \(!e, !external) -> do
    insertIfNeq external e

  insert (bundleEqToSimple $ BundleData (Set.fromList comps) (Set.fromList resources) Set.empty) entity

triggerAddEvent :: ProcessedBundleData -> Entity -> System ()
triggerAddEvent !bundle !entity =
  for_ bundle.elements $ \(!x) -> do
    let !(ComponentId (# _, !target #)) = x.id
    case target of
      Nothing ->
        triggerAddEventC x.component entity
      Just !target ->
        triggerAddEventR x.component target entity

triggerAddEventC :: ErasedComponent -> Entity -> System ()
triggerAddEventC (ErasedComponent (_ :: c)) !entity = do
  let !context = HookContext {entity}

  !m <- meta @c
  !hooks <- single $ mkGet m (C @ComponentAddHooks)
  for_ hooks $ \(ComponentAddHooks !h) -> do
    for_ h $ \(!h) -> h context

  trigger $ OnAdd @c entity

triggerAddEventR :: ErasedComponent -> Entity -> Entity -> System ()
triggerAddEventR (ErasedComponent (_ :: c)) !target !entity = do
  let !context = HookContextRel {entity, target}

  !m <- meta @c
  Just !hooks <- single $ mkGet m (M @ComponentAddHooksRel)
  for_ hooks $ \(ComponentAddHooksRel !h) -> do
    for_ h $ \(!h) -> h context

  trigger $ OnAddRel @c entity target

triggerSetEvent :: ProcessedBundleData -> Entity -> System ()
triggerSetEvent !bundle !entity =
  for_ bundle.elements $ \(!x) -> do
    let !(ComponentId (# _, !target #)) = x.id
    case target of
      Nothing ->
        triggerSetEventC x.component entity
      Just !target ->
        triggerSetEventR x.component target entity

triggerSetEventC :: ErasedComponent -> Entity -> System ()
triggerSetEventC (ErasedComponent (_ :: c)) !entity = do
  let !context = HookContext {entity}

  !m <- meta @c
  Just !hooks <- single $ mkGet m (M @ComponentSetHooks)
  for_ hooks $ \(ComponentSetHooks !h) -> do
    for_ h $ \(!h) -> h context

  trigger $ OnSet @c entity

triggerSetEventR :: ErasedComponent -> Entity -> Entity -> System ()
triggerSetEventR (ErasedComponent (_ :: c)) !target !entity = do
  let !context = HookContextRel {entity, target}

  !m <- meta @c
  Just !hooks <- single $ mkGet m (M @ComponentSetHooksRel)
  for_ hooks $ \(ComponentSetHooksRel !h) -> do
    for_ h $ \(!h) -> h context

  trigger $ OnSetRel @c entity target