packages feed

ohhecs-0.0.2: src/Games/ECS/Component.hs

{-# OPTIONS_GHC "-Wno-orphans" #-}

-- |
-- Module      :  Games.ECS.Component
-- Description : Components and their infrastructure.
-- Copyright   :  (C) 2020 Sophie Taylor
-- License     :  AGPL-3.0-or-later
-- Maintainer  :  Sophie Taylor <sophie@spacekitteh.moe>
-- Stability   :  experimental
-- Portability: GHC
--
-- Components are what hold data in an ECS system.
module Games.ECS.Component where

import Control.Applicative
import Control.Lens
import Data.Coerce
import Data.Foldable qualified as DF
import Data.Kind (Type)
import Data.Maybe qualified as DM
import Data.Proxy
import Data.String
import Data.XML.Types (Element)
import GHC.Generics
import GHC.OverloadedLabels
import GHC.TypeLits
import Games.ECS.Component.Store
import Games.ECS.Entity
import Games.ECS.Serialisation
import Games.ECS.Slot
import Games.ECS.World

class PseudoComponent c

-- type family DefaultStorageForFlags (b :: Bool) where
--     type instance DefaultStorageForFlags False = ComponentStore
--     type instance DefaultStorageForFlags True =

-- | A component represented in the entity component system.
class
  ( AsEmpty (Storage c c),
    KnownSymbol (CanonicalName c),
    At (Storage c c),
    Index (Storage c c) ~ Entity,
    HasEntitySet (Storage c c)
  ) =>
  Component c
  where
  -- | What to name accessors if the type name isn't ergonomic.
  type CanonicalName c :: Symbol

  -- | Is the type a flag component?
  type IsFlag c :: Bool

  type IsFlag c = False

  -- | What datatype to store all instances of a component in. Allows for a generalisation of
  -- "Structure-of-Arrays". Defaults to `Data.HashMap.Strict.HashMap`.
  type Storage c :: Type -> Type

  type Storage c = ComponentStore

  -- | Arity of the component. Defaults to `Normal`.
  type Prop c :: Props

  type Prop c = Normal

  -- | A prism for serialising and deserialising the component as XML.
  _ComponentFromXML :: String -> Prism' Element c
  {-# INLINE _ComponentFromXML #-}
  default _ComponentFromXML :: (XMLSerialise c) => String -> Prism' Element c
  _ComponentFromXML name =
    prism'
      (serialise name)
      ( \n -> case deserialise name n of
          Right a -> Just a
          Left _ -> Nothing
      )

  -- | The collection of entities held in storage.
  {-# INLINE entityKeys #-}
  entityKeys :: Fold (Storage c c) EntitySet
  entityKeys = entitySet

  -- | Construct new storage.
  {-# INLINE emptyStorage #-}
  emptyStorage :: Storage c c
  emptyStorage = review _Empty () :: (Storage c c)

  -- | An AffineTraversal lookup of a given `Entity`'s component. This probably should be in `World.hs`.
  entityHasComponent :: Entity -> IndexedTraversal' Entity (Storage c c) c
  default entityHasComponent ::
    (IxValue (Storage c c) ~ c) =>
    Entity ->
    IndexedTraversal' Entity (Storage c c) c
  entityHasComponent ent = conjoined (ix ent) (iix ent)
  {-# INLINE entityHasComponent #-}

  -- | When the component is required, provide a default value.
  defaultValue :: ((Prop c) ~ Required) => c
  defaultValue = error ("No default value specified for required component " Prelude.++ symbolVal (Proxy :: Proxy (CanonicalName c)) Prelude.++ "!")

type ComponentReference (name :: Symbol) c = (Component c) => TaggedComponent name Entity









-- | A component comprised of multiple other components.
class (Component c) => CompositeComponent c where
  type SubComponentIndex c :: Type

  -- | Look up the entity references of the subcomponents.
  subComponentReferences :: IndexedFold (SubComponentIndex c) c Entity

  {-# INLINE components #-}

  -- | An IndexedTraversal' of the subcomponents of a composite component.
  components ::
    forall p f worldType.
    (World worldType, Indexable (SubComponentIndex c) p, Applicative f) =>
    -- | The composite component
    c ->
    p (worldType Individual) (f (worldType Individual)) ->
    worldType Storing ->
    f (worldType Storing)
  components comp = conjoined normalVer indexedVer
    where
      normalVer prof world = result
        where
          applied :: [f (worldType Individual)]
          applied =
            fmap (prof) $
              DM.mapMaybe
                (lookupEntity world)
                (comp ^.. subComponentReferences)

          result = liftA3 DF.foldl' (pure (flip storeEntity)) (pure world) (sequenceA applied)
      indexedVer prof world = result
        where
          applied :: [f (worldType Individual)]
          applied =
            fmap (uncurry (Control.Lens.indexed prof)) $
              DM.mapMaybe
                ( \(partName', ent) -> case lookupEntity world ent of
                    Just partIndividual -> Just (partName', partIndividual)
                    Nothing -> Nothing
                )
                (comp ^@.. subComponentReferences)

          result = liftA3 DF.foldl' (pure (flip storeEntity)) (pure world) (sequenceA applied)

-- | Indicates a component behaves like a sensor.
class (Component c) => SensoryComponent c

-- sensors :: c -> IndexedTraversal' Entity (worldType Storing) (worldType Individual)

-- | Indicates that a component can perform actions in the world.
class (Component c) => EffectorComponent c

class (Component c) => AttributeComponent c

-- | Indicates whether a component is a simple Boolean flag.
class (Component c) => FlagComponent c

-- | A component representing an /intent/ to do something.
class (Component c) => IntentComponent c

class (Component c) => ReferenceComponent c

-- | A component which indicates an ability to emit intents.
class (IntentComponent (Intent c), Component c) => CapabilityComponent c where
  type Intent c :: Type

-- TODO look at `specs`  storage mechanisms
-- TODO look at unity archetypes (the storage kind)

-- Look at bitsets, hibitsets, bloom filters, slotmaps

-- | Storage for 'Unique' components. As a 'Unique' component will have, at most, a single instance in the world, we only need to store the component and its 'Entity' naively.
newtype UniqueStore a = UniqueStore (Maybe (Entity, a)) deriving (Eq, Show, Generic)

instance HasEntitySet (UniqueStore a) where
  {-# INLINE entitySet #-}
  entitySet = (coerced :: Iso' (UniqueStore a) (Maybe (Entity, a))) . _Just . _1 . Control.Lens.to singletonEntitySet

instance AsEmpty (UniqueStore a) where
  {-# INLINE _Empty #-}
  _Empty = coerced @(UniqueStore a) @(UniqueStore a) @(Maybe (Entity, a)) @(Maybe (Entity, a)) . _Nothing

instance Ixed (UniqueStore a)

type instance Index (UniqueStore a) = Entity

type instance IxValue (UniqueStore a) = a

instance (Index (UniqueStore a) ~ Entity, (IxValue (UniqueStore a)) ~ a) => At (UniqueStore a) where
  {- at ::
     Functor f =>
     Entity ->
     (Maybe a -> f (Maybe a)) ->
     UniqueStore a ->
     f (UniqueStore a)-}
  {-# INLINE at #-}
  at e f us =
    f lookedUp <&> \case
      Nothing -> maybe us (const (UniqueStore Nothing)) lookedUp
      Just v' -> UniqueStore (Just (e, v'))
    where
      lookedUp =
        ( case us of
            UniqueStore (Just (ref, a)) | e == ref -> Just a
            _ -> Nothing
        )

-- | This is an unlawful instance due to `entityHasComponent` allowing write-back.
-- FIXME: Write a valid definition of entityHasComponent, or make it only a psuedocomponent.
-- instance Component Entity where
--  type Prop Entity = Required

-- Some (future) psuedocomponents?

data Has entity component = Has

type HasA component entity = Has entity component

data Hasn't entity component = Hasn't

-- | Simplified type signature to use in world definitions.
type AComponent name s a = Field name s (Prop a) a

instance ({-HasType (Field name acc p a) (hkd acc), Generic (hkd acc),-} EntityProperty name hkd acc p a, accessorType ~ OpticsFor name hkd acc p a) => IsLabel name accessorType where
  fromLabel = accessor @name @hkd @acc @p @a
  {-# INLINE fromLabel #-}

-- instance (HasType (Tagged name a) (hkd Individual), (Tagged name a) ~ (Field name hkd Individual p a), EntityProperty name hkd Individual p a) => HasField name (hkd Individual) a where

--  getField = unTagged . getTyped

-- | A component with an arbitrary name tag.
newtype TaggedComponent name a = Tagged {unTagged :: a} deriving newtype (Eq, Show, Ord, Generic)

{-# INLINE untag #-}

-- | Get the raw component.
untag :: TaggedComponent name a -> a
untag = unTagged

instance {-# OVERLAPPING #-} (KnownSymbol name, KnownSymbol (AppendSymbol name "TaggedComponent wrapper"), XMLPickler [Node] a) => XMLPickler [Node] (TaggedComponent name a) where
  {-# INLINE xpickle #-}
  xpickle = xpElemNodes (fromString (symbolVal (Proxy :: Proxy name))) $ (fromString $ (symbolVal (Proxy :: Proxy (AppendSymbol name "TaggedComponent wrapper"))), "") <?+> (xpWrap Tagged untag (("inside Tagged", "") <?+> xpickle))

instance {-# OVERLAPPING #-} (Eq a, KnownSymbol name, KnownSymbol (AppendSymbol name "TaggedComponent wrapper"), XMLPickler [Node] a) => XMLPickler [Node] (TaggedComponent name (Maybe a)) where
  {-# INLINE xpickle #-}
  xpickle = xpDefault (Tagged Nothing) $ xpElemNodes (fromString (symbolVal (Proxy :: Proxy name))) $ (fromString $ (symbolVal (Proxy :: Proxy (AppendSymbol name "TaggedComponent wrapper"))), "") <?+> (xpWrap Tagged untag (("inside Tagged", "") <?+> xpOption (("inside option", "") <?+> ({-xpMayFail-} xpickle))))

-- | An instance for wrapper components. Don't bother with the wrapper constructor or fieldname; just the wrapped data.
instance {-# OVERLAPPING #-} (KnownSymbol name, XMLPickler [Node] a, Datatype d, ty ~ (M1 D d (M1 C c'' (M1 S c (K1 i a))))) => GXmlPickler [Node] (M1 D d (M1 C c'' (M1 S c (K1 i (TaggedComponent name (Maybe a)))))) where
  {-# INLINE gxpickleContentsf #-}
  gxpickleContentsf _ = {-xpElemNodes (fromString . formatElement $ datatypeName (undefined :: ty p))-} xpElemNodes (fromString (symbolVal (Proxy :: Proxy name))) $ (fromString $ datatypeName (undefined :: M1 D d f p), "") <?+> (xpWrap (M1 . M1 . M1 . K1 . Tagged) (unTagged . unK1 . unM1 . unM1 . unM1) (xpOption ({-xpMayFail-} xpickle)))

-- | The core mechanics of the higher-kinded representation for the ECS system.
type family Field (name :: Symbol) (acc :: Access) (p :: Props) (a :: Type) :: Type where
  Field name Individual Required a = TaggedComponent name a
  Field name Individual Normal a = TaggedComponent name (Maybe a)
  Field name Individual Unique a = TaggedComponent name (Maybe a)
  Field name Storing Unique a = TaggedComponent name (UniqueStore a)
  Field name Storing Required a = TaggedComponent name (Storage a a)
  Field name Storing Normal a = TaggedComponent name (Storage a a)

instance {-# OVERLAPS #-} (Show a) => Show (TaggedComponent (name :: Symbol) (Maybe a)) where
  show (Tagged Nothing) = ""
  show (Tagged (Just a)) = show a

-- | Plumbing class for a higher-kinded data representation of a game world.
-- | Generalisation of "Array of Structures" vs "Structure of Arrays".
class {-((CompTypeClass name a {- ,(CompTypeClassFun name a) ~ (CompType' name hkd acc p a)-})) => -} EntityProperty (name :: Symbol) (hkd :: Access -> Type) (acc :: Access) (p :: Props) (a :: Type) where
  accessor :: OpticsFor name hkd acc p a
  injectToField :: (acc ~ Individual) => a -> Field name Individual p a
  maybeGet :: (acc ~ Individual) => Field name Individual p a -> Maybe a
  {-# INLINE CONLIKE injectMaybe #-}
  injectMaybe :: (acc ~ Individual) => Maybe a -> Field name Individual p a
  injectMaybe (Just a) = injectToField @name @hkd @acc @p @a a
  injectMaybe Nothing = defaultField @name @hkd @acc @p @a

  defaultField :: (acc ~ Individual) => Field name Individual p a
  defaultStorage :: (acc ~ Storing) => Field name Storing p a
  storage :: (acc ~ Storing) => Lens' (hkd acc) (Storage a a)

instance forall name hkd a. (HasType (Field name Individual Required a) (hkd Individual), World hkd, Component a, Prop a ~ Required) => EntityProperty name hkd Individual Required a where
  {-# INLINE CONLIKE injectToField #-}
  {-# INLINE CONLIKE maybeGet #-}
  {-# INLINE CONLIKE defaultField #-}
  {-# INLINE CONLIKE accessor #-}
  injectToField = Tagged
  defaultStorage = error "what"
  maybeGet = Just . untag
  storage = error "what"
  defaultField = injectToField @name @hkd @Individual @Required @a defaultValue
  accessor =
    IndexedLens
      ( conjoined ((lens (getTyped @(TaggedComponent name a)) (\(s :: hkd Individual) (a :: TaggedComponent name a) -> setTyped a s)) . coerced) ((ilens getter (\(s :: hkd Individual) (a :: TaggedComponent name a) -> setTyped a s)) . coerced)
      )
    where
      getter :: hkd Individual -> (Entity, Field name Individual Required a)
      getter s = (s ^. entityReference, (getTyped @(TaggedComponent name a)) $ s)

{-{-# RULES "component/set . get" forall w. set accessor (view accessor w) w = w #-}

{-# RULES "component/set . get" forall v w. view accessor (set accessor v w) = v #-}-}

instance forall name hkd a. (HasType (Field name Individual Normal a) (hkd Individual), World hkd, Component a) => EntityProperty name hkd Individual Normal a where
  {-# INLINE CONLIKE injectToField #-}
  {-# INLINE CONLIKE maybeGet #-}
  {-# INLINE CONLIKE defaultField #-}
  {-# INLINE CONLIKE accessor #-}
  injectToField = Tagged . Just
  maybeGet = untag
  defaultField = Tagged Nothing
  defaultStorage = error "what"
  storage = error "what"
  accessor =
    IndexedTraversal $
      (conjoined ((lens (unTagged . boringGetter) (\(s :: hkd Individual) (a :: Maybe a) -> setTyped (Tagged @name a) s)) . _Just) ((ilens getter (\(s :: hkd Individual) (a :: TaggedComponent name (Maybe a)) -> setTyped a s)) . (coerced :: Iso' (TaggedComponent name (Maybe a)) (Maybe a)) . _Just))
    where
      boringGetter :: hkd Individual -> Field name Individual Normal a
      boringGetter s = (getTyped @(TaggedComponent name (Maybe a))) $ s
      getter :: hkd Individual -> (Entity, Field name Individual Normal a)
      getter s = (s ^. entityReference, (getTyped @(TaggedComponent name (Maybe a))) $ s)

instance forall name hkd a. (HasType (Field name Individual Unique a) (hkd Individual), World hkd, Component a) => EntityProperty name hkd Individual Unique a where
  {-# INLINE CONLIKE injectToField #-}
  {-# INLINE CONLIKE maybeGet #-}
  {-# INLINE CONLIKE defaultField #-}
  {-# INLINE CONLIKE accessor #-}
  injectToField = Tagged . Just
  maybeGet = untag
  storage = error "what"
  defaultStorage = error "what"
  defaultField = Tagged Nothing
  accessor =
    IndexedTraversal
      (conjoined ((lens (unTagged . boringGetter) (\(s :: hkd Individual) (a :: Maybe a) -> setTyped (Tagged @name a) s)) . _Just) ((ilens getter (\(s :: hkd Individual) (a :: TaggedComponent name (Maybe a)) -> setTyped a s)) . (coerced :: Iso' (TaggedComponent name (Maybe a)) (Maybe a)) . _Just))
    where
      getter :: hkd Individual -> (Entity, Field name Individual Unique a)
      getter s = (s ^. entityReference, (getTyped @(TaggedComponent name (Maybe a))) $ s)
      boringGetter :: hkd Individual -> Field name Individual Normal a
      boringGetter s = (getTyped @(TaggedComponent name (Maybe a))) $ s

instance
  {-# OVERLAPPABLE #-}
  ( OpticsFor name hkd 'Storing prop a
      ~ ReifiedIndexedTraversal' Entity (hkd 'Storing) a,
    HasType (Field name Storing prop a) (hkd Storing),
    EntityIndexedTraversable (Storage a) a,
    World hkd,
    Component a,
    Coercible (Field name 'Storing prop a) (Storage a a),
    (Field name 'Storing prop a ~ TaggedComponent name (Storage a a))
  ) =>
  EntityProperty name hkd Storing prop a
  where
  {-# INLINE CONLIKE injectToField #-}
  {-# INLINE CONLIKE accessor #-}
  {-# INLINE CONLIKE storage #-}
  injectToField = error "what"
  maybeGet = error "what"
  defaultField = error "what"
  defaultStorage = Tagged (emptyStorage @a) :: TaggedComponent name ((Storage a) a)
  storage = unwrappedField
    where
      taggedStorageField :: Lens' (hkd Storing) (Field name Storing prop a)
      taggedStorageField = (typed @(Field name Storing prop a) @(hkd Storing))
      unwrappedField :: Lens' (hkd Storing) (Storage a a)
      unwrappedField = (taggedStorageField . coerced)
  accessor = IndexedTraversal @Entity (storage @name @hkd @Storing @prop @a .> entitiesTraversed)

instance {-# OVERLAPPING #-} (Storage a a ~ UniqueStore a, HasType (Field name Storing Unique a) (hkd Storing), World hkd, Component a) => EntityProperty name hkd Storing Unique a where
  {-# INLINE CONLIKE injectToField #-}
  {-# INLINE CONLIKE accessor #-}
  {-# INLINE CONLIKE storage #-}
  injectToField = error "what"
  maybeGet = error "what"
  defaultField = error "what"
  defaultStorage = Tagged (UniqueStore Nothing)
  storage = unwrappedField
    where
      taggedStorageField :: Lens' (hkd Storing) (Field name Storing Unique a)
      taggedStorageField = (typed @(Field name Storing Unique a) @(hkd Storing))
      unwrappedField :: Lens' (hkd Storing) (UniqueStore a)
      unwrappedField = (taggedStorageField . coerced)
  accessor =
    IndexedTraversal @Entity
      ( conjoined
          (storage @name @hkd @Storing @Unique @a . (coerced :: Iso' (UniqueStore a) (Maybe (Entity, a))) . _Just . traversed)
          ((storage @name @hkd @Storing @Unique @a :: Lens' (hkd Storing) (UniqueStore a)) . (coerced :: Iso' (UniqueStore a) (Maybe (Entity, a))) . _Just .> itraversed)
      )