packages feed

free-game-1.2: FreeGame/Backend/GLFW.hs

{-# LANGUAGE Rank2Types, BangPatterns, ViewPatterns, CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  FreeGame.Backend.GLFW
-- Copyright   :  (C) 2013 Fumiaki Kinoshita
-- License     :  BSD-style (see the file LICENSE)
--
-- Maintainer  :  Fumiaki Kinoshita <fumiexcel@gmail.com>
-- Stability   :  experimental
-- Portability :  non-portable
--
----------------------------------------------------------------------------
module FreeGame.Backend.GLFW (Game(..), Frame, runGame) where
import Control.Lens (view)
import Control.Monad.IO.Class
import Control.Monad.Reader
import Control.Monad.Trans.Iter
import Control.Monad.Trans.Resource
import Data.BoundingBox
import Data.Functor.Identity
import Data.IORef
import FreeGame.Class
import FreeGame.Data.Bitmap
import FreeGame.Instances ()
import FreeGame.Types
import FreeGame.UI
import Linear
import qualified Data.IntMap.Strict as IM
import qualified Data.Map.Strict as Map
import qualified FreeGame.Internal.GLFW as G
import qualified Graphics.Rendering.OpenGL.GL as GL
import qualified Graphics.UI.GLFW as GLFW

keyCallback :: IORef (Map.Map Key ButtonState) -> GLFW.Window -> GLFW.Key -> Int -> GLFW.KeyState -> GLFW.ModifierKeys -> IO ()
keyCallback keyBuffer _ key _ GLFW.KeyState'Pressed _ = modifyIORef' keyBuffer $ Map.adjust buttonDown (toEnum $ fromEnum key)
keyCallback keyBuffer _ key _ GLFW.KeyState'Released _ = modifyIORef' keyBuffer $ Map.adjust buttonUp (toEnum $ fromEnum key)
keyCallback _ _ _ _ _ _ = return ()

mouseButtonCallback :: IORef (Map.Map Int ButtonState) -> GLFW.Window -> GLFW.MouseButton -> GLFW.MouseButtonState -> GLFW.ModifierKeys -> IO ()
mouseButtonCallback mouseBuffer _ btn GLFW.MouseButtonState'Pressed _ = modifyIORef' mouseBuffer $ Map.adjust buttonDown (fromEnum btn)
mouseButtonCallback mouseBuffer _ btn GLFW.MouseButtonState'Released _ = modifyIORef' mouseBuffer $ Map.adjust buttonUp (fromEnum btn)

mouseEnterCallback :: IORef Bool -> GLFW.Window -> GLFW.CursorState -> IO ()
mouseEnterCallback ref _ GLFW.CursorState'InWindow = writeIORef ref True
mouseEnterCallback ref _ GLFW.CursorState'NotInWindow = writeIORef ref False

runGame :: WindowMode -> BoundingBox2 -> Game a -> IO (Maybe a)
runGame mode bbox m = G.withGLFW mode bbox (execGame m)

initialKeyBuffer :: Map.Map Key ButtonState
initialKeyBuffer = Map.fromList $ zip [minBound..] (repeat Release)

initialMouseBuffer :: Map.Map Int ButtonState
initialMouseBuffer = Map.fromList $ zip [0..7] (repeat Release)

execGame :: Game a -> G.System -> IO (Maybe a)
execGame m system = do
    textureStorage <- newIORef IM.empty
    keyBuffer <- newIORef initialKeyBuffer
    mouseBuffer <- newIORef initialMouseBuffer
    mouseIn <- newIORef True
    scroll <- newIORef (V2 0 0)
    GLFW.setKeyCallback (G.theWindow system) $ Just $ keyCallback keyBuffer
    GLFW.setMouseButtonCallback (G.theWindow system) $ Just $ mouseButtonCallback mouseBuffer
    GLFW.setCursorEnterCallback (G.theWindow system) $ Just $ mouseEnterCallback mouseIn
    GLFW.setScrollCallback (G.theWindow system) $ Just $ \_ x y -> modifyIORef scroll (+V2 x y)
    let drawLocation = Location id id
    runResourceT $ gameLoop Env{..} m

data Env = Env
    { system :: G.System
    , textureStorage :: IORef (IM.IntMap G.Texture)
    , keyBuffer :: IORef (Map.Map Key ButtonState)
    , mouseBuffer :: IORef (Map.Map Int ButtonState)
    , mouseIn :: IORef Bool
    , scroll :: IORef (V2 Double)
    , drawLocation :: Location ()
    }

newtype Game a = Game { unGame :: IterT Frame a }
    deriving (Functor, Applicative, Monad, MonadIO, Affine, Picture2D, Mouse, Keyboard, FreeGame, Local, MonadFree Identity)

instance MonadResource Game where
    liftResourceT = Game . lift . liftResourceT

newtype Frame a = Frame { unFrame :: ReaderT Env (ResourceT IO) a }
    deriving (Functor, Applicative, Monad, MonadResource, MonadIO, MonadReader Env)

gameLoop :: Env -> Game a -> ResourceT IO (Maybe a)
gameLoop env@Env{..} (Game m0) = flip fix m0 $ \self m -> do
    liftIO $ G.beginFrame system

    r <- runReaderT (unFrame (runIterT m)) env

    b <- liftIO $ do
        modifyIORef' keyBuffer (Map.map buttonStay)
        modifyIORef' mouseBuffer (Map.map buttonStay)
        writeIORef scroll (V2 0 0 :: V2 Double)
        G.endFrame system

    if b
        then return Nothing
        else either (return . Just) self r

withLocation :: (Location () -> Location ()) -> Env -> Env
withLocation f de = de { drawLocation = f $ drawLocation de }

instance FreeGame Frame where
    preloadBitmap (Bitmap bmp h) = Frame $ ReaderT $ \Env{..} -> do
        m <- liftIO $ readIORef textureStorage
        case IM.lookup h m of
            Just _ -> return ()
            Nothing -> do
                t <- liftIO $ G.installTexture bmp
                liftIO $ writeIORef textureStorage $ IM.insert h t m
                void $ register $ G.releaseTexture t >> modifyIORef' textureStorage (IM.delete h)

    takeScreenshot = Frame $ ReaderT $ \Env{..} -> liftIO (G.screenshot system >>= liftBitmapIO)
    clearColor (V4 r g b a) = liftIO $ GL.clearColor GL.$= GL.Color4 r g b a
    setTitle str = Frame $ ReaderT $ \Env{..} -> liftIO $ GLFW.setWindowTitle (G.theWindow system) str
    showCursor = Frame $ ReaderT $ \Env{..} -> liftIO $ GLFW.setCursorInputMode (G.theWindow system) GLFW.CursorInputMode'Normal
    hideCursor = Frame $ ReaderT $ \Env{..} -> liftIO $ GLFW.setCursorInputMode (G.theWindow system) GLFW.CursorInputMode'Hidden
    setFPS n = Frame $ ReaderT $ \Env{..} -> liftIO $ writeIORef (G.theFPS system) n
    getFPS = Frame $ ReaderT $ \Env{..} -> liftIO (readIORef (G.currentFPS system))
    getBoundingBox = Frame $ ReaderT $ \Env{..} -> liftIO (readIORef (G.refRegion system))
    setBoundingBox bbox@(view (size zero)-> V2 w h) = Frame $ ReaderT $ \Env{..} -> do
        liftIO $ GLFW.setWindowSize (G.theWindow system) (floor w) (floor h)
        liftIO $ writeIORef (G.refRegion system) bbox

instance Keyboard Frame where
    keyStates_ = Frame $ ReaderT $ \Env{..} -> liftIO (readIORef keyBuffer)

instance Mouse Frame where
    mouseButtons_ = Frame $ ReaderT $ \Env{..} -> liftIO (readIORef mouseBuffer)
    globalMousePosition = Frame $ ReaderT $ \Env{..} -> do
        (x, y) <- liftIO $ GLFW.getCursorPos (G.theWindow system)
        pure $ V2 x y
    mouseScroll = Frame $ ReaderT $ \Env{..} -> liftIO (readIORef scroll)
    mouseInWindow = Frame $ ReaderT $ \Env{..} -> liftIO (readIORef mouseIn)

mapReaderWith :: (Env -> Env) -> (IO a -> IO a) -> Frame a -> Frame a
mapReaderWith f g m = Frame $ ReaderT $ \s -> transResourceT g (runReaderT (unFrame m) (f s))
{-# INLINE mapReaderWith #-}

instance Affine Frame where
    translate v = mapReaderWith (withLocation $ translate v) (G.translate v)
    {-# INLINE translate #-}
    rotateD t = mapReaderWith (withLocation $ rotateD t) (G.rotateD t)
    {-# INLINE rotateD #-}
    rotateR t = let t' = t / pi * 180 in mapReaderWith (withLocation $ rotateR t) (G.rotateD t')
    {-# INLINE rotateR #-}
    scale v = mapReaderWith (withLocation $ scale v) (G.scale v)
    {-# INLINE scale #-}

instance Picture2D Frame where
    bitmap (Bitmap bmp h) = Frame $ ReaderT $ \Env{..} -> do
        m <- liftIO $ readIORef textureStorage
        case IM.lookup h m of
            Just t -> liftIO $ G.drawTexture t
            Nothing -> do
                t <- liftIO $ G.installTexture bmp
                liftIO $ writeIORef textureStorage $ IM.insert h t m
                _ <- register $ modifyIORef' textureStorage $ IM.delete h
                liftIO $ G.drawTexture t
    bitmapOnce (Bitmap bmp _) = liftIO $ do
        t <- G.installTexture bmp
        G.drawTexture t
        G.releaseTexture t

    circle r = liftIO (G.circle r)
    {-# INLINE circle #-}
    circleOutline r = liftIO (G.circleOutline r)
    {-# INLINE circleOutline #-}
    polygon vs = liftIO (G.polygon vs)
    {-# INLINE polygon #-}
    polygonOutline vs = liftIO (G.polygonOutline vs)
    {-# INLINE polygonOutline #-}
    line vs = liftIO (G.line vs)
    {-# INLINE line #-}
    thickness t = mapReaderWith id (G.thickness t)
    {-# INLINE thickness #-}
    color c = mapReaderWith id (G.color c)
    {-# INLINE color #-}
    blendMode m = mapReaderWith id (G.blendMode m)
    {-# INLINE blendMode #-}

instance Local Frame where
    getLocation = asks $ coerceLocation . drawLocation