packages feed

spade-0.1.0.0: src/Interpreter/Lib/SDL.hs

module Interpreter.Lib.SDL where

import Control.Monad.IO.Class
import Control.Monad.Loops
import Control.Monad.State.Strict
import qualified Data.ByteString as BS
import System.Process (createPipe)
import Data.Coerce
import Data.IORef
import Data.Int (Int32)
import qualified Data.List as DL
import qualified Data.Map as M
import Data.Maybe
import Data.WAVE
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
import Data.Word
import Foreign.C.Types
import SDL hiding (Keycode, Scancode, get)
import qualified SDL
import SDL.Mixer as SDLM

import Interpreter.Common
import Interpreter.Interpreter

makeSinWaveChunk :: Double -> BS.ByteString
makeSinWaveChunk freq = BS.pack $
  fmap (\n ->
         let t = fromIntegral n / 22050 :: Double
         in round $ 127 + (127 * sin (2 * pi * freq * t)))
      [0 :: Int32 .. 22050]

createGraphicsWindow :: BuiltInFnWithDoc ['("width", Int), '("height", Int), '("accelerated", Maybe Bool)]
createGraphicsWindow ((coerce -> w) :> (coerce -> h) :> (coerce -> maccelerated) :>_) =
  initGraphics (Just (w, h)) (fromMaybe False maccelerated)

createGraphicsFullscreen :: BuiltInFnWithDoc '[ '("accelerated", Maybe Bool)]
createGraphicsFullscreen ((coerce -> maccelerated) :>_) =
  initGraphics Nothing (fromMaybe False maccelerated)

initGraphics :: Maybe (Int, Int) -> Bool -> InterpretM (Maybe Value)
initGraphics md acc = do
  let windowName = "S.P.A.D.E Program"
  (renderer, window) <- liftIO $ do
    SDL.initialize [SDL.InitVideo, SDL.InitAudio, SDL.InitEvents, SDL.InitTimer]
    SDLM.openAudio SDLM.defaultAudio 256
    window <- case md of
      Just (w, h) -> SDL.createWindow windowName (windowConfig w h)
      Nothing     -> SDL.createWindow windowName fullscreenConfig
    renderer <- case acc of
      True -> SDL.createRenderer window (-1) SDL.defaultRenderer
      _ -> SDL.createRenderer window (-1) $ SDL.defaultRenderer { rendererType = SoftwareRenderer }
    pure (renderer, window)
  sdlWindowRefs <- isSDLWindows <$> get
  liftIO $ modifyIORef sdlWindowRefs (\l -> (window : l))
  modify (\x -> x
    { isDefaultWindow = Just window
    , isDefaultRenderer = Just renderer
    , isAccelerated = Just acc
    })
  pure $ Just $ SDLValue $ Renderer renderer

fullscreenConfig :: SDL.WindowConfig
fullscreenConfig = SDL.defaultWindow
  { windowHighDPI = False
  , windowMode = FullscreenDesktop
  }

windowConfig :: Int -> Int -> SDL.WindowConfig
windowConfig w h = SDL.defaultWindow
  { windowHighDPI = False
  , windowInitialSize = (SDL.V2 (fromIntegral w) (fromIntegral h))
  , windowMode = Windowed
  }

setLogicalSize :: BuiltInFnWithDoc ['("x", CInt), '("y", CInt)]
setLogicalSize ((coerce -> (lx :: CInt)) :> (coerce -> (ly :: CInt)) :> _) =
  isDefaultRenderer <$> get >>= \case
    Just renderer  -> do
      SDL.V2 x y <- getWindowSize'
      SDL.rendererScale renderer SDL.$= (SDL.V2 (realToFrac x/realToFrac lx) (realToFrac y/realToFrac ly))
      pure Nothing
    Nothing -> throwErr $ SDLError "Graphics not Initialized"

getDefaultWindow :: InterpretM SDL.Window
getDefaultWindow = isDefaultWindow <$> get >>= \case
  Just x  -> pure x
  Nothing -> throwErr $ SDLError "Graphics not Initialized"

getDefaultRenderer :: InterpretM SDL.Renderer
getDefaultRenderer = isDefaultRenderer <$> get >>= \case
  Just x  -> pure x
  Nothing -> throwErr $ SDLError "Graphics not Initialized"

draw :: BuiltInFnWithDoc '[]
draw _ = do
  draw'
  pure Nothing

draw' :: InterpretM ()
draw' = getDefaultRenderer >>= SDL.present

drawIfNotAccelerated :: InterpretM ()
drawIfNotAccelerated = (isAccelerated <$> get) >>= \case
  (Just False) -> draw'
  _            -> pure ()

