X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=src%2Fmods.cpp;h=dae49233978c29fa01fef5378486a92af6b6f59e;hb=fd9f195fcc2e4e592dbad3290876486eb08318b2;hp=6a7ab79aade2870804b74119d46f5bb381d3b64c;hpb=306d1ab866a3ce820e95f4faf805684cd4122ae4;p=minetest.git diff --git a/src/mods.cpp b/src/mods.cpp index 6a7ab79aa..dae492339 100644 --- a/src/mods.cpp +++ b/src/mods.cpp @@ -17,78 +17,94 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include +#include +#include #include "mods.h" #include "filesys.h" -#include "strfnd.h" #include "log.h" #include "subgame.h" #include "settings.h" -#include "strfnd.h" +#include "porting.h" +#include "convert_json.h" -std::map getModsInPath(std::string path) +static bool parseDependsLine(std::istream &is, + std::string &dep, std::set &symbols) { - std::map result; - std::vector dirlist = fs::GetDirListing(path); - for(u32 j=0; j 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.empty(); +} - // 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); - } +void parseModContents(ModSpec &spec) +{ + // NOTE: this function works in mutual recursion with getModsInPath + Settings info; + info.readConfigFile((spec.path+DIR_DELIM+"mod.conf").c_str()); + + if (info.exists("name")) + spec.name = info.get("name"); - ModSpec spec(modname, modpath, depends); - result.insert(std::make_pair(modname,spec)); + 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); + } + } } } - return result; } -std::map flattenModTree(std::map mods) +std::map getModsInPath(std::string path, bool part_of_modpack) { + // NOTE: this function works in mutual recursion with parseModContents + std::map result; - for(std::map::iterator it = mods.begin(); - it != mods.end(); ++it) - { - ModSpec mod = (*it).second; - if(mod.is_modpack) - { - std::map content = - flattenModTree(mod.modpack_content); - result.insert(content.begin(),content.end()); - result.insert(std::make_pair(mod.name,mod)); - } - else //not a modpack - { - result.insert(std::make_pair(mod.name,mod)); - } + std::vector dirlist = fs::GetDirListing(path); + for (const fs::DirListNode &dln : dirlist) { + if(!dln.dir) + continue; + const std::string &modname = dln.name; + // Ignore all directories beginning with a ".", especially + // VCS directories like ".git" or ".svn" + if (modname[0] == '.') + continue; + std::string modpath = path + DIR_DELIM + modname; + + ModSpec spec(modname, modpath); + spec.part_of_modpack = part_of_modpack; + parseModContents(spec); + result.insert(std::make_pair(modname, spec)); } return result; } @@ -96,175 +112,310 @@ std::map flattenModTree(std::map mod std::vector flattenMods(std::map mods) { std::vector result; - for(std::map::iterator it = mods.begin(); - it != mods.end(); ++it) - { - ModSpec mod = (*it).second; - if(mod.is_modpack) - { + for (const auto &it : mods) { + const ModSpec &mod = it.second; + if (mod.is_modpack) { std::vector content = flattenMods(mod.modpack_content); result.reserve(result.size() + content.size()); result.insert(result.end(),content.begin(),content.end()); - - } + + } 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) +ModConfiguration::ModConfiguration(const std::string &worldpath) { - 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) +void ModConfiguration::printUnsatisfiedModsError() const { - addMods(filterMods(flattenMods(getModsInPath(path)),exclude_mods)); + for (const ModSpec &mod : m_unsatisfied_mods) { + errorstream << "mod \"" << mod.name << "\" has unsatisfied dependencies: "; + for (const std::string &unsatisfied_depend : mod.unsatisfied_depends) + errorstream << " \"" << unsatisfied_depend << "\""; + errorstream << std::endl; + } } +void ModConfiguration::addModsInPath(const std::string &path) +{ + addMods(flattenMods(getModsInPath(path))); +} -void ModConfiguration::addMods(std::vector new_mods) +void ModConfiguration::addMods(const 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); + // 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 (const ModSpec &mod : new_mods) { + if (mod.part_of_modpack != (bool)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]; + warningstream<<"Mod name conflict detected: \"" + < &mods) +{ + Settings conf; + std::set load_mod_names; + + conf.readConfigFile(settings_path.c_str()); + std::vector names = conf.getNames(); + for (const std::string &name : names) { + if (name.compare(0,9,"load_mod_")==0 && conf.getBool(name)) + load_mod_names.insert(name.substr(9)); + } + + std::vector addon_mods; + for (const std::string &i : mods) { + std::vector addon_mods_in_path = flattenMods(getModsInPath(i)); + for (std::vector::const_iterator it = addon_mods_in_path.begin(); + it != addon_mods_in_path.end(); ++it) { + const ModSpec& mod = *it; + if (load_mod_names.count(mod.name) != 0) + addon_mods.push_back(mod); + else + conf.setBool("load_mod_" + mod.name, false); + } + } + conf.updateConfigFile(settings_path.c_str()); + + addMods(addon_mods); + checkConflictsAndDeps(); + + // complain about mods declared to be loaded, but not found + for (const ModSpec &addon_mod : addon_mods) + load_mod_names.erase(addon_mod.name); + + std::vector unsatisfiedMods = getUnsatisfiedMods(); + + for (const ModSpec &unsatisfiedMod : unsatisfiedMods) + load_mod_names.erase(unsatisfiedMod.name); + + if (!load_mod_names.empty()) { + errorstream << "The following mods could not be found:"; + for (const std::string &mod : load_mod_names) + errorstream << " \"" << mod << "\""; + errorstream << std::endl; + } +} + +void ModConfiguration::checkConflictsAndDeps() +{ + // report on name conflicts + if (!m_name_conflicts.empty()) { + std::string s = "Unresolved name conflicts for mods "; + for (std::unordered_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::resolveDependencies() +{ + // Step 1: Compile a list of the mod names we're working with + std::set modnames; + for (const ModSpec &mod : m_unsatisfied_mods) { + modnames.insert(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); + // Step 2: get dependencies (including optional dependencies) + // of each mod, split mods into satisfied and unsatisfied + std::list satisfied; + std::list unsatisfied; + for (ModSpec mod : m_unsatisfied_mods) { + mod.unsatisfied_depends = mod.depends; + // check which optional dependencies actually exist + for (const std::string &optdep : mod.optdepends) { + 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 - m_unsatisfied_mods.push_back(mod_new); + unsatisfied.push_back(mod); } - // Step 2: mods without unmet dependencies can be appended to + // Step 3: mods without unmet dependencies can be appended to // the sorted list. - while(!satisfied_mods.empty()) - { - ModSpec mod = satisfied_mods.back(); + while(!satisfied.empty()){ + ModSpec mod = satisfied.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(); ) - { + satisfied.pop_back(); + for (auto it = unsatisfied.begin(); it != unsatisfied.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 + 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()); } -// If failed, returned modspec has name=="" -static ModSpec findCommonMod(const std::string &modname) +ServerModConfiguration::ServerModConfiguration(const std::string &worldpath): + ModConfiguration(worldpath) { - // 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 paths; + std::string path_user = porting::path_user + DIR_DELIM + "clientmods"; + paths.insert(path); + paths.insert(path_user); - // Add common mods without dependency handling - std::vector inexistent_common_mods; - Settings gameconf; - if(getGameConfig(gamespec.path, gameconf)){ - if(gameconf.exists("common_mods")){ - Strfnd f(gameconf.get("common_mods")); - while(!f.atend()){ - std::string modname = trim(f.next(",")); - if(modname.empty()) - continue; - ModSpec spec = findCommonMod(modname); - if(spec.name.empty()) - inexistent_common_mods.push_back(modname); - else - m_sorted_mods.push_back(spec); - } - } + std::string settings_path = path_user + DIR_DELIM + "mods.conf"; + addModsFromConfig(settings_path, paths); +} +#endif + +ModMetadata::ModMetadata(const std::string &mod_name): + m_mod_name(mod_name) +{ +} + +void ModMetadata::clear() +{ + Metadata::clear(); + m_modified = true; +} + +bool ModMetadata::save(const std::string &root_path) +{ + Json::Value json; + for (StringMap::const_iterator it = m_stringvars.begin(); + it != m_stringvars.end(); ++it) { + json[it->first] = it->second; } - if(!inexistent_common_mods.empty()){ - std::string s = "Required common mods "; - for(u32 i=0; i = ... line. - std::string worldmt = worldpath+DIR_DELIM+"world.mt"; - Settings worldmt_settings; - worldmt_settings.readConfigFile(worldmt.c_str()); - std::vector names = worldmt_settings.getNames(); - std::set exclude_mod_names; - for(std::vector::iterator it = names.begin(); - it != names.end(); ++it) - { - std::string name = *it; - // for backwards compatibility: exclude only mods which are - // explicitely excluded. if mod is not mentioned at all, it is - // enabled. So by default, all installed mods are enabled. - if (name.compare(0,9,"load_mod_") == 0 && - !worldmt_settings.getBool(name)) - { - exclude_mod_names.insert(name.substr(9)); - } + if (w_ok) { + m_modified = false; + } else { + errorstream << "ModMetadata[" << m_mod_name << "]: failed write file." << std::endl; + } + return w_ok; +} + +bool ModMetadata::load(const std::string &root_path) +{ + m_stringvars.clear(); + + std::ifstream is((root_path + DIR_DELIM + m_mod_name).c_str(), std::ios_base::binary); + if (!is.good()) { + return false; } - for(std::set::const_iterator i = gamespec.addon_mods_paths.begin(); - i != gamespec.addon_mods_paths.end(); ++i) - addModsInPathFiltered((*i),exclude_mod_names); + Json::Value root; + Json::CharReaderBuilder builder; + builder.settings_["collectComments"] = false; + std::string errs; + + if (!Json::parseFromStream(builder, is, &root, &errs)) { + errorstream << "ModMetadata[" << m_mod_name << "]: failed read data " + "(Json decoding failure). Message: " << errs << std::endl; + return false; + } + + const Json::Value::Members attr_list = root.getMemberNames(); + for (const auto &it : attr_list) { + Json::Value attr_value = root[it]; + m_stringvars[it] = attr_value.asString(); + } + + return true; +} + +bool ModMetadata::setString(const std::string &name, const std::string &var) +{ + m_modified = Metadata::setString(name, var); + return m_modified; }