packages feed

twirl-0.4.0.3: src/Twirl/Inputs.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns #-}

module Twirl.Inputs (
  InputState
  , ControllerAxis (..)
  , ControllerButton (..)
  , MouseButton (..)
  , updateInputs
  , closeControllers
  , emptyInputState
  , numControllers
  , isControllerButtonDown
  , controllerAxis
  , isKeyDown
  , isMouseDown
  , mousePosition
) where

import Control.Monad.IO.Class (MonadIO)
import Data.Foldable (foldlM)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Word (Word8)
import Foreign.C.Types (CInt)
import GHC.Int (Int16, Int32)
import SDL (
  ControllerAxisEventData (..)
  , ControllerButtonEventData (..)
  , ControllerDeviceEventData (..)
  , Event (..)
  , EventPayload (ControllerAxisEvent, ControllerButtonEvent, ControllerDeviceEvent, KeyboardEvent, QuitEvent)
  , InputMotion (Pressed, Released)
  , KeyboardEventData (..)
  , Keycode
  , Keysym (..)
  , MouseButton (..)
  , Point (..)
  , V2 (..)
  , getAbsoluteMouseLocation
  , getMouseButtons
  , keyboardEventKeyMotion
  , pollEvents
 )
import SDL.Input.GameController (
  ControllerButton (..)
  , ControllerButtonState (..)
  , ControllerDeviceConnection (..)
 )
import SDL.Raw.Event (gameControllerClose, gameControllerFromInstanceID, gameControllerGetJoystick, gameControllerOpen, joystickInstanceID)
import SDL.Raw.Types (GameController)
import Twirl.Graphics

data ControllerAxis = ControllerAxisLeftX | ControllerAxisLeftY | ControllerAxisRightX | ControllerAxisRightY | ControllerAxisTriggerLeft | ControllerAxisTriggerRight

data ControllerAxes = ControllerAxes
  { controllerAxisLeftX :: Int16
  , controllerAxisLeftY :: Int16
  , controllerAxisRightX :: Int16
  , controllerAxisRightY :: Int16
  , controllerAxisTriggerLeft :: Int16
  , controllerAxisTriggerRight :: Int16
  }
  deriving (Show)

data ControllerState = ControllerState
  { controllerStateButtons :: Set ControllerButton
  , controllerStateAxes :: ControllerAxes
  }
  deriving (Show)

emptyControllerState :: ControllerState
emptyControllerState =
  ControllerState
    { controllerStateButtons = Set.empty
    , controllerStateAxes =
        ControllerAxes
          { controllerAxisLeftX = 0
          , controllerAxisLeftY = 0
          , controllerAxisRightX = 0
          , controllerAxisRightY = 0
          , controllerAxisTriggerLeft = 0
          , controllerAxisTriggerRight = 0
          }
    }

data InputState = InputState
  { controllers :: Map Int ControllerState
  , keyDownSet :: Set Keycode
  , instanceIdToControllerNo :: Map Int32 Int
  , mouseButtons :: (MouseButton -> Bool)
  , mousePos :: (Int, Int)
  }
  deriving (Show)

instance Show (MouseButton -> Bool) where
  show _ = "<mouseButtonFunction>"

updateInputs :: MonadIO m => InputState -> m (InputState, Bool)
updateInputs inputState = do
  events <- pollEvents
  mouseInputFn <- getMouseButtons
  mouseLocation <- getAbsoluteMouseLocation
  let quitApp = any ((== QuitEvent) . eventPayload) events
  updatedInputs <- handleEvents events inputState{mouseButtons = mouseInputFn, mousePos = toMousePos mouseLocation}
  pure (updatedInputs, quitApp)

toMousePos :: Point V2 CInt -> (Int, Int)
toMousePos (P (V2 x y)) = (fromIntegral x, fromIntegral y)

emptyInputState :: InputState
emptyInputState = InputState{controllers = Map.empty, keyDownSet = Set.empty, instanceIdToControllerNo = Map.empty, mouseButtons = const False, mousePos = (0, 0)}

