From 969d2b3eb1f30e257823d9220697e47735be68e3 Mon Sep 17 00:00:00 2001 From: Kahrl Date: Fri, 3 May 2013 23:58:22 +0200 Subject: [PATCH] Optional dependencies and properly handle mod name conflicts again --- doc/lua_api.txt | 11 ++ src/guiConfigureWorld.cpp | 22 ++- src/mods.cpp | 375 +++++++++++++++++++++++++------------- src/mods.h | 76 ++++---- src/scriptapi.cpp | 3 +- src/server.cpp | 6 +- 6 files changed, 324 insertions(+), 169 deletions(-) diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 597f98c2c..02ca7cba3 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -120,6 +120,17 @@ depends.txt: List of mods that have to be loaded before loading this mod. A single line contains a single modname. + Optional dependencies can be defined by appending a question mark + to a single modname. Their meaning is that if the specified mod + is missing, that does not prevent this mod from being loaded. + +optdepends.txt: + An alternative way of specifying optional dependencies. + Like depends.txt, a single line contains a single modname. + + NOTE: This file exists for compatibility purposes only and + support for it will be removed from the engine by the end of 2013. + init.lua: The main Lua script. Running this script should register everything it wants to register. Subsequent execution depends on minetest calling the diff --git a/src/guiConfigureWorld.cpp b/src/guiConfigureWorld.cpp index b2debfbd2..f94ed7d17 100644 --- a/src/guiConfigureWorld.cpp +++ b/src/guiConfigureWorld.cpp @@ -407,14 +407,26 @@ bool GUIConfigureWorld::OnEvent(const SEvent& event) delete[] text; menu->drop(); - ModConfiguration modconf(m_wspec.path); - if(!modconf.isConsistent()) + try { - wchar_t* text = wgettext("Warning: Configuration not consistent. "); + ModConfiguration modconf(m_wspec.path); + if(!modconf.isConsistent()) + { + wchar_t* text = wgettext("Warning: Configuration not consistent. "); + GUIMessageMenu *menu = + new GUIMessageMenu(Environment, Parent, -1, m_menumgr, + text ); + delete[] text; + menu->drop(); + } + } + catch(ModError &err) + { + errorstream<drop(); } diff --git a/src/mods.cpp b/src/mods.cpp index 6a7ab79aa..64c319992 100644 --- a/src/mods.cpp +++ b/src/mods.cpp @@ -24,9 +24,74 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "subgame.h" #include "settings.h" #include "strfnd.h" +#include -std::map getModsInPath(std::string path) +static bool parseDependsLine(std::istream &is, + std::string &dep, std::set &symbols) { + std::getline(is, dep); + dep = trim(dep); + symbols.clear(); + size_t pos = dep.size(); + while(pos > 0 && !string_allowed(dep.substr(pos-1, 1), MODNAME_ALLOWED_CHARS)){ + // last character is a symbol, not part of the modname + symbols.insert(dep[pos-1]); + --pos; + } + dep = trim(dep.substr(0, pos)); + return dep != ""; +} + +void parseModContents(ModSpec &spec) +{ + // NOTE: this function works in mutual recursion with getModsInPath + + spec.depends.clear(); + spec.optdepends.clear(); + spec.is_modpack = false; + spec.modpack_content.clear(); + + // Handle modpacks (defined by containing modpack.txt) + std::ifstream modpack_is((spec.path+DIR_DELIM+"modpack.txt").c_str()); + if(modpack_is.good()){ //a modpack, recursively get the mods in it + modpack_is.close(); // We don't actually need the file + spec.is_modpack = true; + spec.modpack_content = getModsInPath(spec.path, true); + + // modpacks have no dependencies; they are defined and + // tracked separately for each mod in the modpack + } + else{ // not a modpack, parse the dependencies + std::ifstream is((spec.path+DIR_DELIM+"depends.txt").c_str()); + while(is.good()){ + std::string dep; + std::set symbols; + if(parseDependsLine(is, dep, symbols)){ + if(symbols.count('?') != 0){ + spec.optdepends.insert(dep); + } + else{ + spec.depends.insert(dep); + } + } + } + + // FIXME: optdepends.txt is deprecated + // remove this code at some point in the future + std::ifstream is2((spec.path+DIR_DELIM+"optdepends.txt").c_str()); + while(is2.good()){ + std::string dep; + std::set symbols; + if(parseDependsLine(is2, dep, symbols)) + spec.optdepends.insert(dep); + } + } +} + +std::map getModsInPath(std::string path, bool part_of_modpack) +{ + // NOTE: this function works in mutual recursion with parseModContents + std::map result; std::vector dirlist = fs::GetDirListing(path); for(u32 j=0; j getModsInPath(std::string path) continue; std::string modpath = path + DIR_DELIM + modname; - // Handle modpacks (defined by containing modpack.txt) - std::ifstream modpack_is((modpath+DIR_DELIM+"modpack.txt").c_str(), - std::ios_base::binary); - if(modpack_is.good()) //a modpack, recursively get the mods in it - { - modpack_is.close(); // We don't actually need the file - ModSpec spec(modname,modpath); - spec.modpack_content = getModsInPath(modpath); - spec.is_modpack = true; - result.insert(std::make_pair(modname,spec)); - } - else // not a modpack, add the modspec - { - std::set depends; - std::ifstream is((modpath+DIR_DELIM+"depends.txt").c_str(), - std::ios_base::binary); - while(is.good()) - { - std::string dep; - std::getline(is, dep); - dep = trim(dep); - if(dep != "") - depends.insert(dep); - } - - ModSpec spec(modname, modpath, depends); - result.insert(std::make_pair(modname,spec)); - } + ModSpec spec(modname, modpath); + spec.part_of_modpack = part_of_modpack; + parseModContents(spec); + result.insert(std::make_pair(modname, spec)); } return result; } +ModSpec findCommonMod(const std::string &modname) +{ + // Try to find in {$user,$share}/games/common/$modname + std::vector find_paths; + find_paths.push_back(porting::path_user + DIR_DELIM + "games" + + DIR_DELIM + "common" + DIR_DELIM + "mods" + DIR_DELIM + modname); + find_paths.push_back(porting::path_share + DIR_DELIM + "games" + + DIR_DELIM + "common" + DIR_DELIM + "mods" + DIR_DELIM + modname); + for(u32 i=0; i flattenModTree(std::map mods) { std::map result; @@ -109,109 +170,18 @@ std::vector flattenMods(std::map mods) } else //not a modpack { - // infostream << "inserting mod " << mod.name << std::endl; result.push_back(mod); } } return result; } -std::vector filterMods(std::vector mods, - std::set exclude_mod_names) -{ - std::vector result; - for(std::vector::iterator it = mods.begin(); - it != mods.end(); ++it) - { - ModSpec& mod = *it; - if(exclude_mod_names.count(mod.name) == 0) - result.push_back(mod); - } - return result; -} - -void ModConfiguration::addModsInPathFiltered(std::string path, std::set exclude_mods) -{ - addMods(filterMods(flattenMods(getModsInPath(path)),exclude_mods)); -} - - -void ModConfiguration::addMods(std::vector new_mods) -{ - // Step 1: remove mods in sorted_mods from unmet dependencies - // of new_mods. new mods without unmet dependencies are - // temporarily stored in satisfied_mods - std::vector satisfied_mods; - for(std::vector::iterator it = m_sorted_mods.begin(); - it != m_sorted_mods.end(); ++it) - { - ModSpec mod = *it; - for(std::vector::iterator it_new = new_mods.begin(); - it_new != new_mods.end(); ++it_new) - { - ModSpec& mod_new = *it_new; - //infostream << "erasing dependency " << mod.name << " from " << mod_new.name << std::endl; - mod_new.unsatisfied_depends.erase(mod.name); - } - } - - // split new mods into satisfied and unsatisfied - for(std::vector::iterator it = new_mods.begin(); - it != new_mods.end(); ++it) - { - ModSpec mod_new = *it; - if(mod_new.unsatisfied_depends.empty()) - satisfied_mods.push_back(mod_new); - else - m_unsatisfied_mods.push_back(mod_new); - } - - // Step 2: mods without unmet dependencies can be appended to - // the sorted list. - while(!satisfied_mods.empty()) - { - ModSpec mod = satisfied_mods.back(); - m_sorted_mods.push_back(mod); - satisfied_mods.pop_back(); - for(std::list::iterator it = m_unsatisfied_mods.begin(); - it != m_unsatisfied_mods.end(); ) - { - ModSpec& mod2 = *it; - mod2.unsatisfied_depends.erase(mod.name); - if(mod2.unsatisfied_depends.empty()) - { - satisfied_mods.push_back(mod2); - it = m_unsatisfied_mods.erase(it); - } - else - ++it; - } - } -} - -// If failed, returned modspec has name=="" -static ModSpec findCommonMod(const std::string &modname) -{ - // Try to find in {$user,$share}/games/common/$modname - std::vector find_paths; - find_paths.push_back(porting::path_user + DIR_DELIM + "games" + - DIR_DELIM + "common" + DIR_DELIM + "mods" + DIR_DELIM + modname); - find_paths.push_back(porting::path_share + DIR_DELIM + "games" + - DIR_DELIM + "common" + DIR_DELIM + "mods" + DIR_DELIM + modname); - for(u32 i=0; i common_mods; std::vector inexistent_common_mods; Settings gameconf; if(getGameConfig(gamespec.path, gameconf)){ @@ -225,7 +195,7 @@ ModConfiguration::ModConfiguration(std::string worldpath) if(spec.name.empty()) inexistent_common_mods.push_back(modname); else - m_sorted_mods.push_back(spec); + common_mods.insert(std::make_pair(modname, spec)); } } } @@ -238,10 +208,11 @@ ModConfiguration::ModConfiguration(std::string worldpath) s += " could not be found."; throw ModError(s); } + addMods(flattenMods(common_mods)); - // Add all world mods and all game mods - addModsInPath(worldpath + DIR_DELIM + "worldmods"); + // Add all game mods and all world mods addModsInPath(gamespec.gamemods_path); + addModsInPath(worldpath + DIR_DELIM + "worldmods"); // check world.mt file for mods explicitely declared to be // loaded or not by a load_mod_ = ... line. @@ -264,7 +235,155 @@ ModConfiguration::ModConfiguration(std::string worldpath) } } - for(std::set::const_iterator i = gamespec.addon_mods_paths.begin(); - i != gamespec.addon_mods_paths.end(); ++i) - addModsInPathFiltered((*i),exclude_mod_names); + // Collect all mods in gamespec.addon_mods_paths, + // excluding those in the set exclude_mod_names + std::vector addon_mods; + for(std::set::const_iterator it_path = gamespec.addon_mods_paths.begin(); + it_path != gamespec.addon_mods_paths.end(); ++it_path) + { + std::vector addon_mods_in_path = flattenMods(getModsInPath(*it_path)); + for(std::vector::iterator it = addon_mods_in_path.begin(); + it != addon_mods_in_path.end(); ++it) + { + ModSpec& mod = *it; + if(exclude_mod_names.count(mod.name) == 0) + addon_mods.push_back(mod); + } + } + + addMods(addon_mods); + + // report on name conflicts + if(!m_name_conflicts.empty()){ + std::string s = "Unresolved name conflicts for mods "; + for(std::set::const_iterator it = m_name_conflicts.begin(); + it != m_name_conflicts.end(); ++it) + { + if(it != m_name_conflicts.begin()) s += ", "; + s += std::string("\"") + (*it) + "\""; + } + s += "."; + throw ModError(s); + } + + // get the mods in order + resolveDependencies(); +} + +void ModConfiguration::addModsInPath(std::string path) +{ + addMods(flattenMods(getModsInPath(path))); +} + +void ModConfiguration::addMods(std::vector new_mods) +{ + // Maintain a map of all existing m_unsatisfied_mods. + // Keys are mod names and values are indices into m_unsatisfied_mods. + std::map existing_mods; + for(u32 i = 0; i < m_unsatisfied_mods.size(); ++i){ + existing_mods[m_unsatisfied_mods[i].name] = i; + } + + // Add new mods + for(int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack){ + // First iteration: + // Add all the mods that come from modpacks + // Second iteration: + // Add all the mods that didn't come from modpacks + + std::set seen_this_iteration; + + for(std::vector::const_iterator it = new_mods.begin(); + it != new_mods.end(); ++it){ + const ModSpec &mod = *it; + if(mod.part_of_modpack != want_from_modpack) + continue; + if(existing_mods.count(mod.name) == 0){ + // GOOD CASE: completely new mod. + m_unsatisfied_mods.push_back(mod); + existing_mods[mod.name] = m_unsatisfied_mods.size() - 1; + } + else if(seen_this_iteration.count(mod.name) == 0){ + // BAD CASE: name conflict in different levels. + u32 oldindex = existing_mods[mod.name]; + const ModSpec &oldmod = m_unsatisfied_mods[oldindex]; + errorstream<<"WARNING: Mod name conflict detected: \"" + < modnames; + for(std::vector::iterator it = m_unsatisfied_mods.begin(); + it != m_unsatisfied_mods.end(); ++it){ + modnames.insert((*it).name); + } + + // Step 2: get dependencies (including optional dependencies) + // of each mod, split mods into satisfied and unsatisfied + std::list satisfied; + std::list unsatisfied; + for(std::vector::iterator it = m_unsatisfied_mods.begin(); + it != m_unsatisfied_mods.end(); ++it){ + ModSpec mod = *it; + mod.unsatisfied_depends = mod.depends; + // check which optional dependencies actually exist + for(std::set::iterator it_optdep = mod.optdepends.begin(); + it_optdep != mod.optdepends.end(); ++it_optdep){ + std::string optdep = *it_optdep; + if(modnames.count(optdep) != 0) + mod.unsatisfied_depends.insert(optdep); + } + // if a mod has no depends it is initially satisfied + if(mod.unsatisfied_depends.empty()) + satisfied.push_back(mod); + else + unsatisfied.push_back(mod); + } + + // Step 3: mods without unmet dependencies can be appended to + // the sorted list. + while(!satisfied.empty()){ + ModSpec mod = satisfied.back(); + m_sorted_mods.push_back(mod); + satisfied.pop_back(); + for(std::list::iterator it = unsatisfied.begin(); + it != unsatisfied.end(); ){ + ModSpec& mod2 = *it; + mod2.unsatisfied_depends.erase(mod.name); + if(mod2.unsatisfied_depends.empty()){ + satisfied.push_back(mod2); + it = unsatisfied.erase(it); + } + else{ + ++it; + } + } + } + + // Step 4: write back list of unsatisfied mods + m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end()); } diff --git a/src/mods.h b/src/mods.h index 32bcfb471..a8100fcfd 100644 --- a/src/mods.h +++ b/src/mods.h @@ -30,6 +30,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include +#define MODNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_" + class ModError : public std::exception { public: @@ -53,23 +55,32 @@ struct ModSpec std::string path; //if normal mod: std::set depends; + std::set optdepends; std::set unsatisfied_depends; + bool part_of_modpack; bool is_modpack; // if modpack: std::map modpack_content; - ModSpec(const std::string name_="", const std::string path_="", - const std::set depends_=std::set()): + ModSpec(const std::string name_="", const std::string path_=""): name(name_), path(path_), - depends(depends_), - unsatisfied_depends(depends_), - is_modpack(false), - modpack_content() + depends(), + optdepends(), + unsatisfied_depends(), + part_of_modpack(false), + is_modpack(false), + modpack_content() {} }; -std::map getModsInPath(std::string path); +// Retrieves depends, optdepends, is_modpack and modpack_content +void parseModContents(ModSpec &mod); + +std::map getModsInPath(std::string path, bool part_of_modpack = false); + +// If failed, returned modspec has name=="" +ModSpec findCommonMod(const std::string &modname); // expands modpack contents, but does not replace them. std::map flattenModTree(std::map mods); @@ -77,10 +88,6 @@ std::map flattenModTree(std::map mod // replaces modpack Modspecs with their content std::vector flattenMods(std::map mods); -// removes Mods mentioned in exclude_mod_names -std::vector filterMods(std::vector mods, - std::set exclude_mod_names); - // a ModConfiguration is a subset of installed mods, expected to have // all dependencies fullfilled, so it can be used as a list of mods to // load when the game starts. @@ -89,26 +96,13 @@ class ModConfiguration public: ModConfiguration(): m_unsatisfied_mods(), - m_sorted_mods() + m_sorted_mods(), + m_name_conflicts() {} ModConfiguration(std::string worldpath); - // adds all mods in the given path. used for games, modpacks - // and world-specific mods (worldmods-folders) - void addModsInPath(std::string path) - { - addMods(flattenMods(getModsInPath(path))); - } - - // adds all mods in the given path whose name does not appear - // in the exclude_mods set. - void addModsInPathFiltered(std::string path, std::set exclude_mods); - - // adds all mods in the set. - void addMods(std::vector mods); - // checks if all dependencies are fullfilled. bool isConsistent() { @@ -120,17 +114,27 @@ public: return m_sorted_mods; } - std::list getUnsatisfiedMods() + std::vector getUnsatisfiedMods() { return m_unsatisfied_mods; } private: + // adds all mods in the given path. used for games, modpacks + // and world-specific mods (worldmods-folders) + void addModsInPath(std::string path); - // mods with unmet dependencies. This is a list and not a - // vector because we want easy removal of elements at every - // position. - std::list m_unsatisfied_mods; + // adds all mods in the set. + void addMods(std::vector new_mods); + + // move mods from m_unsatisfied_mods to m_sorted_mods + // in an order that satisfies dependencies + void resolveDependencies(); + + // mods with unmet dependencies. Before dependencies are resolved, + // this is where all mods are stored. Afterwards this contains + // only the ones with really unsatisfied dependencies. + std::vector m_unsatisfied_mods; // list of mods sorted such that they can be loaded in the // given order with all dependencies being fullfilled. I.e., @@ -138,6 +142,16 @@ private: // appear earlier in the vector. std::vector m_sorted_mods; + // set of mod names for which an unresolved name conflict + // exists. A name conflict happens when two or more mods + // at the same level have the same name but different paths. + // Levels (mods in higher levels override mods in lower levels): + // 1. common mod in modpack; 2. common mod; + // 3. game mod in modpack; 4. game mod; + // 5. world mod in modpack; 6. world mod; + // 7. addon mod in modpack; 8. addon mod. + std::set m_name_conflicts; + }; #endif diff --git a/src/scriptapi.cpp b/src/scriptapi.cpp index f0fe1950e..26759a059 100644 --- a/src/scriptapi.cpp +++ b/src/scriptapi.cpp @@ -76,8 +76,7 @@ bool scriptapi_loadmod(lua_State *L, const std::string &scriptpath, { ModNameStorer modnamestorer(L, modname); - if(!string_allowed(modname, "abcdefghijklmnopqrstuvwxyz" - "0123456789_")){ + if(!string_allowed(modname, MODNAME_ALLOWED_CHARS)){ errorstream<<"Error loading mod \""< unsatisfied_mods = modconf.getUnsatisfiedMods(); + std::vector unsatisfied_mods = modconf.getUnsatisfiedMods(); // complain about mods with unsatisfied dependencies if(!modconf.isConsistent()) { - for(std::list::iterator it = unsatisfied_mods.begin(); + for(std::vector::iterator it = unsatisfied_mods.begin(); it != unsatisfied_mods.end(); ++it) { ModSpec mod = *it; @@ -745,7 +745,7 @@ Server::Server( for(std::vector::iterator it = m_mods.begin(); it != m_mods.end(); ++it) load_mod_names.erase((*it).name); - for(std::list::iterator it = unsatisfied_mods.begin(); + for(std::vector::iterator it = unsatisfied_mods.begin(); it != unsatisfied_mods.end(); ++it) load_mod_names.erase((*it).name); if(!load_mod_names.empty())