setDrawColor :: BuiltInFnWithDoc ['("red_component", Word8), '("green_component", Word8), '("blue_component", Word8)]
setDrawColor ((coerce -> r) :> (coerce -> g) :> (coerce -> b) :> _) = do
  getDefaultRenderer >>= \renderer -> do
    SDL.rendererDrawColor renderer $= V4 r g b 0
  pure Nothing

clear :: BuiltInFnWithDoc '[]
clear _ = do
  getDefaultRenderer >>= SDL.clear
  pure Nothing

drawPoint :: BuiltInFnWithDoc ['("x", CInt), '("y", CInt)]
drawPoint ((coerce -> x) :> (coerce -> y) :> _) = do
  renderer <- getDefaultRenderer
  SDL.drawPoint renderer (mkPoint x y)
  drawIfNotAccelerated
  pure Nothing

drawPoints :: BuiltInFnWithDoc '[ '("points", VS.Vector (SDL.Point V2 CInt))]
drawPoints ((coerce -> v) :> _) = do
  renderer <- getDefaultRenderer
  SDL.drawPoints renderer v
  drawIfNotAccelerated
  pure Nothing

drawLines :: BuiltInFnWithDoc '[ '("points", VS.Vector (SDL.Point V2 CInt))]
drawLines ((coerce -> v) :> _) = do
  renderer <- getDefaultRenderer
  SDL.drawLines renderer v
  if VS.length v > 0 then do
    let last' = VS.last v
    SDL.drawPoint renderer last'
    SDL.drawPoint renderer last'
  else pure ()
  drawIfNotAccelerated
  pure Nothing

drawLine :: BuiltInFnWithDoc ['("start_x", CInt), '("start_y", CInt), '("end_x", CInt), '("end_y", CInt)]
drawLine ((coerce -> x) :> (coerce -> y) :> (coerce -> xEnd) :> (coerce -> yEnd) :>_) = do
  renderer <- getDefaultRenderer
  let endpoint = mkPoint xEnd yEnd
  SDL.drawLine renderer (mkPoint x y) endpoint
  SDL.drawPoint renderer endpoint
  SDL.drawPoint renderer endpoint
    -- Due to some bug in SDL, without these extra call
    -- the next draw item appear to have a stray pixel with the same color
    -- that was used to draw this one.
  drawIfNotAccelerated
  pure Nothing

drawBox :: BuiltInFnWithDoc ['("start_x", CInt), '("start_y", CInt), '("width", CInt), '("height", CInt), '("fill", Maybe Bool)]
drawBox ((coerce -> x) :> (coerce -> y) :> (coerce -> width) :> (coerce -> height) :> (coerce -> fill) :> _) = do
  renderer <- getDefaultRenderer
  let
    f = case fill of
      Just b -> b
      Nothing -> False
  if f
    then SDL.fillRect renderer $ Just $ SDL.Rectangle (mkPoint x y) (SDL.V2 width height)
    else SDL.drawRect renderer $ Just $ SDL.Rectangle (mkPoint x y) (SDL.V2 width height)
  drawIfNotAccelerated
  pure Nothing

drawCircle :: BuiltInFnWithDoc ['("center_x", Double), '("center_y", Double), '("radius", Double)]
drawCircle ((coerce -> x) :> (coerce -> y) :> (coerce -> radius) :> _) = do
  renderer <- getDefaultRenderer
  let fullCircle = 2.0 * pi
  let segments = 50
  let (oneSegment :: Double) = fullCircle/segments
  let points = VS.fromList $ (\a -> mkPoint (round $ x + cos (oneSegment * a) * radius) (round $ y + sin (oneSegment * a) * radius)) <$> [0..segments]
  SDL.drawLines renderer points
  let last' = VS.last points
  SDL.drawPoint renderer last'
  SDL.drawPoint renderer last'
  drawIfNotAccelerated
  pure Nothing

waitForSDLKey :: BuiltInFnWithDoc '[]
waitForSDLKey _ = do
  void $ iterateWhile id $ do
    events <- pollEvents
    pure $ ((length $ filter filterEvent events) == 0)
  pure Nothing
  where
    filterEvent :: Event -> Bool
    filterEvent event =
      case eventPayload event of
        KeyboardEvent keyboardEvent ->
          (keyboardEventKeyMotion keyboardEvent == Pressed)
        _ -> False

getWindowSize' :: InterpretM (SDL.V2 CInt)
getWindowSize' = do
  window <- getDefaultWindow
  liftIO $ SDL.get (windowSize window)

getWindowSize :: BuiltInFnWithDoc '[]
getWindowSize _ = do
  (SDL.V2 x y) <- getWindowSize'
  pure $ Just $ ObjectValue $ M.fromList [("width", NumberValue $ NumberInt $ fromIntegral x), ("height", NumberValue $ NumberInt $ fromIntegral y)]

getKeyboardState :: BuiltInFnWithDoc '[]
getKeyboardState _ = do
  SDL.pumpEvents
  fn <- SDL.getKeyboardState
  pure $ Just $ SDLValue $ KeyboardState $ SDLKeyboardStateCallback fn

wasKeyDownIn :: BuiltInFnWithDoc '[ '("keyboard_state", SDLKeyboardStateCallback), '("key", SDL.Scancode) ]
wasKeyDownIn ((coerce -> (SDLKeyboardStateCallback cb)) :> (coerce -> scancode ) :> _) =
  pure $ Just $ BoolValue $ cb scancode

getKeys :: BuiltInFnWithDoc '[]
getKeys _ = do
  events <- pollEvents
  pure $ Just $ ArrayValue $ DL.foldl' convertEvent V.empty events
  where
    convertEvent :: V.Vector Value -> Event -> V.Vector Value
    convertEvent inp event =
      case eventPayload event of
        KeyboardEvent keyboardEvent ->
            V.cons (SDLValue $ Keycode ((keysymKeycode (keyboardEventKeysym keyboardEvent)))) inp
        _ -> inp

builtInSetSampleVolume :: BuiltInFnWithDoc '[ '("channel", Channel), '("volume", Int)]
builtInSetSampleVolume ((coerce -> (channel :: Channel)) :> (coerce -> volume) :> _) = do
  SDLM.setVolume volume channel
  pure Nothing

builtInSetSampleLRVolume :: BuiltInFnWithDoc '[ '("sample", Channel), '("volume_left", Int), '("volume_right", Int)]
builtInSetSampleLRVolume ((coerce -> (channel :: Channel)) :> (coerce -> volumel) :> (coerce -> volumer) :> _) = do
  void $ SDLM.effectPan channel volumel volumer
  pure Nothing

builtInPlaySoundSample :: BuiltInFnWithDoc '[ '("sample", Sample), '("channel", Int)]
builtInPlaySoundSample ((coerce -> sample) :> (coerce -> (channel :: Int)) :> _) = do
  void $ SDLM.playOn (fromIntegral channel) SDLM.Forever sample
  pure Nothing

makeSound :: (Int, [WAVESample]) -> InterpretM Value
makeSound (sampleCount, samplesRaw) = do
  (rEnd, wEnd) <- liftIO createPipe
  let waveData = WAVE (WAVEHeader 1 44100 16 $ Just sampleCount) [samplesRaw]
  waveEncodedData <- liftIO $ do
    hPutWAVE wEnd waveData
    BS.hGetContents rEnd
  chunk <- liftIO $ SDLM.decode waveEncodedData
  pure $ SDLValue $ SoundSample chunk

builtInMakeTone :: BuiltInFnWithDoc '[ '("freq", Double) ]
builtInMakeTone ((coerce -> freq) :> _) = do
  let samplesInOneCycle = 44100 / freq
  let multiplier = (2 * pi)/samplesInOneCycle
  let samplesRaw = [doubleToSample $ sin (realToFrac x * multiplier) | x <- [0 .. (round samplesInOneCycle - 1)]]
  Just <$> makeSound (round samplesInOneCycle, samplesRaw)