getControllerAxis :: ControllerAxis -> ControllerState -> Int16
getControllerAxis ControllerAxisLeftX controllerState = controllerAxisLeftX $ controllerStateAxes controllerState
getControllerAxis ControllerAxisLeftY controllerState = controllerAxisLeftY $ controllerStateAxes controllerState
getControllerAxis ControllerAxisRightX controllerState = controllerAxisRightX $ controllerStateAxes controllerState
getControllerAxis ControllerAxisRightY controllerState = controllerAxisRightY $ controllerStateAxes controllerState
getControllerAxis ControllerAxisTriggerLeft controllerState = controllerAxisTriggerLeft $ controllerStateAxes controllerState
getControllerAxis ControllerAxisTriggerRight controllerState = controllerAxisTriggerRight $ controllerStateAxes controllerState

-- | Get number of controllers
--
-- Returns the maximum number of controllers that have been connected simultaneously. This means that if
-- you connect four controllers and unplug one, this function will return 4.
numControllers :: InputState -> Int
numControllers InputState{controllers} = (length . Map.elems) controllers

-- | Query the state of controller buttons
--
-- Returns True if the queried button is down on the queried controller. Returns False if the controller is disconnected or doesn't exist
-- (e.g. you query controller number 4 but there are only 2 controllers).
isControllerButtonDown ::
  InputState ->
  -- | Controller number to be queried
  Int ->
  -- | The Controller button to be queried
  ControllerButton ->
  Bool
isControllerButtonDown InputState{controllers} controllerNumber button =
  case Map.lookup controllerNumber controllers of
    Nothing -> False
    Just controllerState -> Set.member button (controllerStateButtons controllerState)

-- | Query axes of controllers
--
-- For the stick axes, the returned value is between -1.0 and 1.0, where 0.0 is the neutral position.
-- For triggers the value is between 0.0 and 1.0 where 0.0 is the neutral position. Bear in mind that
-- usually the level of quality of controller sticks is so low that the sticks don't return exactly to
-- the 0.0 position, so you have to ignore some values which are too close to zero.
controllerAxis ::
  InputState ->
  -- | Controller number
  Int ->
  -- | The queried axis
  ControllerAxis ->
  Double
controllerAxis InputState{controllers} controllerNumber axis =
  case Map.lookup controllerNumber controllers of
    Nothing -> 0
    Just controllerState -> (fromIntegral $ getControllerAxis axis controllerState) / 32767

-- | Query keyboard state
isKeyDown :: InputState -> Keycode -> Bool
isKeyDown InputState{keyDownSet} keycode = Set.member keycode keyDownSet

-- | Query mouse button state
isMouseDown :: InputState -> MouseButton -> Bool
isMouseDown InputState{mouseButtons} = mouseButtons

-- | Get mouse position on screen
mousePosition :: InputState -> (Int, Int)
mousePosition InputState{mousePos} = mousePos

handleEvents :: MonadIO m => [Event] -> InputState -> m InputState
handleEvents events keySet =
  foldlM
    handleEvent
    keySet
    events

