packages feed

sc2-lowlevel-0.1.0.0: src/Network/SC2/LowLevel/Process.hs

{-# LANGUAGE ScopedTypeVariables, FlexibleContexts, DataKinds, MonoLocalBinds #-}
{-# LANGUAGE CPP #-}
module Network.SC2.LowLevel.Process
       ( ExecOptions(..)
       , ConnectOptions(..)
       , Starcraft
       , runRemote
       , runLocal
       , sendRequest
       , readResponse
       ) where

import Network.Socket (withSocketsDo)
import System.Process
import System.Directory (withCurrentDirectory, getHomeDirectory)
import qualified Network.WebSockets as WS
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Control.Concurrent
import Control.Exception
import qualified Proto.S2clientprotocol.Sc2api as A
import Data.ProtoLens
import Network.SC2.LowLevel.Mainable
import Network.SC2.LowLevel.Requestable
import Network.SC2.LowLevel.Requests
import Data.List(intercalate)
import System.FilePath
import System.Info
import Network.SC2.Internal.Directories
import Control.Monad.Freer



data ExecOptions = ExecOptions
    { executable :: Maybe FilePath
    , workingDirectory :: Maybe FilePath
    , windowWidth :: Word
    , windowHeight :: Word
    , connection :: ConnectOptions
    } deriving (Eq, Show)

data ConnectOptions =
  ConnectOptions
  { listenAddress :: String
  , listenPort :: Word
  } deriving (Eq, Show)

instance OptParseable ExecOptions where
  optParse = ExecOptions
             <$> optional (strOption (long "executable" <> short 'e' <> metavar "PATH" <> help "path to the starcraft 2 executable"))
             <*> optional (strOption (long "working-dir" <> short 'w' <> metavar "PATH" <> help "change to this directory before launching starcraft"))
             <*> option auto (long "window-width" <> metavar "WIDTH" <> help "width of starcraft 2 window" <> value 1024)
             <*> option auto (long "window-height" <> metavar "HEIGHT" <> help "height of starcraft 2 window" <> value 768)
             <*> optParse

instance OptParseable ConnectOptions where
  optParse = ConnectOptions
             <$> strOption (long "address" <> metavar "ADDR" <> help "address to use to talk to SC2" <> value "127.0.0.1")
             <*> option auto (long "port" <> metavar "PORT" <> help "port to use to talk to SC2" <> value 5000)

data Starcraft =
  Starcraft
  { processHandle :: Maybe ProcessHandle
  , processConn :: WS.Connection
  }

starcraftConnectIntern :: (WS.Connection -> IO ()) -> ConnectOptions -> IO ()
starcraftConnectIntern act opt = withSocketsDo $ do
  tryConnect 60
  runOurClient $ \conn -> do
    --WS.forkPingThread conn 10
    act conn
  where
    runOurClient = WS.runClient (listenAddress opt) (fromIntegral (listenPort opt)) "/sc2api"
    
    tryConnect i | i > 0 = runOurClient (const (pure ())) `catch` \(x :: SomeException) -> tryAgain i
                 | otherwise = runOurClient (const (pure ()))
    tryAgain i = threadDelay 1000000 >> tryConnect (i - 1)

runRemote :: (Starcraft -> IO ()) -> ConnectOptions -> IO ()
runRemote act = starcraftConnectIntern (act . Starcraft Nothing)

runLocal :: (Starcraft -> Eff '[IO] ()) -> ExecOptions -> IO ()
runLocal act opt = do              
  paths <- makeSC2Paths (executable opt)
  withCurrentDirectory (supportDir paths) $ withCreateProcess (procinfo paths) handler
  where
    handler stdin stdout stderr ph = do      
      starcraftConnectIntern (\conn ->
                                 let sc = Starcraft (Just ph) conn
                                     --unwrappedAct = runM (act sc)
                                 in runM $ act sc >> sendRequest sc (toRequest QuitGame))
        (connection opt)
      waitForProcess ph
      return ()

    
--Work around process lib's Windows bug
#ifdef mingw32_HOST_OS 
    procinfo paths = shell $ "\"" ++ (gameExecutable paths) ++ "\" " ++  (intercalate " "  args)
#else
    procinfo paths = proc (gameExecutable paths) args
#endif
    args = ["-listen", listenAddress $ connection opt, "-port", show (listenPort $ connection opt), "-displayMode", "0", "-windowwidth", show (windowWidth opt), "-windowheight", show (windowHeight opt)]


sendRequest :: Member IO r => Starcraft -> A.Request -> Eff r ()
sendRequest sc msg = send $ WS.sendBinaryData (processConn sc) $ encodeMessage msg

--TODO: Cache responses per Step
readResponse :: Member IO r => Starcraft -> Eff r (Either T.Text A.Response)
                     --FIXME: use Control.Monad.Freer.Error
readResponse sc = do -- Will this kill performance, deconstructing and reconstructing the Either each time?
  receivedData <- send $ WS.receiveData (processConn sc)    
  let resp = decodeMessage receivedData
  case resp of
    Left s -> return (Left (T.pack s))
    Right r -> return (Right r)