LambdaHack-0.2.6.5: Game/LambdaHack/Action/Save.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Saving and restoring games and player diaries.
module Game.LambdaHack.Action.Save
( saveGameFile, restoreGame, rmBkpSaveDiary, saveGameBkp
) where
import System.Directory
import System.FilePath
import qualified Control.Exception as Ex hiding (handle)
import Control.Monad
import Control.Concurrent
import System.IO.Unsafe (unsafePerformIO) -- horrors
import Data.Text (Text)
import qualified Data.Text as T
import Game.LambdaHack.Utils.File
import Game.LambdaHack.State
import Game.LambdaHack.Msg
import Game.LambdaHack.Config
-- | Save a simple serialized version of the current player diary.
saveDiary :: FilePath -> Diary -> IO ()
saveDiary configDiaryFile diary = encodeEOF configDiaryFile diary
saveLock :: MVar ()
{-# NOINLINE saveLock #-}
saveLock = unsafePerformIO newEmptyMVar
-- | Save a simple serialized version of the current state.
-- Protected by a lock to avoid corrupting the file.
saveGameFile :: ConfigUI -> State -> IO ()
saveGameFile ConfigUI{configSaveFile} state = do
putMVar saveLock ()
encodeEOF configSaveFile state
takeMVar saveLock
-- | Try to create a directory. Hide errors due to,
-- e.g., insufficient permissions, because the game can run
-- in the current directory just as well.
tryCreateDir :: FilePath -> IO ()
tryCreateDir dir =
Ex.catch
(createDirectory dir)
(\ e -> case e :: Ex.IOException of _ -> return ())
-- | Try to copy over data files. Hide errors due to,
-- e.g., insufficient permissions, because the game can run
-- without data files just as well.
tryCopyDataFiles :: ConfigUI -> (FilePath -> IO FilePath) -> IO ()
tryCopyDataFiles ConfigUI{ configScoresFile
, configRulesCfgFile
, configUICfgFile } pathsDataFile = do
rulesFile <- pathsDataFile $ takeFileName configRulesCfgFile <.> ".default"
uiFile <- pathsDataFile $ takeFileName configUICfgFile <.> ".default"
scoresFile <- pathsDataFile $ takeFileName configScoresFile
let newRulesFile = configRulesCfgFile <.> ".ini"
newUIFile = configUICfgFile <.> ".ini"
newScoresFile = configScoresFile
Ex.catch
(copyFile rulesFile newRulesFile >>
copyFile uiFile newUIFile >>
copyFile scoresFile newScoresFile)
(\ e -> case e :: Ex.IOException of _ -> return ())
-- | Restore a saved game, if it exists. Initialize directory structure,
-- if needed.
restoreGame :: ConfigUI -> (FilePath -> IO FilePath) -> Text
-> IO (Either (State, Diary, Msg) (Diary, Msg))
restoreGame config@ConfigUI{ configAppDataDir
, configDiaryFile
, configSaveFile
, configBkpFile } pathsDataFile title = do
ab <- doesDirectoryExist configAppDataDir
-- If the directory can't be created, the current directory will be used.
unless ab $ do
tryCreateDir configAppDataDir
-- Possibly copy over data files. No problem if it fails.
tryCopyDataFiles config pathsDataFile
-- If the diary file does not exist, create an empty diary.
-- TODO: when diary gets corrupted, start a new one, too.
diary <-
do db <- doesFileExist configDiaryFile
if db
then strictDecodeEOF configDiaryFile
else defaultDiary
-- If the savefile exists but we get IO errors, we show them,
-- back up the savefile and move it out of the way and start a new game.
-- If the savefile was randomly corrupted or made read-only,
-- that should solve the problem. If the problems are more serious,
-- the other functions will most probably also throw exceptions,
-- this time without trying to fix it up.
sb <- doesFileExist configSaveFile
bb <- doesFileExist configBkpFile
Ex.catch
(if sb
then do
renameFile configSaveFile configBkpFile
state <- strictDecodeEOF configBkpFile
let msg = "Welcome back to" <+> title <> "."
return $ Left (state, diary, msg)
else
if bb
then do
state <- strictDecodeEOF configBkpFile
let msg = "No savefile found. Restoring from a backup savefile."
return $ Left (state, diary, msg)
else return $ Right (diary, "Welcome to" <+> title <> "!"))
(\ e -> case e :: Ex.SomeException of
_ -> let msg = "Starting a new game, because restore failed."
<+> "The error message was:"
<+> (T.unwords . T.lines) (showT e)
in return $ Right (diary, msg))
-- | Save the diary and a backup of the save game file, in case of crashes.
-- This is only a backup, so no problem is the game is shut down
-- before saving finishes, so we don't wait on the mvar. However,
-- if a previous save is already in progress, we skip this save.
saveGameBkp :: ConfigUI -> State -> Diary -> IO ()
saveGameBkp ConfigUI{ configDiaryFile
, configSaveFile
, configBkpFile } state diary = do
b <- tryPutMVar saveLock ()
when b $
void $ forkIO $ do
saveDiary configDiaryFile diary -- save diary often in case of crashes
encodeEOF configSaveFile state
renameFile configSaveFile configBkpFile
takeMVar saveLock
-- | Remove the backup of the savegame and save the player diary.
-- Should be called before any non-error exit from the game.
-- Sometimes the backup file does not exist and it's OK.
-- We don't bother reporting any other removal exceptions, either,
-- because the backup file is relatively unimportant.
-- We wait on the mvar, because saving the diary at game shutdown is important.
rmBkpSaveDiary :: ConfigUI -> Diary -> IO ()
rmBkpSaveDiary ConfigUI{ configDiaryFile
, configBkpFile } diary = do
putMVar saveLock ()
saveDiary configDiaryFile diary -- save diary often in case of crashes
bb <- doesFileExist configBkpFile
when bb $ removeFile configBkpFile
takeMVar saveLock