diff --git a/src/Dependency/Dependency.h b/src/Dependency/Dependency.h new file mode 100755 index 0000000..cc2dabf --- /dev/null +++ b/src/Dependency/Dependency.h @@ -0,0 +1,193 @@ +/* This is a difficult problem so here is some extra documentation on it. + +Problem: Need to sort a flat list of custom objects via 3 values: object ID, hard dependencies, and optional dependencies. + +* Items with no dependencies can just be added anywhere. +* Items with hard dependencies have to be added after all their dependencies or not at all. + * Should have a warning if mod is totally removed. +* Items with optional dependencies should be added after their dependencies, but if they are missing can just be added anywhere. +* Items can have multiple hard and/or optional dependencies. +* Circular dependencies are not allowed and cause errors or removal of whole circle and any dependents with a warning. + * Preferable to just remove one mod to fix the Circular dependancy. + +Leave it open for expansion as later additional hooks may be added, for example force a specific object to the beginning or end of the list while still following the other restraints. Another expansion would be version checking to make sure the right version of a mod is loaded. + +*/ + +/* +Current issues with this algorithm: +* Need way to prevent infinite loops in the recursive function. +* I think there is an issue removing an item from a vector your iterating through. (I.e. unsorted list) +* Better error handling. +* The breaks in 2nd level for loops should also break the outer loop. Since if you find it in the one list it won't be in the other. (Hmm, At least it should not. Counteractive to have an opt depend that is also a hard depend) +* I don't think the resulting list would be thread-able. Example, we have 40 mods and would like to thread initialization. We cannot just divide the list since we don't know what mod in the second list may depend on mods in the 1st list. + +*/ + +#ifndef DEPENDENCY_H +#define DEPENDENCY_H + +#include + +//utility function to check that all depends are matched. +// Expects string list of hard and optional depends names and the unsorted list from which to remove the mod. +// @mod: Mod config currently getting checked. +// @sorted: Vector of mod configs that have been sorted. Provided so we can add the mod if it succeeds. +// @unsorted: Vector of mod configs that have not been sorted. Provided so we can remove the checked mod from it. +// @depends: This mods depend vector so we don't need to re-split it. +// @optdepends: This mods optional depend vector so we don't need to re-split it. +// @return: Boolean on weather we succeded in sorting the mod (true) or not (false). +bool testAndHandleDependsRequirment(std::vector< std::shared_ptr >::iterator& mod_it, + std::vector< std::shared_ptr >& sorted, + std::vector< std::shared_ptr >& unsorted, + std::vector< std::string >& depends, std::vector< std::string >& optdepends) { + + //if both dependency types are met we are good. + if (depends.size() == 0 && optdepends.size() == 0) { + sorted.push_back(*mod_it); + mod_it = unsorted.erase(mod_it); + mod_it--; + + return true; + } + + return false; +} + + + +//recursive function to sort a mod making sure all dependencies are added first +// @mod: mod config currently being sorted. +// @sorted: Reference to vector of already sorted mod configs. +// @unsorted: Reference to vector of mod configs that need sorting. +// @return: Weather the current mod was successful. +//TODO: Need to prevent infinite loop on circular deps +bool sortMod(std::vector< std::shared_ptr >::iterator& mod_it, + std::vector< std::shared_ptr >& sorted, + std::vector< std::shared_ptr >& unsorted) { + + //these are configurable values + //TODO pull from a settings ConfigStore instead + std::string id_key = "id", + depends_key = "depends", + optdepends_key = "opt depends", + list_separator = ","; + + //pull bits needed from config + std::string id; + std::vector< std::string > depends, optdepends; + + id = (*mod_it)->getStr( id_key ); + + toVector( (*mod_it)->getStr( depends_key ), depends, list_separator.c_str()[0] ); + toVector( (*mod_it)->getStr( optdepends_key ), optdepends, list_separator.c_str()[0] ); + + //make sure we actually have any depends to try and match + //Return from here is BEST CASE + if (testAndHandleDependsRequirment(mod_it, sorted, unsorted, depends, optdepends)) { + return true; + } + + //look through sorted to see if all deps have been added + //Return from here is GOOD CASE + for (auto sorted_mod = sorted.begin(); sorted_mod != sorted.end(); sorted_mod++) { + + for (auto need = depends.begin(); need != depends.end(); need++) { + //if dep is found remove from list and stop searching + if ( *need == (*sorted_mod)->getStr( id_key ) ) { + depends.erase( need ); + break; + } + } + + for (auto need = optdepends.begin(); need != optdepends.end(); need++) { + //if dep is found remove from list and stop searching + if ( *need == (*sorted_mod)->getStr( id_key ) ) { + optdepends.erase( need ); + break; + } + } + + //continue until all deps satisfied or end of sorted list + if (testAndHandleDependsRequirment(mod_it, sorted, unsorted, depends, optdepends)) { + return true; + } + } + + //look through all unsorted for deps + //Return from here is OK CASE + for (auto unsorted_mod = unsorted.begin(); unsorted_mod != unsorted.end(); unsorted_mod++) { + + //when dep is found in unsorted list run sortMod on it starting a new layer of recursion + //if returned with success remove dep from list since it has been added + //if returned with fail and hard dep return fail state then dependency cannot be resolved + // and we cannot add this mod + + for (auto need = depends.begin(); need != depends.end(); need++) { + //if dep is found in unsorted list attempt to sort it now + if ( *need == (*unsorted_mod)->getStr( id_key ) ) { + //This is the recursive bit + if( sortMod( unsorted_mod, sorted, unsorted ) ) { + depends.erase( need ); + break; + } else { + mod_it = unsorted.erase(mod_it); + mod_it--; + return false; //This mod cannot be sorted + } + } + } + for (auto need = optdepends.begin(); need != optdepends.end(); need++) { + //if dep is found in unsorted list attempt to sort it now + if ( *need == (*unsorted_mod)->getStr( id_key ) ) { + //This is the recursive bit + if( sortMod( unsorted_mod, sorted, unsorted ) ) { + need = optdepends.erase( need ); + need--; + break; + } //optional + } + } + + + //continue until all deps satisfied or end of unsorted list + if (testAndHandleDependsRequirment(mod_it, sorted, unsorted, depends, optdepends)) { + return true; + } + } + + //ignore any remaining optional dependencies + //Return from here is WORST CASE + optdepends.clear(); + + //if any hard deps remain will error out and return a fail state. + if ( ! testAndHandleDependsRequirment(mod_it, sorted, unsorted, depends, optdepends) ) { + //TODO handle error + //cannot resolve this mod's deps so we remove it permanently + mod_it = unsorted.erase(mod_it); + mod_it--; + return false; + + //Otherwise the mod has been sorted so we return success state. + } else { + return true; + } +} + + + +//Handles kicking off sortMod functions on everything unsorted that isn't handled by recursion. +// @unsorted_list: Vector of Mod Configs that need to be ordered based on dependencies. +// @return: Vector of Mod Configs in the order they should be loaded. +std::vector< std::shared_ptr > + sortForLoadOrder ( std::vector< std::shared_ptr >& unsorted_list ){ + std::vector< std::shared_ptr > sorted; + + for (auto mod_it = unsorted_list.begin(); mod_it != unsorted_list.end(); mod_it++) { + sortMod(mod_it, sorted, unsorted_list); + } + + return sorted; +} + +#endif diff --git a/src/ModdingFrameworkCore.h b/src/ModdingFrameworkCore.h index 53b9169..9e73b9d 100755 --- a/src/ModdingFrameworkCore.h +++ b/src/ModdingFrameworkCore.h @@ -3,13 +3,15 @@ #ifndef ModdingFrameworkCore_H #define ModdingFrameworkCore_H - +#include "defaults.h" #include "Configuration/ConfigLoaderBase.h" #include "Configuration/ConfigLoaderINI.h" - +#include "Configuration/ConfigStore.h" #include "Configuration/ConfigManager.h" +#include "Dependency/Dependency.h" + #include #include #include @@ -18,7 +20,7 @@ class ModdingFrameworkCore { private: //path to mod directory - std::string mods_path = ""; + std::string mods_path{""}; //class in charge of managing config file loading ConfigManager config_manager; @@ -26,19 +28,31 @@ class ModdingFrameworkCore { public: - //Mod configuration data for loading + //settings used by the library + std::shared_ptr default_settings; + + //Mod configuration data std::vector< std::shared_ptr > mod_config; - ModdingFrameworkCore(std::string _mods_path, bool register_default_loaders = true) - : mods_path(_mods_path), config_manager(), mod_config() { - + ModdingFrameworkCore(std::string _mods_path, bool register_default_loaders = true, + std::shared_ptr _default_settings = buildDefaultSettings()) + : mods_path(_mods_path), config_manager(), default_settings(_default_settings), mod_config() { + //allows user to not load default config loaders if they so desire if (register_default_loaders) { registerConfigLoader( std::make_shared() ); } - //TODO needs to go somewhere else besides the constructor method to let user register more loaders and the like + //TODO setup needs to go somewhere else besides the constructor method to let user register more loaders and the like + + //1. Load mod configuration data mod_config = config_manager.loadModDirConfigs(mods_path); + + //2. Construct loading order based on Dependancies + sortForLoadOrder(mod_config); + + //3. Follow load order and pass each mod to the appropriate mod implementation class for initialization + //modtype_manager->loadMods() }; diff --git a/src/defaults.h b/src/defaults.h new file mode 100755 index 0000000..dc539e7 --- /dev/null +++ b/src/defaults.h @@ -0,0 +1,38 @@ + + +#ifndef DEFAULTS_H +#define DEFAULTS_H + +#include "Configuration/ConfigStore.h" + +//Generate a settings ConfigStore that can be changed by the user to configure some aspects of the library. +std::shared_ptr buildDefaultSettings() { + std::shared_ptr default_settings = std::make_shared(); + + //############### + //##Configuration Loading + + //name of configuration file that declares details about the mod. + //Note: allows any supported extention + default_settings->set("mod config name", "config"); + + //############ + //##Dependancy handling + + //Variable to use as the id + default_settings->set("mod id key", "id"); + + //variable to check for required dependancies + default_settings->set("required dependancies key", "depends"); + + //variable to check for optional dependancies + default_settings->set("optional dependancies key", "opt depends"); + + //must be a single character + default_settings->set("list separator", ","); + + return default_settings; +} + + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b917735..029ffe1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,7 +11,12 @@ ADD_EXECUTABLE( test_ConfigStore test_ConfigStore.cpp ) target_link_libraries( test_ConfigStore ModdingFramework) ADD_TEST( ConfigStore test_ConfigStore ) -#add ConfigLoader INI +#add ConfigLoader INI test ADD_EXECUTABLE( test_ConfigLoaderINI test_ConfigLoaderINI.cpp ) target_link_libraries( test_ConfigLoaderINI ModdingFramework) ADD_TEST( ConfigLoaderINI test_ConfigLoaderINI ) + +#add buildLoadOrder test +ADD_EXECUTABLE( test_buildLoadOrder test_buildLoadOrder.cpp ) +target_link_libraries( test_buildLoadOrder ModdingFramework) +ADD_TEST( buildLoadOrder test_buildLoadOrder ) diff --git a/tests/test_buildLoadOrder.cpp b/tests/test_buildLoadOrder.cpp new file mode 100755 index 0000000..3cc39e2 --- /dev/null +++ b/tests/test_buildLoadOrder.cpp @@ -0,0 +1,105 @@ + + +#include "TestBase.h" +#include "Configuration/ConfigStore.h" +#include "Dependency/Dependency.h" + +#include + +//helper function to create ConfigStores that mimic id and dependancy info +std::shared_ptr makeModConfig(std::string id, std::string depends = "", std::string optdepends = "") { + std::shared_ptr mod_config = std::make_shared(); + + mod_config->set("id", id); + mod_config->set("depends", depends); + mod_config->set("opt depends", optdepends); + + return mod_config; +} + +//create a string showing the mod order. +std::string makeOrderNameString( std::vector< std::shared_ptr > ordered_list) { + + auto it = ordered_list.begin(); + std::string return_str = (*it)->getStr("id"); + it++; + + for (; it < ordered_list.end(); it++){ + return_str += ", " + (*it)->getStr("id"); + } + + return return_str; +} + +int main (int argc, char* argv[]) { + + //TEST_BEGIN( "test1" ); + //ASSERT_EQUAL( 0, 0 ); + //ASSERT_THROW( 0 == 0 ); + //EXPECT_EXCEPTION( fakeFileLoad("does_not_exist.txt"), FileNotFound ); + //TEST_END(); + + //build out test data + TEST_BEGIN( "Testing buildLoadOrder complex" ); + std::vector< std::shared_ptr > mod_configs; + + //no depends, no dependants, placed first + mod_configs.push_back(makeModConfig( "111" )); + + //hard depend that is added later + mod_configs.push_back(makeModConfig( "44f", "15f" )); + mod_configs.push_back(makeModConfig( "15f" )); + + //chain dependancy in order + mod_configs.push_back(makeModConfig( "1a" )); + mod_configs.push_back(makeModConfig( "1b", "1a" )); + mod_configs.push_back(makeModConfig( "1c", "1b" )); + + //chain dependancy out of order + mod_configs.push_back(makeModConfig( "2c", "2b" )); + mod_configs.push_back(makeModConfig( "2b", "2a" )); + mod_configs.push_back(makeModConfig( "2a" )); + + + //no depends, no dependants, placed middleish + mod_configs.push_back(makeModConfig( "222" )); + + //optional depend that is added later + mod_configs.push_back(makeModConfig( "hgw", "", "hhh" )); + mod_configs.push_back(makeModConfig( "hhh" )); + + //optional depend added after + mod_configs.push_back(makeModConfig( "aaa" )); + mod_configs.push_back(makeModConfig( "bbb", "", "aaa" )); + + //optional depend and required depend + mod_configs.push_back(makeModConfig( "ccc", "aaa", "bbb" )); + + //missing optional depend + mod_configs.push_back(makeModConfig( "dup", "", "not_here" )); + + //optional depend on something with a missing optional depend + mod_configs.push_back(makeModConfig( "lup", "", "dup" )); + + //depend on mod with missing optional depend + mod_configs.push_back(makeModConfig( "rup", "dup" )); + + //missing required depend + mod_configs.push_back(makeModConfig( "ohno", "not_here" )); + + //optional depend on mod with missing depend + mod_configs.push_back(makeModConfig( "ohyes", "", "ohno" )); + + //no depends, no dependants, added lastish + mod_configs.push_back(makeModConfig( "333" )); + + //sort it + std::vector< std::shared_ptr > sorted_mods = sortForLoadOrder(mod_configs); + std::string modOrder = makeOrderNameString(sorted_mods); + + //expected output order based on test data: + ASSERT_THROW( modOrder == "111, 15f, 44f, 1a, 1b, 1c, 2a, 2b, 2c, 222, hhh, hgw, aaa, bbb, ccc, dup, lup, rup, ohyes, 333" ); + TEST_END(); + + return 0; +}