packages feed

nomyx-core-1.0.0: src/Nomyx/Core/Session.hs

{-# LANGUAGE DoAndIfThenElse     #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | This module manages multi-player commands.
module Nomyx.Core.Session (
  -- * Player managament
  newPlayer,
  -- * Game management
  newGame, newGame',
  forkGame,
  joinGame,
  delGame,
  leaveGame,
  -- * Rule management
  submitRule,
  adminSubmitRule,
  checkRule,
  -- * Library management
  newRuleTemplate,
  addRuleTemplate,
  delRuleTemplate,
  updateLibrary,
  -- * Modules
  newModule,  
  -- * IO
  inputResult,
  applyTimeEvent,
  -- * settings
  playerSettings,
  adminPass,
  globalSettings,
  playAs,
  -- * Session
  updateSession,
  evalSession,
  )
where

import           Control.Concurrent.STM
import           Control.Lens
import           Control.Monad.State
import qualified Data.Acid.Advanced                  as A (query', update')
import           Data.List
import           Data.Maybe
import           Data.Time                           as T
import           Debug.Trace.Helpers
import           Nomyx.Language
import           Nomyx.Core.Engine                   as G
import           Nomyx.Core.Engine.Interpret
import           Nomyx.Core.Multi
import           Nomyx.Core.Profile
import           Nomyx.Core.Serialize
import           Nomyx.Core.Types
import           Nomyx.Core.Utils
import           Nomyx.Core.Mail
import           System.IO.PlafCompat
import           System.Log.Logger
import           Imprevu.Evaluation 

-- | add a new player
newPlayer :: PlayerNumber -> PlayerSettings -> StateT Session IO ()
newPlayer uid ps = do
   info uid $ "New player: " ++ (_pPlayerName ps)
   s <- get
   --void $ A.update' (acidAuth $ _profiles s) (SetDefaultSessionTimeout $ 3600 * 24 * 7 *25)
   void $ A.update' (_acidProfiles s) (NewProfileData uid ps (_mLibrary $ _multi s))

-- | starts a new game
newGame :: GameName -> GameDesc -> PlayerNumber -> Bool -> StateT Session IO ()
newGame name desc pn isPublic = zoom multi $ newGame' name desc pn isPublic

newGame' :: GameName -> GameDesc -> PlayerNumber -> Bool -> StateT Multi IO ()
newGame' name desc pn isPublic = do
      gs <- use gameInfos
      if not $ any ((== name) . view gameNameLens) gs then do
         info pn $ " creating a new game with name: " ++ name
         t <- lift T.getCurrentTime
         -- create a game with zero players
         lg <- lift $ initialGameInfo name desc isPublic (Just pn) t
         void $ gameInfos %= (lg : )
      else warn pn $ "this name is already used"

forkGame :: GameName -> GameName -> GameDesc -> Bool -> PlayerNumber -> StateT Session IO ()
forkGame fromgn newgn desc isPublic pn = zoom multi $ do
   gms <- use gameInfos
   case filter ((== fromgn) . view gameNameLens) gms of
      [gi] -> do
         info pn $ "Forking game: " ++ fromgn
         time <- liftIO T.getCurrentTime
         let lg = ((game . gameName) .~ newgn) .
                  ((game . gameDesc) .~ desc) $ _loggedGame gi
         let gi' = GameInfo {
            _loggedGame     = lg,
            _ownedBy        = Just pn,
            _forkedFromGame = Just fromgn,
            _isPublic       = isPublic,
            _startedAt      = time}
         void $ gameInfos %= (gi' : )
      _ -> warn pn $ "Forking game: no game by that name: " ++ fromgn

-- | join a game (also view it for conveniency)
joinGame :: GameName -> PlayerNumber -> StateT Session IO ()
joinGame gn pn = do
   info pn $ "joining game: " ++ gn
   s <- get
   name <- lift $ Nomyx.Core.Profile.getPlayerName pn s
   inGameDo gn $ G.execGameEvent $ JoinGame pn name

-- | delete a game.
delGame :: GameName -> StateT Session IO ()
delGame gn = do
   info 0 $ "deleting game: " ++ gn
   zoom multi $ void $ gameInfos %= filter ((/= gn) . view gameNameLens)

-- | leave a game.
leaveGame :: GameName -> PlayerNumber -> StateT Session IO ()
leaveGame game pn = do
   info pn $ "left game " ++ game
   inGameDo game $ G.execGameEvent $ LeaveGame pn

-- | insert a rule in pending rules.
submitRule :: RuleTemplate -> PlayerNumber -> GameName -> StateT Session IO ()
submitRule rt pn gn = do
   info pn $ "proposed " ++ show rt
   compileOK <- compileRule rt pn gn Propose "Rule submitted OK! See \"Constitution\" or \"Actions\" tabs for actions."
   when compileOK $ do
      s <- get
      liftIO $ sendMailsSubmitRule s rt pn gn

adminSubmitRule :: RuleTemplate -> PlayerNumber -> GameName -> StateT Session IO ()
adminSubmitRule rt pn gn = do
   info pn $ "admin proposed " ++ show rt
   void $ compileRule rt pn gn SystemAdd "Admin rule submitted OK!"

checkRule :: RuleTemplate -> PlayerNumber -> GameName -> StateT Session IO ()
checkRule rt pn gn = do
   info pn $ "check rule " ++ show rt
   void $ compileRule rt pn gn Check "Rule compiled OK. Now you can submit it!"

compileRule :: RuleTemplate -> PlayerNumber -> GameName -> RuleEv -> String -> StateT Session IO Bool
compileRule rt pn gn re msg = do
   mods <- getModules pn rt
   mrr <- liftIO $ interpretRule (_rRuleCode rt) mods
   case mrr of
      Right r -> do
         info pn "proposed rule compiled OK "
         inGameDo gn $ G.execGameEvent' (Just r) (ProposeRuleEv re pn rt mods)
         modifyProfile pn (pLastRule .~ Just (rt, msg))
         return True
      Left e -> do
         let errorMsg = showInterpreterError e
         submitRuleError rt pn gn errorMsg 
         return False

getModules :: PlayerNumber -> RuleTemplate -> StateT Session IO [ModuleInfo]
getModules pn rt = do
   s <- get
   prof <- fromJust <$> getProfile s pn 
   let mods = _mModules $ _pLibrary prof
   return $ catMaybes $ map (getModule mods) (_rDeclarations rt)

getModule :: [ModuleInfo] -> FilePath -> Maybe ModuleInfo
getModule ms fp = listToMaybe $ filter (\(ModuleInfo fp' _) -> (fp==fp')) ms

newModule :: PlayerNumber -> ModuleInfo -> StateT Session IO ()
newModule pn modi = do
  info pn " inserted new module"
  modifyProfile pn $ (pLibrary . mModules) %~ (addModule modi)

addModule :: ModuleInfo -> [ModuleInfo] -> [ModuleInfo]
addModule mi mis = case (find (\mi' -> (_modPath mi) == (_modPath mi'))) mis of
  (Just mi') -> replace mi' mi mis
  Nothing -> mi:mis

submitRuleError :: RuleTemplate -> PlayerNumber -> GameName -> String -> StateT Session IO ()
submitRuleError sr pn gn errorMsg = do
   inGameDo gn $ execGameEvent $ GLog (Just pn) ("Error in submitted rule: " ++ errorMsg)
   warn pn ("Error in submitted rule: " ++ errorMsg)
   modifyProfile pn (pLastRule .~ Just (sr, errorMsg))

newRuleTemplate :: PlayerNumber -> RuleTemplate -> StateT Session IO ()
newRuleTemplate pn rt = do
  info pn " inserted new template"
  modifyProfile pn $ (pLibrary . mTemplates) %~ (addRuleTemplate rt)

updateLibrary :: PlayerNumber -> Library -> StateT Session IO ()
updateLibrary pn lib = do
  info pn " updated library"
  modifyProfile pn $ pLibrary .~ lib

addRuleTemplate :: RuleTemplate -> [RuleTemplate] -> [RuleTemplate]
addRuleTemplate rt rts = case (find (\rt' -> (_rName rt) == (_rName rt'))) rts of
  (Just rt') -> replace rt' rt rts
  Nothing -> rt:rts

delRuleTemplate :: RuleName -> PlayerNumber -> StateT Session IO ()
delRuleTemplate rn pn = do
  info pn $ "del template " ++ show rn
  modifyProfile pn $ (pLibrary . mTemplates) %~ filter (\rt -> _rName rt /= rn)

inputResult :: PlayerNumber -> EventNumber -> Input -> InputData -> GameName -> StateT Session IO ()
inputResult pn en is id gn = inGameDo gn $ execGameEvent $ InputResult pn en is id

applyTimeEvent :: UTCTime -> Game -> StateT Session IO () 
applyTimeEvent t g = do
   let ts = G.getTimeEvents t g
   when (not $ null ts) $ debug 0 "Time event found"
   mapM_ (\t -> inGameDo (_gameName g) $ execGameEvent $ TimeEvent t) ts


-- | update player settings
playerSettings :: PlayerSettings -> PlayerNumber -> StateT Session IO ()
playerSettings playerSettings pn = modifyProfile pn (pPlayerSettings .~ playerSettings)

playAs :: Maybe PlayerNumber -> PlayerNumber -> GameName -> StateT Session IO ()
playAs playAs pn g = inGameDo g $ do
   info pn $ unwords ["playing as ", show playAs, "in game" ++ g]
   pls <- use (game . players)
   case find ((== pn) . view playerNumber) pls of
      Nothing -> warn pn "player not in game"
      Just pi -> (game . players) .= replaceWith ((== pn) . view playerNumber) (pi {_playingAs = playAs}) pls

adminPass :: String -> PlayerNumber -> StateT Session IO ()
adminPass pass pn = do
   s <- get
   if pass == (_adminPassword $ _mSettings $ _multi s) then do
      info pn "getting admin rights"
      modifyProfile pn $ pIsAdmin .~ True
   else do
      warn pn "submitted wrong admin password"
      modifyProfile pn $ pIsAdmin .~ False

globalSettings :: Bool -> StateT Session IO ()
globalSettings mails = (multi . mSettings . mailSettings . sendMails) .= mails

-- | Utility functions

getNewPlayerNumber :: StateT Session IO PlayerNumber
getNewPlayerNumber = do
   s <- get
   pfd <- A.query' (_acidProfiles s) AskProfileDataNumber
   return $ pfd + 1

-- | this function apply the given game actions to the game the player is in.
inAllGamesDo :: StateT LoggedGame IO a -> StateT Session IO ()
inAllGamesDo action = do
   t <- lift T.getCurrentTime
   gis <- use (multi . gameInfos)
   forM_ gis $ \gi -> do
         (_, mylg) <- lift $ runStateT action (set (game . currentTime) t (_loggedGame gi))
         zoom multi $ modifyGame (gi {_loggedGame = mylg})

inGameDo :: GameName -> StateT LoggedGame IO () -> StateT Session IO ()
inGameDo gn action = zoom multi $ do
   (gs :: [GameInfo]) <- use gameInfos
   case find ((==gn) . view gameNameLens) gs of
      Nothing -> warn 0 "No game by that name"
      Just (gi::GameInfo) -> do
         t <- lift T.getCurrentTime
         mylg <- lift $ execWithGame' t action (_loggedGame gi)
         modifyGame (gi {_loggedGame = mylg})

-- update a session by executing a command.
-- we set a watchdog in case the evaluation would not finish
updateSession :: TVar Session -> StateT Session IO () -> IO ()
updateSession ts sm = do
   s <- atomically $ readTVar ts
   let delay = _watchdog $ _mSettings $ _multi s
   ms <- evalWithWatchdog delay s (evalSession sm)
   case ms of
      Just s' -> do
         atomically $ writeTVar ts s'
         save $ _multi s
      Nothing -> warn 0 "thread timed out, session discarded"

evalSession :: StateT Session IO () -> Session -> IO Session
evalSession sm s = do
   s' <- execStateT sm s
   writeFile nullFileName $ show $ _multi s' --dirty hack to force deep evaluation --deepseq (_multi s') (return ())
   return s'

warn, info :: (MonadIO m) => Int -> String -> m ()
info pn s = liftIO $ infoM "Nomyx.Core.Session" ("Player " ++ (show pn) ++ " " ++ s)
warn pn s = liftIO $ warningM "Nomyx.Core.Session" ("Player " ++ (show pn) ++ " " ++ s)
debug pn s = liftIO $ debugM "Nomyx.Core.Session" ("Player " ++ (show pn) ++ " " ++ s)