{-# LANGUAGE GADTs, KindSignatures, RankNTypes #-}
module Game.LambdaHack.Client.UI.Frontend
(
FrontReq(..), ChanFrontend(..), chanFrontendIO
, frontendName
#ifdef EXPOSE_INTERNAL
, FrontSetup, getKey, fchanFrontend, display, defaultMaxFps, microInSec
, frameTimeoutThread, lazyStartup, nullStartup, seqFrame
#endif
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import Control.Concurrent
import Control.Concurrent.Async
import qualified Control.Concurrent.STM as STM
import Control.Monad.ST.Strict
import Data.IORef
import qualified Data.Vector.Generic as G
import qualified Data.Vector.Unboxed as U
import Data.Word
import Game.LambdaHack.Client.ClientOptions
import Game.LambdaHack.Client.UI.Content.Screen
import Game.LambdaHack.Client.UI.Frame
import qualified Game.LambdaHack.Client.UI.Frontend.Chosen as Chosen
import Game.LambdaHack.Client.UI.Frontend.Common
import qualified Game.LambdaHack.Client.UI.Frontend.Teletype as Teletype
import Game.LambdaHack.Client.UI.Key (KMP (..))
import qualified Game.LambdaHack.Client.UI.Key as K
import Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import qualified Game.LambdaHack.Definition.Color as Color
data FrontReq :: * -> * where
FrontFrame :: Frame -> FrontReq ()
FrontDelay :: Int -> FrontReq ()
FrontKey :: [K.KM] -> Frame -> FrontReq KMP
FrontPressed :: FrontReq Bool
FrontDiscardKey :: FrontReq ()
FrontResetKeys :: FrontReq ()
FrontAdd :: KMP -> FrontReq ()
FrontAutoYes :: Bool -> FrontReq ()
FrontShutdown :: FrontReq ()
FrontPrintScreen :: FrontReq ()
newtype ChanFrontend = ChanFrontend (forall a. FrontReq a -> IO a)
data FrontSetup = FrontSetup
{ fautoYesRef :: IORef Bool
, fasyncTimeout :: Async ()
, fdelay :: MVar Int
}
chanFrontendIO :: ScreenContent -> ClientOptions -> IO ChanFrontend
chanFrontendIO coscreen soptions = do
let startup | sfrontendNull soptions = nullStartup coscreen
| sfrontendLazy soptions = lazyStartup coscreen
#ifndef REMOVE_TELETYPE
| sfrontendTeletype soptions = Teletype.startup coscreen
#endif
| otherwise = Chosen.startup coscreen soptions
maxFps = fromMaybe defaultMaxFps $ smaxFps soptions
delta = max 1 $ microInSec `div` maxFps
rf <- startup
fautoYesRef <- newIORef $ not $ sdisableAutoYes soptions
fdelay <- newMVar 0
fasyncTimeout <- async $ frameTimeoutThread delta fdelay rf
let fs = FrontSetup{..}
return $ fchanFrontend fs rf
getKey :: FrontSetup -> RawFrontend -> [K.KM] -> Frame -> IO KMP
getKey fs rf@RawFrontend{fchanKey} keys frame = do
autoYes <- readIORef $ fautoYesRef fs
if autoYes && (null keys || K.spaceKM `elem` keys) then do
display rf frame
return $! KMP {kmpKeyMod = K.spaceKM, kmpPointer = originPoint}
else do
display rf frame
kmp <- STM.atomically $ STM.readTQueue fchanKey
if null keys || kmpKeyMod kmp `elem` keys
then return kmp
else getKey fs rf keys frame
fchanFrontend :: FrontSetup -> RawFrontend -> ChanFrontend
fchanFrontend fs@FrontSetup{..} rf =
ChanFrontend $ \case
FrontFrame frontFrame -> display rf frontFrame
FrontDelay k -> modifyMVar_ fdelay $ return . (+ k)
FrontKey frontKeyKeys frontKeyFrame ->
getKey fs rf frontKeyKeys frontKeyFrame
FrontPressed -> do
noKeysPending <- STM.atomically $ STM.isEmptyTQueue (fchanKey rf)
return $! not noKeysPending
FrontDiscardKey ->
void $ STM.atomically $ STM.tryReadTQueue (fchanKey rf)
FrontResetKeys -> resetChanKey (fchanKey rf)
FrontAdd kmp -> STM.atomically $ STM.writeTQueue (fchanKey rf) kmp
FrontAutoYes b -> writeIORef fautoYesRef b
FrontShutdown -> do
cancel fasyncTimeout
void $ tryTakeMVar $ fshowNow rf
fshutdown rf
FrontPrintScreen -> fprintScreen rf
display :: RawFrontend -> Frame -> IO ()
display rf@RawFrontend{fshowNow, fcoscreen=ScreenContent{rwidth, rheight}}
(m, upd) = do
let new :: forall s. ST s (G.Mutable U.Vector s Word32)
new = do
v <- unFrameBase m
unFrameForall upd v
return v
singleFrame = PointArray.Array rwidth rheight (U.create new)
putMVar fshowNow ()
fdisplay rf $ SingleFrame singleFrame
defaultMaxFps :: Int
defaultMaxFps = 24
microInSec :: Int
microInSec = 1000000
frameTimeoutThread :: Int -> MVar Int -> RawFrontend -> IO ()
frameTimeoutThread delta fdelay RawFrontend{..} = do
let loop = do
threadDelay delta
let delayLoop = do
delay <- readMVar fdelay
when (delay > 0) $ do
threadDelay $ delta * delay
modifyMVar_ fdelay $ return . subtract delay
delayLoop
delayLoop
let showFrameAndRepeatIfKeys = do
takeMVar fshowNow
readMVar fshowNow
noKeysPending <- STM.atomically $ STM.isEmptyTQueue fchanKey
unless noKeysPending $ do
void $ swapMVar fdelay 0
showFrameAndRepeatIfKeys
showFrameAndRepeatIfKeys
loop
loop
frontendName :: String
frontendName = Chosen.frontendName
lazyStartup :: ScreenContent -> IO RawFrontend
lazyStartup coscreen = createRawFrontend coscreen (\_ -> return ()) (return ())
nullStartup :: ScreenContent -> IO RawFrontend
nullStartup coscreen = createRawFrontend coscreen seqFrame (return ())
seqFrame :: SingleFrame -> IO ()
seqFrame SingleFrame{singleFrame} =
let seqAttr () attr = Color.colorToRGB (Color.fgFromW32 attr)
`seq` Color.bgFromW32 attr
`seq` Color.charFromW32 attr == ' '
`seq` ()
in return $! PointArray.foldlA' seqAttr () singleFrame