handleEvent :: MonadIO m => InputState -> Event -> m InputState
handleEvent inputState event =
  case eventPayload event of
    KeyboardEvent keyboardEvent ->
      case keyboardEventKeyMotion keyboardEvent of
        Pressed -> pure inputState{keyDownSet = Set.insert (keysymKeycode $ keyboardEventKeysym keyboardEvent) (keyDownSet inputState)}
        Released -> pure inputState{keyDownSet = Set.delete (keysymKeycode $ keyboardEventKeysym keyboardEvent) (keyDownSet inputState)}
    ControllerDeviceEvent (ControllerDeviceEventData{controllerDeviceEventConnection, controllerDeviceEventWhich}) -> case controllerDeviceEventConnection of
      -- Type cast is necessary because SDL returns either device index or instance id
      -- (https://wiki.libsdl.org/SDL_ControllerDeviceEvent) depending on whether the
      -- controller was added or removed, but Haskell bindings always report to return
      -- instance id (which must be wrong since added but unopened controllers don't have
      -- an instance id yet)
      ControllerDeviceAdded -> gameControllerOpen (fromIntegral controllerDeviceEventWhich) >>= addControllerMapping inputState
      ControllerDeviceRemoved -> do
        controllerPtr <- gameControllerFromInstanceID controllerDeviceEventWhich
        updatedInputs <- removeControllerMapping inputState controllerPtr
        gameControllerClose controllerPtr
        pure updatedInputs
      _ -> pure inputState
    ControllerButtonEvent e -> do
      let updatedState = updateInputStateController e inputState
      pure updatedState
    ControllerAxisEvent e -> do
      let updatedState = updateInputStateControllerAxis e inputState
      pure updatedState
    _ -> pure inputState

addControllerMapping :: MonadIO m => InputState -> GameController -> m InputState
addControllerMapping inputState@InputState{instanceIdToControllerNo} controller = do
  instanceId <- gameControllerGetJoystick controller >>= joystickInstanceID
  let controllerNo = nextFreeController inputState
  pure inputState{instanceIdToControllerNo = Map.insert instanceId controllerNo instanceIdToControllerNo}

nextFreeController :: InputState -> Int
nextFreeController InputState{instanceIdToControllerNo} =
  let mappedControllers = Set.fromList $ Map.elems instanceIdToControllerNo
   in findSmallestNaturalNotInSet mappedControllers
 where
  findSmallestNaturalNotInSet :: Set Int -> Int
  findSmallestNaturalNotInSet set =
    let go n = if not (Set.member n set) then n else go (n + 1)
     in go 0

removeControllerMapping :: MonadIO m => InputState -> GameController -> m InputState
removeControllerMapping inputState@InputState{instanceIdToControllerNo} controller = do
  instanceId <- gameControllerGetJoystick controller >>= joystickInstanceID
  pure inputState{instanceIdToControllerNo = Map.delete instanceId instanceIdToControllerNo}

closeControllers :: InputState -> TwirlMonad ()
closeControllers InputState{instanceIdToControllerNo} = do
  let controllerIds = Map.keys instanceIdToControllerNo
  controllerPtrs <- mapM gameControllerFromInstanceID controllerIds
  mapM_ gameControllerClose controllerPtrs

updateInputStateController :: ControllerButtonEventData -> InputState -> InputState
updateInputStateController ControllerButtonEventData{controllerButtonEventWhich, controllerButtonEventState, controllerButtonEventButton} inputState@InputState{controllers, instanceIdToControllerNo} =
  let controller = fromMaybe (nextFreeController inputState) $ Map.lookup controllerButtonEventWhich instanceIdToControllerNo
   in inputState{controllers = Map.alter (updateControllerState controllerButtonEventState controllerButtonEventButton) controller controllers}

updateControllerState :: ControllerButtonState -> ControllerButton -> Maybe ControllerState -> Maybe ControllerState
updateControllerState stateChange button maybeState =
  let state = fromMaybe emptyControllerState maybeState
      action = case stateChange of
        ControllerButtonPressed -> Set.insert
        ControllerButtonReleased -> Set.delete
        _ -> flip const
   in pure state{controllerStateButtons = action button (controllerStateButtons state)}

updateInputStateControllerAxis :: ControllerAxisEventData -> InputState -> InputState
updateInputStateControllerAxis ControllerAxisEventData{controllerAxisEventWhich, controllerAxisEventValue, controllerAxisEventAxis} inputState@InputState{controllers, instanceIdToControllerNo} =
  let controller = fromMaybe (nextFreeController inputState) $ Map.lookup controllerAxisEventWhich instanceIdToControllerNo
   in inputState{controllers = Map.alter (updateControllerStateAxis controllerAxisEventValue controllerAxisEventAxis) controller controllers}

updateControllerStateAxis :: Int16 -> Word8 -> Maybe ControllerState -> Maybe ControllerState
updateControllerStateAxis value axis maybeState =
  let state = fromMaybe emptyControllerState maybeState
   in pure state{controllerStateAxes = updateControllerAxis (controllerStateAxes state) value axis}

updateControllerAxis :: ControllerAxes -> Int16 -> Word8 -> ControllerAxes
updateControllerAxis axes value axis = case axis of
  0 -> axes{controllerAxisLeftX = value}
  1 -> axes{controllerAxisLeftY = value}
  2 -> axes{controllerAxisRightX = value}
  3 -> axes{controllerAxisRightY = value}
  4 -> axes{controllerAxisTriggerLeft = value}
  _ -> axes{controllerAxisTriggerRight = value}