swarm-0.7.0.0: src/swarm-util/Swarm/ResourceLoading.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- SPDX-License-Identifier: BSD-3-Clause
-- Description: Fetching game data
--
-- Various utilities related to loading game data files.
module Swarm.ResourceLoading (
-- * Generic data access
getDataDirSafe,
getDataFileNameSafe,
-- * Concrete data access
getSwarmConfigIniFile,
getSwarmSavePath,
getSwarmHistoryPath,
getSwarmAchievementsPath,
-- ** Loading text files
readAppData,
NameGenerator (..),
initNameGenerator,
) where
import Control.Algebra (Has)
import Control.Effect.Lift (Lift, sendIO)
import Control.Effect.Throw (Throw, liftEither, throwError)
import Control.Exception (catch)
import Control.Exception.Base (IOException)
import Control.Monad (forM, when, (<=<))
import Data.Array (Array, listArray)
import Data.Map (Map)
import Data.Map qualified as M
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Paths_swarm (getDataDir)
import Swarm.Failure
import Swarm.Util
import System.Directory (
XdgDirectory (..),
createDirectoryIfMissing,
doesDirectoryExist,
doesFileExist,
getXdgDirectory,
listDirectory,
)
import System.FilePath
import Witch
-- | Read-only lists of adjectives and words for use in building random robot names
data NameGenerator = NameGenerator
{ adjList :: Array Int Text
, nameList :: Array Int Text
}
-- | Get subdirectory from swarm data directory.
--
-- This will first look in Cabal generated path and then
-- try a @data@ directory in 'XdgData' path.
--
-- The idea is that when installing with Cabal/Stack the first
-- is preferred, but when the players install a binary they
-- need to extract the `data` archive to the XDG directory.
getDataDirSafe ::
(Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) =>
AssetData ->
FilePath ->
m FilePath
getDataDirSafe asset p = do
d <- (`appDir` p) <$> sendIO getDataDir
de <- sendIO $ doesDirectoryExist d
if de
then return d
else do
xd <- (`appDir` p) <$> sendIO (getSwarmXdgDataSubdir False "data")
xde <- sendIO $ doesDirectoryExist xd
if xde then return xd else throwError $ AssetNotLoaded (Data asset) xd $ DoesNotExist Directory
where
appDir r = \case
"" -> r
"." -> r
d -> r </> d
-- | Get file from swarm data directory.
--
-- See the note in 'getDataDirSafe'.
getDataFileNameSafe ::
(Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) =>
AssetData ->
FilePath ->
m FilePath
getDataFileNameSafe asset name = do
d <- getDataDirSafe asset "."
let fp = d </> name
fe <- sendIO $ doesFileExist fp
if fe
then return fp
else throwError $ AssetNotLoaded (Data asset) fp $ DoesNotExist File
getSwarmConfigIniFile :: Bool -> IO (Bool, FilePath)
getSwarmConfigIniFile createDirs = do
swarmConfig <- getXdgDirectory XdgConfig "swarm"
when createDirs (createDirectoryIfMissing True swarmConfig)
let ini = swarmConfig </> "config.ini"
iniExists <- doesFileExist ini
return (iniExists, ini)
-- | Get path to swarm data, optionally creating necessary
-- directories. This could fail if user has bad permissions
-- on his own @$HOME@ or @$XDG_DATA_HOME@ which is unlikely.
getSwarmXdgDataSubdir :: Bool -> FilePath -> IO FilePath
getSwarmXdgDataSubdir createDirs subDir = do
swarmData <- (</> subDir) <$> getXdgDirectory XdgData "swarm"
when createDirs (createDirectoryIfMissing True swarmData)
pure swarmData
getSwarmXdgDataFile :: Bool -> FilePath -> IO FilePath
getSwarmXdgDataFile createDirs filepath = do
let (subDir, file) = splitFileName filepath
d <- getSwarmXdgDataSubdir createDirs subDir
return $ d </> file
-- | Get path to swarm saves, optionally creating necessary
-- directories.
getSwarmSavePath :: Bool -> IO FilePath
getSwarmSavePath createDirs = getSwarmXdgDataSubdir createDirs "saves"
-- | Get path to swarm history, optionally creating necessary
-- directories.
getSwarmHistoryPath :: Bool -> IO FilePath
getSwarmHistoryPath createDirs = getSwarmXdgDataFile createDirs "history"
-- | Get a path to the directory where achievement records are
-- stored. If the argument is set to @True@, create the directory if
-- it does not exist.
getSwarmAchievementsPath :: Bool -> IO FilePath
getSwarmAchievementsPath createDirs = getSwarmXdgDataSubdir createDirs "achievement"
-- | Read all the @.txt@ files in the @data/@ directory.
readAppData ::
(Has (Throw SystemFailure) sig m, Has (Lift IO) sig m) =>
m (Map Text Text)
readAppData = do
d <- getDataDirSafe AppAsset "."
dirMembers :: [FilePath] <-
(liftEither <=< sendIO) $
(pure <$> listDirectory d) `catch` \(e :: IOException) ->
return . Left . AssetNotLoaded (Data AppAsset) d . SystemFailure . CustomFailure . T.pack $ show e
let fs = filter ((== ".txt") . takeExtension) dirMembers
filesList <- sendIO $ forM fs (\f -> (into @Text (dropExtension f),) <$> readFileMayT (d </> f))
return $ M.fromList . mapMaybe sequenceA $ filesList
initNameGenerator :: Has (Throw SystemFailure) sig m => Map Text Text -> m NameGenerator
initNameGenerator appDataMap = do
adjs <- getDataLines "adjectives"
names <- getDataLines "names"
return $
NameGenerator
{ adjList = makeArr adjs
, nameList = makeArr names
}
where
makeArr xs = listArray (0, length xs - 1) xs
getDataLines f = case M.lookup f appDataMap of
Nothing ->
throwError $
AssetNotLoaded (Data NameGeneration) (into @FilePath f <.> "txt") (DoesNotExist File)
Just content -> return . drop 1 . T.lines $ content