{-# LANGUAGE RecordWildCards #-} -------------------------------------------------------------------------------- -- | -- Module : Internal -- License : Public Domain -- -- Maintainer : Douglas Burke -- Stability : experimental -- Portability : RecordWildCards -- -- Internal types for connecting to the Raspberry-PI version -- of MineCraft. Most users are expected to use "Network.MineCraft.Pi.Client" -- rather than this module, but it is provided in case the former -- is not sufficient. -- -------------------------------------------------------------------------------- module Network.MineCraft.Pi.Client.Internal ( MCPI , runMCPI , runMCPI' , query , command ) where import qualified Control.Exception as CE import Control.Exception (bracket) import Control.Monad (when) import Control.Monad.IO.Class import Control.Monad.Trans.Reader import Data.List (intercalate) import Network.BSD import Network.Socket import System.Exit import System.IO -- | Represent a program that communicates with a MineCraft PI -- server. -- type MCPI = ReaderT ConnInfo IO -- | Connection information. data ConnInfo = ConnInfo { _ciHandle :: Handle -- ^ Connection to the MineCraft program , _ciDebug :: Bool -- ^ Should messages to and from MineCraft -- be printed to @stderr@. } -- | Commands do not return anything, queries do. type Command = String type Query = String type Argument = String -- | The port used by MineCraft is fixed. mcPort :: String mcPort = "4711" -- | Open a connection to the Minecraft server or call -- exitFailure, after displaying an error message to @stderr@. -- openMCPI :: Bool -- ^ Set to @True@ to get debugging messages printed to @stderr@. -> IO ConnInfo openMCPI flag = do let ehdl :: CE.IOException -> IO () ehdl _ = hPutStrLn stderr "ERROR: Unable to connect to MineCraft-PI. Is it running?" >> exitFailure as <- getAddrInfo Nothing Nothing (Just mcPort) let a = head as -- note: getAddrInfo never returns an empty list sock <- socket (addrFamily a) Stream defaultProtocol setSocketOption sock KeepAlive 1 connect sock (addrAddress a) `CE.catch` ehdl h <- socketToHandle sock ReadWriteMode hSetBuffering h LineBuffering return $ ConnInfo h flag -- | Close the connection. closeMCPI :: ConnInfo -> IO () closeMCPI = hClose . _ciHandle logMsg :: ConnInfo -> String -> String -> MCPI () logMsg ConnInfo {..} hdr msg = when _ciDebug $ liftIO $ hPutStrLn stderr $ "*DBG*" ++ hdr ++ "*" ++ msg -- It would be nice to do the argument marshalling here, i.e. have -- something like @command :: Command -> [Argument] -> MCPI ()@ -- which would be run like @command "player.setTile" [Pos 0 0 0]@, -- but I do not want to deal with heterogeneous lists at this time. -- Instead, we force the caller to do the conversion. -- addArgs :: String -> [Argument] -> String addArgs a bs = a ++ "(" ++ intercalate "," bs ++ ")" -- | Run a MineCraft command. command :: Command -> [Argument] -> MCPI () command comm args = do ci <- ask let commstr = addArgs comm args logMsg ci "COMMAND" commstr liftIO $ hPutStrLn (_ciHandle ci) commstr -- | Run a MineCraft query, returning the response. query :: Query -> [Argument] -> MCPI String query qry args = do ci <- ask let qrystr = addArgs qry args logMsg ci "QUERY" qrystr liftIO $ hPutStrLn (_ciHandle ci) qrystr ans <- liftIO $ hGetLine (_ciHandle ci) logMsg ci "RESPONSE" ans return ans -- | Run a Raspberry-PI program. The flag determines whether the -- messages sent to, and received from, the server, are -- printed to @stderr@. -- -- An exception is raised if the server is not running, or -- can not be contacted. runMCPI' :: Bool -> MCPI a -> IO a runMCPI' flag p = bracket (openMCPI flag) closeMCPI (runReaderT p) -- | Run a Raspberry-PI program. -- -- An exception is raised if the server is not running, or -- can not be contacted. runMCPI :: MCPI a -> IO a runMCPI = runMCPI' False