packages feed

ohhecs-0.0.2: src/Games/ECS/World/TH.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE Trustworthy #-}

-- |
-- Module      :  Games.ECS.World.TH
-- Description : Template Haskell derivation of 'World' classes
-- Copyright   :  (C) 2020 Sophie Taylor
-- License     :  AGPL-3.0-or-later
-- Maintainer  :  Sophie Taylor <sophie@spacekitteh.moe>
-- Stability   :  experimental
-- Portability: GHC
--
-- Automatic derivation of the 'World' class, implementing the bulk of the higher-kinded data pattern.
module Games.ECS.World.TH
  ( makeWorld,
  )
where

import Control.Applicative
import Control.Lens
import Control.Monad
import Data.Coerce
import Data.Foldable
import Data.IntSet qualified as IS
import Data.Kind qualified as DK
import Data.List (findIndex)
import GHC.Generics
import Games.ECS.Prototype
import Games.ECS.Component
import Games.ECS.Component.TH.Internal
import Games.ECS.Entity
import Games.ECS.SaveLoad
import Games.ECS.Serialisation
import Games.ECS.Slot
import Games.ECS.World
import Language.Haskell.TH
import Language.Haskell.TH.Datatype qualified as D
import Language.Haskell.TH.Syntax
import Witherable

aComponentName :: Name
aComponentName = ''Games.ECS.Component.AComponent

-- worldClassName :: Name
-- worldClassName = ''Games.ECS.World.World

-- entRefFieldName :: Name
-- entRefFieldName = ''Games.ECS.World.EntRefField

-- setter'Name :: Name
-- setter'Name = ''Control.Lens.Setter'

extractComponentTypes :: Type -> Maybe (Type, Type, Type)
extractComponentTypes = \case
  AppT (AppT (AppT (ConT acName) name@(LitT (StrTyLit _))) s) a | acName == aComponentName -> Just (name, a, s)
  _ -> Nothing

-- | Find the position of the entity reference field in a (product) world type. We need this so we can ensure
-- we initialise it with the correct type.
findEntRefFieldPosition :: (MonadFail m) => [Type] -> m Int
findEntRefFieldPosition fields = do
  let idx = flip findIndex fields $ \case
        AppT (ConT _entRefFieldName) _ -> True
        _ -> False

  case idx of
    Just i -> pure i
    Nothing -> fail "World doesn't have an EntRefField!"

-- | Body for constructing a new individual given an entity reference
constructAnEntityWithEntRef :: forall m w. (Quote m) => Int -> m Exp -> [(Type, Type)] -> m Type -> Code m (Entity -> w Individual)
constructAnEntityWithEntRef entRefPosition constructor fieldTypes worldType =
  constructAnEntityWithEntRefAndValues entRefPosition constructor (fmap (\(a, b) -> (a, b, Nothing)) fieldTypes) worldType

-- | Creates an expression for constructing a new individual, with the given entity reference value and initial component values.
constructAnEntityWithEntRefAndValues :: forall m w. (Quote m) => Int -> m Exp -> [(Type, Type, Maybe (m Exp))] -> m Type -> Code m (Entity -> w Individual)
constructAnEntityWithEntRefAndValues entRefPosition constructor fieldTypes worldType =
  let prefix = map fieldFunc (take entRefPosition fieldTypes)
      postfix = map fieldFunc (drop entRefPosition fieldTypes)
      header = foldl appE constructor prefix
      mid = appE header
      ending ent = unsafeCodeCoerce $ foldl appE (mid ent) postfix
      fieldFunc :: (Type, Type, Maybe (m Exp)) -> m Exp
      fieldFunc (_name, _a, Just val) = val
      fieldFunc (name, a, Nothing) = [|defaultField @($(pure name)) @($worldType) @Individual @(Prop $(pure a)) @($(pure a))|]
   in [||\ent -> $$(ending [|ent|])||]

