packages feed

blubber-0.0.1: src-exec/blubber.hs

{- |
Module      :  $Header$
Description :  The blubber client entry point.
Copyright   :  (c) plaimi 2015
License     :  GPL-3

Maintainer  :  blubber@plaimi.net
-} module Main where

import Control.Arrow
  (
  (***),
  )
import Control.Concurrent
  (
  forkIO,
  killThread,
  ThreadId,
  )
import Control.Concurrent.Chan
  (
  Chan,
  newChan,
  readChan,
  writeChan,
  )
import Control.Monad
  (
  forever,
  join,
  void,
  when,
  )
import Data.Functor
  (
  (<$>),
  )
import Data.IORef
  (
  IORef,
  newIORef,
  readIORef,
  writeIORef,
  )
import Data.Map
  (
  toList,
  )
import Data.Serialize
  (
  decode,
  encode,
  )
import Graphics.Gloss.Data.Display
  (
  Display (InWindow),
  )
import Graphics.Gloss.Data.Color
  (
  black,
  red,
  yellow,
  )
import Graphics.Gloss.Data.Picture
  (
  Picture (Color, Pictures, Scale, Text, Translate),
  circleSolid,
  )
import Graphics.Gloss.Interface.IO.Game
  (
  Event (EventKey, EventMotion),
  Key (Char, SpecialKey),
  SpecialKey (KeySpace, KeyEsc),
  KeyState (Down),
  playIO,
  )
import Network.Socket
  (
  Family (AF_INET),
  HostAddress,
  PortNumber,
  Socket,
  SockAddr (SockAddrInet),
  SocketType (Datagram),
  inet_addr,
  socket,
  withSocketsDo,
  )
import Network.Socket.ByteString
  (
  recvFrom,
  sendTo,
  )
import System.Exit
  (
  exitSuccess,
  )
import System.Environment
  (
  getArgs,
  )

import Blubber.Server.Message
  (
  Message (AddPlayer, Connect, Disconnect, SrcPls, UpdateVelocity),
  Reply (SrcLink, View),
  )
import Blubber.Server.Entity
  (
  position,
  radius,
  )
import Blubber.Server.ViewPort
  (
  visibleNeutrals,
  visiblePlayers,
  ViewPort,
  viewHeight,
  viewPos,
  viewWidth,
  )
import Blubber.Server.Vector
  (
  vx,
  vy,
  )

-- | The 'Game' needs to keep track of its 'ViewPort' -- which is the part of
-- the blubber 'World' it is privy to, and the name of the player.
data Game = MkGame
          {vp      :: ViewPort
          -- ^ The 'ViewPort'.
          ,pn      :: String
          -- ^ The player's name.
          ,listenT :: ThreadId
          -- ^ The listen thread.
          ,talkT   :: ThreadId
          -- ^ The talk thread.
          ,ip      :: String
          -- ^ The IP connected to.
          } deriving (Show)

windowWidth :: Int
-- | The width of the initial game window.
windowWidth = 1600

windowHeight :: Int
-- | The height of the initial game window.
windowHeight = 900

main :: IO ()
-- | Our 'main' entry to the client. It sets up a socket to send stuff from,
--   and connects to the server using the passed in IP, port number, & player
--   name(or not, and crashes via error...). Then it sets up a 'Chan' and an
--   'IORef', before forking off 'talk' and 'listen', and launching
--   the game with 'playIO'.
--
-- 'playIO' sets up an 'InWindow' that's redrawn using 'draw'. 'handle' takes
-- care of the input, and 'step' updates the game.
main = withSocketsDo $ do
  as <- getArgs
  let (a, p, n) = case as of
             (a':p':n') -> (a', fromInteger . read $ p', unwords n')
             _          -> error "start with ./blubber [IP] [PORT] [NAME]"
  i   <- inet_addr a
  s   <- socket AF_INET Datagram 0
  ivp <- getInitialViewPort s i p
  c   <- newChan
  v   <- newIORef ivp
  q   <- newIORef False
  l   <- forkIO $ forever $ listen s v
  t   <- forkIO $ forever $ talk s q c i p
  playIO
    (InWindow "blubber" (windowWidth, windowHeight) (0, 0))
    black
    60
    MkGame {vp      = ivp
           ,pn      = n
           ,listenT = l
           ,talkT   = t
           ,ip      = a
           }
    draw
    (handle c)
    (step q v)

