MazesOfMonad-1.0.8: src/MoresmauJP/Rpg/Save.hs
-- | Saving and loading characters and games
-- (c) JP Moresmau 2009
module MoresmauJP.Rpg.Save where
import Control.Monad
import Control.Monad.Trans
import Data.List
import Data.Maybe
import Data.Time
import MoresmauJP.Maze1.Maze
import MoresmauJP.Rpg.Character
import MoresmauJP.Rpg.MazeObjects
import System.Directory
import System.FilePath
import System.IO
import System.Locale
import Text.Printf
import System.IO.Error (catchIOError)
data RPGState = RPGState {
rpgCharacter::(Maybe Character)
,mgs::(Maybe RPGGameState)
,fp:: FilePath}
deriving (Show,Read,Eq)
data RPGGameState = RPGGameState {
mazegameworld::GameWorld
,objects::MazeObjects
,tickCount::Int
}
deriving (Show,Read,Eq)
data BackupState = BackupState {
bckDate::UTCTime,
bckCharacter::Character,
bckGame::Maybe RPGGameState
}
deriving (Show,Read)
backupExtension="sav"
toBackupState :: RPGState -> IO(BackupState)
toBackupState (RPGState {rpgCharacter=Nothing})=error "toBackupState: empty game state"
toBackupState (RPGState {rpgCharacter=(Just char),mgs=mgs})=do
t<-getCurrentTime
return (BackupState t char mgs)
fromBackupState :: RPGState -> BackupState -> RPGState
fromBackupState rs bs = rs{rpgCharacter=(Just $ bckCharacter bs), mgs=(bckGame bs)}
getFile :: String -> String -> RPGState -> IO(FilePath)
getFile name ext (RPGState {fp=fp})= do
createDirectoryIfMissing True fp
let fileName=fp </> (makeValid $ (addExtension name ext))
return fileName
getGameFileForName :: RPGState -> Name -> IO(FilePath)
getGameFileForName rs name=getFile name backupExtension rs
getGameFileForCharacter :: RPGState -> Character -> IO(FilePath)
getGameFileForCharacter rs c=getGameFileForName rs (name c)
getBackupFileForCharacter :: RPGState -> Character -> IO(FilePath)
getBackupFileForCharacter rs c=checkBackupFile rs (name c) 1
checkBackupFile rs name ix = do
f<-getFile (addExtension name (show ix)) backupExtension rs
ex<-doesFileExist f
if not ex
then return f
else checkBackupFile rs name (ix+1)
saveCurrent :: RPGState -> IO (Either String (String,String))
saveCurrent (RPGState {rpgCharacter=Nothing})=return $ Right ("Nothing to save","")
saveCurrent rs@(RPGState {rpgCharacter=Just c1})=do
if isOutOfService c1
then do
catchIOError (
do
fileNameC<-getFile (name c1) backupExtension rs
removeFile fileNameC
return $ Right ("Character erased","")
)
(\e -> return (Left ("Could not erase character: " ++ (show e))))
else do
saveCurrentGame rs
saveCurrentGame :: RPGState -> IO (Either String (String,String))
saveCurrentGame (RPGState {rpgCharacter=Nothing})=error "saveCurrentGame: no character"
saveCurrentGame (RPGState {mgs=Nothing})=error "saveCurrentGame: no game state"
saveCurrentGame rs@(RPGState {rpgCharacter=Just c1})=do
fileNameG<-getGameFileForCharacter rs c1
saveGame rs fileNameG
listFiles :: RPGState -> String -> IO ([FilePath])
listFiles (RPGState {fp=fp}) ext =do
createDirectoryIfMissing True fp
fps<-getDirectoryContents fp
return $ (map (makeRelative fp)) $ filter (isSuffixOf ext) fps
listGames :: RPGState -> Name -> IO ([Either String (FilePath,String)])
listGames rs name= do
files<-listFiles rs backupExtension
let myFiles=filter (isPrefixOf name) files
foldM (\list x->do
m<-withBackup rs x (\bs->do
if isJust $ bckGame bs
then return $ Just (dropExtension x,formatTime defaultTimeLocale (iso8601DateFormat $ Just "%R") $ bckDate bs)
else return Nothing
)
case m of
Right (Just x)-> return ((Right x):list)
Right (Nothing)-> return list
Left a-> return ((Left a):list)
) [] myFiles
deleteCharacter :: RPGState -> String -> IO (Either String String)
deleteCharacter rs@(RPGState {fp=fp}) name=do
files<-listFiles rs backupExtension
let myFiles= filter (isPrefixOf name) files
catchIOError (do
mapM_ (removeFile . (combine fp)) myFiles
return $ Right (printf "Character %s deleted" name)
)
(\e->return $ Left (printf "The character couldn't be deleted: %s" (show e)))
deleteGame :: RPGState -> String -> IO (Either String String)
deleteGame rs name=do
file<-getFile name backupExtension rs
catchIOError (do
if hasExtension name
then do
removeFile file
return $ Right "Game deleted"
else
withBackupCond rs (takeFileName file)
(\(BackupState{bckCharacter=c})->do
l<-saveCharacter rs c
case l of
Right _-> return $ Right "Game deleted"
a->return a)
)
(\e->return $ Left (printf "The game couldn't be deleted: %s" (show e)))
listCharacters :: RPGState -> IO ([String])
listCharacters rs= do
files<-listFiles rs backupExtension
return $ filter (not . hasExtension) $ map dropExtension files
saveBackup :: RPGState -> IO (Either String (String,String))
saveBackup (RPGState {rpgCharacter=Nothing}) = return $ Right ("Nothing to save","")
saveBackup rs@(RPGState {rpgCharacter=(Just c1)}) = do
fileNameB<-getBackupFileForCharacter rs c1
saveGame rs fileNameB
saveGame :: RPGState -> String -> IO (Either String (String,String))
saveGame rs fileNameB= do
catchIOError (
do
bs<-toBackupState rs
writeFile fileNameB (show bs)
return (Right (printf "Game saved to %s" fileNameB,dropExtension $ takeFileName fileNameB))
)
(\e -> return (Left (printf "Could not save game: %s" (show e))))
loadBackup :: RPGState -> String -> IO (Either String (String,RPGState))
loadBackup rs name = do
withBackupCond rs (addExtension name backupExtension)
(\bs->do
let game=fromBackupState rs bs
return $ case (mgs game) of
Just _->Right ("Game loaded",game)
Nothing->Left "This backup contains no started game")
doesCharacterExists :: RPGState -> String -> IO (Bool)
doesCharacterExists rs name=do
fileName<-getFile name backupExtension rs
doesFileExist fileName
doesCurrentGameExists :: RPGState -> String -> IO(Bool)
doesCurrentGameExists rs shortName =do
fileName<-getFile shortName backupExtension rs
exists<-doesFileExist fileName
if exists
then do
e<-withBackup rs fileName
(\bs->do
let game=fromBackupState rs bs
return (isJust (mgs game)))
return $ case e of
Right b->b
Left _->False
else return False
saveCharacter :: RPGState -> Character -> IO (Either String String)
saveCharacter rs c=do
fileName<-getFile (name c) backupExtension rs
catchIOError (do
t<-getCurrentTime
let bs=(BackupState t c Nothing)
writeFile fileName (show bs)
return (Right $ "Character saved to "++fileName)
)
(\e -> return (Left $ printf "Could not save character: %s" (show e)))
readF :: (MonadIO m) => RPGState -> String -> (String -> IO (Either String a)) -> (a -> m b) -> m (Either String b)
readF (RPGState {fp=fp}) shortName act act2 = do
let sn'=if (takeExtension shortName)==("."++backupExtension)
then shortName
else addExtension shortName backupExtension
let fileName=fp </> sn'
exists<-liftIO $ doesFileExist fileName
if exists
then do
a <- liftIO $ catchIOError (do
a<- withFile fileName ReadMode (\h -> do
s<-hGetContents h
(act s)
)
return a)
(\err-> return (Left (printf "The file %s could not be read: %s" fileName (show err))))
case a of
Right b -> do
d<-act2 b
return $ Right d
Left c -> return $ Left c
else return $ Left (printf "File %s does not exist" fileName)
withBackup rs name m = do
readF rs name readBackup
m
withCharacter rs name m = do
readF rs name readBackup
(\bs-> m $ bckCharacter bs)
withBackupCond rs name m = do
ei<-readF rs name readBackup
m
return $ case ei of
Right (Left a)->Left a
Right (Right a)->Right a
Left a -> Left a
readBackup s=do
catchIOError (do
c<-(readIO s)::IO BackupState
return $ Right c
)
(\err-> return (Left (printf "The backup file could not be parsed: %s" (show err))))