packages feed

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