-- TODO: factor out parts common with Client.ConfigIO -- | Personal game configuration file support. module Game.LambdaHack.Server.Action.ConfigIO ( mkConfigRules, dump ) where import Control.Arrow (first, (***)) import Control.DeepSeq import qualified Data.Char as Char import qualified Data.ConfigFile as CF import qualified Data.EnumMap.Strict as EM import Data.List import qualified Data.Map.Strict as M import qualified Data.Text as T import System.Directory import System.Environment import System.FilePath import qualified System.Random as R import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Server.Config import Game.LambdaHack.Utils.Assert -- TODO: Refactor the client and server ConfigIO.hs, after -- https://github.com/kosmikus/LambdaHack/issues/45. overrideCP :: CP -> FilePath -> IO CP overrideCP cp@(CP defCF) cfile = do b <- doesFileExist cfile if not b then return cp else do c <- CF.readfile defCF cfile return $ toCP $ forceEither c -- | Read a player configuration file and use it to override -- options from a default config. Currently we can't unset options, -- only override. The default config, passed in argument @configDefault@, -- is expected to come from a default configuration file included via CPP. -- The player configuration comes from file @cfile@. mkConfig :: String -> FilePath -> IO CP mkConfig configDefault cfile = do let delComment = map (drop 2) $ init . drop 3 $ lines configDefault unConfig = unlines delComment -- Evaluate, to catch config errors ASAP. !defCF = forceEither $ CF.readstring CF.emptyCP unConfig !defCP = toCP defCF overrideCP defCP cfile -- | Personal data directory for the game. Depends on the OS and the game, -- e.g., for LambdaHack under Linux it's @~\/.LambdaHack\/@. appDataDir :: IO FilePath appDataDir = do progName <- getProgName let name = takeWhile Char.isAlphaNum progName getAppUserDataDirectory name -- | Dumps the current configuration to a file. dump :: Config -> FilePath -> IO () dump Config{configSelfString} fn = do current <- getCurrentDirectory let path = current fn writeFile path configSelfString -- | Simplified setting of an option in a given section. Overwriting forbidden. set :: CP -> CF.SectionSpec -> CF.OptionSpec -> String -> CP set (CP conf) s o v = if CF.has_option conf s o then assert `failure`"Overwritten config option: " ++ s ++ "." ++ o else CP $ forceEither $ CF.set conf s o v -- | Gets a random generator from the config or, -- if not present, generates one and updates the config with it. getSetGen :: CP -- ^ config -> String -- ^ name of the generator -> IO (R.StdGen, CP) getSetGen config option = case getOption config "engine" option of Just sg -> return (read sg, config) Nothing -> do -- Pick the randomly chosen generator from the IO monad -- and record it in the config for debugging (can be 'D'umped). g <- R.newStdGen let gs = show g c = set config "engine" option gs return (g, c) -- | The content of the configuration file. It's parsed -- in a case sensitive way (unlike by default in ConfigFile). newtype CP = CP CF.ConfigParser instance Show CP where show (CP conf) = show $ CF.to_string conf -- | Switches all names to case sensitive (unlike by default in -- the "ConfigFile" library) and wraps in the constructor. toCP :: CF.ConfigParser -> CP toCP cf = CP $ cf {CF.optionxform = id} -- | In case of corruption, just fail. forceEither :: Show a => Either a b -> b forceEither (Left a) = assert `failure` a forceEither (Right b) = b -- | A simplified access to an option in a given section, -- with simple error reporting (no internal errors are caught nor hidden). -- If there is no such option, gives Nothing. getOption :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> Maybe a getOption (CP conf) s o = if CF.has_option conf s o then Just $ forceEither $ CF.get conf s o else Nothing -- | Simplified access to an option in a given section. -- Fails if the option is not present. get :: CF.Get_C a => CP -> CF.SectionSpec -> CF.OptionSpec -> a get (CP conf) s o = if CF.has_option conf s o then forceEither $ CF.get conf s o else assert `failure` "Unknown config option: " ++ s ++ "." ++ o -- | An association list corresponding to a section. Fails if no such section. getItems :: CP -> CF.SectionSpec -> [(String, String)] getItems (CP conf) s = if CF.has_section conf s then forceEither $ CF.items conf s else assert `failure` "Unknown config section: " ++ s parseConfigRules :: FilePath -> CP -> Config parseConfigRules dataDir cp = let configSelfString = let CP conf = cp in CF.to_string conf configCaves = let section = getItems cp "caves" readCaves = EM.fromList . map (toEnum *** first T.pack) in M.fromList $ map (T.pack *** (readCaves . read)) section configFirstDeathEnds = get cp "engine" "firstDeathEnds" configFovMode = get cp "engine" "fovMode" configSaveBkpClips = get cp "engine" "saveBkpClips" configAppDataDir = dataDir configScoresFile = dataDir get cp "file" "scoresFile" configRulesCfgFile = dataDir "config.rules" configHeroNames = let toNumber (ident, name) = case stripPrefix "HeroName_" ident of Just n -> (read n, T.pack name) Nothing -> assert `failure` ("wrong hero name id " ++ ident) section = getItems cp "heroName" in map toNumber section configPlayers = let section = getItems cp "players" in M.fromList $ map (T.pack *** read) section configScenario = let section = getItems cp "scenario" in M.fromList $ map (T.pack *** read) section in Config{..} -- | Read and parse rules config file and supplement it with random seeds. mkConfigRules :: Kind.Ops RuleKind -> IO (Config, R.StdGen, R.StdGen) mkConfigRules corule = do let cpRulesDefault = rcfgRulesDefault $ Kind.stdRuleset corule appData <- appDataDir cpRules <- mkConfig cpRulesDefault $ appData "config.rules.ini" (dungeonGen, cp2) <- getSetGen cpRules "dungeonRandomGenerator" (startingGen, cp3) <- getSetGen cp2 "startingRandomGenerator" let conf = parseConfigRules appData cp3 -- Catch syntax errors ASAP. !res = deepseq conf (conf, dungeonGen, startingGen) return res