packages feed

apecs-roaring-1.0: src/Apecs/Stores/Query.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

{-| Joins over Roaring-indexed stores ('Apecs.Stores.Flags.Flags' and 'Apecs.Stores.Roaring.Roaring'), computed as bitmap operations.

Wrap a tuple in 'Every' to iterate the entities that have every component in it:

@
cmap $ \\(Every (Enemy, Health hp, _ :: Not Dead)) -> Health (hp - 1)
@

The members of an 'Every' come from intersecting the indexes and subtracting the 'Apecs.Not' ones, without visiting entities outside the result.
'count' sizes the same join without listing it.
At least one term must not be wrapped in 'Apecs.Not'.
-}
module Apecs.Stores.Query
  ( Every (..)
  , EveryStore (..)
  , Indexes (..)
  , Plan
  , count
  ) where

import Apecs.Components (NotStore (..))
import Apecs.Core (Component (..), Elem, ExplGet (..), ExplMembers (..), Has (..), SystemT)
import Apecs.Stores.Flags (Flags)
import Apecs.Stores.Index (HasIndex (..), Index)
import Apecs.Stores.Index qualified as Index
import Apecs.Stores.Roaring (Roaring)
import Control.Exception (bracket)
import Control.Monad (forM_)
import Control.Monad.IO.Class (MonadIO (..))
import Data.Bitmap.Roaring.FFI.Bitmap64 (Bitmap64)
import Data.Bitmap.Roaring.FFI.Bitmap64 qualified as Bitmap64
import Data.Vector.Unboxed qualified as Unboxed

newtype Every t = Every t

newtype EveryStore s = EveryStore s

type instance Elem (EveryStore s) = Every (Elem s)

instance (Component t) => Component (Every t) where
  type Storage (Every t) = EveryStore (Storage t)

instance (Has w m t) => Has w m (Every t) where
  getStore = EveryStore <$> getStore
  {-# INLINE getStore #-}

instance (ExplGet m s) => ExplGet m (EveryStore s) where
  explGet (EveryStore s) ety = Every <$> explGet s ety
  {-# INLINE explGet #-}
  explExists (EveryStore s) = explExists s
  {-# INLINE explExists #-}

instance (MonadIO m, Indexes s) => ExplMembers m (EveryStore s) where
  explMembers (EveryStore s) = liftIO . members $ plan s
  {-# INLINE explMembers #-}

data Plan = Plan [Index] [Index]

instance Semigroup Plan where
  Plan required excluded <> Plan required' excluded' = Plan (required <> required') (excluded <> excluded')

class Indexes s where
  plan :: s -> Plan

instance Indexes (Flags c) where
  plan flags = Plan [getIndex flags] []

instance Indexes (Roaring s) where
  plan roaring = Plan [getIndex roaring] []

instance (HasIndex s) => Indexes (NotStore s) where
  plan (NotStore s) = Plan [] [getIndex s]

instance (Indexes s) => Indexes (EveryStore s) where
  plan (EveryStore s) = plan s

instance (Indexes a, Indexes b) => Indexes (a, b) where
  plan (a, b) = plan a <> plan b

instance (Indexes a, Indexes b, Indexes c) => Indexes (a, b, c) where
  plan (a, b, c) = plan a <> plan b <> plan c

instance (Indexes a, Indexes b, Indexes c, Indexes d) => Indexes (a, b, c, d) where
  plan (a, b, c, d) = plan a <> plan b <> plan c <> plan d

count :: forall c w m. (Has w m c, Indexes (Storage c), MonadIO m) => SystemT w m Int
count = do
  store <- getStore @w @m @c
  liftIO . cardinality $ plan store

members :: Plan -> IO (Unboxed.Vector Int)
members = \case
  Plan [index] [] -> Index.toVector index
  joined -> evaluate joined Index.bitmapToVector

cardinality :: Plan -> IO Int
cardinality = \case
  Plan [index] [] -> Index.size index
  Plan [left, right] [] -> fromIntegral <$> pairwise Bitmap64.andCardinality left right
  Plan [left] [right] -> fromIntegral <$> pairwise Bitmap64.andnotCardinality left right
  joined -> evaluate joined $ fmap fromIntegral . Bitmap64.getCardinality

pairwise :: (Bitmap64 -> Bitmap64 -> IO a) -> Index -> Index -> IO a
pairwise operation left right = Index.with left $ \leftBitmap -> Index.with right $ operation leftBitmap

evaluate :: Plan -> (Bitmap64 -> IO a) -> IO a
evaluate (Plan required excluded) action = case required of
  [] -> fail "Apecs.Stores.Query: a query needs at least one term outside Not"
  first : rest -> bracket (seed first rest excluded) Bitmap64.free $ \scratch -> do
    forM_ (drop 1 rest) $ \index -> Index.with index $ Bitmap64.andInplace scratch
    forM_ (dropSeeded rest excluded) $ \index -> Index.with index $ Bitmap64.andnotInplace scratch
    action scratch
  where
    seed first rest exclusions = case (rest, exclusions) of
      (second : _, _) -> fresh $ pairwise Bitmap64.and first second
      ([], exclusion : _) -> fresh $ pairwise Bitmap64.andnot first exclusion
      ([], []) -> fresh $ Index.with first Bitmap64.copy
    dropSeeded rest exclusions = case rest of
      [] -> drop 1 exclusions
      _ -> exclusions
    fresh construct = construct >>= Index.checked "Apecs.Stores.Query.evaluate"