]> git.lizzy.rs Git - dragonfireclient.git/blob - src/content/mods.cpp
refacto: hide mesh_cache inside the rendering engine
[dragonfireclient.git] / src / content / mods.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
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.
9
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.
14
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.
18 */
19
20 #include <cctype>
21 #include <fstream>
22 #include <json/json.h>
23 #include <algorithm>
24 #include "content/mods.h"
25 #include "filesys.h"
26 #include "log.h"
27 #include "content/subgames.h"
28 #include "settings.h"
29 #include "porting.h"
30 #include "convert_json.h"
31 #include "script/common/c_internal.h"
32
33 bool parseDependsString(std::string &dep, std::unordered_set<char> &symbols)
34 {
35         dep = trim(dep);
36         symbols.clear();
37         size_t pos = dep.size();
38         while (pos > 0 &&
39                         !string_allowed(dep.substr(pos - 1, 1), MODNAME_ALLOWED_CHARS)) {
40                 // last character is a symbol, not part of the modname
41                 symbols.insert(dep[pos - 1]);
42                 --pos;
43         }
44         dep = trim(dep.substr(0, pos));
45         return !dep.empty();
46 }
47
48 static void log_mod_deprecation(const ModSpec &spec, const std::string &warning)
49 {
50         auto handling_mode = get_deprecated_handling_mode();
51         if (handling_mode != DeprecatedHandlingMode::Ignore) {
52                 std::ostringstream os;
53                 os << warning << " (" << spec.name << " at " << spec.path << ")" << std::endl;
54
55                 if (handling_mode == DeprecatedHandlingMode::Error) {
56                         throw ModError(os.str());
57                 } else {
58                         warningstream << os.str();
59                 }
60         }
61 }
62
63 void parseModContents(ModSpec &spec)
64 {
65         // NOTE: this function works in mutual recursion with getModsInPath
66
67         spec.depends.clear();
68         spec.optdepends.clear();
69         spec.is_modpack = false;
70         spec.modpack_content.clear();
71
72         // Handle modpacks (defined by containing modpack.txt)
73         std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
74         std::ifstream modpack2_is((spec.path + DIR_DELIM + "modpack.conf").c_str());
75         if (modpack_is.good() || modpack2_is.good()) {
76                 if (modpack_is.good())
77                         modpack_is.close();
78
79                 if (modpack2_is.good())
80                         modpack2_is.close();
81
82                 spec.is_modpack = true;
83                 spec.modpack_content = getModsInPath(spec.path, true);
84
85         } else {
86                 Settings info;
87                 info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str());
88
89                 if (info.exists("name"))
90                         spec.name = info.get("name");
91                 else
92                         log_mod_deprecation(spec, "Mods not having a mod.conf file with the name is deprecated.");
93
94                 if (info.exists("author"))
95                         spec.author = info.get("author");
96
97                 if (info.exists("release"))
98                         spec.release = info.getS32("release");
99
100                 // Attempt to load dependencies from mod.conf
101                 bool mod_conf_has_depends = false;
102                 if (info.exists("depends")) {
103                         mod_conf_has_depends = true;
104                         std::string dep = info.get("depends");
105                         // clang-format off
106                         dep.erase(std::remove_if(dep.begin(), dep.end(),
107                                         static_cast<int (*)(int)>(&std::isspace)), dep.end());
108                         // clang-format on
109                         for (const auto &dependency : str_split(dep, ',')) {
110                                 spec.depends.insert(dependency);
111                         }
112                 }
113
114                 if (info.exists("optional_depends")) {
115                         mod_conf_has_depends = true;
116                         std::string dep = info.get("optional_depends");
117                         // clang-format off
118                         dep.erase(std::remove_if(dep.begin(), dep.end(),
119                                         static_cast<int (*)(int)>(&std::isspace)), dep.end());
120                         // clang-format on
121                         for (const auto &dependency : str_split(dep, ',')) {
122                                 spec.optdepends.insert(dependency);
123                         }
124                 }
125
126                 // Fallback to depends.txt
127                 if (!mod_conf_has_depends) {
128                         std::vector<std::string> dependencies;
129
130                         std::ifstream is((spec.path + DIR_DELIM + "depends.txt").c_str());
131
132                         if (is.good())
133                                 log_mod_deprecation(spec, "depends.txt is deprecated, please use mod.conf instead.");
134
135                         while (is.good()) {
136                                 std::string dep;
137                                 std::getline(is, dep);
138                                 dependencies.push_back(dep);
139                         }
140
141                         for (auto &dependency : dependencies) {
142                                 std::unordered_set<char> symbols;
143                                 if (parseDependsString(dependency, symbols)) {
144                                         if (symbols.count('?') != 0) {
145                                                 spec.optdepends.insert(dependency);
146                                         } else {
147                                                 spec.depends.insert(dependency);
148                                         }
149                                 }
150                         }
151                 }
152
153                 if (info.exists("description"))
154                         spec.desc = info.get("description");
155                 else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc))
156                         log_mod_deprecation(spec, "description.txt is deprecated, please use mod.conf instead.");
157         }
158 }
159
160 std::map<std::string, ModSpec> getModsInPath(
161                 const std::string &path, bool part_of_modpack)
162 {
163         // NOTE: this function works in mutual recursion with parseModContents
164
165         std::map<std::string, ModSpec> result;
166         std::vector<fs::DirListNode> dirlist = fs::GetDirListing(path);
167         std::string modpath;
168
169         for (const fs::DirListNode &dln : dirlist) {
170                 if (!dln.dir)
171                         continue;
172
173                 const std::string &modname = dln.name;
174                 // Ignore all directories beginning with a ".", especially
175                 // VCS directories like ".git" or ".svn"
176                 if (modname[0] == '.')
177                         continue;
178
179                 modpath.clear();
180                 modpath.append(path).append(DIR_DELIM).append(modname);
181
182                 ModSpec spec(modname, modpath, part_of_modpack);
183                 parseModContents(spec);
184                 result.insert(std::make_pair(modname, spec));
185         }
186         return result;
187 }
188
189 std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods)
190 {
191         std::vector<ModSpec> result;
192         for (const auto &it : mods) {
193                 const ModSpec &mod = it.second;
194                 if (mod.is_modpack) {
195                         std::vector<ModSpec> content = flattenMods(mod.modpack_content);
196                         result.reserve(result.size() + content.size());
197                         result.insert(result.end(), content.begin(), content.end());
198
199                 } else // not a modpack
200                 {
201                         result.push_back(mod);
202                 }
203         }
204         return result;
205 }
206
207 ModConfiguration::ModConfiguration(const std::string &worldpath)
208 {
209 }
210
211 void ModConfiguration::printUnsatisfiedModsError() const
212 {
213         for (const ModSpec &mod : m_unsatisfied_mods) {
214                 errorstream << "mod \"" << mod.name
215                             << "\" has unsatisfied dependencies: ";
216                 for (const std::string &unsatisfied_depend : mod.unsatisfied_depends)
217                         errorstream << " \"" << unsatisfied_depend << "\"";
218                 errorstream << std::endl;
219         }
220 }
221
222 void ModConfiguration::addModsInPath(const std::string &path)
223 {
224         addMods(flattenMods(getModsInPath(path)));
225 }
226
227 void ModConfiguration::addMods(const std::vector<ModSpec> &new_mods)
228 {
229         // Maintain a map of all existing m_unsatisfied_mods.
230         // Keys are mod names and values are indices into m_unsatisfied_mods.
231         std::map<std::string, u32> existing_mods;
232         for (u32 i = 0; i < m_unsatisfied_mods.size(); ++i) {
233                 existing_mods[m_unsatisfied_mods[i].name] = i;
234         }
235
236         // Add new mods
237         for (int want_from_modpack = 1; want_from_modpack >= 0; --want_from_modpack) {
238                 // First iteration:
239                 // Add all the mods that come from modpacks
240                 // Second iteration:
241                 // Add all the mods that didn't come from modpacks
242
243                 std::set<std::string> seen_this_iteration;
244
245                 for (const ModSpec &mod : new_mods) {
246                         if (mod.part_of_modpack != (bool)want_from_modpack)
247                                 continue;
248
249                         if (existing_mods.count(mod.name) == 0) {
250                                 // GOOD CASE: completely new mod.
251                                 m_unsatisfied_mods.push_back(mod);
252                                 existing_mods[mod.name] = m_unsatisfied_mods.size() - 1;
253                         } else if (seen_this_iteration.count(mod.name) == 0) {
254                                 // BAD CASE: name conflict in different levels.
255                                 u32 oldindex = existing_mods[mod.name];
256                                 const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
257                                 warningstream << "Mod name conflict detected: \""
258                                               << mod.name << "\"" << std::endl
259                                               << "Will not load: " << oldmod.path
260                                               << std::endl
261                                               << "Overridden by: " << mod.path
262                                               << std::endl;
263                                 m_unsatisfied_mods[oldindex] = mod;
264
265                                 // If there was a "VERY BAD CASE" name conflict
266                                 // in an earlier level, ignore it.
267                                 m_name_conflicts.erase(mod.name);
268                         } else {
269                                 // VERY BAD CASE: name conflict in the same level.
270                                 u32 oldindex = existing_mods[mod.name];
271                                 const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
272                                 warningstream << "Mod name conflict detected: \""
273                                               << mod.name << "\"" << std::endl
274                                               << "Will not load: " << oldmod.path
275                                               << std::endl
276                                               << "Will not load: " << mod.path
277                                               << std::endl;
278                                 m_unsatisfied_mods[oldindex] = mod;
279                                 m_name_conflicts.insert(mod.name);
280                         }
281
282                         seen_this_iteration.insert(mod.name);
283                 }
284         }
285 }
286
287 void ModConfiguration::addModsFromConfig(
288                 const std::string &settings_path, const std::set<std::string> &mods)
289 {
290         Settings conf;
291         std::set<std::string> load_mod_names;
292
293         conf.readConfigFile(settings_path.c_str());
294         std::vector<std::string> names = conf.getNames();
295         for (const std::string &name : names) {
296                 if (name.compare(0, 9, "load_mod_") == 0 && conf.get(name) != "false" &&
297                                 conf.get(name) != "nil")
298                         load_mod_names.insert(name.substr(9));
299         }
300
301         std::vector<ModSpec> addon_mods;
302         for (const std::string &i : mods) {
303                 std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(i));
304                 for (std::vector<ModSpec>::const_iterator it = addon_mods_in_path.begin();
305                                 it != addon_mods_in_path.end(); ++it) {
306                         const ModSpec &mod = *it;
307                         if (load_mod_names.count(mod.name) != 0)
308                                 addon_mods.push_back(mod);
309                         else
310                                 conf.setBool("load_mod_" + mod.name, false);
311                 }
312         }
313         conf.updateConfigFile(settings_path.c_str());
314
315         addMods(addon_mods);
316         checkConflictsAndDeps();
317
318         // complain about mods declared to be loaded, but not found
319         for (const ModSpec &addon_mod : addon_mods)
320                 load_mod_names.erase(addon_mod.name);
321
322         std::vector<ModSpec> unsatisfiedMods = getUnsatisfiedMods();
323
324         for (const ModSpec &unsatisfiedMod : unsatisfiedMods)
325                 load_mod_names.erase(unsatisfiedMod.name);
326
327         if (!load_mod_names.empty()) {
328                 errorstream << "The following mods could not be found:";
329                 for (const std::string &mod : load_mod_names)
330                         errorstream << " \"" << mod << "\"";
331                 errorstream << std::endl;
332         }
333 }
334
335 void ModConfiguration::checkConflictsAndDeps()
336 {
337         // report on name conflicts
338         if (!m_name_conflicts.empty()) {
339                 std::string s = "Unresolved name conflicts for mods ";
340                 for (std::unordered_set<std::string>::const_iterator it =
341                                                 m_name_conflicts.begin();
342                                 it != m_name_conflicts.end(); ++it) {
343                         if (it != m_name_conflicts.begin())
344                                 s += ", ";
345                         s += std::string("\"") + (*it) + "\"";
346                 }
347                 s += ".";
348                 throw ModError(s);
349         }
350
351         // get the mods in order
352         resolveDependencies();
353 }
354
355 void ModConfiguration::resolveDependencies()
356 {
357         // Step 1: Compile a list of the mod names we're working with
358         std::set<std::string> modnames;
359         for (const ModSpec &mod : m_unsatisfied_mods) {
360                 modnames.insert(mod.name);
361         }
362
363         // Step 2: get dependencies (including optional dependencies)
364         // of each mod, split mods into satisfied and unsatisfied
365         std::list<ModSpec> satisfied;
366         std::list<ModSpec> unsatisfied;
367         for (ModSpec mod : m_unsatisfied_mods) {
368                 mod.unsatisfied_depends = mod.depends;
369                 // check which optional dependencies actually exist
370                 for (const std::string &optdep : mod.optdepends) {
371                         if (modnames.count(optdep) != 0)
372                                 mod.unsatisfied_depends.insert(optdep);
373                 }
374                 // if a mod has no depends it is initially satisfied
375                 if (mod.unsatisfied_depends.empty())
376                         satisfied.push_back(mod);
377                 else
378                         unsatisfied.push_back(mod);
379         }
380
381         // Step 3: mods without unmet dependencies can be appended to
382         // the sorted list.
383         while (!satisfied.empty()) {
384                 ModSpec mod = satisfied.back();
385                 m_sorted_mods.push_back(mod);
386                 satisfied.pop_back();
387                 for (auto it = unsatisfied.begin(); it != unsatisfied.end();) {
388                         ModSpec &mod2 = *it;
389                         mod2.unsatisfied_depends.erase(mod.name);
390                         if (mod2.unsatisfied_depends.empty()) {
391                                 satisfied.push_back(mod2);
392                                 it = unsatisfied.erase(it);
393                         } else {
394                                 ++it;
395                         }
396                 }
397         }
398
399         // Step 4: write back list of unsatisfied mods
400         m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end());
401 }
402
403 #ifndef SERVER
404 ClientModConfiguration::ClientModConfiguration(const std::string &path) :
405                 ModConfiguration(path)
406 {
407         std::set<std::string> paths;
408         std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
409         paths.insert(path);
410         paths.insert(path_user);
411
412         std::string settings_path = path_user + DIR_DELIM + "mods.conf";
413         addModsFromConfig(settings_path, paths);
414 }
415 #endif
416
417 ModMetadata::ModMetadata(const std::string &mod_name) : m_mod_name(mod_name)
418 {
419 }
420
421 void ModMetadata::clear()
422 {
423         Metadata::clear();
424         m_modified = true;
425 }
426
427 bool ModMetadata::save(const std::string &root_path)
428 {
429         Json::Value json;
430         for (StringMap::const_iterator it = m_stringvars.begin();
431                         it != m_stringvars.end(); ++it) {
432                 json[it->first] = it->second;
433         }
434
435         if (!fs::PathExists(root_path)) {
436                 if (!fs::CreateAllDirs(root_path)) {
437                         errorstream << "ModMetadata[" << m_mod_name
438                                     << "]: Unable to save. '" << root_path
439                                     << "' tree cannot be created." << std::endl;
440                         return false;
441                 }
442         } else if (!fs::IsDir(root_path)) {
443                 errorstream << "ModMetadata[" << m_mod_name << "]: Unable to save. '"
444                             << root_path << "' is not a directory." << std::endl;
445                 return false;
446         }
447
448         bool w_ok = fs::safeWriteToFile(
449                         root_path + DIR_DELIM + m_mod_name, fastWriteJson(json));
450
451         if (w_ok) {
452                 m_modified = false;
453         } else {
454                 errorstream << "ModMetadata[" << m_mod_name << "]: failed write file."
455                             << std::endl;
456         }
457         return w_ok;
458 }
459
460 bool ModMetadata::load(const std::string &root_path)
461 {
462         m_stringvars.clear();
463
464         std::ifstream is((root_path + DIR_DELIM + m_mod_name).c_str(),
465                         std::ios_base::binary);
466         if (!is.good()) {
467                 return false;
468         }
469
470         Json::Value root;
471         Json::CharReaderBuilder builder;
472         builder.settings_["collectComments"] = false;
473         std::string errs;
474
475         if (!Json::parseFromStream(builder, is, &root, &errs)) {
476                 errorstream << "ModMetadata[" << m_mod_name
477                             << "]: failed read data "
478                                "(Json decoding failure). Message: "
479                             << errs << std::endl;
480                 return false;
481         }
482
483         const Json::Value::Members attr_list = root.getMemberNames();
484         for (const auto &it : attr_list) {
485                 Json::Value attr_value = root[it];
486                 m_stringvars[it] = attr_value.asString();
487         }
488
489         return true;
490 }
491
492 bool ModMetadata::setString(const std::string &name, const std::string &var)
493 {
494         m_modified = Metadata::setString(name, var);
495         return m_modified;
496 }