ohhecs 0.0.1 → 0.0.2
raw patch · 9 files changed
+236/−110 lines, 9 filesdep −polysemydep −polysemy-plugindep ~xml-conduit
Dependencies removed: polysemy, polysemy-plugin
Dependency ranges changed: xml-conduit
Files
- ChangeLog.md +2/−0
- ohhecs.cabal +7/−14
- src/Games/ECS.hs +67/−0
- src/Games/ECS/Component.hs +8/−0
- src/Games/ECS/Prototype.hs +11/−96
- src/Games/ECS/Prototype/PrototypeID.hs +40/−0
- src/Games/ECS/Prototype/SpawnedFromPrototype.hs +92/−0
- src/Games/ECS/World.hs +4/−0
- src/Games/ECS/World/TH.hs +5/−0
ChangeLog.md view
@@ -1,5 +1,7 @@ Changelog for [`ohhecs` package](https://hackage.haskell.org/package/ohhecs) # 0.0+## 0.0.2+Switched prototype IDs to be text-based, allowing for a much nicer representation. ## 0.0.1 Initial release.
ohhecs.cabal view
@@ -4,14 +4,15 @@ name: ohhecs category: Game Engine, Game synopsis: An Entity-Component-Systems engine core.-version: 0.0.1+version: 0.0.2 description: Please see the README on GitLab at <https://gitlab.com/spacekitteh/spaceRL#readme> author: Sophie Taylor maintainer: sophie@spacekitteh.moe-copyright: 2020- Sophie Taylor+copyright: 2020-2024 Sophie Taylor license: AGPL-3.0-or-later license-file: LICENSE build-type: Simple+tested-with: GHC == 9.8.2 extra-doc-files: README.md ChangeLog.md@@ -20,18 +21,9 @@ type: git location: https://gitlab.com/spacekitteh/spaceRL -flag with_polysemy- description: Include instances for Polysemy.- default: False- common config default-language: GHC2021 ghc-options: -Werror=deriving-defaults -Werror=unused-packages -Werror=unused-imports -Wsafe -Wtrustworthy-safe -Wunused-packages -fno-ignore-asserts -foptimal-applicative-do -fsimplifier-phases=4 -flate-specialise -fspec-constr-keen -fspec-constr -fliberate-case -flate-dmd-anal -Wno-error=inline-rule-shadowing -Wall -Wno-orphans -Wno-unticked-promoted-constructors -Widentities -Wredundant-constraints- if flag(with_polysemy)- ghc-options: -fplugin=Polysemy.Plugin- build-depends:- polysemy >=1.2.3 && < 1.3- , polysemy-plugin >= 0.4.5.2 && < 0.5 default-extensions: GADTs@@ -75,7 +67,6 @@ import: config exposed-modules: Games.ECS- -- Games.ECS.Archetype Games.ECS.Component Games.ECS.Component.Store Games.ECS.Component.TH@@ -83,6 +74,8 @@ Games.ECS.MessageQueue Games.ECS.SaveLoad Games.ECS.Prototype+ Games.ECS.Prototype.PrototypeID+ Games.ECS.Prototype.SpawnedFromPrototype Games.ECS.Serialisation Games.ECS.Slot Games.ECS.System@@ -96,7 +89,7 @@ -- pkgconfig-depends: -- zlib, bzip2 build-depends:- base >=4.19 && <5+ base >=4.18 && <5 , ghc-prim >= 0.11.0 && < 0.12 , byte-order >= 0.1.3 && < 0.2 , assert-failure >= 0.1.3 && < 0.2@@ -110,7 +103,7 @@ , unordered-containers >= 0.2.20 && < 0.3 , ordered-containers >= 0.2.3 && < 0.3 , xml-types >=0.3.8 && < 0.4- , xml-conduit >= 1.9.1 && < 1.10+ , xml-conduit >= 1.9.1 && <= 1.10 , xml-picklers >= 0.3.6 && < 0.4 , vector-th-unbox >=0.2.2 && < 0.3 , witherable >= 0.4.2 && < 0.5
src/Games/ECS.hs view
@@ -8,6 +8,63 @@ -- Portability: GHC -- -- This is the top-level interface to OhhECS. A brief tutorial should probably go here.+--+-- @+-- +-- import GHC.Generics+-- import Control.Lens+-- +-- data Position = Position Int Int+-- deriving stock (Generic, Eq, Show)+-- +-- instance Num Position where+-- (Position a b) + (Position c d) = Position (a + c) (b + d)+-- (*) = undefined+-- abs = undefined+-- signum = undefined+-- fromInteger = undefined+-- negate = undefined+-- +-- instance 'Component' Position where+-- type 'CanonicalName' Position = "position"+-- +-- 'makeHasComponentClass' ''Position+-- +-- data GameWorld s where+-- GameWorld ::+-- 'EntRefField' s ->+-- 'AComponent' "position" s Position ->+-- GameWorld s+-- deriving stock (Generic) +-- deriving instance Show (GameWorld Individual)+-- deriving instance Show (GameWorld Storing)+-- deriving instance Eq (GameWorld Individual)+-- deriving instance Eq (GameWorld Storing) +-- 'makeWorld' ''GameWorld +-- +-- data MovementSystem = MovementSystem deriving (Eq, Show, Generic)+-- +-- instance (UsingPosition worldType Individual, Monad m) => 'System' \"MovementSystem\" MovementSystem worldType m where+-- type 'ComponentFilters' \"MovementSystem\" MovementSystem worldType m = (UsingPosition worldType Individual) +-- 'runSystem' world = traverseOf ('entitiesWith' (withPosition)) processCritter world+-- where+-- processCritter :: worldType 'Individual' -> m (worldType 'Individual')+-- processCritter oldCritter = do+-- let newCritter = oldCritter & position .~ (Position 1 0)+-- pure newCritter+-- +-- someEntity :: GameWorld 'Individual'+-- someEntity = ('createNewEntityWithRef' ('EntRef' 123)) & addPosition .~ Position 4 5+-- +-- myWorld :: GameWorld 'Storing'+-- myWorld = newWorld & 'storeEntity' someEntity+-- +-- changed :: IO (GameWorld 'Storing')+-- changed = 'runSystem' @\"MovementSystem\" myWorld+--+-- @+--+ module Games.ECS ( module Games.ECS, module Games.ECS.Util.Misc,@@ -22,6 +79,8 @@ module Games.ECS.System, module Games.ECS.Slot, module Games.ECS.Prototype,+ module Games.ECS.Prototype.PrototypeID,+ module Games.ECS.Prototype.SpawnedFromPrototype, module Games.ECS.MessageQueue, ) where@@ -33,6 +92,8 @@ import Games.ECS.Entity import Games.ECS.MessageQueue import Games.ECS.Prototype+import Games.ECS.Prototype.PrototypeID+import Games.ECS.Prototype.SpawnedFromPrototype import Games.ECS.SaveLoad import Games.ECS.Serialisation import Games.ECS.Slot@@ -43,3 +104,9 @@ -- | A filter over entities in a world is just an `IndexedTraversal'` that preserves the selection predicate. type EntityFilter worldType = IndexedTraversal' Entity (worldType Storing) (worldType Individual)+++-- $setup+-- >>> :{ ++-- :}
src/Games/ECS/Component.hs view
@@ -102,6 +102,14 @@ 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
src/Games/ECS/Prototype.hs view
@@ -11,41 +11,20 @@ -- Portability: GHC -- -- Prototypes are exemplar individuals which form a template.-module Games.ECS.Prototype where+module Games.ECS.Prototype (module Games.ECS.Prototype, module Games.ECS.Prototype.PrototypeID) where -import Control.Exception.Assert.Sugar-import Control.Lens-import Control.Monad.IO.Class-import Data.Coerce-import Data.HashMap.Strict qualified as HMS-import Data.Hashable-import Data.Ix-import Data.Vector.Unboxed.Deriving-import GHC.Generics-import Games.ECS.Component-import Games.ECS.Component.TH-import Games.ECS.Entity-import Games.ECS.Serialisation-import Games.ECS.World --- | A prototype's ID is distinct from its entity reference in that it is stable, and in a unique namespace.-newtype PrototypeID = PrototypeID {_unPrototypeID :: Int}- deriving newtype (Eq, Ord, Enum, Bounded, Ix)- deriving newtype (XMLPickleAsAttribute)- deriving stock (Generic)- deriving newtype (Hashable)--makeClassy ''PrototypeID+import Control.Lens -instance Show PrototypeID where- {-# INLINE show #-}- show (PrototypeID ref) = "Prototype ID: " ++ show ref+--import Data.Coerce -derivingUnbox- "PrototypeID"- [t|PrototypeID -> Int|]- [|coerce|]- [|coerce|]+--import Data.Ix+--import Data.Vector.Unboxed.Deriving+import GHC.Generics+import Games.ECS.Serialisation+import Games.ECS.Prototype.PrototypeID+import Games.ECS.Component.TH+import Games.ECS.Component -- | A component for denoting that an individual is a prototype, to be instantiated later. data IsPrototype = IsPrototype@@ -76,72 +55,8 @@ ) ) --- | Marks an entity as being spawned from a prototype.-data SpawnedFromPrototype = SpawnedFromPrototype- { -- | The raw `Entity ` which it is spawned from.- _prototypeEntity :: !Entity,- -- | The t`PrototypeID` it is spawned from.- _spawnedFromPrototypeID :: Maybe PrototypeID- }- deriving stock (Eq, Generic, Show)--makeLenses ''SpawnedFromPrototype--instance {-# OVERLAPS #-} XMLPickler [Node] SpawnedFromPrototype where- {-# INLINE xpickle #-}- xpickle =- xpWrap- (\(pEnt, pid) -> SpawnedFromPrototype pEnt pid)- (\(SpawnedFromPrototype pEnt pid) -> (pEnt, pid))- ( xpElemAttrs- "spawnedFromPrototype"- ( xpPair- (pickleAsAttribute "entRef")- (pickleAsAttribute "prototypeID")- )- )- instance Component IsPrototype where type CanonicalName IsPrototype = "isPrototype" -makeHasComponentClass ''IsPrototype -instance Component SpawnedFromPrototype where- type CanonicalName SpawnedFromPrototype = "spawnedFromPrototype"--makeHasComponentClass ''SpawnedFromPrototype--{-# INLINEABLE spawnPrototype #-}---- | Spawn a new individual with the given prototype `Entity` reference. Returns the new individual, and the new world.-spawnPrototype :: (UsingSpawnedFromPrototype w Individual, UsingIsPrototype w Individual, MonadIO m) => Entity -> w Storing -> m (Maybe (w Individual, w Storing))-spawnPrototype proto world = do- let protoCritter = lookupEntity world proto- case protoCritter of- Nothing -> pure Nothing- Just critter -> do- let protoID = critter ^? isPrototype . prototypeID- newID <- liftIO newUniqueEntRef- let newCritter = critter & unsafeEntityReference .~ newID & addSpawnedFromPrototype .~ SpawnedFromPrototype proto protoID & removeIsPrototype- updatedWorld = world & storeEntity newCritter- pure (Just (newCritter, updatedWorld))---- | A dictionary between a t`PrototypeID` and the characterising `Entity`.-type PrototypeNameMap = HMS.HashMap PrototypeID Entity---- | Spawns a new individual with a given t`PrototypeID`, which is looked up in the associated map. Returns the new individual, and the new world.-{-# INLINEABLE spawnNamedPrototype #-}-spawnNamedPrototype :: (UsingSpawnedFromPrototype w Individual, UsingIsPrototype w Individual, MonadIO m) => PrototypeNameMap -> PrototypeID -> w Storing -> m (Maybe (w Individual, w Storing))-spawnNamedPrototype prototypeMap proto world = do- case prototypeMap ^? ix proto of- Nothing -> assert (False `blame` "Prototype ID doesn't exist!" `swith` proto) (pure Nothing)- Just ent -> do- spawned <- spawnPrototype ent world- case spawned of- Nothing -> assert (False `blame` ("Prototype ID " ++ show proto ++ " existed, but the entity it refers to doesn't!") `swith` ent) (pure Nothing)- Just m -> pure (Just m)---- | All the prototypical individuals in a world.-{-# INLINE prototypes #-}-prototypes :: (HasIsPrototype w) => IndexedTraversal' Entity (w Storing) (w Individual)-prototypes = entitiesWith withIsPrototype+makeHasComponentClass ''IsPrototype
+ src/Games/ECS/Prototype/PrototypeID.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE Trustworthy #-}+-- |+-- Module : Games.ECS.Prototype.PrototypeID+-- Description : Prototype definitions+-- Copyright : (C) 2020 Sophie Taylor+-- License : AGPL-3.0-or-later+-- Maintainer : Sophie Taylor <sophie@spacekitteh.moe>+-- Stability : experimental+-- Portability: GHC+--+-- Prototype IDs are essentially just strings.++module Games.ECS.Prototype.PrototypeID where++import Games.ECS.Serialisation+import Control.Lens +import Data.Interned.Text+import Data.Hashable+import GHC.Generics +import Data.String++-- | A prototype's ID is distinct from its entity reference in that it is stable, and in a unique namespace.+newtype PrototypeID = PrototypeID {_unPrototypeID :: InternedText}+ deriving newtype (Eq, Ord, Show)+ deriving newtype (XMLPickleAsAttribute)+ deriving stock (Generic)+ deriving newtype (Hashable)+ deriving newtype IsString+makeClassy ''PrototypeID+++instance {-# OVERLAPS #-} XMLPickler [Node] PrototypeID where+ {-# INLINE xpickle #-}+ xpickle =+ xpWrap+ PrototypeID+ _unPrototypeID+ xpickle+
+ src/Games/ECS/Prototype/SpawnedFromPrototype.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE Trustworthy #-}+-- |+-- Module : Games.ECS.Prototype.SpawnedFromPrototype+-- Description : Prototype definitions+-- Copyright : (C) 2020 Sophie Taylor+-- License : AGPL-3.0-or-later+-- Maintainer : Sophie Taylor <sophie@spacekitteh.moe>+-- Stability : experimental+-- Portability: GHC+--+-- A component to mark an entity as being spawned from a prototype.+module Games.ECS.Prototype.SpawnedFromPrototype where++import Control.Exception.Assert.Sugar +import Games.ECS.World+import Control.Monad.IO.Class++import Games.ECS.Prototype ++import Games.ECS.Serialisation+import Games.ECS.Entity+import Games.ECS.Component+import Games.ECS.Component.TH+import GHC.Generics+import Control.Lens ++-- | Marks an entity as being spawned from a prototype.+data SpawnedFromPrototype = SpawnedFromPrototype+ { -- | The raw `Entity ` which it is spawned from.+ _prototypeEntity :: !Entity,+ -- | The t`PrototypeID` it is spawned from.+ _spawnedFromPrototypeID :: Maybe PrototypeID+ }+ deriving stock (Eq, Generic, Show)++makeLenses ''SpawnedFromPrototype++instance {-# OVERLAPS #-} XMLPickler [Node] SpawnedFromPrototype where+ {-# INLINE xpickle #-}+ xpickle =+ xpWrap+ (\(pEnt, pid) -> SpawnedFromPrototype pEnt pid)+ (\(SpawnedFromPrototype pEnt pid) -> (pEnt, pid))+ ( xpElemAttrs+ "spawnedFromPrototype"+ ( xpPair+ (pickleAsAttribute "entRef")+ (pickleAsAttribute "prototypeID")+ )+ )+++ +instance Component SpawnedFromPrototype where+ type CanonicalName SpawnedFromPrototype = "spawnedFromPrototype"++makeHasComponentClass ''SpawnedFromPrototype++{-# INLINEABLE spawnPrototype #-}++-- | Spawn a new individual with the given prototype `Entity` reference. Returns the new individual, and the new world.+spawnPrototype :: (UsingSpawnedFromPrototype w Individual, UsingIsPrototype w Individual, MonadIO m) => Entity -> w Storing -> m (Maybe (w Individual, w Storing))+spawnPrototype proto world = do+ let protoCritter = lookupEntity world proto+ case protoCritter of+ Nothing -> pure Nothing+ Just critter -> do+ let protoID = critter ^? isPrototype . prototypeID+ newID <- liftIO newUniqueEntRef+ let newCritter = critter & unsafeEntityReference .~ newID & addSpawnedFromPrototype .~ SpawnedFromPrototype proto protoID & removeIsPrototype+ updatedWorld = world & storeEntity newCritter+ pure (Just (newCritter, updatedWorld))++-- | Spawns a new individual with a given t`PrototypeID`, which is looked up in the associated map. Returns the new individual, and the new world.+{-# INLINEABLE spawnNamedPrototype #-}+spawnNamedPrototype :: (UsingSpawnedFromPrototype w Individual, UsingIsPrototype w Individual,MonadIO m) => PrototypeID -> w Storing -> m (Maybe (w Individual, w Storing))+spawnNamedPrototype prototypeName world = do+ let proto = world ^? prototype prototypeName . entityReference+ case proto of+ Nothing -> assert (False `blame` "Prototype ID doesn't exist!" `swith` proto) (pure Nothing)+ Just ent -> do+ spawned <- spawnPrototype ent world+ case spawned of+ Nothing -> assert (False `blame` ("Prototype ID " ++ show proto ++ " existed, but the entity it refers to doesn't!") `swith` ent) (pure Nothing)+ Just m -> pure (Just m)++-- | All the prototypical individuals in a world.+{-# INLINE prototypes #-}+prototypes :: (HasIsPrototype w) => IndexedTraversal' Entity (w Storing) (w Individual)+prototypes = entitiesWith withIsPrototype+
src/Games/ECS/World.hs view
@@ -36,6 +36,7 @@ import GHC.Base import GHC.Num import Games.ECS.Entity+import Games.ECS.Prototype.PrototypeID import Games.ECS.Slot import System.IO.Unsafe (unsafePerformIO) @@ -101,6 +102,9 @@ -- | Check if a given entity exists in the world, and if so, return the individual. lookupEntity :: w Storing -> Entity -> Maybe (w Individual) + -- | Get a prototype specification from its name.+ prototype :: HasPrototypeID p => p -> AffineTraversal' (w Storing) (w Individual)+ -- | An IndexedTraversal' which returns the individuals associated to the entities given as input. {-# INLINE lookupEntities #-} lookupEntities :: forall f p fol. (Indexable Entity p, Applicative f, Foldable fol) => fol Entity -> p (w Individual) (f (w Individual)) -> w Storing -> f (w Storing) -- Foldable f => f Entity -> IndexedTraversal' Entity (w Storing) (w Individual)
src/Games/ECS/World/TH.hs view
@@ -25,6 +25,7 @@ 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@@ -196,6 +197,10 @@ 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