packages feed

sc2-lowlevel (empty) → 0.1.0.0

raw patch · 11 files changed

+886/−0 lines, 11 filesdep +basedep +bytestringdep +containers

Dependencies added: base, bytestring, containers, data-default-class, directory, filepath, freer-simple, lens, lens-labels, network, optparse-applicative, process, proto-lens, sc2-proto, sc2-support, text, websockets

Files

+ README.md view
@@ -0,0 +1,1 @@+# sc2-lowlevel
+ sc2-lowlevel.cabal view
@@ -0,0 +1,58 @@+cabal-version: 1.12
++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: 25400b98b796e0d25a9a8b131009076019ecd02d14cf44da0f7ce1fc2189d3a6++name:           sc2-lowlevel+version:        0.1.0.0+synopsis:       Low-level Starcraft II API+description:    This package is a wrapper around the sc2-proto library, handling things such as launching the process and sending and receiving messages over a websocket.+category:       Network, FFI, Game+homepage:       https://github.com/spacekitteh/sc2hs+maintainer:     Sophie Taylor <sophie@spacekitteh.moe>+license:        BSD3+build-type:     Simple+extra-source-files:+    README.md++source-repository head+  type: git+  location: https://gitlab.com/spacekitteh/sc2hs.git++library+  exposed-modules:+      Network.SC2.LowLevel+      Network.SC2.LowLevel.Convert+      Network.SC2.LowLevel.Mainable+      Network.SC2.LowLevel.Process+      Network.SC2.LowLevel.Protocol+      Network.SC2.LowLevel.Requestable+      Network.SC2.LowLevel.Requests+      Network.SC2.LowLevel.Split+      Network.SC2.LowLevel.Types+  other-modules:+      Paths_sc2_lowlevel+  hs-source-dirs:+      src+  build-depends:+      base >=4.7 && <5+    , bytestring >=0.10+    , containers >=0.6+    , data-default-class >=0.1.2+    , directory >=1.3+    , filepath >=1.4+    , freer-simple >=1.2+    , lens >=4.17+    , lens-labels >=0.3+    , network >=2.8+    , optparse-applicative >=0.14+    , process >=1.6+    , proto-lens >=0.4+    , sc2-proto+    , sc2-support+    , text >=1.2+    , websockets >=0.12+  default-language: Haskell2010
+ src/Network/SC2/LowLevel.hs view
@@ -0,0 +1,11 @@+module Network.SC2.LowLevel
+  ( module Network.SC2.LowLevel.Mainable
+  , module Network.SC2.LowLevel.Process
+  , module Network.SC2.LowLevel.Protocol
+  , module Network.SC2.LowLevel.Requests
+  ) where
+
+import Network.SC2.LowLevel.Mainable
+import Network.SC2.LowLevel.Process
+import Network.SC2.LowLevel.Protocol
+import Network.SC2.LowLevel.Requests
+ src/Network/SC2/LowLevel/Convert.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE FlexibleInstances, TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedLabels #-}
+module Network.SC2.LowLevel.Convert where
+
+import Network.SC2.LowLevel.Types
+import qualified Proto.S2clientprotocol.Common as C
+import qualified Proto.S2clientprotocol.Sc2api as A
+import qualified Proto.S2clientprotocol.Raw as R
+import Data.ProtoLens (defMessage)
+import qualified Network.SC2.Constants.Units as Units
+import Lens.Labels.Unwrapped ()
+import Control.Lens
+import Data.Coerce
+
+
+convertRace :: Race' a -> C.Race
+convertRace Terran = C.Terran
+convertRace Zerg = C.Zerg
+convertRace Protoss = C.Protoss
+convertRace (Random _) = C.Random
+
+convertRaceBack :: C.Race -> a -> Race' a
+convertRaceBack C.Terran = const Terran
+convertRaceBack C.Zerg = const Zerg
+convertRaceBack C.Protoss = const Protoss
+convertRaceBack C.Random = Random
+
+convertPlayer :: Player (Race' a) -> A.PlayerSetup
+convertPlayer Observer = defMessage & #type' .~ A.Observer
+convertPlayer (Participant r) = defMessage & #type' .~ A.Participant & #race .~ convertRace r --A.playerSetup (Just A.Participant) (Just (convertRace r)) Nothing
+convertPlayer (Computer r d) = defMessage & #type' .~ A.Computer & #race .~ convertRace r & #difficulty .~ d--A.playerSetup (Just A.Computer) (Just (convertRace r)) (Just d)
+
+class ConvertProto a where
+  type Unproto a
+  convertTo :: Unproto a -> a
+  convertFrom :: a -> Maybe (Unproto a)
+
+instance ConvertProto C.PointI where
+  type Unproto C.PointI = (Int, Int)
+  convertTo (x, y) = defMessage & #x .~ fromIntegral x & #y .~ fromIntegral y 
+  convertFrom p = (,) <$> (fromIntegral <$> p^. #maybe'x) <*> (fromIntegral <$> p^. #maybe'y)--(fromIntegral p^.x, fromIntegral p^.y)
+
+instance ConvertProto C.RectangleI where
+  type Unproto C.RectangleI = ((Int, Int), (Int, Int))
+  convertTo (a, b) = defMessage & #p0 .~ (convertTo a) & #p1 .~ (convertTo b) 
+  convertFrom r = (,) <$> (convertFrom =<< r^. #maybe'p0) <*> (convertFrom =<< r^. #maybe'p1)
+
+instance ConvertProto C.Point2D where
+  type Unproto C.Point2D = Point
+  convertTo (x, y) = defMessage & #x .~ x & #y .~ y 
+  convertFrom p = (,) <$> p^. #maybe'x <*>  p^. #maybe'y
+
+instance ConvertProto C.Size2DI where
+  type Unproto C.Size2DI = (Int, Int)
+  convertTo (x, y) = defMessage & #x .~ fromIntegral x & #y .~ fromIntegral y 
+  convertFrom sz = (,) <$> (fromIntegral <$> sz^. #maybe'x) <*> (fromIntegral <$> sz^. #maybe'y)
+
+instance ConvertProto C.ImageData where
+  type Unproto C.ImageData = ImageData
+  convertTo (ImageData bits size dat) = defMessage & #bitsPerPixel .~ fromIntegral bits & #size .~ convertTo size & #data' .~ dat
+  convertFrom i = ImageData <$> (fromIntegral <$> i^. #maybe'bitsPerPixel) <*> (convertFrom =<< i^. #maybe'size) <*> i^. #maybe'data'
+
+instance ConvertProto R.Unit where
+  type Unproto R.Unit = Unit
+  convertTo _ = error "Why are you converting Unit back to proto"
+  convertFrom m = Just Unit {tag = tag', unitType = unitType', alliance = alliance', owner = owner'} where
+    tag' = UnitID (m ^. #tag) 
+    unitType' = Units.fromInt (m ^. #unitType)
+    alliance' = m ^. #alliance
+    owner' = PlayerID $fromIntegral (m ^. #owner)
+
+instance ConvertProto R.ActionRawUnitCommand'Target where
+  type Unproto R.ActionRawUnitCommand'Target = Target
+  convertTo (TargetPoint p) = R.ActionRawUnitCommand'TargetWorldSpacePos (convertTo p)
+  convertTo (TargetUnit u) = R.ActionRawUnitCommand'TargetUnitTag (coerce u)
+  convertFrom = error "ConvertFrom ActionRawUnitCommand'Target TODO" -- TODO
+  
+ src/Network/SC2/LowLevel/Mainable.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE FlexibleInstances #-}
+module Network.SC2.LowLevel.Mainable
+       ( module Options.Applicative
+       , (<>)
+       , OptParseable(..)
+       , Mainable(..)
+       , withMain
+       ) where
+
+import Options.Applicative
+import Data.Semigroup ((<>))
+import Data.Proxy
+
+class OptParseable a where
+    optParse :: Parser a
+
+class Mainable a where
+    type MainOpt a
+    mainParse :: Proxy a -> Parser (MainOpt a)
+    withMainArgs :: MainOpt a -> a -> IO ()
+
+withMain :: (Mainable a) => InfoMod (MainOpt a) -> a -> IO ()
+withMain im a = do
+  args <- execParser $ info (mainParse (pure a) <**> helper) im
+  withMainArgs args a
+
+instance (OptParseable a, Mainable b) => Mainable (a -> b) where
+    type MainOpt (a -> b) = (a, MainOpt b)
+    mainParse p = (,) <$> optParse <*> mainParse (p <*> Proxy)
+    withMainArgs (a, b) f = withMainArgs b (f a)
+
+instance Mainable (IO ()) where
+    type MainOpt (IO ()) = ()
+    mainParse _ = pure ()
+    withMainArgs _ = id
+ src/Network/SC2/LowLevel/Process.hs view
@@ -0,0 +1,118 @@+{-# 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)
+ src/Network/SC2/LowLevel/Protocol.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE ConstraintKinds, AllowAmbiguousTypes#-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE TypeOperators #-}
+{-#LANGUAGE OverloadedLabels #-}
+module Network.SC2.LowLevel.Protocol
+       ( module Control.Monad.Freer
+       , module Network.SC2.LowLevel.Split
+       , SC2Control(..)
+       , SC2
+       , SC2LowLevel
+       , unsafeRequest
+       , unsafeResponse
+       , getStatus
+       , request
+       , syncRequest'
+       , syncRequest
+       , runSC2Control
+       , runSC2
+       ) where
+
+import Control.Monad.Freer
+import Control.Monad.Freer.Internal
+import Control.Monad.Freer.Error
+import Data.Traversable
+import Data.Foldable
+import Control.Monad
+import Data.Text
+import qualified Proto.S2clientprotocol.Sc2api as A
+import Network.SC2.LowLevel.Requestable
+import Network.SC2.LowLevel.Process
+import Network.SC2.LowLevel.Split
+import Lens.Labels.Unwrapped ()
+import Control.Lens
+import Numeric.Natural
+import Data.ProtoLens (defMessage)
+
+data SC2Control a where
+  SC2Request :: A.Request -> SC2Control()
+  SC2Requests :: [A.Request] -> SC2Control()
+  SC2Response :: SC2Control(Either Text A.Response)
+  SC2Responses :: forall t. Traversable t => Natural -> SC2Control (t (Either Text A.Response)) -- TODO
+  SC2Status :: SC2Control A.Status
+
+unsafeRequest :: (Member SC2Control r) => A.Request -> Eff r ()
+unsafeRequest = send . SC2Request
+
+unsafeResponse :: (Member SC2Control r) => Eff r (Either Text A.Response)
+unsafeResponse = send SC2Response
+
+getStatus :: (Member SC2Control r) => Eff r A.Status
+getStatus = send SC2Status
+
+type SC2 r = (Member SC2Control r, Member Split r)
+
+request :: (SC2 r, Requestable a) => a -> Eff r ()
+request r = unsafeRequest (toRequest r) >> fork (void unsafeResponse)
+
+syncRequest' :: (SC2 r, Requestable a) => a -> Eff r () -> Eff r (Either Text (ResponseOf a))
+--FIXME: use Control.Monad.Freer.Error
+syncRequest' r act = do
+  unsafeRequest (toRequest r)
+  act
+  suspend
+  resp <- unsafeResponse
+  return (resp >>= fromResponse (pure r))
+
+syncRequest :: (SC2 r, Requestable a) => a -> Eff r (Either Text (ResponseOf a))
+syncRequest r = syncRequest' r (pure ())
+
+runSC2Control :: Member IO r => Eff (SC2Control ': r) a -> Starcraft ->  Eff r a
+runSC2Control m sc = runNatS A.Launched go m
+  where
+    go :: Member IO r =>  A.Status -> SC2Control a -> Eff r (A.Status, a)
+    go s (SC2Request r) = (s,) <$> sendRequest sc r
+    go s (SC2Requests rs) = do
+      responses <- traverse (sendRequest sc) rs
+      return (s, fold responses)
+    go s SC2Response = getResponse s
+    go s (SC2Responses n) = error "Network.SC2.LowLevel.Protocol.SC2Control.Responses: Not implemented" -- TODO
+    go s SC2Status = return (s, s)
+    getResponse :: Member IO r => A.Status -> Eff r (A.Status, Either Text A.Response)
+    getResponse s = do
+      resp <- readResponse sc
+      case resp of --TODO: horrible, lens it instead
+                   --FIXME: use Control.Monad.Freer.Error
+        Left _ -> return (s, resp)
+        Right r -> case r^. #maybe'status of
+          Nothing -> return (s, resp)
+          Just s' -> return (s', resp)
+    
+    
+
+    runNatS :: s -> (forall a. s -> eff a -> Eff effs (s, a)) -> Eff (eff ': effs) b -> Eff effs b
+    runNatS s0 f = handleRelayS s0 (const pure) $ \s e -> ((f s e) >>=) . uncurry
+
+
+--type SC2LowLevel' rest a = (Members '[SC2Control, Split] rest) => Eff rest a
+type SC2LowLevel' effs a = Eff ('[SC2Control, Split] :++: (effs )) a
+type SC2LowLevel effs a = SC2LowLevel' (effs :++: '[IO]) a
+runSC2:: Member IO effs => SC2LowLevel' ( effs) () -> Starcraft -> Eff (effs) ()--IO (Either Text ())
+runSC2 bot  s= runSplit $ runSC2Control bot s
+ src/Network/SC2/LowLevel/Requestable.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Network.SC2.LowLevel.Requestable
+  ( Requestable(..)
+  , extractResponse
+  , extractResponseErr
+  ) where
+
+import Data.Proxy
+import Data.Text
+import Data.Maybe
+import qualified Proto.S2clientprotocol.Sc2api as A
+import qualified Proto.S2clientprotocol.Sc2api_Fields as A
+import Data.Default.Class
+import Control.Lens
+
+class Requestable a where
+  type ResponseOf a
+  toRequest :: a -> A.Request
+  fromResponse :: Proxy a -> A.Response -> Either Text (ResponseOf a)
+
+instance Requestable A.Request where
+  type ResponseOf A.Request = A.Response
+  toRequest = id
+  fromResponse _ = Right
+
+extractResponse :: (A.Response -> Maybe a) -> A.Response -> Either Text a
+extractResponse f r = maybe (Left ( (intercalate (pack "\n") (r ^. A.error)))) Right (f r)
+
+extractResponseErr :: (A.Response -> Maybe a) -> (a -> Maybe b) -> (a -> Maybe Text) -> A.Response -> Either Text b
+extractResponseErr f v e r =
+  do x <- extractResponse f r
+     case e x of
+       Nothing  -> maybe (Left "error parsing payload") Right (v x)
+       Just err -> Left (err)
+ src/Network/SC2/LowLevel/Requests.hs view
@@ -0,0 +1,229 @@+{-#LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-#LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE FlexibleContexts #-}
+module Network.SC2.LowLevel.Requests
+       ( module Network.SC2.LowLevel.Types
+       , Ping(..)
+       , PingResponse(..)
+       , Fog(..)
+       , Realtime(..)
+       , Seed(..)
+       , CreateGame(..)
+       , JoinGame(..)
+       , RestartGame(..)
+       -- , StartReplay(..)
+       , LeaveGame(..)
+       , QuickSave(..)
+       , QuickLoad(..)
+       , QuitGame(..)
+       , GameInfo(..)
+       , GameInfoResponse(..)
+       , Step(..)
+       , AvailableMaps(..)
+       ) where
+
+import qualified Proto.S2clientprotocol.Common as C
+import qualified Proto.S2clientprotocol.Sc2api as A
+import qualified Proto.S2clientprotocol.Raw as R
+import qualified Proto.S2clientprotocol.Raw_Fields as R
+
+import Control.Lens
+import Control.Monad
+import Data.Default.Class
+import qualified Data.Text as T
+import Data.Void
+import Network.SC2.LowLevel.Requestable
+import Network.SC2.LowLevel.Types
+import Network.SC2.LowLevel.Convert
+import Lens.Labels.Unwrapped ()
+import Data.ProtoLens (defMessage)
+
+data Ping = Ping
+          deriving (Show, Eq)
+
+data PingResponse =
+  PingResponse
+  { gameVersion :: T.Text
+  , dataVersion :: T.Text
+  , dataBuild :: Int
+  , baseBuild :: Int
+  } deriving (Show, Eq)
+
+instance Requestable Ping where
+  type ResponseOf Ping = PingResponse
+  toRequest _ = defMessage& #ping .~ defMessage
+  fromResponse _ = extractResponse (view #maybe'ping >=> convert)
+    where convert r = PingResponse <$> r^. #maybe'gameVersion <*> r^. #maybe'dataVersion <*> (fromIntegral <$> r^. #maybe'dataBuild ) <*> (fromIntegral <$> r^. #maybe'baseBuild)
+          
+
+data Fog = Fog | NoFog
+         deriving (Show, Eq, Enum)
+
+data Realtime = Stepped | Realtime
+              deriving (Show, Eq, Enum)
+
+data Seed = Seed Int | RandomSeed
+          deriving (Show, Eq)
+
+data CreateGame = CreateGame Map [Player Race]
+                | CreateGameFull Map [Player Race] Fog Seed Realtime
+                deriving (Show, Eq)
+
+instance Requestable CreateGame where
+  type ResponseOf CreateGame = ()
+  toRequest (CreateGame map players) = toRequest (CreateGameFull map players Fog RandomSeed Stepped)
+  toRequest (CreateGameFull map players fog seed rt) = defMessage& #createGame .~ mods defMessage
+    where
+      mods = mapmod map . playermod . fogmod . seedmod seed . rtmod
+
+      fogmod :: A.RequestCreateGame -> A.RequestCreateGame
+      fogmod = #disableFog .~ (fog == NoFog)
+
+      seedmod :: Seed -> A.RequestCreateGame -> A.RequestCreateGame
+      seedmod RandomSeed = id
+      seedmod (Seed s) = #randomSeed .~ fromIntegral s
+
+      rtmod :: A.RequestCreateGame -> A.RequestCreateGame
+      rtmod = #realtime .~ (rt == Realtime)
+
+      mapmod :: Map -> A.RequestCreateGame -> A.RequestCreateGame
+      mapmod (BattlenetMap m) = #battlenetMapName .~ m
+      mapmod (LocalMap m d) = #localMap .~ (defMessage & #mapPath .~ m & #maybe'mapData .~ d)
+
+      playermod :: A.RequestCreateGame -> A.RequestCreateGame
+      playermod = #playerSetup .~ fmap convertPlayer players
+  fromResponse _ = void . extractResponseErr (view #maybe'createGame) Just (view #maybe'errorDetails)
+
+
+-- FIXME as observer? ports? render / featurelayer interface?
+data JoinGame = JoinGame Race [Interface Void]
+              deriving (Show, Eq)
+
+instance Requestable JoinGame where
+  type ResponseOf JoinGame = PlayerID
+  toRequest (JoinGame r ifaces) = defMessage& #joinGame .~ mod defMessage
+    where
+      mod = racemod . foldr (.) id (fmap ifacemod ifaces)
+
+      racemod :: A.RequestJoinGame -> A.RequestJoinGame
+      racemod = #race .~ convertRace r
+
+      ifacemod :: Interface Void -> A.RequestJoinGame -> A.RequestJoinGame
+      ifacemod Raw = #options . #raw .~ True
+      ifacemod Score = #options . #score .~ True
+  fromResponse _ = fmap (PlayerID . fromIntegral) . extractResponseErr (view #maybe'joinGame) (view #maybe'playerId) (view #maybe'errorDetails)
+
+data RestartGame = RestartGame
+                 deriving (Show, Eq)
+
+
+instance Requestable RestartGame where
+  type ResponseOf RestartGame = ()
+  toRequest _ = defMessage& #restartGame .~ defMessage
+  fromResponse _ = void . extractResponseErr (view #maybe'restartGame) Just (view #maybe'errorDetails)
+
+-- StartReplay
+
+data LeaveGame = LeaveGame
+               deriving (Show, Eq)
+
+instance Requestable LeaveGame where
+  type ResponseOf LeaveGame = ()
+  toRequest _ = defMessage& #leaveGame .~ defMessage
+  fromResponse _ = void . extractResponse (view #maybe'leaveGame)
+
+data QuickSave = QuickSave
+               deriving (Show, Eq)
+
+instance Requestable QuickSave where
+  type ResponseOf QuickSave = ()
+  toRequest _ = defMessage& #quickSave .~ defMessage
+  fromResponse _ = void . extractResponse (view #maybe'quickSave)
+
+data QuickLoad = QuickLoad
+               deriving (Show, Eq)
+
+instance Requestable QuickLoad where
+  type ResponseOf QuickLoad = ()
+  toRequest _ = defMessage& #quickLoad .~ defMessage
+  fromResponse _ = void . extractResponse (view #maybe'quickLoad)
+
+data QuitGame = QuitGame
+          deriving (Show, Eq)
+
+instance Requestable QuitGame where
+  type ResponseOf QuitGame = ()
+  toRequest _ = defMessage& #quit .~ defMessage
+  fromResponse _ = void . extractResponse (view #maybe'quit)
+
+
+
+instance Requestable GameInfo where
+  type ResponseOf GameInfo = GameInfoResponse
+  toRequest _ = defMessage& #gameInfo .~ defMessage
+  fromResponse _ = extractResponse (view #maybe'gameInfo >=> convert)
+    where
+      convert gi = do
+        mname <- gi^. #maybe'mapName
+        let mods = gi^. #modNames
+        localpath <- T.unpack <$> (gi ^. #maybe'localMapPath) 
+        players <- traverse convertPlayer (gi^. #playerInfo) -- TODO: Lensify this wholefunction
+        let raw = convertRaw =<< gi ^. #maybe'startRaw
+        let ifaces = (snd . iface #maybe'raw id Raw . iface #maybe'score id Score . iface #maybe'featureLayer (const True) (FeatureLayer ()) . iface #maybe'render (const True) (Render ())) (gi, [])
+        return (GameInfoResponse mname mods localpath players raw ifaces)
+      iface :: Lens' A.InterfaceOptions (Maybe b) -> (b -> Bool) -> a -> (A.ResponseGameInfo, [a]) -> (A.ResponseGameInfo, [a])
+      iface l t x (gi, xs) = case gi ^. #options . l of
+        Just b | t b -> (gi, x : xs)
+        _            -> (gi, xs)
+      convertPlayer p = do
+        pid <- PlayerID . fromIntegral <$> (p ^. #maybe'playerId)
+        typ <- p ^. #maybe'type'
+        ourtyp <- case typ of
+          A.Observer -> return Observer
+          A.Participant -> do
+            race <- convertRace p
+            return (Participant race)
+          A.Computer -> do
+            race <- convertRace p
+            diff <- p ^. #maybe'difficulty
+            return (Computer race diff)
+        return (pid, ourtyp)
+      convertRace p = do
+        req <-  p ^. #maybe'raceRequested
+        let act =  p ^. #maybe'raceActual
+        return (convertRaceBack req (flip convertRaceBack (error "convertRace") <$> act))
+      convertRaw :: R.StartRaw -> Maybe StartRaw
+      convertRaw r = do
+        msize <- convertFrom =<< (r^. #maybe'mapSize)
+        pagrid <- convertFrom =<< (r^. #maybe'pathingGrid)
+        theight <- convertFrom =<< (r^. #maybe'terrainHeight)
+        plgrid <- convertFrom =<< (r^. #maybe'placementGrid)
+        parea <- convertFrom =<< (r^. #maybe'playableArea)
+        starts <- traverse convertFrom (r^. #startLocations)
+        return (StartRaw msize pagrid theight plgrid parea starts)
+
+-- FIXME RequestObservation
+
+-- FIXME RequestAction
+
+data Step = Step
+          | StepMany Word
+
+instance Requestable Step where
+  type ResponseOf Step = ()
+  toRequest Step = toRequest (StepMany 1)
+  toRequest (StepMany i) = defMessage& #step . #count .~ fromIntegral i
+  fromResponse _ = void . extractResponse (view #maybe'step)
+
+data AvailableMaps = AvailableMaps
+                   deriving (Show, Eq)
+
+instance Requestable AvailableMaps where
+  type ResponseOf AvailableMaps = [Map]
+  toRequest _ = defMessage& #availableMaps .~ defMessage
+  fromResponse _ = extractResponse (fmap makeMaps . view #maybe'availableMaps) -- TODO: Lensify fmap
+    where makeMaps :: A.ResponseAvailableMaps -> [Map]
+          makeMaps m = (LocalMap  <$> m ^. #localMapPaths <*> pure Nothing)
+                       ++ (BattlenetMap  <$> m ^. #battlenetMapNames)
+ src/Network/SC2/LowLevel/Split.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GADTs #-}
+module Network.SC2.LowLevel.Split
+       ( module Control.Monad.Freer
+       , Split(..)
+       , suspend
+       , fork
+       , halt
+       , stepSplit
+       , runSplit
+       ) where
+
+import Control.Monad.Freer
+import Control.Monad.Freer.Internal (handleRelay)
+import Data.Maybe
+import Data.Sequence
+
+data Split a where
+  Split :: Maybe a -> Maybe a -> Split a
+
+suspend :: (Member Split r) => Eff r ()
+suspend = send (Split Nothing (Just ()))
+
+fork :: (Member Split r) => Eff r () -> Eff r ()
+fork t = do
+  side <- send (Split (Just False) (Just True))
+  case side of
+    False -> return ()
+    True  -> t >> halt
+
+halt :: (Member Split r) => Eff r a
+halt = send (Split Nothing Nothing)
+
+-- threads to run now, and threads to run later
+data Status r = Status (Maybe (Eff r (Status r))) (Maybe (Eff r (Status r)))
+
+stepSplit :: Eff (Split ': r) () -> Eff r (Status r)
+stepSplit = handleRelay (\_ -> pure (Status Nothing Nothing)) go
+  where
+    go (Split now later) next = pure (Status (fmap next now) (fmap next later))
+
+runSplit :: Eff (Split ': r) () -> Eff r ()
+runSplit = go empty . stepSplit
+  where
+    go :: Seq (Eff r (Status r)) -> Eff r (Status r) -> Eff r ()
+    go threads act = do
+      Status now later <- act
+      let newthreads = catMaybe now threads later
+      case viewl newthreads of
+        EmptyL  -> pure ()
+        t :< ts -> go ts t
+
+    catMaybe :: Maybe a -> Seq a -> Maybe a -> Seq a
+    catMaybe mx t my = (maybe id (flip (|>)) my . maybe id (<|) mx) t
+ src/Network/SC2/LowLevel/Types.hs view
@@ -0,0 +1,157 @@+{-#LANGUAGE RankNTypes, DataKinds, ConstraintKinds, FlexibleContexts, GADTs, StandaloneDeriving, GeneralisedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances#-}
+module Network.SC2.LowLevel.Types
+  ( module Network.SC2.LowLevel.Types
+  , A.Difficulty(..)
+  , A.Status(..)
+--  , R.Alliance(..)
+  , R.CloakState(..)
+  ,
+  ) where
+
+import Data.Void
+import qualified Proto.S2clientprotocol.Sc2api as A
+import qualified Proto.S2clientprotocol.Raw as R
+import qualified Data.ByteString as BS
+import Lens.Labels (HasLens')
+import Data.Text
+import Data.Word
+import Data.Functor.Identity
+import Network.SC2.Constants.Entities
+import Data.Coerce
+type Point = (Float, Float)
+type PointI = (Int,Int)
+-- FIXME better image format
+data ImageData = ImageData Int (Int, Int) BS.ByteString
+  deriving (Show, Eq)
+
+data Race' a = Terran
+             | Zerg
+             | Protoss
+             | Random a
+             deriving (Show, Eq)
+
+type Race = Race' ()
+type RaceResolved = Race' (Maybe (Race' Void))
+
+data Interface c = Raw | Score | FeatureLayer c | Render c
+                 deriving (Show, Eq)
+
+newtype PlayerID = PlayerID Int
+                 deriving (Show, Eq)
+
+data Player r = Observer
+              | Participant r
+              | Computer r A.Difficulty
+              deriving (Show, Eq)
+
+data Map = BattlenetMap Text
+         | LocalMap Text (Maybe BS.ByteString)
+         deriving (Show, Eq)
+
+
+data ChatChannel = Broadcast | Team deriving (Eq, Show)
+
+type RawTag = Word64
+
+newtype UnitID = UnitID {unRawTag :: RawTag} deriving (Eq, Show)
+type Alliance = R.Alliance
+data Unit = Unit {
+    tag :: UnitID,
+    unitType :: UnitType,
+    alliance :: Alliance,
+    owner :: PlayerID
+} deriving (Eq, Show)
+
+type IdentifiesUnit a = Lens.Labels.HasLens' a "unitTag" RawTag
+
+
+data Target where
+  TargetPoint :: Point -> Target
+--  TargetScreen :: PointI -> Target
+--  TargetMinimap :: PointI -> Target
+  TargetUnit :: UnitID -> Target
+
+deriving instance Eq Target
+deriving instance Show Target
+
+class Targetable a where
+  asTarget :: a -> Target
+
+instance Targetable Point where
+  asTarget = TargetPoint
+
+instance Targetable Unit where
+  asTarget = TargetUnit . tag
+
+instance Targetable Target where
+  asTarget = id
+
+data GameInfo = GameInfo
+              deriving (Show, Eq)
+
+data GameInfoResponse =
+  GameInfoResponse
+  { mapName :: Text
+  , modNames :: [Text]
+  , localMapPath :: FilePath
+  , playerInfo :: [(PlayerID, Player RaceResolved)]
+  , startRaw :: Maybe StartRaw
+  , interfaces :: [Interface ()]
+  } deriving (Eq, Show)
+
+data StartRaw =
+  StartRaw
+  { mapSize :: (Int, Int)
+  , pathingGrid :: ImageData
+  , terrainHeight :: ImageData
+  , placementGrid :: ImageData
+  , playableArea :: ((Int, Int), (Int, Int))
+  , startLocations :: [Point]
+  } deriving (Eq, Show)
+data PlayerCommon = PlayerCommon 
+  {
+    playerID :: Int,
+    minerals :: Int,
+    vespene :: Int
+
+ --TODO
+  }
+
+data PlayerResources = PlayerResources
+
+type UnitContainer = [Unit]
+
+data Units where
+      ControlGroup :: GroupID ->  Units
+      Ungrouped :: UnitContainer -> Units
+  
+
+
+unitsToTags :: Units -> [RawTag]
+unitsToTags (Ungrouped u) = fmap (coerce . tag) u
+
+individualUnit :: Unit -> Units
+individualUnit u = Ungrouped ([u])
+  
+  
+data MapInfo = MapInfo
+data Order = Order {
+    abilityID :: AbilityType,
+    target :: Target,
+    queue :: Bool
+}
+
+class Orderable a where
+  toIDs :: a -> [UnitID]
+
+instance Orderable Units where
+  toIDs (Ungrouped u) = fmap tag u
+instance Orderable Unit where
+  toIDs u = [tag u]
+instance Orderable UnitContainer where
+  toIDs = fmap tag
+
+toTags :: Orderable a => a -> [RawTag]
+toTags = fmap (coerce) . toIDs
+
+newtype GroupID = GroupID Word deriving (Eq, Show, Enum, Bounded)