builtInMakeSoundSample :: BuiltInFnWithDoc '[ '("samplecount", Int), '("callback", Callback)]
builtInMakeSoundSample ((coerce -> sampleCount) :> (coerce -> (cb :: Callback)) :> _) = do
  samplesRaw <- getSamples sampleCount
  Just <$> makeSound (sampleCount , samplesRaw)
  where
    getSamples :: Int -> InterpretM [WAVESample]
    getSamples sc = mapM (\x -> mapFn x) [1..(fromIntegral sc)]

    mapFn :: Integer -> InterpretM WAVESample
    mapFn si = (doubleToSample . (fromValue @Double) . fromMaybe (throwErr MissingProcedureReturn)) <$>
      evaluateCallback cb [NumberValue $ NumberInt si]

builtInMakeSoundSampleFromFile :: BuiltInFnWithDoc '[ '("filepath", FilePath)]
builtInMakeSoundSampleFromFile ((coerce -> filePath) :> _) = do
  chunk <- liftIO $ SDLM.load filePath
  pure $ Just $ SDLValue $ SoundSample chunk

cleanupSDL :: InterpretM ()
cleanupSDL = do
  sdlWindowRefs <- isSDLWindows <$> get
  windows <- liftIO $ readIORef sdlWindowRefs
  mapM_ (liftIO . SDL.destroyWindow) windows
  modify (\x -> x { isDefaultRenderer = Nothing })
  modify (\x -> x { isDefaultWindow = Nothing })
  SDLM.closeAudio
  SDL.quit

keycodes :: Value
keycodes = ObjectValue $ M.fromList
  [ ("up", SDLValue $ Keycode KeycodeUp)
  , ("down", SDLValue $ Keycode KeycodeDown)
  , ("left", SDLValue $ Keycode KeycodeLeft)
  , ("right", SDLValue $ Keycode KeycodeRight)
  , ("a", SDLValue $ Keycode KeycodeA)
  , ("b", SDLValue $ Keycode KeycodeB)
  , ("c", SDLValue $ Keycode KeycodeC)
  , ("q", SDLValue $ Keycode KeycodeQ)
  , ("s", SDLValue $ Keycode KeycodeS)
  ]

scancodes :: Value
scancodes = ObjectValue $ M.fromList
  [ ("up", SDLValue $ Scancode ScancodeUp)
  , ("down", SDLValue $ Scancode ScancodeDown)
  , ("left", SDLValue $ Scancode ScancodeLeft)
  , ("right", SDLValue $ Scancode ScancodeRight)
  , ("q", SDLValue $ Scancode ScancodeQ)
  ]