Browse Source

Added dependancy resolution code and tests for the new code. It takes a list of loaded mod ConfigStores. Based on each mod's id, depends, and opt depends the code creates a new lists that will load mods in an order that any dependant mod will show up before it's dependancy. It has only been lightly tested and could use better error handeling especially for mods that cannot be loaded due to a missing hard dependancy. However its' basic functionality is good for now.

master
DomtronVox 7 months ago
parent
commit
205536d595
5 changed files with 364 additions and 9 deletions
  1. +193
    -0
      src/Dependency/Dependency.h
  2. +22
    -8
      src/ModdingFrameworkCore.h
  3. +38
    -0
      src/defaults.h
  4. +6
    -1
      tests/CMakeLists.txt
  5. +105
    -0
      tests/test_buildLoadOrder.cpp

+ 193
- 0
src/Dependency/Dependency.h View File

@@ -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 <vector>

//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<ConfigStore> >::iterator& mod_it,
std::vector< std::shared_ptr<ConfigStore> >& sorted,
std::vector< std::shared_ptr<ConfigStore> >& 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<ConfigStore> >::iterator& mod_it,
std::vector< std::shared_ptr<ConfigStore> >& sorted,
std::vector< std::shared_ptr<ConfigStore> >& 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<ConfigStore> >
sortForLoadOrder ( std::vector< std::shared_ptr<ConfigStore> >& unsorted_list ){
std::vector< std::shared_ptr<ConfigStore> > sorted;

for (auto mod_it = unsorted_list.begin(); mod_it != unsorted_list.end(); mod_it++) {
sortMod(mod_it, sorted, unsorted_list);
}
return sorted;
}

#endif

+ 22
- 8
src/ModdingFrameworkCore.h View File

@@ -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 <memory>
#include <unordered_map>
#include <vector>
@@ -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<ConfigStore> default_settings;

//Mod configuration data
std::vector< std::shared_ptr<ConfigStore> > 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<ConfigStore> _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<ConfigLoaderINI>() );
}

//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()
};



+ 38
- 0
src/defaults.h View File

@@ -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<ConfigStore> buildDefaultSettings() {
std::shared_ptr<ConfigStore> default_settings = std::make_shared<ConfigStore>();

//###############
//##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

+ 6
- 1
tests/CMakeLists.txt View File

@@ -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 )

+ 105
- 0
tests/test_buildLoadOrder.cpp View File

@@ -0,0 +1,105 @@


#include "TestBase.h"
#include "Configuration/ConfigStore.h"
#include "Dependency/Dependency.h"

#include <iostream>

//helper function to create ConfigStores that mimic id and dependancy info
std::shared_ptr<ConfigStore> makeModConfig(std::string id, std::string depends = "", std::string optdepends = "") {
std::shared_ptr<ConfigStore> mod_config = std::make_shared<ConfigStore>();

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<ConfigStore> > 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<ConfigStore> > 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<ConfigStore> > 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;
}

Loading…
Cancel
Save