# include "configurationmanager.hpp"
# include <fstream>
# include <components/debug/debuglog.hpp>
# include <components/files/configfileparser.hpp>
# include <components/fallback/validate.hpp>
# include <boost/program_options/variables_map.hpp>
# include <boost/program_options/options_description.hpp>
/**
* \ namespace Files
*/
namespace Files
{
namespace bpo = boost : : program_options ;
static const char * const openmwCfgFile = " openmw.cfg " ;
# if defined(_WIN32) || defined(__WINDOWS__)
static const char * const applicationName = " OpenMW " ;
# else
static const char * const applicationName = " openmw " ;
# endif
const char * const localToken = " ?local? " ;
const char * const userConfigToken = " ?userconfig? " ;
const char * const userDataToken = " ?userdata? " ;
const char * const globalToken = " ?global? " ;
ConfigurationManager : : ConfigurationManager ( bool silent )
: mFixedPath ( applicationName )
, mSilent ( silent )
{
setupTokensMapping ( ) ;
// Initialize with fixed paths, will be overridden in `readConfiguration`.
mUserDataPath = mFixedPath . getUserDataPath ( ) ;
mScreenshotPath = mFixedPath . getUserDataPath ( ) / " screenshots " ;
}
ConfigurationManager : : ~ ConfigurationManager ( )
{
}
void ConfigurationManager : : setupTokensMapping ( )
{
mTokensMapping . insert ( std : : make_pair ( localToken , & FixedPath < > : : getLocalPath ) ) ;
mTokensMapping . insert ( std : : make_pair ( userConfigToken , & FixedPath < > : : getUserConfigPath ) ) ;
mTokensMapping . insert ( std : : make_pair ( userDataToken , & FixedPath < > : : getUserDataPath ) ) ;
mTokensMapping . insert ( std : : make_pair ( globalToken , & FixedPath < > : : getGlobalDataPath ) ) ;
}
static bool hasReplaceConfig ( const bpo : : variables_map & variables )
{
if ( variables [ " replace " ] . empty ( ) )
return false ;
for ( const std : : string & var : variables [ " replace " ] . as < std : : vector < std : : string > > ( ) )
{
if ( var = = " config " )
return true ;
}
return false ;
}
void ConfigurationManager : : readConfiguration ( bpo : : variables_map & variables ,
const bpo : : options_description & description , bool quiet )
{
bool silent = mSilent ;
mSilent = quiet ;
std : : optional < bpo : : variables_map > config = loadConfig ( mFixedPath . getLocalPath ( ) , description ) ;
if ( config )
mActiveConfigPaths . push_back ( mFixedPath . getLocalPath ( ) ) ;
else
{
mActiveConfigPaths . push_back ( mFixedPath . getGlobalConfigPath ( ) ) ;
config = loadConfig ( mFixedPath . getGlobalConfigPath ( ) , description ) ;
}
if ( ! config )
{
if ( ! quiet )
Log ( Debug : : Error ) < < " Neither local config nor global config are available. " ;
mSilent = silent ;
return ;
}
std : : stack < std : : filesystem : : path > extraConfigDirs ;
addExtraConfigDirs ( extraConfigDirs , variables ) ;
if ( ! hasReplaceConfig ( variables ) )
addExtraConfigDirs ( extraConfigDirs , * config ) ;
std : : vector < bpo : : variables_map > parsedConfigs { * std : : move ( config ) } ;
std : : set < std : : filesystem : : path > alreadyParsedPaths ; // needed to prevent infinite loop in case of a circular link
alreadyParsedPaths . insert ( std : : filesystem : : path ( mActiveConfigPaths . front ( ) ) ) ;
while ( ! extraConfigDirs . empty ( ) )
{
std : : filesystem : : path path = extraConfigDirs . top ( ) ;
extraConfigDirs . pop ( ) ;
if ( alreadyParsedPaths . count ( path ) > 0 )
{
if ( ! quiet )
Log ( Debug : : Warning ) < < " Repeated config dir: " < < path ;
continue ;
}
alreadyParsedPaths . insert ( path ) ;
config = loadConfig ( path , description ) ;
if ( config & & hasReplaceConfig ( * config ) & & parsedConfigs . size ( ) > 1 )
{
mActiveConfigPaths . resize ( 1 ) ;
parsedConfigs . resize ( 1 ) ;
Log ( Debug : : Info ) < < " Skipping previous configs except " < < ( mActiveConfigPaths . front ( ) / " openmw.cfg " ) < <
" due to replace=config in " < < ( path / " openmw.cfg " ) ;
}
mActiveConfigPaths . push_back ( path ) ;
if ( config )
{
addExtraConfigDirs ( extraConfigDirs , * config ) ;
parsedConfigs . push_back ( * std : : move ( config ) ) ;
}
}
for ( auto it = parsedConfigs . rbegin ( ) ; it ! = parsedConfigs . rend ( ) ; + + it )
{
auto composingVariables = separateComposingVariables ( variables , description ) ;
for ( auto & [ k , v ] : * it )
{
auto variable = variables . find ( k ) ;
if ( variable = = variables . end ( ) )
variables . insert ( { k , v } ) ;
else if ( variable - > second . defaulted ( ) )
variable - > second = v ;
}
mergeComposingVariables ( variables , composingVariables , description ) ;
}
mUserDataPath = variables [ " user-data " ] . as < Files : : MaybeQuotedPath > ( ) ;
if ( mUserDataPath . empty ( ) )
{
if ( ! quiet )
Log ( Debug : : Warning ) < < " Error: `user-data` is not specified " ;
mUserDataPath = mFixedPath . getUserDataPath ( ) ;
}
mScreenshotPath = mUserDataPath / " screenshots " ;
std : : filesystem : : create_directories ( getUserConfigPath ( ) ) ;
std : : filesystem : : create_directories ( mScreenshotPath ) ;
// probably not necessary but validate the creation of the screenshots directory and fallback to the original behavior if it fails
if ( ! std : : filesystem : : is_directory ( mScreenshotPath ) )
mScreenshotPath = mUserDataPath ;
if ( ! quiet )
{
Log ( Debug : : Info ) < < " Logs dir: " < < getUserConfigPath ( ) . string ( ) ; //TODO(Project579): This will probably break in windows with unicode paths
Log ( Debug : : Info ) < < " User data dir: " < < mUserDataPath . string ( ) ; //TODO(Project579): This will probably break in windows with unicode paths
Log ( Debug : : Info ) < < " Screenshots dir: " < < mScreenshotPath . string ( ) ; //TODO(Project579): This will probably break in windows with unicode paths
}
mSilent = silent ;
}
void ConfigurationManager : : addExtraConfigDirs ( std : : stack < std : : filesystem : : path > & dirs ,
const bpo : : variables_map & variables ) const
{
auto configIt = variables . find ( " config " ) ;
if ( configIt = = variables . end ( ) )
return ;
Files : : PathContainer newDirs = asPathContainer ( configIt - > second . as < Files : : MaybeQuotedPathContainer > ( ) ) ;
for ( auto it = newDirs . rbegin ( ) ; it ! = newDirs . rend ( ) ; + + it )
dirs . push ( * it ) ;
}
void ConfigurationManager : : addCommonOptions ( bpo : : options_description & description )
{
description . add_options ( )
( " config " , bpo : : value < Files : : MaybeQuotedPathContainer > ( ) - > default_value ( Files : : MaybeQuotedPathContainer ( ) , " " )
- > multitoken ( ) - > composing ( ) , " additional config directories " )
( " replace " , bpo : : value < std : : vector < std : : string > > ( ) - > default_value ( std : : vector < std : : string > ( ) , " " ) - > multitoken ( ) - > composing ( ) ,
" settings where the values from the current source should replace those from lower-priority sources instead of being appended " )
( " user-data " , bpo : : value < Files : : MaybeQuotedPath > ( ) - > default_value ( Files : : MaybeQuotedPath ( ) , " " ) ,
" set user data directory (used for saves, screenshots, etc) " ) ;
}
bpo : : variables_map separateComposingVariables ( bpo : : variables_map & variables , const bpo : : options_description & description )
{
bpo : : variables_map composingVariables ;
for ( auto itr = variables . begin ( ) ; itr ! = variables . end ( ) ; )
{
if ( description . find ( itr - > first , false ) . semantic ( ) - > is_composing ( ) )
{
composingVariables . emplace ( * itr ) ;
itr = variables . erase ( itr ) ;
}
else
+ + itr ;
}
return composingVariables ;
}
void mergeComposingVariables ( bpo : : variables_map & first , bpo : : variables_map & second ,
const bpo : : options_description & description )
{
// There are a few places this assumes all variables are present in second, but it's never crashed in the wild, so it looks like that's guaranteed.
std : : set < std : : string > replacedVariables ;
if ( description . find_nothrow ( " replace " , false ) )
{
auto replace = second [ " replace " ] ;
if ( ! replace . defaulted ( ) & & ! replace . empty ( ) )
{
std : : vector < std : : string > replaceVector = replace . as < std : : vector < std : : string > > ( ) ;
replacedVariables . insert ( replaceVector . begin ( ) , replaceVector . end ( ) ) ;
}
}
for ( const auto & option : description . options ( ) )
{
if ( option - > semantic ( ) - > is_composing ( ) )
{
std : : string name = option - > canonical_display_name ( ) ;
auto firstPosition = first . find ( name ) ;
if ( firstPosition = = first . end ( ) )
{
first . emplace ( name , second [ name ] ) ;
continue ;
}
if ( replacedVariables . count ( name ) | | firstPosition - > second . defaulted ( ) | | firstPosition - > second . empty ( ) )
{
firstPosition - > second = second [ name ] ;
continue ;
}
if ( second [ name ] . defaulted ( ) | | second [ name ] . empty ( ) )
continue ;
boost : : any & firstValue = firstPosition - > second . value ( ) ;
const boost : : any & secondValue = second [ name ] . value ( ) ;
if ( firstValue . type ( ) = = typeid ( Files : : MaybeQuotedPathContainer ) )
{
auto & firstPathContainer = boost : : any_cast < Files : : MaybeQuotedPathContainer & > ( firstValue ) ;
const auto & secondPathContainer = boost : : any_cast < const Files : : MaybeQuotedPathContainer & > ( secondValue ) ;
firstPathContainer . insert ( firstPathContainer . end ( ) , secondPathContainer . begin ( ) , secondPathContainer . end ( ) ) ;
}
else if ( firstValue . type ( ) = = typeid ( std : : vector < std : : string > ) )
{
auto & firstVector = boost : : any_cast < std : : vector < std : : string > & > ( firstValue ) ;
const auto & secondVector = boost : : any_cast < const std : : vector < std : : string > & > ( secondValue ) ;
firstVector . insert ( firstVector . end ( ) , secondVector . begin ( ) , secondVector . end ( ) ) ;
}
else if ( firstValue . type ( ) = = typeid ( Fallback : : FallbackMap ) )
{
auto & firstMap = boost : : any_cast < Fallback : : FallbackMap & > ( firstValue ) ;
const auto & secondMap = boost : : any_cast < const Fallback : : FallbackMap & > ( secondValue ) ;
std : : map < std : : string , std : : string > tempMap ( secondMap . mMap ) ;
tempMap . merge ( firstMap . mMap ) ;
firstMap . mMap . swap ( tempMap ) ;
}
else
Log ( Debug : : Error ) < < " Unexpected composing variable type. Curse boost and their blasted arcane templates. " ;
}
}
}
void ConfigurationManager : : processPath ( std : : filesystem : : path & path , const std : : filesystem : : path & basePath ) const
{
std : : string str = path . string ( ) ; //TODO(Project579): This will probably break in windows with unicode paths
if ( str . empty ( ) | | str [ 0 ] ! = ' ? ' )
{
if ( ! path . is_absolute ( ) )
path = basePath / path ;
return ;
}
std : : string : : size_type pos = str . find ( ' ? ' , 1 ) ;
if ( pos ! = std : : string : : npos & & pos ! = 0 )
{
auto tokenIt = mTokensMapping . find ( str . substr ( 0 , pos + 1 ) ) ;
if ( tokenIt ! = mTokensMapping . end ( ) )
{
std : : filesystem : : path tempPath ( ( ( mFixedPath ) . * ( tokenIt - > second ) ) ( ) ) ; //TODO(Project579): This will probably break in windows with unicode paths
if ( pos < str . length ( ) - 1 )
{
// There is something after the token, so we should
// append it to the path
tempPath / = str . substr ( pos + 1 , str . length ( ) - pos ) ;
}
path = tempPath ; //TODO(Project579): This will probably break in windows with unicode paths
}
else
{
if ( ! mSilent )
Log ( Debug : : Warning ) < < " Path starts with unknown token: " < < path ;
path . clear ( ) ;
}
}
}
void ConfigurationManager : : processPaths ( Files : : PathContainer & dataDirs , const std : : filesystem : : path & basePath ) const
{
for ( auto & path : dataDirs )
processPath ( path , basePath ) ;
}
void ConfigurationManager : : processPaths ( boost : : program_options : : variables_map & variables , const std : : filesystem : : path & basePath ) const
{
for ( auto & [ name , var ] : variables )
{
if ( var . defaulted ( ) )
continue ;
if ( var . value ( ) . type ( ) = = typeid ( MaybeQuotedPathContainer ) )
{
auto & pathContainer = boost : : any_cast < MaybeQuotedPathContainer & > ( var . value ( ) ) ;
for ( MaybeQuotedPath & path : pathContainer )
processPath ( path , basePath ) ;
}
else if ( var . value ( ) . type ( ) = = typeid ( MaybeQuotedPath ) )
{
std : : filesystem : : path & path = boost : : any_cast < Files : : MaybeQuotedPath & > ( var . value ( ) ) ;
processPath ( path , basePath ) ;
}
}
}
void ConfigurationManager : : filterOutNonExistingPaths ( Files : : PathContainer & dataDirs ) const
{
dataDirs . erase ( std : : remove_if ( dataDirs . begin ( ) , dataDirs . end ( ) ,
[ this ] ( const std : : filesystem : : path & p )
{
bool exists = std : : filesystem : : is_directory ( p ) ;
if ( ! exists & & ! mSilent )
Log ( Debug : : Warning ) < < " No such dir: " < < p ;
return ! exists ;
} ) ,
dataDirs . end ( ) ) ;
}
std : : optional < bpo : : variables_map > ConfigurationManager : : loadConfig (
const std : : filesystem : : path & path , const bpo : : options_description & description ) const
{
std : : filesystem : : path cfgFile ( path ) ;
cfgFile / = std : : string ( openmwCfgFile ) ;
if ( std : : filesystem : : is_regular_file ( cfgFile ) )
{
if ( ! mSilent )
Log ( Debug : : Info ) < < " Loading config file: " < < cfgFile . string ( ) ; //TODO(Project579): This will probably break in windows with unicode paths
std : : ifstream configFileStream ( cfgFile ) ;
if ( configFileStream . is_open ( ) )
{
bpo : : variables_map variables ;
bpo : : store ( Files : : parse_config_file ( configFileStream , description , true ) , variables ) ;
processPaths ( variables , path ) ;
return variables ;
}
else if ( ! mSilent )
Log ( Debug : : Error ) < < " Loading failed. " ;
}
return std : : nullopt ;
}
const std : : filesystem : : path & ConfigurationManager : : getGlobalPath ( ) const
{
return mFixedPath . getGlobalConfigPath ( ) ;
}
const std : : filesystem : : path & ConfigurationManager : : getUserConfigPath ( ) const
{
if ( mActiveConfigPaths . empty ( ) )
return mFixedPath . getUserConfigPath ( ) ;
else
return mActiveConfigPaths . back ( ) ;
}
const std : : filesystem : : path & ConfigurationManager : : getUserDataPath ( ) const
{
return mUserDataPath ;
}
const std : : filesystem : : path & ConfigurationManager : : getLocalPath ( ) const
{
return mFixedPath . getLocalPath ( ) ;
}
const std : : filesystem : : path & ConfigurationManager : : getGlobalDataPath ( ) const
{
return mFixedPath . getGlobalDataPath ( ) ;
}
const std : : filesystem : : path & ConfigurationManager : : getCachePath ( ) const
{
return mFixedPath . getCachePath ( ) ;
}
const std : : filesystem : : path & ConfigurationManager : : getInstallPath ( ) const
{
return mFixedPath . getInstallPath ( ) ;
}
const std : : filesystem : : path & ConfigurationManager : : getScreenshotPath ( ) const
{
return mScreenshotPath ;
}
void parseArgs ( int argc , const char * const argv [ ] , bpo : : variables_map & variables ,
const bpo : : options_description & description )
{
bpo : : store (
bpo : : command_line_parser ( argc , argv ) . options ( description ) . allow_unregistered ( ) . run ( ) ,
variables
) ;
}
void parseConfig ( std : : istream & stream , bpo : : variables_map & variables , const bpo : : options_description & description )
{
bpo : : store ( Files : : parse_config_file ( stream , description , true ) , variables ) ;
}
std : : istream & operator > > ( std : : istream & istream , MaybeQuotedPath & MaybeQuotedPath )
{
// If the stream starts with a double quote, read from stream using boost::filesystem::path rules, then discard anything remaining.
// This prevents boost::program_options getting upset that we've not consumed the whole stream.
// If it doesn't start with a double quote, read the whole thing verbatim
if ( istream . peek ( ) = = ' " ' )
{
std : : string intermediate ;
istream > > std : : quoted ( intermediate , ' " ' , ' & ' ) ;
static_cast < std : : filesystem : : path & > ( MaybeQuotedPath ) = intermediate ;
if ( istream & & ! istream . eof ( ) & & istream . peek ( ) ! = EOF )
{
std : : string remainder { std : : istreambuf_iterator ( istream ) , { } } ;
Log ( Debug : : Warning ) < < " Trailing data in path setting. Used ' " < < MaybeQuotedPath . string ( ) < < " ' but ' " < < remainder < < " ' remained " ; //TODO(Project579): This will probably break in windows with unicode paths
}
}
else
{
std : : string intermediate { std : : istreambuf_iterator ( istream ) , { } } ;
static_cast < std : : filesystem : : path & > ( MaybeQuotedPath ) = intermediate ;
}
return istream ;
}
PathContainer asPathContainer ( const MaybeQuotedPathContainer & MaybeQuotedPathContainer )
{
return PathContainer ( MaybeQuotedPathContainer . begin ( ) , MaybeQuotedPathContainer . end ( ) ) ;
}
} /* namespace Files */