2024-11-09 20:29:07 +00:00
/*
* ModManager.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "ModManager.h"
#include "ModDescription.h"
#include "ModScope.h"
2024-11-14 15:41:22 +00:00
#include "../constants/StringConstants.h"
2024-11-09 20:29:07 +00:00
#include "../filesystem/Filesystem.h"
#include "../json/JsonNode.h"
2024-11-11 11:19:14 +00:00
#include "../texts/CGeneralTextHandler.h"
2024-11-09 20:29:07 +00:00
VCMI_LIB_NAMESPACE_BEGIN
2024-11-14 18:22:02 +00:00
static std :: string getModDirectory ( const TModID & modName )
2024-11-09 20:29:07 +00:00
{
std :: string result = modName ;
boost :: to_upper ( result );
boost :: algorithm :: replace_all ( result , "." , "/MODS/" );
2024-11-14 18:22:02 +00:00
return "MODS/" + result ;
}
static std :: string getModSettingsDirectory ( const TModID & modName )
{
return getModDirectory ( modName ) + "/MODS/" ;
2024-11-09 20:29:07 +00:00
}
static JsonPath getModDescriptionFile ( const TModID & modName )
{
2024-11-14 18:22:02 +00:00
return JsonPath :: builtin ( getModDirectory ( modName ) + "/mod" );
2024-11-09 20:29:07 +00:00
}
ModsState :: ModsState ()
{
modList . push_back ( ModScope :: scopeBuiltin ());
std :: vector < TModID > testLocations = scanModsDirectory ( "MODS/" );
while ( ! testLocations . empty ())
{
std :: string target = testLocations . back ();
testLocations . pop_back ();
modList . push_back ( boost :: algorithm :: to_lower_copy ( target ));
for ( const auto & submod : scanModsDirectory ( getModSettingsDirectory ( target )))
testLocations . push_back ( target + '.' + submod );
}
}
2024-11-14 15:41:22 +00:00
TModList ModsState :: getInstalledMods () const
2024-11-09 20:29:07 +00:00
{
return modList ;
}
2024-11-14 15:41:22 +00:00
uint32_t ModsState :: computeChecksum ( const TModID & modName ) const
{
boost :: crc_32_type modChecksum ;
// first - add current VCMI version into checksum to force re-validation on VCMI updates
2024-11-14 15:50:39 +00:00
modChecksum . process_bytes ( static_cast < const void *> ( GameConstants :: VCMI_VERSION . data ()), GameConstants :: VCMI_VERSION . size ());
2024-11-14 15:41:22 +00:00
// second - add mod.json into checksum because filesystem does not contains this file
if ( modName != ModScope :: scopeBuiltin ())
{
auto modConfFile = getModDescriptionFile ( modName );
ui32 configChecksum = CResourceHandler :: get ( "initial" ) -> load ( modConfFile ) -> calculateCRC32 ();
2024-11-14 15:50:39 +00:00
modChecksum . process_bytes ( static_cast < const void *> ( & configChecksum ), sizeof ( configChecksum ));
2024-11-14 15:41:22 +00:00
}
// third - add all detected text files from this mod into checksum
const auto & filesystem = CResourceHandler :: get ( modName );
auto files = filesystem -> getFilteredFiles ([]( const ResourcePath & resID )
{
return resID . getType () == EResType :: JSON && boost :: starts_with ( resID . getName (), "CONFIG" );
});
for ( const ResourcePath & file : files )
{
ui32 fileChecksum = filesystem -> load ( file ) -> calculateCRC32 ();
2024-11-14 15:50:39 +00:00
modChecksum . process_bytes ( static_cast < const void *> ( & fileChecksum ), sizeof ( fileChecksum ));
2024-11-14 15:41:22 +00:00
}
return modChecksum . checksum ();
}
2024-11-14 18:22:02 +00:00
double ModsState :: getInstalledModSizeMegabytes ( const TModID & modName ) const
{
ResourcePath resDir ( getModDirectory ( modName ), EResType :: DIRECTORY );
std :: string path = CResourceHandler :: get () -> getResourceName ( resDir ) -> string ();
size_t sizeBytes = 0 ;
for ( boost :: filesystem :: recursive_directory_iterator it ( path ); it != boost :: filesystem :: recursive_directory_iterator (); ++ it )
{
if ( ! boost :: filesystem :: is_directory ( * it ))
sizeBytes += boost :: filesystem :: file_size ( * it );
}
2024-11-18 14:40:15 +00:00
double sizeMegabytes = sizeBytes / static_cast < double > ( 1024 * 1024 );
2024-11-14 18:22:02 +00:00
return sizeMegabytes ;
}
2024-11-09 20:29:07 +00:00
std :: vector < TModID > ModsState :: scanModsDirectory ( const std :: string & modDir ) const
{
size_t depth = boost :: range :: count ( modDir , '/' );
const auto & modScanFilter = [ & ]( const ResourcePath & id ) -> bool
{
if ( id . getType () != EResType :: DIRECTORY )
return false ;
if ( ! boost :: algorithm :: starts_with ( id . getName (), modDir ))
return false ;
if ( boost :: range :: count ( id . getName (), '/' ) != depth )
return false ;
return true ;
};
auto list = CResourceHandler :: get ( "initial" ) -> getFilteredFiles ( modScanFilter );
//storage for found mods
std :: vector < TModID > foundMods ;
for ( const auto & entry : list )
{
std :: string name = entry . getName ();
name . erase ( 0 , modDir . size ()); //Remove path prefix
if ( name . empty ())
continue ;
if ( name . find ( '.' ) != std :: string :: npos )
continue ;
2024-11-14 15:41:22 +00:00
if ( ModScope :: isScopeReserved ( boost :: to_lower_copy ( name )))
continue ;
2024-11-09 20:29:07 +00:00
if ( ! CResourceHandler :: get ( "initial" ) -> existsResource ( JsonPath :: builtin ( entry . getName () + "/MOD" )))
continue ;
foundMods . push_back ( name );
}
return foundMods ;
}
///////////////////////////////////////////////////////////////////////////////
ModsPresetState :: ModsPresetState ()
{
2024-11-18 14:40:15 +00:00
static const JsonPath settingsPath = JsonPath :: builtin ( "config/modSettings.json" );
if ( CResourceHandler :: get ( "local" ) -> existsResource ( ResourcePath ( settingsPath )))
{
modConfig = JsonNode ( settingsPath );
}
else
{
// Probably new install. Create initial configuration
CResourceHandler :: get ( "local" ) -> createResource ( settingsPath . getOriginalName () + ".json" );
}
2024-11-09 20:29:07 +00:00
2024-12-15 19:43:43 +00:00
if ( modConfig [ "presets" ]. isNull () || modConfig [ "presets" ]. Struct (). empty ())
2024-11-09 20:29:07 +00:00
{
modConfig [ "activePreset" ] = JsonNode ( "default" );
if ( modConfig [ "activeMods" ]. isNull ())
createInitialPreset (); // new install
else
importInitialPreset (); // 1.5 format import
}
2024-12-15 19:43:43 +00:00
auto allPresets = getAllPresets ();
if ( ! vstd :: contains ( allPresets , modConfig [ "activePreset" ]. String ()))
modConfig [ "activePreset" ] = JsonNode ( allPresets . front ());
2024-12-23 18:00:35 +00:00
logGlobal -> debug ( "Loading following mod settings: %s" , modConfig . toCompactString ());
2024-11-09 20:29:07 +00:00
}
void ModsPresetState :: createInitialPreset ()
{
// TODO: scan mods directory for all its content? Probably unnecessary since this looks like new install, but who knows?
2024-11-14 15:41:22 +00:00
modConfig [ "presets" ][ "default" ][ "mods" ]. Vector (). emplace_back ( "vcmi" );
2024-11-09 20:29:07 +00:00
}
void ModsPresetState :: importInitialPreset ()
{
JsonNode preset ;
for ( const auto & mod : modConfig [ "activeMods" ]. Struct ())
{
if ( mod . second [ "active" ]. Bool ())
2024-11-14 15:41:22 +00:00
preset [ "mods" ]. Vector (). emplace_back ( mod . first );
2024-11-09 20:29:07 +00:00
for ( const auto & submod : mod . second [ "mods" ]. Struct ())
preset [ "settings" ][ mod . first ][ submod . first ] = submod . second [ "active" ];
}
modConfig [ "presets" ][ "default" ] = preset ;
}
2024-11-11 11:19:14 +00:00
const JsonNode & ModsPresetState :: getActivePresetConfig () const
2024-11-09 20:29:07 +00:00
{
const std :: string & currentPresetName = modConfig [ "activePreset" ]. String ();
const JsonNode & currentPreset = modConfig [ "presets" ][ currentPresetName ];
2024-11-11 11:19:14 +00:00
return currentPreset ;
}
TModList ModsPresetState :: getActiveRootMods () const
{
2024-12-17 16:37:55 +00:00
return getRootMods ( getActivePreset ());
}
TModList ModsPresetState :: getRootMods ( const std :: string & presetName ) const
{
const JsonNode & modsToActivateJson = modConfig [ "presets" ][ presetName ][ "mods" ];
2024-11-14 15:50:39 +00:00
auto modsToActivate = modsToActivateJson . convertTo < std :: vector < TModID >> ();
2024-11-20 20:44:24 +00:00
if ( ! vstd :: contains ( modsToActivate , ModScope :: scopeBuiltin ()))
modsToActivate . push_back ( ModScope :: scopeBuiltin ());
2024-11-09 20:29:07 +00:00
return modsToActivate ;
}
2024-11-11 11:19:14 +00:00
std :: map < TModID , bool > ModsPresetState :: getModSettings ( const TModID & modID ) const
{
const JsonNode & modSettingsJson = getActivePresetConfig ()[ "settings" ][ modID ];
2024-11-14 15:50:39 +00:00
auto modSettings = modSettingsJson . convertTo < std :: map < TModID , bool >> ();
2024-11-11 11:19:14 +00:00
return modSettings ;
}
2024-11-14 15:41:22 +00:00
std :: optional < uint32_t > ModsPresetState :: getValidatedChecksum ( const TModID & modName ) const
{
const JsonNode & node = modConfig [ "validatedMods" ][ modName ];
if ( node . isNull ())
return std :: nullopt ;
else
return node . Integer ();
}
2024-11-14 20:56:19 +00:00
void ModsPresetState :: setModActive ( const TModID & modID , bool isActive )
{
size_t dotPos = modID . find ( '.' );
if ( dotPos != std :: string :: npos )
{
std :: string rootMod = modID . substr ( 0 , dotPos );
std :: string settingID = modID . substr ( dotPos + 1 );
setSettingActive ( rootMod , settingID , isActive );
}
else
{
if ( isActive )
addRootMod ( modID );
else
eraseRootMod ( modID );
}
}
void ModsPresetState :: addRootMod ( const TModID & modName )
{
const std :: string & currentPresetName = modConfig [ "activePreset" ]. String ();
JsonNode & currentPreset = modConfig [ "presets" ][ currentPresetName ];
if ( ! vstd :: contains ( currentPreset [ "mods" ]. Vector (), JsonNode ( modName )))
currentPreset [ "mods" ]. Vector (). emplace_back ( modName );
}
void ModsPresetState :: setSettingActive ( const TModID & modName , const TModID & settingName , bool isActive )
2024-11-11 11:19:14 +00:00
{
const std :: string & currentPresetName = modConfig [ "activePreset" ]. String ();
JsonNode & currentPreset = modConfig [ "presets" ][ currentPresetName ];
currentPreset [ "settings" ][ modName ][ settingName ]. Bool () = isActive ;
}
2024-11-26 13:31:32 +00:00
void ModsPresetState :: removeOldMods ( const TModList & modsToKeep )
{
const std :: string & currentPresetName = modConfig [ "activePreset" ]. String ();
JsonNode & currentPreset = modConfig [ "presets" ][ currentPresetName ];
vstd :: erase_if ( currentPreset [ "mods" ]. Vector (), [ & ]( const JsonNode & entry ){
return ! vstd :: contains ( modsToKeep , entry . String ());
});
vstd :: erase_if ( currentPreset [ "settings" ]. Struct (), [ & ]( const auto & entry ){
return ! vstd :: contains ( modsToKeep , entry . first );
});
}
2024-11-14 20:56:19 +00:00
void ModsPresetState :: eraseRootMod ( const TModID & modName )
2024-11-11 11:19:14 +00:00
{
2024-11-14 20:56:19 +00:00
const std :: string & currentPresetName = modConfig [ "activePreset" ]. String ();
JsonNode & currentPreset = modConfig [ "presets" ][ currentPresetName ];
vstd :: erase ( currentPreset [ "mods" ]. Vector (), JsonNode ( modName ));
2024-11-11 11:19:14 +00:00
}
2024-11-14 20:56:19 +00:00
void ModsPresetState :: eraseModSetting ( const TModID & modName , const TModID & settingName )
2024-11-11 11:19:14 +00:00
{
2024-11-14 20:56:19 +00:00
const std :: string & currentPresetName = modConfig [ "activePreset" ]. String ();
JsonNode & currentPreset = modConfig [ "presets" ][ currentPresetName ];
2024-11-26 21:04:56 +00:00
currentPreset [ "settings" ][ modName ]. Struct (). erase ( settingName );
2024-11-11 11:19:14 +00:00
}
std :: vector < TModID > ModsPresetState :: getActiveMods () const
{
TModList activeRootMods = getActiveRootMods ();
TModList allActiveMods ;
for ( const auto & activeMod : activeRootMods )
{
2024-11-20 20:44:24 +00:00
assert ( ! vstd :: contains ( allActiveMods , activeMod ));
2024-11-12 20:00:21 +00:00
allActiveMods . push_back ( activeMod );
2024-11-11 11:19:14 +00:00
for ( const auto & submod : getModSettings ( activeMod ))
2024-11-20 20:44:24 +00:00
{
2024-11-11 11:19:14 +00:00
if ( submod . second )
2024-11-20 20:44:24 +00:00
{
assert ( ! vstd :: contains ( allActiveMods , activeMod + '.' + submod . first ));
2024-11-11 11:19:14 +00:00
allActiveMods . push_back ( activeMod + '.' + submod . first );
2024-11-20 20:44:24 +00:00
}
}
2024-11-11 11:19:14 +00:00
}
return allActiveMods ;
}
2024-11-14 15:41:22 +00:00
void ModsPresetState :: setValidatedChecksum ( const TModID & modName , std :: optional < uint32_t > value )
{
if ( value . has_value ())
modConfig [ "validatedMods" ][ modName ]. Integer () = * value ;
else
modConfig [ "validatedMods" ]. Struct (). erase ( modName );
}
void ModsPresetState :: saveConfigurationState () const
{
std :: fstream file ( CResourceHandler :: get () -> getResourceName ( ResourcePath ( "config/modSettings.json" )) -> c_str (), std :: ofstream :: out | std :: ofstream :: trunc );
file << modConfig . toCompactString ();
}
2024-12-08 20:56:19 +00:00
void ModsPresetState :: createNewPreset ( const std :: string & presetName )
{
if ( modConfig [ "presets" ][ presetName ]. isNull ())
modConfig [ "presets" ][ presetName ][ "mods" ]. Vector (). emplace_back ( "vcmi" );
}
void ModsPresetState :: deletePreset ( const std :: string & presetName )
{
if ( modConfig [ "presets" ]. Struct (). size () < 2 )
throw std :: runtime_error ( "Unable to delete last preset!" );
modConfig [ "presets" ]. Struct (). erase ( presetName );
}
void ModsPresetState :: activatePreset ( const std :: string & presetName )
{
if ( modConfig [ "presets" ]. Struct (). count ( presetName ) == 0 )
throw std :: runtime_error ( "Unable to activate non-exinsting preset!" );
modConfig [ "activePreset" ]. String () = presetName ;
}
void ModsPresetState :: renamePreset ( const std :: string & oldPresetName , const std :: string & newPresetName )
{
if ( oldPresetName == newPresetName )
throw std :: runtime_error ( "Unable to rename preset to the same name!" );
if ( modConfig [ "presets" ]. Struct (). count ( oldPresetName ) == 0 )
throw std :: runtime_error ( "Unable to rename non-existing last preset!" );
if ( modConfig [ "presets" ]. Struct (). count ( newPresetName ) != 0 )
throw std :: runtime_error ( "Unable to rename preset - preset with such name already exists!" );
modConfig [ "presets" ][ newPresetName ] = modConfig [ "presets" ][ oldPresetName ];
modConfig [ "presets" ]. Struct (). erase ( oldPresetName );
if ( modConfig [ "activePreset" ]. String () == oldPresetName )
modConfig [ "activePreset" ]. String () = newPresetName ;
}
std :: vector < std :: string > ModsPresetState :: getAllPresets () const
{
std :: vector < std :: string > presets ;
for ( const auto & preset : modConfig [ "presets" ]. Struct ())
presets . push_back ( preset . first );
return presets ;
}
std :: string ModsPresetState :: getActivePreset () const
{
return modConfig [ "activePreset" ]. String ();
}
2024-12-17 14:38:50 +00:00
JsonNode ModsPresetState :: exportCurrentPreset () const
{
JsonNode data = getActivePresetConfig ();
std :: string presetName = getActivePreset ();
data [ "name" ] = JsonNode ( presetName );
vstd :: erase_if ( data [ "settings" ]. Struct (), [ & ]( const auto & pair ){
return ! vstd :: contains ( data [ "mods" ]. Vector (), JsonNode ( pair . first ));
});
return data ;
}
2024-12-17 16:37:55 +00:00
std :: string ModsPresetState :: importPreset ( const JsonNode & newConfig )
2024-12-17 14:38:50 +00:00
{
std :: string importedPresetName = newConfig [ "name" ]. String ();
if ( importedPresetName . empty ())
throw std :: runtime_error ( "Attempt to import invalid preset" );
modConfig [ "presets" ][ importedPresetName ] = newConfig ;
modConfig [ "presets" ][ importedPresetName ]. Struct (). erase ( "name" );
2024-12-17 16:37:55 +00:00
return importedPresetName ;
2024-12-17 14:38:50 +00:00
}
2024-11-13 17:25:59 +00:00
ModsStorage :: ModsStorage ( const std :: vector < TModID > & modsToLoad , const JsonNode & repositoryList )
2024-11-09 20:29:07 +00:00
{
JsonNode coreModConfig ( JsonPath :: builtin ( "config/gameConfig.json" ));
coreModConfig . setModScope ( ModScope :: scopeBuiltin ());
2024-11-13 17:25:59 +00:00
mods . try_emplace ( ModScope :: scopeBuiltin (), ModScope :: scopeBuiltin (), coreModConfig , JsonNode ());
2024-11-09 20:29:07 +00:00
2025-02-20 15:54:56 +00:00
// MODS COMPATIBILITY: in 1.6, repository list contains mod list directly, in 1.7 it is located in 'availableMods' node
const auto & availableRepositoryMods = repositoryList [ "availableMods" ]. isNull () ? repositoryList : repositoryList [ "availableMods" ];
2024-11-09 20:29:07 +00:00
for ( auto modID : modsToLoad )
{
if ( ModScope :: isScopeReserved ( modID ))
continue ;
JsonNode modConfig ( getModDescriptionFile ( modID ));
modConfig . setModScope ( modID );
if ( modConfig [ "modType" ]. isNull ())
{
logMod -> error ( "Can not load mod %s - invalid mod config file!" , modID );
continue ;
}
2025-02-20 15:54:56 +00:00
mods . try_emplace ( modID , modID , modConfig , availableRepositoryMods [ modID ]);
2024-11-13 17:25:59 +00:00
}
2025-02-20 15:54:56 +00:00
for ( const auto & mod : availableRepositoryMods . Struct ())
2024-11-13 17:25:59 +00:00
{
if ( vstd :: contains ( modsToLoad , mod . first ))
continue ;
if ( mod . second [ "modType" ]. isNull () || mod . second [ "name" ]. isNull ())
continue ;
mods . try_emplace ( mod . first , mod . first , JsonNode (), mod . second );
2024-11-09 20:29:07 +00:00
}
}
const ModDescription & ModsStorage :: getMod ( const TModID & fullID ) const
{
2024-12-23 18:00:35 +00:00
try {
return mods . at ( fullID );
}
catch ( const std :: out_of_range & )
{
// rethrow with better error message
throw std :: out_of_range ( "Failed to find mod " + fullID );
}
2024-11-09 20:29:07 +00:00
}
2024-11-13 17:25:59 +00:00
TModList ModsStorage :: getAllMods () const
{
TModList result ;
for ( const auto & mod : mods )
result . push_back ( mod . first );
return result ;
}
2024-11-09 20:29:07 +00:00
ModManager :: ModManager ()
2024-11-13 17:25:59 +00:00
: ModManager ( JsonNode ())
2024-11-12 20:00:21 +00:00
{
}
2024-11-13 17:25:59 +00:00
ModManager :: ModManager ( const JsonNode & repositoryList )
2024-11-09 20:29:07 +00:00
: modsState ( std :: make_unique < ModsState > ())
, modsPreset ( std :: make_unique < ModsPresetState > ())
{
2024-11-14 15:41:22 +00:00
modsStorage = std :: make_unique < ModsStorage > ( modsState -> getInstalledMods (), repositoryList );
2024-11-12 20:00:21 +00:00
2024-11-11 11:19:14 +00:00
eraseMissingModsFromPreset ();
addNewModsToPreset ();
2024-11-09 20:29:07 +00:00
std :: vector < TModID > desiredModList = modsPreset -> getActiveMods ();
2024-11-24 17:15:41 +00:00
ModDependenciesResolver newResolver ( desiredModList , * modsStorage );
updatePreset ( newResolver );
2024-11-09 20:29:07 +00:00
}
ModManager ::~ ModManager () = default ;
const ModDescription & ModManager :: getModDescription ( const TModID & modID ) const
{
2024-11-13 17:25:59 +00:00
assert ( boost :: to_lower_copy ( modID ) == modID );
2024-11-09 20:29:07 +00:00
return modsStorage -> getMod ( modID );
}
2024-11-21 21:11:02 +00:00
bool ModManager :: isModSettingActive ( const TModID & rootModID , const TModID & modSettingID ) const
{
return modsPreset -> getModSettings ( rootModID ). at ( modSettingID );
}
2024-11-09 20:29:07 +00:00
bool ModManager :: isModActive ( const TModID & modID ) const
{
2024-11-14 20:56:19 +00:00
return vstd :: contains ( getActiveMods (), modID );
2024-11-09 20:29:07 +00:00
}
const TModList & ModManager :: getActiveMods () const
{
2024-11-14 20:56:19 +00:00
return depedencyResolver -> getActiveMods ();
2024-11-09 20:29:07 +00:00
}
2024-11-14 15:41:22 +00:00
uint32_t ModManager :: computeChecksum ( const TModID & modName ) const
{
return modsState -> computeChecksum ( modName );
}
std :: optional < uint32_t > ModManager :: getValidatedChecksum ( const TModID & modName ) const
{
return modsPreset -> getValidatedChecksum ( modName );
}
void ModManager :: setValidatedChecksum ( const TModID & modName , std :: optional < uint32_t > value )
{
modsPreset -> setValidatedChecksum ( modName , value );
}
void ModManager :: saveConfigurationState () const
{
modsPreset -> saveConfigurationState ();
}
2024-11-13 17:25:59 +00:00
TModList ModManager :: getAllMods () const
2024-11-12 20:00:21 +00:00
{
2024-11-13 17:25:59 +00:00
return modsStorage -> getAllMods ();
2024-11-12 20:00:21 +00:00
}
2024-11-14 18:22:02 +00:00
double ModManager :: getInstalledModSizeMegabytes ( const TModID & modName ) const
{
return modsState -> getInstalledModSizeMegabytes ( modName );
}
2024-11-11 11:19:14 +00:00
void ModManager :: eraseMissingModsFromPreset ()
{
2025-01-01 21:01:46 +00:00
const TModList & installedMods = getInstalledValidMods ();
2024-11-11 11:19:14 +00:00
const TModList & rootMods = modsPreset -> getActiveRootMods ();
2024-11-26 13:31:32 +00:00
modsPreset -> removeOldMods ( installedMods );
2024-11-11 11:19:14 +00:00
for ( const auto & rootMod : rootMods )
{
const auto & modSettings = modsPreset -> getModSettings ( rootMod );
for ( const auto & modSetting : modSettings )
{
TModID fullModID = rootMod + '.' + modSetting . first ;
if ( ! vstd :: contains ( installedMods , fullModID ))
{
2024-11-14 20:56:19 +00:00
modsPreset -> eraseModSetting ( rootMod , modSetting . first );
2024-11-11 11:19:14 +00:00
continue ;
}
}
}
}
void ModManager :: addNewModsToPreset ()
{
2025-01-01 21:01:46 +00:00
const TModList & installedMods = getInstalledValidMods ();
2024-11-11 11:19:14 +00:00
for ( const auto & modID : installedMods )
{
size_t dotPos = modID . find ( '.' );
if ( dotPos == std :: string :: npos )
continue ; // only look up submods aka mod settings
std :: string rootMod = modID . substr ( 0 , dotPos );
std :: string settingID = modID . substr ( dotPos + 1 );
const auto & modSettings = modsPreset -> getModSettings ( rootMod );
if ( ! modSettings . count ( settingID ))
2024-11-20 20:44:24 +00:00
modsPreset -> setSettingActive ( rootMod , settingID , ! modsStorage -> getMod ( modID ). keepDisabled ());
2024-11-11 11:19:14 +00:00
}
}
2025-01-01 21:01:46 +00:00
TModList ModManager :: getInstalledValidMods () const
{
TModList installedMods = modsState -> getInstalledMods ();
TModList validMods = modsStorage -> getAllMods ();
TModList result ;
for ( const auto & modID : installedMods )
if ( vstd :: contains ( validMods , modID ))
result . push_back ( modID );
return result ;
}
2024-11-14 20:56:19 +00:00
TModList ModManager :: collectDependenciesRecursive ( const TModID & modID ) const
{
TModList result ;
TModList toTest ;
toTest . push_back ( modID );
while ( ! toTest . empty ())
{
2024-11-15 11:54:43 +00:00
TModID currentModID = toTest . back ();
const auto & currentMod = getModDescription ( currentModID );
2024-11-14 20:56:19 +00:00
toTest . pop_back ();
2024-11-15 11:54:43 +00:00
result . push_back ( currentModID );
2024-11-14 20:56:19 +00:00
2024-11-15 11:54:43 +00:00
if ( ! currentMod . isInstalled ())
2024-11-20 15:24:52 +00:00
throw std :: runtime_error ( "Unable to enable mod " + modID + "! Dependency " + currentModID + " is not installed!" );
2024-11-15 11:54:43 +00:00
for ( const auto & dependency : currentMod . getDependencies ())
2024-11-14 20:56:19 +00:00
{
if ( ! vstd :: contains ( result , dependency ))
toTest . push_back ( dependency );
}
}
return result ;
}
2024-11-19 22:15:05 +00:00
void ModManager :: tryEnableMods ( const TModList & modList )
2024-11-14 20:56:19 +00:00
{
2024-11-19 22:15:05 +00:00
TModList requiredActiveMods ;
TModList additionalActiveMods = getActiveMods ();
2024-11-14 20:56:19 +00:00
2024-11-19 22:15:05 +00:00
for ( const auto & modName : modList )
{
for ( const auto & dependency : collectDependenciesRecursive ( modName ))
2024-11-20 20:44:24 +00:00
{
2024-11-19 22:15:05 +00:00
if ( ! vstd :: contains ( requiredActiveMods , dependency ))
2024-11-20 20:44:24 +00:00
{
2024-11-19 22:15:05 +00:00
requiredActiveMods . push_back ( dependency );
2024-11-20 20:44:24 +00:00
vstd :: erase ( additionalActiveMods , dependency );
}
}
2024-11-19 22:15:05 +00:00
assert ( ! vstd :: contains ( additionalActiveMods , modName ));
assert ( vstd :: contains ( requiredActiveMods , modName )); // FIXME: fails on attempt to enable broken mod / translation to other language
}
2024-11-14 20:56:19 +00:00
ModDependenciesResolver testResolver ( requiredActiveMods , * modsStorage );
testResolver . tryAddMods ( additionalActiveMods , * modsStorage );
2024-11-26 13:31:32 +00:00
TModList additionalActiveSubmods ;
for ( const auto & modName : modList )
{
if ( modName . find ( '.' ) != std :: string :: npos )
continue ;
auto modSettings = modsPreset -> getModSettings ( modName );
for ( const auto & entry : modSettings )
{
TModID fullModID = modName + '.' + entry . first ;
if ( entry . second && ! vstd :: contains ( requiredActiveMods , fullModID ))
additionalActiveSubmods . push_back ( fullModID );
}
}
testResolver . tryAddMods ( additionalActiveSubmods , * modsStorage );
2024-11-19 22:15:05 +00:00
for ( const auto & modName : modList )
if ( ! vstd :: contains ( testResolver . getActiveMods (), modName ))
2024-12-23 18:00:35 +00:00
logGlobal -> error ( "Failed to enable mod '%s'! This may be caused by a recursive dependency!" , modName );
2024-11-14 20:56:19 +00:00
updatePreset ( testResolver );
}
void ModManager :: tryDisableMod ( const TModID & modName )
{
auto desiredActiveMods = getActiveMods ();
assert ( vstd :: contains ( desiredActiveMods , modName ));
vstd :: erase ( desiredActiveMods , modName );
ModDependenciesResolver testResolver ( desiredActiveMods , * modsStorage );
if ( vstd :: contains ( testResolver . getActiveMods (), modName ))
2024-11-20 15:24:52 +00:00
throw std :: runtime_error ( "Failed to disable mod! Mod " + modName + " remains enabled!" );
2024-11-14 20:56:19 +00:00
2024-11-20 20:44:24 +00:00
modsPreset -> setModActive ( modName , false );
2024-11-14 20:56:19 +00:00
updatePreset ( testResolver );
}
void ModManager :: updatePreset ( const ModDependenciesResolver & testResolver )
{
const auto & newActiveMods = testResolver . getActiveMods ();
const auto & newBrokenMods = testResolver . getBrokenMods ();
for ( const auto & modID : newActiveMods )
2024-11-15 11:54:43 +00:00
{
2025-01-01 21:01:46 +00:00
assert ( vstd :: contains ( getInstalledValidMods (), modID ));
2024-11-14 20:56:19 +00:00
modsPreset -> setModActive ( modID , true );
2024-11-15 11:54:43 +00:00
}
2024-11-14 20:56:19 +00:00
for ( const auto & modID : newBrokenMods )
2024-11-20 20:44:24 +00:00
{
const auto & mod = getModDescription ( modID );
2024-12-18 21:33:06 +00:00
if ( mod . getTopParentID (). empty () || vstd :: contains ( newActiveMods , mod . getTopParentID ()))
2024-11-20 20:44:24 +00:00
modsPreset -> setModActive ( modID , false );
}
2024-11-14 20:56:19 +00:00
std :: vector < TModID > desiredModList = modsPreset -> getActiveMods ();
2024-11-24 17:15:41 +00:00
// Try to enable all existing compatibility patches. Ignore on failure
for ( const auto & rootMod : modsPreset -> getActiveRootMods ())
{
for ( const auto & modSetting : modsPreset -> getModSettings ( rootMod ))
{
if ( modSetting . second )
continue ;
TModID fullModID = rootMod + '.' + modSetting . first ;
const auto & modDescription = modsStorage -> getMod ( fullModID );
if ( modDescription . isCompatibility ())
desiredModList . push_back ( fullModID );
}
}
2024-11-14 20:56:19 +00:00
depedencyResolver = std :: make_unique < ModDependenciesResolver > ( desiredModList , * modsStorage );
modsPreset -> saveConfigurationState ();
}
ModDependenciesResolver :: ModDependenciesResolver ( const TModList & modsToResolve , const ModsStorage & storage )
{
tryAddMods ( modsToResolve , storage );
}
const TModList & ModDependenciesResolver :: getActiveMods () const
{
return activeMods ;
}
const TModList & ModDependenciesResolver :: getBrokenMods () const
{
return brokenMods ;
}
void ModDependenciesResolver :: tryAddMods ( TModList modsToResolve , const ModsStorage & storage )
2024-11-09 20:29:07 +00:00
{
// Topological sort algorithm.
boost :: range :: sort ( modsToResolve ); // Sort mods per name
2024-11-14 20:56:19 +00:00
std :: vector < TModID > sortedValidMods ( activeMods . begin (), activeMods . end ()); // Vector keeps order of elements (LIFO)
std :: set < TModID > resolvedModIDs ( activeMods . begin (), activeMods . end ()); // Use a set for validation for performance reason, but set does not keep order of elements
2024-11-09 20:29:07 +00:00
std :: set < TModID > notResolvedModIDs ( modsToResolve . begin (), modsToResolve . end ()); // Use a set for validation for performance reason
// Mod is resolved if it has no dependencies or all its dependencies are already resolved
auto isResolved = [ & ]( const ModDescription & mod ) -> bool
{
2024-11-11 11:19:14 +00:00
if ( mod . isTranslation () && CGeneralTextHandler :: getPreferredLanguage () != mod . getBaseLanguage ())
return false ;
2025-02-25 16:19:15 +00:00
if ( ! mod . isCompatible ())
return false ;
2024-11-09 20:29:07 +00:00
if ( mod . getDependencies (). size () > resolvedModIDs . size ())
return false ;
for ( const TModID & dependency : mod . getDependencies ())
if ( ! vstd :: contains ( resolvedModIDs , dependency ))
return false ;
for ( const TModID & softDependency : mod . getSoftDependencies ())
if ( vstd :: contains ( notResolvedModIDs , softDependency ))
return false ;
for ( const TModID & conflict : mod . getConflicts ())
if ( vstd :: contains ( resolvedModIDs , conflict ))
return false ;
for ( const TModID & reverseConflict : resolvedModIDs )
2024-11-14 20:56:19 +00:00
if ( vstd :: contains ( storage . getMod ( reverseConflict ). getConflicts (), mod . getID ()))
2024-11-09 20:29:07 +00:00
return false ;
return true ;
};
while ( true )
{
std :: set < TModID > resolvedOnCurrentTreeLevel ;
for ( auto it = modsToResolve . begin (); it != modsToResolve . end ();) // One iteration - one level of mods tree
{
2024-11-14 20:56:19 +00:00
if ( isResolved ( storage . getMod ( * it )))
2024-11-09 20:29:07 +00:00
{
resolvedOnCurrentTreeLevel . insert ( * it ); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration
2024-11-20 20:44:24 +00:00
assert ( ! vstd :: contains ( sortedValidMods , * it ));
2024-11-09 20:29:07 +00:00
sortedValidMods . push_back ( * it );
it = modsToResolve . erase ( it );
continue ;
}
it ++ ;
}
if ( ! resolvedOnCurrentTreeLevel . empty ())
{
resolvedModIDs . insert ( resolvedOnCurrentTreeLevel . begin (), resolvedOnCurrentTreeLevel . end ());
for ( const auto & it : resolvedOnCurrentTreeLevel )
notResolvedModIDs . erase ( it );
continue ;
}
// If there are no valid mods on the current mods tree level, no more mod can be resolved, should be ended.
break ;
}
2024-11-12 20:00:21 +00:00
assert ( ! sortedValidMods . empty ());
2024-11-09 20:29:07 +00:00
activeMods = sortedValidMods ;
2024-11-14 20:56:19 +00:00
brokenMods . insert ( brokenMods . end (), modsToResolve . begin (), modsToResolve . end ());
2024-11-09 20:29:07 +00:00
}
2024-12-08 20:56:19 +00:00
void ModManager :: createNewPreset ( const std :: string & presetName )
{
modsPreset -> createNewPreset ( presetName );
modsPreset -> saveConfigurationState ();
}
void ModManager :: deletePreset ( const std :: string & presetName )
{
modsPreset -> deletePreset ( presetName );
modsPreset -> saveConfigurationState ();
}
void ModManager :: activatePreset ( const std :: string & presetName )
{
modsPreset -> activatePreset ( presetName );
modsPreset -> saveConfigurationState ();
}
void ModManager :: renamePreset ( const std :: string & oldPresetName , const std :: string & newPresetName )
{
modsPreset -> renamePreset ( oldPresetName , newPresetName );
modsPreset -> saveConfigurationState ();
}
std :: vector < std :: string > ModManager :: getAllPresets () const
{
return modsPreset -> getAllPresets ();
}
std :: string ModManager :: getActivePreset () const
{
return modsPreset -> getActivePreset ();
}
2024-12-17 14:38:50 +00:00
JsonNode ModManager :: exportCurrentPreset () const
{
return modsPreset -> exportCurrentPreset ();
}
2024-12-17 16:37:55 +00:00
std :: tuple < std :: string , TModList > ModManager :: importPreset ( const JsonNode & data )
2024-12-17 14:38:50 +00:00
{
2024-12-17 16:37:55 +00:00
std :: string presetName = modsPreset -> importPreset ( data );
TModList requiredMods = modsPreset -> getRootMods ( presetName );
2025-01-01 21:01:46 +00:00
TModList installedMods = getInstalledValidMods ();
2024-12-17 16:37:55 +00:00
TModList missingMods ;
for ( const auto & modID : requiredMods )
{
if ( ! vstd :: contains ( installedMods , modID ))
missingMods . push_back ( modID );
}
2024-12-17 14:38:50 +00:00
modsPreset -> saveConfigurationState ();
2024-12-17 16:37:55 +00:00
return { presetName , missingMods };
2024-12-17 14:38:50 +00:00
}
2024-11-09 20:29:07 +00:00
VCMI_LIB_NAMESPACE_END