immutaball-core-0.1.0.4.1: Immutaball/Share/Controller.hs
{-# OPTIONS_GHC -fno-warn-tabs #-} -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .
-- Enable warnings:
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
-- State.hs.
{-# LANGUAGE Haskell2010 #-}
module Immutaball.Share.Controller
(
controlImmutaball,
takeAllSDLEventsIndividually,
takeAllSDLEventsList,
takeAllSDLEvents,
stepFrameNoMaxClockPeriod,
stepFrame,
stepEvent,
stepEventNoMaxClockPeriod,
stepClock,
processStepResult,
doesResponseFork,
unimplementedHelper,
-- * SDL utils
isKbdEventDown,
kbdEventChar,
isMousePressed,
getMouseButton
) where
import Prelude ()
import Immutaball.Prelude
import Data.Function hiding (id, (.))
import Data.List
import Control.Concurrent.STM.TMVar
import Control.Lens
import SDL.Event
import SDL.Input.Keyboard
import qualified SDL.Raw.Enum as Raw
import SDL.Vect
import Control.Monad.Trans.MaybeM
import Immutaball.Share.AutoPar
import Immutaball.Share.Config
import Immutaball.Share.Context
import Immutaball.Share.ImmutaballIO
import Immutaball.Share.ImmutaballIO.BasicIO
import Immutaball.Share.SDLManager
import Immutaball.Share.State
import Immutaball.Share.Utils
import Immutaball.Share.Wire
controlImmutaball :: IBContext -> Immutaball -> ImmutaballIO
controlImmutaball cxt0 immutaball0 =
result
where
result :: ImmutaballIO
result = initialFrame
initialFrame :: ImmutaballIO
initialFrame =
mkBIO . GetUs $ \us0 ->
nextFrame us0 [] immutaball0
nextFrame :: Integer -> [Event] -> Immutaball -> ImmutaballIO
nextFrame usNm1 queuedEvents immutaballN =
mkBIO . GetUs $ \usN ->
let dus = max 0 $ usN - usNm1 in
let ds = (fromInteger dus / 1000000.0) :: Double in
let usNm1pMinClockPeriod = usNm1 + (max 0 . round $ 1000000.0 * maybe 0 id (cxt0^.ibStaticConfig.minClockPeriod)) in
if' (usN < usNm1pMinClockPeriod) (mkBIO . DelayUs (usNm1pMinClockPeriod - usN)) id .
takeAllSDLEvents cxt0 $ \events ->
let events' = queuedEvents ++ events in
let events'' = maybe id (take . fromIntegral) (cxt0^.ibStaticConfig.maxFrameEvents) events' in
stepFrame' cxt0 ds usN events'' immutaballN (nextFrame usN)
stepFrame'
| Nothing <- (cxt0^.ibStaticConfig.maxClockPeriod) = stepFrameNoMaxClockPeriod'
| otherwise = stepFrame
stepFrameNoMaxClockPeriod' cxt ds us events immutaball withImmutaball =
stepFrameNoMaxClockPeriod cxt ds us events immutaball (withImmutaball [])
takeAllSDLEventsIndividually :: IBContext -> ([Event] -> ImmutaballIO) -> ImmutaballIO
takeAllSDLEventsIndividually cxt withEvents =
mkAtomically newEmptyTMVar $ \eventStorage ->
flip fix [] $ \me events ->
Fixed . issueSDLCommand (cxt^.ibSDLManagerHandle) (PollEvent eventStorage) .
mkAtomically (takeTMVar eventStorage) $ \mevent ->
case mevent of
Nothing -> withEvents $ reverse events
Just event -> me (event:events)
takeAllSDLEventsList :: IBContext -> ([Event] -> ImmutaballIO) -> ImmutaballIO
takeAllSDLEventsList cxt withEvents =
mkAtomically newEmptyTMVar $ \eventsStorage ->
Fixed . issueSDLCommand (cxt^.ibSDLManagerHandle) (PollEvents eventsStorage) .
mkAtomically (takeTMVar eventsStorage) $ \events ->
withEvents events
takeAllSDLEvents :: IBContext -> ([Event] -> ImmutaballIO) -> ImmutaballIO
takeAllSDLEvents = takeAllSDLEventsIndividually
-- | Step each event then clock.
stepFrameNoMaxClockPeriod :: IBContext -> Double -> Integer -> [Event] -> Immutaball -> (Immutaball -> ImmutaballIO) -> ImmutaballIO
stepFrameNoMaxClockPeriod cxt ds us events immutaball withImmutaball =
foldr
(\event withImmutaballNp1 -> \immutaballN -> stepEventNoMaxClockPeriod cxt event immutaballN withImmutaballNp1)
(\immutaballN -> stepClock cxt ds us immutaballN withImmutaball)
events
immutaball
-- | Step each event then clock.
--
-- This variant handles 'maxClockPeriod' (see documentation in 'StaticConfig').
-- It ensures that if 'maxClockPeriod' amount of time has passed since any
-- clock step, a clock step is inserted before processing the next event,
-- if at least one event has been processed.
stepFrame :: IBContext -> Double -> Integer -> [Event] -> Immutaball -> ([Event] -> Immutaball -> ImmutaballIO) -> ImmutaballIO
stepFrame cxt ds us events immutaball withImmutaball =
let z = (\queued _mclockAtUs _noClock immutaballN -> stepClock cxt ds us immutaballN (withImmutaball queued)) in
let defer = \queued -> z queued Nothing False in
let eventsWithRemaining = zip events (drop 1 $ tails events) in
foldr
(\(event, eventsRemaining) withImmutaballNp1 -> \mclockAtUs noClock immutaballN -> stepEvent cxt event eventsRemaining mclockAtUs noClock immutaballN defer withImmutaballNp1)
(z [])
eventsWithRemaining
((\p -> max 0 . round $ 1000000.0 * p) <$> (cxt^.ibStaticConfig.maxClockPeriod))
True
immutaball
-- | Step an event; variant that handles 'maxClockPeriod'.
--
-- maxClockPeriod: the maybe argument just makes it so that if the current time in
-- milliseconds is >= the threshold, we insert a clock step before processing
-- more events. Then we also use the ‘noClock’ Bool to make sure we process at
-- least one Event at a time.
stepEvent ::
IBContext -> Event -> [Event] ->
Maybe Integer -> Bool -> Immutaball ->
([Event] -> Immutaball -> ImmutaballIO) ->
(Maybe Integer -> Bool -> Immutaball -> ImmutaballIO) ->
ImmutaballIO
stepEvent cxt event eventsRemaining mclockAtUs noClock immutaballN defer withImmutaballNp1 =
case (mclockAtUs, noClock) of
(Just clockAtUs, False) ->
mkBIO . GetUs $ \us_ ->
if' (not $ us_ >= clockAtUs)
(
stepEventNoMaxClockPeriod cxt event immutaballN (withImmutaballNp1 mclockAtUs False)
)
(
defer eventsRemaining immutaballN
)
_ -> stepEventNoMaxClockPeriod cxt event immutaballN (withImmutaballNp1 mclockAtUs noClock)
-- | Step an event.
stepEventNoMaxClockPeriod :: IBContext -> Event -> Immutaball -> (Immutaball -> ImmutaballIO) -> ImmutaballIO
stepEventNoMaxClockPeriod cxt event immutaballN withImmutaballNp1 =
case event of
(Event _ (QuitEvent)) ->
mempty
(Event _ (MouseMotionEvent (MouseMotionEventData _ _ _ (P (V2 x y)) (V2 dx dy)))) ->
let (x', y', dx', dy') = (fromIntegral x, fromIntegral y, fromIntegral dx, fromIntegral dy) in
let mresponse = stepWire immutaballN (pure $ Point x' y' dx' dy') in
processStepResult cxt mresponse withImmutaballNp1
(Event _ (MouseButtonEvent (MouseButtonEventData _ pressed _ mouseButton _ _))) ->
let (button, down) = (fromIntegral $ getMouseButton mouseButton, isMousePressed pressed) in
let mresponse = stepWire immutaballN (pure $ Click button down) in
processStepResult cxt mresponse withImmutaballNp1
(Event _ (KeyboardEvent kbdEvent)) ->
let (char, down) = (fromIntegral $ kbdEventChar kbdEvent, isKbdEventDown kbdEvent) in
let mresponse = stepWire immutaballN (pure $ Keybd char down) in
processStepResult cxt mresponse withImmutaballNp1
_ ->
-- Ignore all unhandled events.
withImmutaballNp1 immutaballN
stepClock :: IBContext -> Double -> Integer -> Immutaball -> (Immutaball -> ImmutaballIO) -> ImmutaballIO
stepClock cxt ds us immutaballN withImmutaballNp1 =
let mresponse = stepWire immutaballN (pure $ Clock ds) in
processStepResult cxt mresponse $ \immutaballNp1 ->
let mresponse_ = stepWire immutaballNp1 (pure . Paint $ (fromIntegral us) / 1000000.0) in
if' (cxt^.ibHeadless)
(withImmutaballNp1 immutaballNp1)
(processStepResult cxt mresponse_ withImmutaballNp1)
processStepResult :: (Foldable t) => IBContext -> ImmutaballM (t Response, Immutaball) -> (Immutaball -> ImmutaballIO) -> ImmutaballIO
processStepResult cxt mresponse withImmutaballNp1 =
runAutoParT mresponse & \mioresponse ->
either (\ioresponse -> Fixed . flip fmap ioresponse) (&) (runMaybeMT mioresponse) $ \(response, immutaballNp1) ->
let failFork = mkBIO . PutStrLn "Error: processStepResult: wire forking is disabled, but the wire requested a fork; aborting" $ mkBIO ExitFailureBasicIOF in
if' (not (cxt^.ibStaticConfig.allowWireForks) && doesResponseFork response) failFork $
flip foldMap response $ \responseI ->
case responseI of
ContinueResponse -> withImmutaballNp1 immutaballNp1
DoneResponse -> mempty
PureFork immutaballFork -> withImmutaballNp1 immutaballFork <> withImmutaballNp1 immutaballNp1
ImmutaballIOFork mimmutaballFork -> Fixed (fmap withImmutaballNp1 mimmutaballFork) <> withImmutaballNp1 immutaballNp1
doesResponseFork :: (Foldable t) => t Response -> Bool
doesResponseFork response
| any isFork responses = True
| lcontinues > 1 = True
| otherwise = False
where
responses :: [Response]
responses = foldr (:) [] response
isFork :: Response -> Bool
isFork (ContinueResponse ) = False
isFork (DoneResponse ) = False
isFork (PureFork _) = True
isFork (ImmutaballIOFork _) = True
isContinue :: Response -> Bool
isContinue (ContinueResponse ) = True
isContinue (DoneResponse ) = False
isContinue (PureFork _) = False
isContinue (ImmutaballIOFork _) = False
lcontinues :: Integer
lcontinues = genericLength . filter isContinue $ responses
unimplementedHelper :: ImmutaballIO
unimplementedHelper =
runBasicImmutaballIO (mkPutStrLn "Internal error: unimplemented." $ mkExitFailureBasicIO)
-- * SDL utils
isKbdEventDown :: KeyboardEventData -> Bool
isKbdEventDown (KeyboardEventData _ Pressed _ _) = True
isKbdEventDown (KeyboardEventData _ Released _ _) = False
kbdEventChar :: KeyboardEventData -> Integer
kbdEventChar (KeyboardEventData _ _ _ (Keysym _ (Keycode keyCode) _)) = (fromIntegral keyCode)
isMousePressed :: InputMotion -> Bool
isMousePressed Pressed = True
isMousePressed Released = False
getMouseButton :: MouseButton -> Integer
getMouseButton (ButtonLeft) = Raw.SDL_BUTTON_LEFT
getMouseButton (ButtonMiddle) = Raw.SDL_BUTTON_MIDDLE
getMouseButton (ButtonRight) = Raw.SDL_BUTTON_RIGHT
getMouseButton (ButtonX1) = Raw.SDL_BUTTON_X1
getMouseButton (ButtonX2) = Raw.SDL_BUTTON_X2
getMouseButton (ButtonExtra button) = fromIntegral button