3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU Lesser General Public License for more details.
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 #include <json/json.h>
24 #include "content/mods.h"
25 #include "database/database.h"
28 #include "content/subgames.h"
31 #include "convert_json.h"
32 #include "script/common/c_internal.h"
34 void ModSpec::checkAndLog() const
36 if (!string_allowed(name, MODNAME_ALLOWED_CHARS)) {
37 throw ModError("Error loading mod \"" + name +
38 "\": Mod name does not follow naming conventions: "
39 "Only characters [a-z0-9_] are allowed.");
42 // Log deprecation messages
43 auto handling_mode = get_deprecated_handling_mode();
44 if (!deprecation_msgs.empty() && handling_mode != DeprecatedHandlingMode::Ignore) {
45 std::ostringstream os;
46 os << "Mod " << name << " at " << path << ":" << std::endl;
47 for (auto msg : deprecation_msgs)
48 os << "\t" << msg << std::endl;
50 if (handling_mode == DeprecatedHandlingMode::Error)
51 throw ModError(os.str());
53 warningstream << os.str();
57 bool parseDependsString(std::string &dep, std::unordered_set<char> &symbols)
61 size_t pos = dep.size();
63 !string_allowed(dep.substr(pos - 1, 1), MODNAME_ALLOWED_CHARS)) {
64 // last character is a symbol, not part of the modname
65 symbols.insert(dep[pos - 1]);
68 dep = trim(dep.substr(0, pos));
72 void parseModContents(ModSpec &spec)
74 // NOTE: this function works in mutual recursion with getModsInPath
77 spec.optdepends.clear();
78 spec.is_modpack = false;
79 spec.modpack_content.clear();
81 // Handle modpacks (defined by containing modpack.txt)
82 std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
83 std::ifstream modpack2_is((spec.path + DIR_DELIM + "modpack.conf").c_str());
84 if (modpack_is.good() || modpack2_is.good()) {
85 if (modpack_is.good())
88 if (modpack2_is.good())
91 spec.is_modpack = true;
92 spec.modpack_content = getModsInPath(spec.path, true);
96 info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str());
98 if (info.exists("name"))
99 spec.name = info.get("name");
101 spec.deprecation_msgs.push_back("Mods not having a mod.conf file with the name is deprecated.");
103 if (info.exists("author"))
104 spec.author = info.get("author");
106 if (info.exists("release"))
107 spec.release = info.getS32("release");
109 // Attempt to load dependencies from mod.conf
110 bool mod_conf_has_depends = false;
111 if (info.exists("depends")) {
112 mod_conf_has_depends = true;
113 std::string dep = info.get("depends");
115 dep.erase(std::remove_if(dep.begin(), dep.end(),
116 static_cast<int (*)(int)>(&std::isspace)), dep.end());
118 for (const auto &dependency : str_split(dep, ',')) {
119 spec.depends.insert(dependency);
123 if (info.exists("optional_depends")) {
124 mod_conf_has_depends = true;
125 std::string dep = info.get("optional_depends");
127 dep.erase(std::remove_if(dep.begin(), dep.end(),
128 static_cast<int (*)(int)>(&std::isspace)), dep.end());
130 for (const auto &dependency : str_split(dep, ',')) {
131 spec.optdepends.insert(dependency);
135 // Fallback to depends.txt
136 if (!mod_conf_has_depends) {
137 std::vector<std::string> dependencies;
139 std::ifstream is((spec.path + DIR_DELIM + "depends.txt").c_str());
142 spec.deprecation_msgs.push_back("depends.txt is deprecated, please use mod.conf instead.");
146 std::getline(is, dep);
147 dependencies.push_back(dep);
150 for (auto &dependency : dependencies) {
151 std::unordered_set<char> symbols;
152 if (parseDependsString(dependency, symbols)) {
153 if (symbols.count('?') != 0) {
154 spec.optdepends.insert(dependency);
156 spec.depends.insert(dependency);
162 if (info.exists("description"))
163 spec.desc = info.get("description");
164 else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc))
165 spec.deprecation_msgs.push_back("description.txt is deprecated, please use mod.conf instead.");
169 std::map<std::string, ModSpec> getModsInPath(
170 const std::string &path, bool part_of_modpack)
172 // NOTE: this function works in mutual recursion with parseModContents
174 std::map<std::string, ModSpec> result;
175 std::vector<fs::DirListNode> dirlist = fs::GetDirListing(path);
178 for (const fs::DirListNode &dln : dirlist) {
182 const std::string &modname = dln.name;
183 // Ignore all directories beginning with a ".", especially
184 // VCS directories like ".git" or ".svn"
185 if (modname[0] == '.')
189 modpath.append(path).append(DIR_DELIM).append(modname);
191 ModSpec spec(modname, modpath, part_of_modpack);
192 parseModContents(spec);
193 result.insert(std::make_pair(modname, spec));
198 std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods)
200 std::vector<ModSpec> result;
201 for (const auto &it : mods) {
202 const ModSpec &mod = it.second;
203 if (mod.is_modpack) {
204 std::vector<ModSpec> content = flattenMods(mod.modpack_content);
205 result.reserve(result.size() + content.size());
206 result.insert(result.end(), content.begin(), content.end());
208 } else // not a modpack
210 result.push_back(mod);
216 ModConfiguration::ModConfiguration(const std::string &worldpath)
220 void ModConfiguration::printUnsatisfiedModsError() const
222 for (const ModSpec &mod : m_unsatisfied_mods) {
223 errorstream << "mod \"" << mod.name
224 << "\" has unsatisfied dependencies: ";
225 for (const std::string &unsatisfied_depend : mod.unsatisfied_depends)
226 errorstream << " \"" << unsatisfied_depend << "\"";
227 errorstream << std::endl;
231 void ModConfiguration::addModsInPath(const std::string &path)
233 addMods(flattenMods(getModsInPath(path)));
236 void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
238 // Maintain a map of all existing m_unsatisfied_mods.
239 // Keys are mod names and values are indices into m_unsatisfied_mods.
240 std::map<std::string, u32> existing_mods;
241 for (u32 i = 0; i < m_unsatisfied_mods.size(); ++i) {
242 existing_mods[m_unsatisfied_mods[i].name] = i;
246 for (int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack) {
248 // Add all the mods that come from modpacks
250 // Add all the mods that didn't come from modpacks
252 std::set<std::string> seen_this_iteration;
254 for (const ModSpec &mod : new_mods) {
255 if (mod.part_of_modpack != (bool)want_from_modpack)
258 if (existing_mods.count(mod.name) == 0) {
259 // GOOD CASE: completely new mod.
260 m_unsatisfied_mods.push_back(mod);
261 existing_mods[mod.name] = m_unsatisfied_mods.size() - 1;
262 } else if (seen_this_iteration.count(mod.name) == 0) {
263 // BAD CASE: name conflict in different levels.
264 u32 oldindex = existing_mods[mod.name];
265 const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
266 warningstream << "Mod name conflict detected: \""
267 << mod.name << "\"" << std::endl
268 << "Will not load: " << oldmod.path
270 << "Overridden by: " << mod.path
272 m_unsatisfied_mods[oldindex] = mod;
274 // If there was a "VERY BAD CASE" name conflict
275 // in an earlier level, ignore it.
276 m_name_conflicts.erase(mod.name);
278 // VERY BAD CASE: name conflict in the same level.
279 u32 oldindex = existing_mods[mod.name];
280 const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
281 warningstream << "Mod name conflict detected: \""
282 << mod.name << "\"" << std::endl
283 << "Will not load: " << oldmod.path
285 << "Will not load: " << mod.path
287 m_unsatisfied_mods[oldindex] = mod;
288 m_name_conflicts.insert(mod.name);
291 seen_this_iteration.insert(mod.name);
296 void ModConfiguration::addModsFromConfig(
297 const std::string &settings_path, const std::set<std::string> &mods)
300 std::set<std::string> load_mod_names;
302 conf.readConfigFile(settings_path.c_str());
303 std::vector<std::string> names = conf.getNames();
304 for (const std::string &name : names) {
305 if (name.compare(0, 9, "load_mod_") == 0 && conf.get(name) != "false" &&
306 conf.get(name) != "nil")
307 load_mod_names.insert(name.substr(9));
310 std::vector<ModSpec> addon_mods;
311 for (const std::string &i : mods) {
312 std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(i));
313 for (std::vector<ModSpec>::const_iterator it = addon_mods_in_path.begin();
314 it != addon_mods_in_path.end(); ++it) {
315 const ModSpec &mod = *it;
316 if (load_mod_names.count(mod.name) != 0)
317 addon_mods.push_back(mod);
319 conf.setBool("load_mod_" + mod.name, false);
322 conf.updateConfigFile(settings_path.c_str());
325 checkConflictsAndDeps();
327 // complain about mods declared to be loaded, but not found
328 for (const ModSpec &addon_mod : addon_mods)
329 load_mod_names.erase(addon_mod.name);
331 std::vector<ModSpec> unsatisfiedMods = getUnsatisfiedMods();
333 for (const ModSpec &unsatisfiedMod : unsatisfiedMods)
334 load_mod_names.erase(unsatisfiedMod.name);
336 if (!load_mod_names.empty()) {
337 errorstream << "The following mods could not be found:";
338 for (const std::string &mod : load_mod_names)
339 errorstream << " \"" << mod << "\"";
340 errorstream << std::endl;
344 void ModConfiguration::checkConflictsAndDeps()
346 // report on name conflicts
347 if (!m_name_conflicts.empty()) {
348 std::string s = "Unresolved name conflicts for mods ";
349 for (std::unordered_set<std::string>::const_iterator it =
350 m_name_conflicts.begin();
351 it != m_name_conflicts.end(); ++it) {
352 if (it != m_name_conflicts.begin())
354 s += std::string("\"") + (*it) + "\"";
360 // get the mods in order
361 resolveDependencies();
364 void ModConfiguration::resolveDependencies()
366 // Step 1: Compile a list of the mod names we're working with
367 std::set<std::string> modnames;
368 for (const ModSpec &mod : m_unsatisfied_mods) {
369 modnames.insert(mod.name);
372 // Step 2: get dependencies (including optional dependencies)
373 // of each mod, split mods into satisfied and unsatisfied
374 std::list<ModSpec> satisfied;
375 std::list<ModSpec> unsatisfied;
376 for (ModSpec mod : m_unsatisfied_mods) {
377 mod.unsatisfied_depends = mod.depends;
378 // check which optional dependencies actually exist
379 for (const std::string &optdep : mod.optdepends) {
380 if (modnames.count(optdep) != 0)
381 mod.unsatisfied_depends.insert(optdep);
383 // if a mod has no depends it is initially satisfied
384 if (mod.unsatisfied_depends.empty())
385 satisfied.push_back(mod);
387 unsatisfied.push_back(mod);
390 // Step 3: mods without unmet dependencies can be appended to
392 while (!satisfied.empty()) {
393 ModSpec mod = satisfied.back();
394 m_sorted_mods.push_back(mod);
395 satisfied.pop_back();
396 for (auto it = unsatisfied.begin(); it != unsatisfied.end();) {
398 mod2.unsatisfied_depends.erase(mod.name);
399 if (mod2.unsatisfied_depends.empty()) {
400 satisfied.push_back(mod2);
401 it = unsatisfied.erase(it);
408 // Step 4: write back list of unsatisfied mods
409 m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end());
413 ClientModConfiguration::ClientModConfiguration(const std::string &path) :
414 ModConfiguration(path)
416 std::set<std::string> paths;
417 std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
419 paths.insert(path_user);
421 std::string settings_path = path_user + DIR_DELIM + "mods.conf";
422 addModsFromConfig(settings_path, paths);
426 ModMetadata::ModMetadata(const std::string &mod_name, ModMetadataDatabase *database):
427 m_mod_name(mod_name), m_database(database)
429 m_database->getModEntries(m_mod_name, &m_stringvars);
432 void ModMetadata::clear()
434 for (const auto &pair : m_stringvars) {
435 m_database->removeModEntry(m_mod_name, pair.first);
440 bool ModMetadata::setString(const std::string &name, const std::string &var)
442 if (Metadata::setString(name, var)) {
444 m_database->removeModEntry(m_mod_name, name);
446 m_database->setModEntry(m_mod_name, name, var);