-- | Creates an expression to construct a new, blank world.
constructDefaultWorld :: (Quote m) => Int -> m Exp -> [(Type, Type)] -> m Type -> Code m (w Storing)
constructDefaultWorld entRefPos constructor fieldTypes worldType = constructWorldWith [|blankEntityStorage|] entRefPos constructor (fmap (\(a, b) -> (a, b, Nothing)) fieldTypes) worldType

constructWorldWith :: forall m w. (Quote m) => m Exp -> Int -> m Exp -> [(Type, Type, Maybe (m Exp))] -> m Type -> Code m (w Storing)
constructWorldWith entStorage entRefPosition constructor fieldTypes worldType = unsafeCodeCoerce ending
  where
    prefix = map fieldFunc (take entRefPosition fieldTypes)
    postfix = map fieldFunc (drop entRefPosition fieldTypes)
    header = foldl appE constructor prefix
    mid = appE header entStorage
    ending = foldl appE mid postfix
    fieldFunc :: (Type, Type, Maybe (m Exp)) -> m Exp
    fieldFunc (_, _, Just val) = val
    fieldFunc (name, a, Nothing) = [|defaultStorage @($(pure name)) @($worldType) @Storing @(Prop $(pure a)) @($(pure a))|]

{-# INLINE traversalIndexedLookups #-}
traversalIndexedLookups :: forall world f p b. (Filterable f, World world, Indexable Entity p) => f Entity -> world Storing -> p (world Individual) b -> f b
traversalIndexedLookups ents world f =
  uncurry (indexed @Entity @p f)
    <$> mapMaybe
      ( \ent -> case lookupEntity world (coerce ent) of
          Just i -> Just (coerce ent, i)
          Nothing -> Nothing
      )
      ents

{-# INLINE traversalNonIndexedLookups #-}
traversalNonIndexedLookups :: (Filterable f, World world) => f Entity -> world Storing -> (world Individual -> b) -> f b
traversalNonIndexedLookups ents world f =
  f
    <$> mapMaybe
      (lookupEntity world . coerce)
      ents

-- | Implements the 'World' typeclass for a type, and creates numerous helper instances.
makeWorld :: forall m. (Quasi m, Quote m) => Name -> m [Dec]
makeWorld worldName = do
  info <- runQ $ D.reifyDatatype worldName

  -- Grab some info we'll need all over the place
  let worldType = runQ $ conT worldName
      [constructor] = D.datatypeCons info
      componentDefinitions' = mapMaybe extractComponentTypes (D.constructorFields constructor)
      componentDefinitions = fmap (\(a, b, _c) -> (a, b)) componentDefinitions'
  let componentAccessors = forM componentDefinitions' (makeComponentAccessor constructor worldType)
  --  Generate an instance of `World`.
  entRefPosition <- findEntRefFieldPosition (D.constructorFields constructor)

  let totalFieldLength = length (D.constructorFields constructor)
      entRefName = mkName "entityReferenceField"
      entRefPat = varP entRefName
      entRef = varE entRefName
      constructorEntityPattern =
        replicate entRefPosition wildP
          <> [entRefPat]
          <> replicate ((totalFieldLength - entRefPosition) - 1) wildP
      (entityPatternForHasType, constructorEntityPatternForHasType, valP, _valE, entRefSetterPat) = makeConstructorPatternAndValuePair constructor entRefPosition
      entityPattern = conP (D.constructorName constructor) constructorEntityPattern
  worldInstance <-
    [d|
      instance {-# OVERLAPS #-} Games.ECS.Slot.HasType (EntRefStoringType) ($worldType Storing) where
        {-# INLINE CONLIKE typed #-}
        {-# INLINE CONLIKE getTyped #-}
        {-# INLINE CONLIKE setTyped #-}
        typed = Control.Lens.lens getTyped (flip setTyped)
        getTyped ($constructorEntityPatternForHasType) = $entityPatternForHasType
        setTyped $valP ($constructorEntityPatternForHasType) = $entRefSetterPat

      instance {-# OVERLAPS #-} Games.ECS.Slot.HasType (Entity) ($worldType Individual) where
        {-# INLINE CONLIKE typed #-}
        {-# INLINE CONLIKE getTyped #-}
        {-# INLINE CONLIKE setTyped #-}
        typed = Control.Lens.lens getTyped (flip setTyped)
        getTyped ($constructorEntityPatternForHasType) = $entityPatternForHasType
        setTyped $valP ($constructorEntityPatternForHasType) = $entRefSetterPat

      -- TODO: Generate rest of`World` instance
      instance (Generic ($worldType 'Individual), Generic ($worldType 'Storing)) => Games.ECS.World.World $worldType where
        newWorld =
          $( unTypeCode $
               constructDefaultWorld
                 entRefPosition
                 (conE $ D.constructorName constructor)
                 componentDefinitions
                 worldType
           )
        entityReference = conjoined (Control.Lens.to $ \ $entityPattern -> $entRef) (ito $ \ $entityPattern -> ($entRef, $entRef))
        {-# INLINE unsafeEntityReference #-}
        unsafeEntityReference =
          lens (getTyped :: $worldType Individual -> Entity) (flip (setTyped :: Entity -> $worldType Individual -> $worldType Individual))

        {-# INLINE createNewEntityWithRef #-}
        createNewEntityWithRef ent =
          $( unTypeCode $
               constructAnEntityWithEntRef
                 entRefPosition
                 (conE $ D.constructorName constructor)
                 componentDefinitions
                 worldType
           )
            ent
        {-# INLINE lookupEntity #-}
        lookupEntity world entityToLookup = if existence then Just result else Nothing
          where
            existence = {-HS.member entityToLookup-} IS.member (coerce entityToLookup) $ world ^. (typed @(EntRefField Storing) @($worldType Storing))
            result = $(unTypeCode $ createIndexedLookups entRefPosition (conE $ D.constructorName constructor) (unsafeCodeCoerce [|created|]) (unsafeCodeCoerce [|world|]) componentDefinitions worldType (unsafeCodeCoerce $ varE $ mkName "entityToLookup"))

        {-# INLINE CONLIKE entityReferences #-}
        entityReferences :: IndexedFold Entity ($worldType Storing) Entity
        entityReferences = conjoined ((typed @(EntRefField Storing) @($worldType Storing)) . knownEntities) ((typed @(EntRefField Storing) @($worldType Storing)) . knownEntities . selfIndex)

        {-# INLINE prototype #-}                           
        prototype :: HasPrototypeID p => p -> AffineTraversal' ($worldType Storing) ($worldType Individual)
        prototype p = (entitiesWith withIsPrototype) . filtered (\c -> c ^?! isPrototype . prototypeID == (p^.prototypeID))
                           
        {-# INLINE lookupEntities #-}
        lookupEntities :: forall f p fol. (Indexable Entity p, Applicative f, Foldable fol) => fol Entity -> p ($worldType Individual) (f ($worldType Individual)) -> $worldType Storing -> f ($worldType Storing)
        lookupEntities ents' = conjoined normalVer indexedVer
          where
            ents = ents' ^.. folded
            normalVer func world = result
              where
                applied :: [f ($worldType Individual)]
                applied = traversalNonIndexedLookups ents world func
                result :: f ($worldType Storing)
                result = liftA3 @f foldl' (pure @f (flip storeEntity)) (pure world) (sequenceA @[] @f applied)
            indexedVer func world = result
              where
                applied :: [f ($worldType Individual)]
                applied = traversalIndexedLookups ents world func
                result :: f ($worldType Storing)
                result = liftA3 @f foldl' (pure @f (flip storeEntity)) (pure world) (sequenceA @[] @f applied)
        {-# INLINE storeEntity #-}
        storeEntity ent world = result
          where
            worldWithEntity = world & (typed @(EntRefField Storing) . at (coerce (ent ^. entityReference))) ?~ ()
            worldWithEntity :: $worldType Storing
            theEntRef = ent ^. entityReference
            result = $(unTypeCode $ createStores entRefPosition (conE $ D.constructorName constructor) (unsafeCodeCoerce [|ent|]) (unsafeCodeCoerce [|worldWithEntity|]) componentDefinitions worldType (unsafeCodeCoerce [|theEntRef|]))

      instance (XMLSerialise ($worldType Individual)) => XMLPickler [Node] ($worldType Storing) where
        {-# INLINE xpickle #-}
        xpickle = worldPickler
      |]

  -- TODO: Generate an `At name FieldType` instance for each Individual's components
  -- TODO: Generate garbage collection function. Note: use Control.Lens.At.sans for this
  -- TODO: Support "global" components - or perhaps just defer to a MonadState
  -- TODO: Generate "Has<X>" classes (elaborated: ReadOnly, WriteOnly, ReadWrite) for each named component, a la classy optics
  -- TODO: Resource Field accessor types
  -- TODO: Enforce intent/queueing system via making optics read-only and using an annotation to say what functions are allowed access, then yeet them with TH
  componentAccessors' <- fmap concat componentAccessors
  pure (worldInstance <> componentAccessors')

createIndexedLookups :: forall m w. (Quote m) => Int -> m Exp -> Code m ((w :: Access -> DK.Type) Individual) -> Code m (w Storing) -> [(Type, Type)] -> m Type -> Code m Entity -> Code m (w Individual)
createIndexedLookups entRefPos constructor _created worldStorage types worldType ent = do
  let applications :: [(Type, Type, Maybe (m Exp))]
      applications =
        types
          <&> ( \(nameType'@(LitT (StrTyLit _name')), comp') ->
                  let comp = pure comp'
                      nameType = pure nameType'
                   in ( ( nameType',
                          comp',
                          Just
                            [|
                              injectMaybe @($nameType) @($worldType) @Individual @(Prop $comp) @($comp) ($(unTypeCode worldStorage) ^. (Games.ECS.Component.storage @($nameType) @($worldType) @Storing @(Prop $comp) @($comp) . (at $(unTypeCode ent))))
                              |]
                        )
                      )
              )
  [||$$(constructAnEntityWithEntRefAndValues entRefPos constructor applications worldType) $$(ent)||]

createStores :: forall m w. (Quote m) => Int -> m Exp -> Code m (w Individual) -> Code m (w Storing) -> [(Type, Type)] -> m Type -> Code m Entity -> Code m (w Storing)
createStores entRefPos constructor individual world types worldType ent = do
  let rawLens nameType componentType = unsafeCodeCoerce [|(typed @(Field $nameType Storing (Prop $componentType) $componentType) @($worldType Storing) . coerced)|]
      rawLens :: m Type -> m Type -> Code m (Getting a0 (w 'Storing) a0)
      applications =
        types
          <&> ( \(nameType'@(LitT (StrTyLit name')), comp') ->
                  let comp = pure comp'
                      viewedField = [||($$world ^. ($$(rawLens (pure nameType') comp)))||]
                   in [|($(unTypeQ . examineCode $ viewedField)) & $(appTypeE [|Control.Lens.at|] [t|Storage $comp $comp|]) $(unTypeCode ent) .~ ($(unTypeCode individual) ^? $(varE (mkName name')))|]
              )

      -- TODO: possibly need some rewrite rules for (set . get)    for the field accessors
      viewed = [|$(unTypeCode world) ^. (typed @(EntRefField Storing))|]
      viewed :: m Exp
      -- TODO: Replace the "coerced" with "injectToField"
      mappedTypesAndValues = Prelude.zipWith (\(a, b) c -> (a, b, Just [|$(c) ^. coerced @(Storage $(pure b) $(pure b))|])) types applications

  constructWorldWith viewed entRefPos constructor mappedTypesAndValues worldType