1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
-- | Basic server monads and related operations.
module Game.LambdaHack.Server.MonadServer
( -- * The server monad
MonadServer( getsServer
, modifyServer
, chanSaveServer -- exposed only to be implemented, not used
, liftIO -- exposed only to be implemented, not used
)
, MonadServerAtomic(..)
-- * Assorted primitives
, getServer, putServer, debugPossiblyPrint, debugPossiblyPrintAndExit
, serverPrint, saveServer, dumpRngs, restoreScore, registerScore
, rndToAction, getSetGen
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
-- Cabal
import qualified Paths_LambdaHack as Self (version)
import qualified Control.Exception as Ex
import qualified Control.Monad.Trans.State.Strict as St
import qualified Data.EnumMap.Strict as EM
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Time.Clock.POSIX
import Data.Time.LocalTime
import System.Exit (exitFailure)
import System.FilePath
import System.IO (hFlush, stdout)
import qualified System.Random.SplitMix32 as SM
import Game.LambdaHack.Atomic
import Game.LambdaHack.Common.ActorState
import Game.LambdaHack.Common.ClientOptions (sbenchmark)
import Game.LambdaHack.Common.Faction
import Game.LambdaHack.Common.File
import qualified Game.LambdaHack.Common.HighScore as HighScore
import Game.LambdaHack.Common.Kind
import Game.LambdaHack.Common.Misc
import Game.LambdaHack.Common.MonadStateRead
import Game.LambdaHack.Common.Perception
import qualified Game.LambdaHack.Common.Save as Save
import Game.LambdaHack.Common.State
import Game.LambdaHack.Common.Types
import Game.LambdaHack.Content.FactionKind
import Game.LambdaHack.Content.RuleKind
import Game.LambdaHack.Core.Random
import Game.LambdaHack.Server.ServerOptions
import Game.LambdaHack.Server.State
class MonadStateRead m => MonadServer m where
getsServer :: (StateServer -> a) -> m a
modifyServer :: (StateServer -> StateServer) -> m ()
chanSaveServer :: m (Save.ChanSave (State, StateServer))
-- We do not provide a MonadIO instance, so that outside
-- nobody can subvert the action monads by invoking arbitrary IO.
liftIO :: IO a -> m a
-- | The monad for executing atomic game state transformations.
class MonadServer m => MonadServerAtomic m where
-- | Execute an atomic command that changes the state
-- on the server and on all clients that can notice it.
execUpdAtomic :: UpdAtomic -> m ()
-- | Execute an atomic command that changes the state
-- on the server only.
execUpdAtomicSer :: UpdAtomic -> m Bool
-- | Execute an atomic command that changes the state
-- on the given single client only.
execUpdAtomicFid :: FactionId -> UpdAtomic -> m ()
-- | Execute an atomic command that changes the state
-- on the given single client only.
-- Catch 'AtomicFail' and indicate if it was in fact raised.
execUpdAtomicFidCatch :: FactionId -> UpdAtomic -> m Bool
-- | Execute an atomic command that only displays special effects.
execSfxAtomic :: SfxAtomic -> m ()
execSendPer :: FactionId -> LevelId
-> Perception -> Perception -> Perception -> m ()
getServer :: MonadServer m => m StateServer
getServer = getsServer id
putServer :: MonadServer m => StateServer -> m ()
putServer s = modifyServer (const s)
debugPossiblyPrint :: MonadServer m => Text -> m ()
debugPossiblyPrint t = do
debug <- getsServer $ sdbgMsgSer . soptions
when debug $ liftIO $ do
T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough
hFlush stdout
-- No moving savefiles aside, to debug more easily.
debugPossiblyPrintAndExit :: MonadServer m => Text -> m ()
debugPossiblyPrintAndExit t = do
debug <- getsServer $ sdbgMsgSer . soptions
when debug $ liftIO $ do
T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough
hFlush stdout
exitFailure
serverPrint :: MonadServer m => Text -> m ()
serverPrint t = liftIO $ do
T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough
hFlush stdout
saveServer :: MonadServer m => m ()
saveServer = do
s <- getState
ser <- getServer
toSave <- chanSaveServer
liftIO $ Save.saveToChan toSave (s, ser)
-- | Dumps to stdout the RNG states from the start of the game.
dumpRngs :: MonadServer m => RNGs -> m ()
dumpRngs rngs = liftIO $ do
T.hPutStr stdout $! tshow rngs <> "\n" -- hPutStrLn not atomic enough
hFlush stdout
-- | Read the high scores dictionary. Return the empty table if no file.
restoreScore :: forall m. MonadServer m => COps -> m HighScore.ScoreDict
restoreScore COps{corule} = do
benchmark <- getsServer $ sbenchmark . sclientOptions . soptions
mscore <- if benchmark then return Nothing else do
let scoresFileName = rscoresFileName corule
dataDir <- liftIO appDataDir
let path bkp = dataDir </> bkp <> scoresFileName
configExists <- liftIO $ doesFileExist (path "")
res <- liftIO $ Ex.try $
if configExists then do
(vlib2, s) <- strictDecodeEOF (path "")
if Save.compatibleVersion vlib2 Self.version
then return $! s `seq` Just s
else do
let msg =
"High score file from incompatible version of game detected."
fail msg
else return Nothing
savePrefix <- getsServer $ ssavePrefixSer . soptions
let defPrefix = ssavePrefixSer defServerOptions
moveAside = savePrefix == defPrefix
handler :: Ex.SomeException -> m (Maybe a)
handler e = do
when moveAside $
liftIO $ renameFile (path "") (path "bkp.")
let msg = "High score restore failed."
<+> (if moveAside
then "The wrong file moved aside."
else "")
<+> "The error message is:"
<+> (T.unwords . T.lines) (tshow e)
serverPrint msg
return Nothing
either handler return res
maybe (return HighScore.empty) return mscore
-- | Generate a new score, register it and save.
registerScore :: MonadServer m => Status -> FactionId -> m ()
registerScore status fid = do
cops@COps{corule} <- getsState scops
total <- getsState $ snd . calculateTotal fid
let scoresFileName = rscoresFileName corule
dataDir <- liftIO appDataDir
-- Re-read the table in case it's changed by a concurrent game.
scoreDict <- restoreScore cops
gameModeId <- getsState sgameModeId
time <- getsState stime
dungeonTotal <- getsState sgold
date <- liftIO getPOSIXTime
tz <- liftIO $ getTimeZone $ posixSecondsToUTCTime date
curChalSer <- getsServer $ scurChalSer . soptions
factionD <- getsState sfactionD
bench <- getsServer $ sbenchmark . sclientOptions . soptions
noConfirmsGame <- isNoConfirmsGame
sbandSpawned <- getsServer sbandSpawned
let fact = factionD EM.! fid
path = dataDir </> scoresFileName
outputScore (worthMentioning, (ntable, pos)) =
-- If testing or fooling around, dump instead of registering.
-- In particular don't register score for the auto-* scenarios.
if bench || noConfirmsGame || gunderAI fact then
debugPossiblyPrint $ T.intercalate "\n"
$ HighScore.showScore tz pos (HighScore.getRecord pos ntable)
++ [" Spawned groups:"
<+> T.unwords (tail (T.words (tshow sbandSpawned)))]
else
let nScoreDict = EM.insert gameModeId ntable scoreDict
in when worthMentioning $ liftIO $
encodeEOF path Self.version (nScoreDict :: HighScore.ScoreDict)
theirVic (fi, fa) | isFoe fid fact fi
&& not (isHorrorFact fa) = Just $ gvictims fa
| otherwise = Nothing
theirVictims = EM.unionsWith (+) $ mapMaybe theirVic $ EM.assocs factionD
ourVic (fi, fa) | isFriend fid fact fi = Just $ gvictims fa
| otherwise = Nothing
ourVictims = EM.unionsWith (+) $ mapMaybe ourVic $ EM.assocs factionD
table = HighScore.getTable gameModeId scoreDict
registeredScore =
HighScore.register table total dungeonTotal time status date curChalSer
(T.unwords $ tail $ T.words $ gname fact)
ourVictims theirVictims
(fhiCondPoly $ gkind fact)
outputScore registeredScore
-- | Invoke pseudo-random computation with the generator kept in the state.
rndToAction :: MonadServer m => Rnd a -> m a
rndToAction r = do
gen1 <- getsServer srandom
let (a, gen2) = St.runState r gen1
modifyServer $ \ser -> ser {srandom = gen2}
return a
-- | Gets a random generator from the user-submitted options or, if not present,
-- generates one.
getSetGen :: MonadServer m => Maybe SM.SMGen -> m SM.SMGen
getSetGen mrng = case mrng of
Just rnd -> return rnd
Nothing -> liftIO SM.newSMGen
|