draw :: Game -> IO Picture
-- | Re'draw' the 'Game' based on the handed in 'ViewPort'. It 'Scale's things
--   according to the 'worldScale', and 'Translate's things according to their
--   'position's. Then it adds some 'Color' and shapes, and uses 'Text' to
--   print the name of the 'blub's -- and the 'mass' of the client player's
--   own 'blub'.
draw g = return
       $ Scale worldScale worldScale
       $ Pictures
       [Pictures ((\e ->
         Translate (viewPortTrans vx) (viewPortTrans vy)
       $ Translate (positionTrans e vx) (positionTrans e vy)
       $ Pictures [Color yellow $ circleSolid (realToFrac . radius $ e)])
     <$> (visibleNeutrals . vp $ g))
       ,Pictures ((\(s, e) ->
         Translate (viewPortTrans vx) (viewPortTrans vy)
       $ Translate (positionTrans e vx) (positionTrans e vy)
       $ Pictures [Color yellow $ circleSolid (realToFrac . radius $ e)
                  ,Scale 0.01 0.01 $ Color red $ Text s])
     <$> toList (visiblePlayers . vp $ g))]
  where worldScale        = min (fromIntegral windowHeight
                                / realToFrac (viewHeight . vp $ g))
                                (fromIntegral windowWidth
                                / realToFrac (viewWidth . vp $ g))
        viewPortTrans d   = -(realToFrac . d . viewPos . vp $ g)
        positionTrans e d = realToFrac . d . position $ e

handle :: Chan Message -> Event -> Game -> IO Game
-- | 'handle' input. If there's an 'EventMotion', write an 'UpdateVelocity' to
--   the passed in channel. If the client hits the space key, try to
--   'AddPlayer'.
handle c (EventMotion (x, y)) g = do
  writeChan c $
    UpdateVelocity . join (***) realToFrac $
      (realToFrac x / fromIntegral windowWidth  * viewWidth  (vp g)
      ,realToFrac y / fromIntegral windowHeight * viewHeight (vp g))
  return g
handle c (EventKey (SpecialKey KeySpace) Down _ _) g = do
  writeChan c $ AddPlayer (pn g)
  return g
handle c (EventKey (Char 'c') Down _ _) g = do
  writeChan c $ SrcPls (ip g)
  return g
handle c (EventKey (SpecialKey KeyEsc) Down _ _) g = do
  writeChan c Disconnect
  return g
handle _ _ g = return g

talk :: Socket -> IORef Bool -> Chan Message -> HostAddress -> PortNumber
          -> IO ()
-- | Every time there's an update to the channel, it means that there's
--   a 'Message' to send -- so send it.
talk s q c i p = do
  m <- readChan c
  void $ sendTo s (encode m) (SockAddrInet p i)
  when (m == Disconnect) $ writeIORef q True

getInitialViewPort :: Socket -> HostAddress -> PortNumber -> IO ViewPort
-- | Try to 'Connect' to the server and get the initial 'ViewPort'. Basically,
--   this should work or you're fucked.
getInitialViewPort s i p = do
  _ <- sendTo s (encode Connect) (SockAddrInet p i)
  (mesg, _) <- recvFrom s 1024
  let m = decode mesg :: Either String Reply
  case m of
    Right (View wp) -> return wp
    _               -> getInitialViewPort s i p

listen :: Socket -> IORef ViewPort -> IO ()
-- | Every time the client receives a message from the server
--
-- If it is a 'ViewPort', then update the 'Game' 'ViewPort'.
--
-- If it is a 'SrcLink', then print the source code link.
listen s v = do
  (mesg, _) <- recvFrom s 1024
  let m = decode mesg :: Either String Reply
  case m of
    Right (View wp)   -> writeIORef v wp
    Right (SrcLink l) -> putStrLn $ "source code link: " ++ l
    _                 -> return ()

step :: IORef Bool -> IORef ViewPort -> Float -> Game -> IO Game
-- | 'step' through the 'Game' one 'step' at the time. This simply reads the
--   newest 'ViewPort' as written by 'listen', and makes a new 'Game'
--   with it.
step q v _ g = do
  quit <- readIORef q
  when quit $ do
    killThread (listenT g)
    killThread (talkT g)
    exitSuccess
  wp <- readIORef v
  return g {vp = wp}