Initial commit

This commit is contained in:
2023-02-19 21:41:32 +01:00
commit 74d5334e72
1522 changed files with 753891 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
set(content_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/content.cpp
${CMAKE_CURRENT_SOURCE_DIR}/mod_configuration.cpp
${CMAKE_CURRENT_SOURCE_DIR}/mods.cpp
${CMAKE_CURRENT_SOURCE_DIR}/subgames.cpp
PARENT_SCOPE
)

122
src/content/content.cpp Normal file
View File

@@ -0,0 +1,122 @@
/*
Minetest
Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <fstream>
#include "content/content.h"
#include "content/subgames.h"
#include "content/mods.h"
#include "filesys.h"
#include "settings.h"
enum ContentType
{
ECT_UNKNOWN,
ECT_MOD,
ECT_MODPACK,
ECT_GAME,
ECT_TXP
};
ContentType getContentType(const ContentSpec &spec)
{
std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
if (modpack_is.good()) {
modpack_is.close();
return ECT_MODPACK;
}
std::ifstream modpack2_is((spec.path + DIR_DELIM + "modpack.conf").c_str());
if (modpack2_is.good()) {
modpack2_is.close();
return ECT_MODPACK;
}
std::ifstream init_is((spec.path + DIR_DELIM + "init.lua").c_str());
if (init_is.good()) {
init_is.close();
return ECT_MOD;
}
std::ifstream game_is((spec.path + DIR_DELIM + "game.conf").c_str());
if (game_is.good()) {
game_is.close();
return ECT_GAME;
}
std::ifstream txp_is((spec.path + DIR_DELIM + "texture_pack.conf").c_str());
if (txp_is.good()) {
txp_is.close();
return ECT_TXP;
}
return ECT_UNKNOWN;
}
void parseContentInfo(ContentSpec &spec)
{
std::string conf_path;
switch (getContentType(spec)) {
case ECT_MOD:
spec.type = "mod";
conf_path = spec.path + DIR_DELIM + "mod.conf";
break;
case ECT_MODPACK:
spec.type = "modpack";
conf_path = spec.path + DIR_DELIM + "modpack.conf";
break;
case ECT_GAME:
spec.type = "game";
conf_path = spec.path + DIR_DELIM + "game.conf";
break;
case ECT_TXP:
spec.type = "txp";
conf_path = spec.path + DIR_DELIM + "texture_pack.conf";
break;
default:
spec.type = "unknown";
break;
}
Settings conf;
if (!conf_path.empty() && conf.readConfigFile(conf_path.c_str())) {
if (conf.exists("title"))
spec.title = conf.get("title");
else if (spec.type == "game" && conf.exists("name"))
spec.title = conf.get("name");
if (spec.type != "game" && conf.exists("name"))
spec.name = conf.get("name");
if (conf.exists("description"))
spec.desc = conf.get("description");
if (conf.exists("author"))
spec.author = conf.get("author");
if (conf.exists("release"))
spec.release = conf.getS32("release");
}
if (spec.desc.empty()) {
std::ifstream is((spec.path + DIR_DELIM + "description.txt").c_str());
spec.desc = std::string((std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>());
}
}

42
src/content/content.h Normal file
View File

@@ -0,0 +1,42 @@
/*
Minetest
Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "config.h"
#include "convert_json.h"
#include "irrlichttypes.h"
struct ContentSpec
{
std::string type;
std::string author;
u32 release = 0;
/// Technical name / Id
std::string name;
/// Human-readable title
std::string title;
/// Short description
std::string desc;
std::string path;
};
void parseContentInfo(ContentSpec &spec);

View File

@@ -0,0 +1,255 @@
/*
Minetest
Copyright (C) 2013-22 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "mod_configuration.h"
#include "log.h"
#include "settings.h"
#include "filesys.h"
void ModConfiguration::printUnsatisfiedModsError() const
{
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, const std::string &virtual_path)
{
addMods(flattenMods(getModsInPath(path, virtual_path)));
}
void ModConfiguration::addMods(const std::vector<ModSpec> &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<std::string, u32> 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<std::string> 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: \""
<< mod.name << "\"" << std::endl
<< "Will not load: " << oldmod.path
<< std::endl
<< "Overridden by: " << mod.path
<< std::endl;
m_unsatisfied_mods[oldindex] = mod;
// If there was a "VERY BAD CASE" name conflict
// in an earlier level, ignore it.
m_name_conflicts.erase(mod.name);
} else {
// VERY BAD CASE: name conflict in the same level.
u32 oldindex = existing_mods[mod.name];
const ModSpec &oldmod = m_unsatisfied_mods[oldindex];
warningstream << "Mod name conflict detected: \""
<< mod.name << "\"" << std::endl
<< "Will not load: " << oldmod.path
<< std::endl
<< "Will not load: " << mod.path
<< std::endl;
m_unsatisfied_mods[oldindex] = mod;
m_name_conflicts.insert(mod.name);
}
seen_this_iteration.insert(mod.name);
}
}
}
void ModConfiguration::addGameMods(const SubgameSpec &gamespec)
{
std::string game_virtual_path;
game_virtual_path.append("games/").append(gamespec.id).append("/mods");
addModsInPath(gamespec.gamemods_path, game_virtual_path);
}
void ModConfiguration::addModsFromConfig(
const std::string &settings_path,
const std::unordered_map<std::string, std::string> &modPaths)
{
Settings conf;
std::unordered_map<std::string, std::string> load_mod_names;
conf.readConfigFile(settings_path.c_str());
std::vector<std::string> names = conf.getNames();
for (const std::string &name : names) {
const auto &value = conf.get(name);
if (name.compare(0, 9, "load_mod_") == 0 && value != "false" &&
value != "nil")
load_mod_names[name.substr(9)] = value;
}
// List of enabled non-game non-world mods
std::vector<ModSpec> addon_mods;
// Map of modname to a list candidate mod paths. Used to list
// alternatives if a particular mod cannot be found.
std::unordered_map<std::string, std::vector<std::string>> candidates;
/*
* Iterate through all installed mods except game mods and world mods
*
* If the mod is enabled, add it to `addon_mods`. *
*
* Alternative candidates for a modname are stored in `candidates`,
* and used in an error message later.
*
* If not enabled, add `load_mod_modname = false` to world.mt
*/
for (const auto &modPath : modPaths) {
std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(modPath.second, modPath.first));
for (const auto &mod : addon_mods_in_path) {
const auto &pair = load_mod_names.find(mod.name);
if (pair != load_mod_names.end()) {
if (is_yes(pair->second) || pair->second == mod.virtual_path) {
addon_mods.push_back(mod);
} else {
candidates[pair->first].emplace_back(mod.virtual_path);
}
} else {
conf.setBool("load_mod_" + mod.name, false);
}
}
}
conf.updateConfigFile(settings_path.c_str());
addMods(addon_mods);
// Remove all loaded mods from `load_mod_names`
// NB: as deps have not yet been resolved, `m_unsatisfied_mods` will contain all mods.
for (const ModSpec &mod : m_unsatisfied_mods)
load_mod_names.erase(mod.name);
// Complain about mods declared to be loaded, but not found
if (!load_mod_names.empty()) {
errorstream << "The following mods could not be found:";
for (const auto &pair : load_mod_names)
errorstream << " \"" << pair.first << "\"";
errorstream << std::endl;
for (const auto &pair : load_mod_names) {
const auto &candidate = candidates.find(pair.first);
if (candidate != candidates.end()) {
errorstream << "Unable to load " << pair.first << " as the specified path "
<< pair.second << " could not be found. "
<< "However, it is available in the following locations:"
<< std::endl;
for (const auto &path : candidate->second) {
errorstream << " - " << path << std::endl;
}
}
}
}
}
void ModConfiguration::checkConflictsAndDeps()
{
// report on name conflicts
if (!m_name_conflicts.empty()) {
std::string s = "Unresolved name conflicts for mods ";
bool add_comma = false;
for (const auto& it : m_name_conflicts) {
if (add_comma)
s.append(", ");
s.append("\"").append(it).append("\"");
add_comma = true;
}
s.append(".");
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<std::string> modnames;
for (const ModSpec &mod : m_unsatisfied_mods) {
modnames.insert(mod.name);
}
// Step 2: get dependencies (including optional dependencies)
// of each mod, split mods into satisfied and unsatisfied
std::list<ModSpec> satisfied;
std::list<ModSpec> 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
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 (auto 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());
}

View File

@@ -0,0 +1,111 @@
/*
Minetest
Copyright (C) 2013-22 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "mods.h"
/**
* ModConfiguration is a subset of installed mods. This class
* is used to resolve dependencies and return a sorted list of mods.
*
* This class should not be extended from, but instead used as a
* component in other classes.
*/
class ModConfiguration
{
public:
/**
* @returns true if all dependencies are fullfilled.
*/
inline bool isConsistent() const { return m_unsatisfied_mods.empty(); }
inline const std::vector<ModSpec> &getUnsatisfiedMods() const
{
return m_unsatisfied_mods;
}
/**
* List of mods sorted such that they can be loaded in the
* given order with all dependencies being fulfilled.
*
* I.e: every mod in this list has only dependencies on mods which
* appear earlier in the vector.
*/
const std::vector<ModSpec> &getMods() const { return m_sorted_mods; }
void printUnsatisfiedModsError() const;
/**
* Adds all mods in the given path. used for games, modpacks
* and world-specific mods (worldmods-folders)
*
* @param path To search, should be absolute
* @param virtual_path Virtual path for this directory, see comment in ModSpec
*/
void addModsInPath(const std::string &path, const std::string &virtual_path);
/**
* Adds all mods in `new_mods`
*/
void addMods(const std::vector<ModSpec> &new_mods);
/**
* Adds game mods
*/
void addGameMods(const SubgameSpec &gamespec);
/**
* Adds mods specifed by a world.mt config
*
* @param settings_path Path to world.mt
* @param modPaths Map from virtual name to mod path
*/
void addModsFromConfig(const std::string &settings_path,
const std::unordered_map<std::string, std::string> &modPaths);
/**
* Call this function once all mods have been added
*/
void checkConflictsAndDeps();
private:
std::vector<ModSpec> m_sorted_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<ModSpec> m_unsatisfied_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. game mod in modpack; 2. game mod;
// 3. world mod in modpack; 4. world mod;
// 5. addon mod in modpack; 6. addon mod.
std::unordered_set<std::string> m_name_conflicts;
};

245
src/content/mods.cpp Normal file
View File

@@ -0,0 +1,245 @@
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <cctype>
#include <fstream>
#include <json/json.h>
#include <algorithm>
#include "content/mods.h"
#include "database/database.h"
#include "filesys.h"
#include "log.h"
#include "content/subgames.h"
#include "settings.h"
#include "porting.h"
#include "convert_json.h"
#include "script/common/c_internal.h"
void ModSpec::checkAndLog() const
{
if (!string_allowed(name, MODNAME_ALLOWED_CHARS)) {
throw ModError("Error loading mod \"" + name +
"\": Mod name does not follow naming conventions: "
"Only characters [a-z0-9_] are allowed.");
}
// Log deprecation messages
auto handling_mode = get_deprecated_handling_mode();
if (!deprecation_msgs.empty() && handling_mode != DeprecatedHandlingMode::Ignore) {
std::ostringstream os;
os << "Mod " << name << " at " << path << ":" << std::endl;
for (auto msg : deprecation_msgs)
os << "\t" << msg << std::endl;
if (handling_mode == DeprecatedHandlingMode::Error)
throw ModError(os.str());
else
warningstream << os.str();
}
}
bool parseDependsString(std::string &dep, std::unordered_set<char> &symbols)
{
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.empty();
}
bool 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)
if (fs::IsFile(spec.path + DIR_DELIM + "modpack.txt") ||
fs::IsFile(spec.path + DIR_DELIM + "modpack.conf")) {
spec.is_modpack = true;
spec.modpack_content = getModsInPath(spec.path, spec.virtual_path, true);
return true;
} else if (!fs::IsFile(spec.path + DIR_DELIM + "init.lua")) {
return false;
}
Settings info;
info.readConfigFile((spec.path + DIR_DELIM + "mod.conf").c_str());
if (info.exists("name"))
spec.name = info.get("name");
else
spec.deprecation_msgs.push_back("Mods not having a mod.conf file with the name is deprecated.");
if (info.exists("author"))
spec.author = info.get("author");
if (info.exists("release"))
spec.release = info.getS32("release");
// Attempt to load dependencies from mod.conf
bool mod_conf_has_depends = false;
if (info.exists("depends")) {
mod_conf_has_depends = true;
std::string dep = info.get("depends");
// clang-format off
dep.erase(std::remove_if(dep.begin(), dep.end(),
static_cast<int (*)(int)>(&std::isspace)), dep.end());
// clang-format on
for (const auto &dependency : str_split(dep, ',')) {
spec.depends.insert(dependency);
}
}
if (info.exists("optional_depends")) {
mod_conf_has_depends = true;
std::string dep = info.get("optional_depends");
// clang-format off
dep.erase(std::remove_if(dep.begin(), dep.end(),
static_cast<int (*)(int)>(&std::isspace)), dep.end());
// clang-format on
for (const auto &dependency : str_split(dep, ',')) {
spec.optdepends.insert(dependency);
}
}
// Fallback to depends.txt
if (!mod_conf_has_depends) {
std::vector<std::string> dependencies;
std::ifstream is((spec.path + DIR_DELIM + "depends.txt").c_str());
if (is.good())
spec.deprecation_msgs.push_back("depends.txt is deprecated, please use mod.conf instead.");
while (is.good()) {
std::string dep;
std::getline(is, dep);
dependencies.push_back(dep);
}
for (auto &dependency : dependencies) {
std::unordered_set<char> symbols;
if (parseDependsString(dependency, symbols)) {
if (symbols.count('?') != 0) {
spec.optdepends.insert(dependency);
} else {
spec.depends.insert(dependency);
}
}
}
}
if (info.exists("description"))
spec.desc = info.get("description");
else if (fs::ReadFile(spec.path + DIR_DELIM + "description.txt", spec.desc))
spec.deprecation_msgs.push_back("description.txt is deprecated, please use mod.conf instead.");
return true;
}
std::map<std::string, ModSpec> getModsInPath(
const std::string &path, const std::string &virtual_path, bool part_of_modpack)
{
// NOTE: this function works in mutual recursion with parseModContents
std::map<std::string, ModSpec> result;
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(path);
std::string mod_path;
std::string mod_virtual_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;
mod_path.clear();
mod_path.append(path).append(DIR_DELIM).append(modname);
mod_virtual_path.clear();
// Intentionally uses / to keep paths same on different platforms
mod_virtual_path.append(virtual_path).append("/").append(modname);
ModSpec spec(modname, mod_path, part_of_modpack, mod_virtual_path);
parseModContents(spec);
result.insert(std::make_pair(modname, spec));
}
return result;
}
std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods)
{
std::vector<ModSpec> result;
for (const auto &it : mods) {
const ModSpec &mod = it.second;
if (mod.is_modpack) {
std::vector<ModSpec> content = flattenMods(mod.modpack_content);
result.reserve(result.size() + content.size());
result.insert(result.end(), content.begin(), content.end());
} else // not a modpack
{
result.push_back(mod);
}
}
return result;
}
ModMetadata::ModMetadata(const std::string &mod_name, ModMetadataDatabase *database):
m_mod_name(mod_name), m_database(database)
{
m_database->getModEntries(m_mod_name, &m_stringvars);
}
void ModMetadata::clear()
{
for (const auto &pair : m_stringvars) {
m_database->removeModEntry(m_mod_name, pair.first);
}
Metadata::clear();
}
bool ModMetadata::setString(const std::string &name, const std::string &var)
{
if (Metadata::setString(name, var)) {
if (var.empty()) {
m_database->removeModEntry(m_mod_name, name);
} else {
m_database->setModEntry(m_mod_name, name, var);
}
return true;
}
return false;
}

129
src/content/mods.h Normal file
View File

@@ -0,0 +1,129 @@
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include "irrlichttypes.h"
#include <list>
#include <set>
#include <vector>
#include <string>
#include <map>
#include <json/json.h>
#include <unordered_set>
#include "util/basic_macros.h"
#include "config.h"
#include "metadata.h"
#include "subgames.h"
class ModMetadataDatabase;
#define MODNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_"
struct ModSpec
{
std::string name;
std::string author;
std::string path;
std::string desc;
int release = 0;
// if normal mod:
std::unordered_set<std::string> depends;
std::unordered_set<std::string> optdepends;
std::unordered_set<std::string> unsatisfied_depends;
bool part_of_modpack = false;
bool is_modpack = false;
/**
* A constructed canonical path to represent this mod's location.
* This intended to be used as an identifier for a modpath that tolerates file movement,
* and cannot be used to read the mod files.
*
* Note that `mymod` is the directory name, not the mod name specified in mod.conf.
*
* Ex:
*
* - mods/mymod
* - mods/mymod (1)
* (^ this would have name=mymod in mod.conf)
* - mods/modpack1/mymod
* - games/mygame/mods/mymod
* - worldmods/mymod
*/
std::string virtual_path;
// For logging purposes
std::vector<const char *> deprecation_msgs;
// if modpack:
std::map<std::string, ModSpec> modpack_content;
ModSpec()
{
}
ModSpec(const std::string &name, const std::string &path, bool part_of_modpack, const std::string &virtual_path) :
name(name), path(path), part_of_modpack(part_of_modpack), virtual_path(virtual_path)
{
}
void checkAndLog() const;
};
/**
* Retrieves depends, optdepends, is_modpack and modpack_content
*
* @returns false if not a mod
*/
bool parseModContents(ModSpec &mod);
/**
* Gets a list of all mods and modpacks in path
*
* @param Path to search, should be absolute
* @param part_of_modpack Is this searching within a modpack?
* @param virtual_path Virtual path for this directory, see comment in ModSpec
* @returns map of mods
*/
std::map<std::string, ModSpec> getModsInPath(const std::string &path,
const std::string &virtual_path, bool part_of_modpack = false);
// replaces modpack Modspecs with their content
std::vector<ModSpec> flattenMods(const std::map<std::string, ModSpec> &mods);
class ModMetadata : public Metadata
{
public:
ModMetadata() = delete;
ModMetadata(const std::string &mod_name, ModMetadataDatabase *database);
~ModMetadata() = default;
virtual void clear();
const std::string &getModName() const { return m_mod_name; }
virtual bool setString(const std::string &name, const std::string &var);
private:
std::string m_mod_name;
ModMetadataDatabase *m_database;
};

423
src/content/subgames.cpp Normal file
View File

@@ -0,0 +1,423 @@
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <common/c_internal.h>
#include "content/subgames.h"
#include "porting.h"
#include "filesys.h"
#include "settings.h"
#include "log.h"
#include "util/strfnd.h"
#include "defaultsettings.h" // for set_default_settings
#include "map_settings_manager.h"
#include "util/string.h"
#ifndef SERVER
#include "client/tile.h" // getImagePath
#endif
// The maximum number of identical world names allowed
#define MAX_WORLD_NAMES 100
namespace
{
bool getGameMinetestConfig(const std::string &game_path, Settings &conf)
{
std::string conf_path = game_path + DIR_DELIM + "minetest.conf";
return conf.readConfigFile(conf_path.c_str());
}
}
void SubgameSpec::checkAndLog() const
{
// Log deprecation messages
auto handling_mode = get_deprecated_handling_mode();
if (!deprecation_msgs.empty() && handling_mode != DeprecatedHandlingMode::Ignore) {
std::ostringstream os;
os << "Game " << title << " at " << path << ":" << std::endl;
for (auto msg : deprecation_msgs)
os << "\t" << msg << std::endl;
if (handling_mode == DeprecatedHandlingMode::Error)
throw ModError(os.str());
else
warningstream << os.str();
}
}
struct GameFindPath
{
std::string path;
bool user_specific;
GameFindPath(const std::string &path, bool user_specific) :
path(path), user_specific(user_specific)
{
}
};
std::string getSubgamePathEnv()
{
char *subgame_path = getenv("MINETEST_SUBGAME_PATH");
return subgame_path ? std::string(subgame_path) : "";
}
SubgameSpec findSubgame(const std::string &id)
{
if (id.empty())
return SubgameSpec();
std::string share = porting::path_share;
std::string user = porting::path_user;
// Get games install locations
Strfnd search_paths(getSubgamePathEnv());
// Get all possible paths fo game
std::vector<GameFindPath> find_paths;
while (!search_paths.at_end()) {
std::string path = search_paths.next(PATH_DELIM);
path.append(DIR_DELIM).append(id);
find_paths.emplace_back(path, false);
path.append("_game");
find_paths.emplace_back(path, false);
}
std::string game_base = DIR_DELIM;
game_base = game_base.append("games").append(DIR_DELIM).append(id);
std::string game_suffixed = game_base + "_game";
find_paths.emplace_back(user + game_suffixed, true);
find_paths.emplace_back(user + game_base, true);
find_paths.emplace_back(share + game_suffixed, false);
find_paths.emplace_back(share + game_base, false);
// Find game directory
std::string game_path;
bool user_game = true; // Game is in user's directory
for (const GameFindPath &find_path : find_paths) {
const std::string &try_path = find_path.path;
if (fs::PathExists(try_path)) {
game_path = try_path;
user_game = find_path.user_specific;
break;
}
}
if (game_path.empty())
return SubgameSpec();
std::string gamemod_path = game_path + DIR_DELIM + "mods";
// Find mod directories
std::unordered_map<std::string, std::string> mods_paths;
mods_paths["mods"] = user + DIR_DELIM + "mods";
if (!user_game && user != share)
mods_paths["share"] = share + DIR_DELIM + "mods";
for (const std::string &mod_path : getEnvModPaths()) {
mods_paths[fs::AbsolutePath(mod_path)] = mod_path;
}
// Get meta
std::string conf_path = game_path + DIR_DELIM + "game.conf";
Settings conf;
conf.readConfigFile(conf_path.c_str());
std::string game_title;
if (conf.exists("title"))
game_title = conf.get("title");
else if (conf.exists("name"))
game_title = conf.get("name");
else
game_title = id;
std::string game_author;
if (conf.exists("author"))
game_author = conf.get("author");
int game_release = 0;
if (conf.exists("release"))
game_release = conf.getS32("release");
std::string menuicon_path;
#ifndef SERVER
menuicon_path = getImagePath(
game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png");
#endif
SubgameSpec spec(id, game_path, gamemod_path, mods_paths, game_title,
menuicon_path, game_author, game_release);
if (conf.exists("name") && !conf.exists("title"))
spec.deprecation_msgs.push_back("\"name\" setting in game.conf is deprecated, please use \"title\" instead");
return spec;
}
SubgameSpec findWorldSubgame(const std::string &world_path)
{
std::string world_gameid = getWorldGameId(world_path, true);
// See if world contains an embedded game; if so, use it.
std::string world_gamepath = world_path + DIR_DELIM + "game";
if (fs::PathExists(world_gamepath)) {
SubgameSpec gamespec;
gamespec.id = world_gameid;
gamespec.path = world_gamepath;
gamespec.gamemods_path = world_gamepath + DIR_DELIM + "mods";
Settings conf;
std::string conf_path = world_gamepath + DIR_DELIM + "game.conf";
conf.readConfigFile(conf_path.c_str());
if (conf.exists("title"))
gamespec.title = conf.get("title");
else if (conf.exists("name"))
gamespec.title = conf.get("name");
else
gamespec.title = world_gameid;
return gamespec;
}
return findSubgame(world_gameid);
}
std::set<std::string> getAvailableGameIds()
{
std::set<std::string> gameids;
std::set<std::string> gamespaths;
gamespaths.insert(porting::path_share + DIR_DELIM + "games");
gamespaths.insert(porting::path_user + DIR_DELIM + "games");
Strfnd search_paths(getSubgamePathEnv());
while (!search_paths.at_end())
gamespaths.insert(search_paths.next(PATH_DELIM));
for (const std::string &gamespath : gamespaths) {
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(gamespath);
for (const fs::DirListNode &dln : dirlist) {
if (!dln.dir)
continue;
// If configuration file is not found or broken, ignore game
Settings conf;
std::string conf_path = gamespath + DIR_DELIM + dln.name +
DIR_DELIM + "game.conf";
if (!conf.readConfigFile(conf_path.c_str()))
continue;
// Add it to result
const char *ends[] = {"_game", NULL};
std::string shorter = removeStringEnd(dln.name, ends);
if (!shorter.empty())
gameids.insert(shorter);
else
gameids.insert(dln.name);
}
}
return gameids;
}
std::vector<SubgameSpec> getAvailableGames()
{
std::vector<SubgameSpec> specs;
std::set<std::string> gameids = getAvailableGameIds();
specs.reserve(gameids.size());
for (const auto &gameid : gameids)
specs.push_back(findSubgame(gameid));
return specs;
}
#define LEGACY_GAMEID "minetest"
bool getWorldExists(const std::string &world_path)
{
return (fs::PathExists(world_path + DIR_DELIM + "map_meta.txt") ||
fs::PathExists(world_path + DIR_DELIM + "world.mt"));
}
//! Try to get the displayed name of a world
std::string getWorldName(const std::string &world_path, const std::string &default_name)
{
std::string conf_path = world_path + DIR_DELIM + "world.mt";
Settings conf;
bool succeeded = conf.readConfigFile(conf_path.c_str());
if (!succeeded) {
return default_name;
}
if (!conf.exists("world_name"))
return default_name;
return conf.get("world_name");
}
std::string getWorldGameId(const std::string &world_path, bool can_be_legacy)
{
std::string conf_path = world_path + DIR_DELIM + "world.mt";
Settings conf;
bool succeeded = conf.readConfigFile(conf_path.c_str());
if (!succeeded) {
if (can_be_legacy) {
// If map_meta.txt exists, it is probably an old minetest world
if (fs::PathExists(world_path + DIR_DELIM + "map_meta.txt"))
return LEGACY_GAMEID;
}
return "";
}
if (!conf.exists("gameid"))
return "";
// The "mesetint" gameid has been discarded
if (conf.get("gameid") == "mesetint")
return "minetest";
return conf.get("gameid");
}
std::string getWorldPathEnv()
{
char *world_path = getenv("MINETEST_WORLD_PATH");
return world_path ? std::string(world_path) : "";
}
std::vector<WorldSpec> getAvailableWorlds()
{
std::vector<WorldSpec> worlds;
std::set<std::string> worldspaths;
Strfnd search_paths(getWorldPathEnv());
while (!search_paths.at_end())
worldspaths.insert(search_paths.next(PATH_DELIM));
worldspaths.insert(porting::path_user + DIR_DELIM + "worlds");
infostream << "Searching worlds..." << std::endl;
for (const std::string &worldspath : worldspaths) {
infostream << " In " << worldspath << ": ";
std::vector<fs::DirListNode> dirvector = fs::GetDirListing(worldspath);
for (const fs::DirListNode &dln : dirvector) {
if (!dln.dir)
continue;
std::string fullpath = worldspath + DIR_DELIM + dln.name;
std::string name = getWorldName(fullpath, dln.name);
// Just allow filling in the gameid always for now
bool can_be_legacy = true;
std::string gameid = getWorldGameId(fullpath, can_be_legacy);
WorldSpec spec(fullpath, name, gameid);
if (!spec.isValid()) {
infostream << "(invalid: " << name << ") ";
} else {
infostream << name << " ";
worlds.push_back(spec);
}
}
infostream << std::endl;
}
// Check old world location
do {
std::string fullpath = porting::path_user + DIR_DELIM + "world";
if (!fs::PathExists(fullpath))
break;
std::string name = "Old World";
std::string gameid = getWorldGameId(fullpath, true);
WorldSpec spec(fullpath, name, gameid);
infostream << "Old world found." << std::endl;
worlds.push_back(spec);
} while (false);
infostream << worlds.size() << " found." << std::endl;
return worlds;
}
void loadGameConfAndInitWorld(const std::string &path, const std::string &name,
const SubgameSpec &gamespec, bool create_world)
{
std::string final_path = path;
// If we're creating a new world, ensure that the path isn't already taken
if (create_world) {
int counter = 1;
while (fs::PathExists(final_path) && counter < MAX_WORLD_NAMES) {
final_path = path + "_" + std::to_string(counter);
counter++;
}
if (fs::PathExists(final_path)) {
throw BaseException("Too many similar filenames");
}
}
Settings *game_settings = Settings::getLayer(SL_GAME);
const bool new_game_settings = (game_settings == nullptr);
if (new_game_settings) {
// Called by main-menu without a Server instance running
// -> create and free manually
game_settings = Settings::createLayer(SL_GAME);
}
getGameMinetestConfig(gamespec.path, *game_settings);
game_settings->removeSecureSettings();
infostream << "Initializing world at " << final_path << std::endl;
fs::CreateAllDirs(final_path);
// Create world.mt if does not already exist
std::string worldmt_path = final_path + DIR_DELIM "world.mt";
if (!fs::PathExists(worldmt_path)) {
Settings conf;
conf.set("world_name", name);
conf.set("gameid", gamespec.id);
conf.set("backend", "sqlite3");
conf.set("player_backend", "sqlite3");
conf.set("auth_backend", "sqlite3");
conf.set("mod_storage_backend", "sqlite3");
conf.setBool("creative_mode", g_settings->getBool("creative_mode"));
conf.setBool("enable_damage", g_settings->getBool("enable_damage"));
if (!conf.updateConfigFile(worldmt_path.c_str())) {
throw BaseException("Failed to update the config file");
}
}
// Create map_meta.txt if does not already exist
std::string map_meta_path = final_path + DIR_DELIM + "map_meta.txt";
if (!fs::PathExists(map_meta_path)) {
MapSettingsManager mgr(map_meta_path);
mgr.setMapSetting("seed", g_settings->get("fixed_map_seed"));
mgr.makeMapgenParams();
mgr.saveMapMeta();
}
// The Settings object is no longer needed for created worlds
if (new_game_settings)
delete game_settings;
}
std::vector<std::string> getEnvModPaths()
{
const char *c_mod_path = getenv("MINETEST_MOD_PATH");
std::vector<std::string> paths;
Strfnd search_paths(c_mod_path ? c_mod_path : "");
while (!search_paths.at_end())
paths.push_back(search_paths.next(PATH_DELIM));
return paths;
}

101
src/content/subgames.h Normal file
View File

@@ -0,0 +1,101 @@
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include <string>
#include <set>
#include <unordered_map>
#include <vector>
class Settings;
struct SubgameSpec
{
std::string id;
std::string title;
std::string author;
int release;
std::string path;
std::string gamemods_path;
/**
* Map from virtual path to mods path
*/
std::unordered_map<std::string, std::string> addon_mods_paths;
std::string menuicon_path;
// For logging purposes
std::vector<const char *> deprecation_msgs;
SubgameSpec(const std::string &id = "", const std::string &path = "",
const std::string &gamemods_path = "",
const std::unordered_map<std::string, std::string> &addon_mods_paths = {},
const std::string &title = "",
const std::string &menuicon_path = "",
const std::string &author = "", int release = 0) :
id(id),
title(title), author(author), release(release), path(path),
gamemods_path(gamemods_path), addon_mods_paths(addon_mods_paths),
menuicon_path(menuicon_path)
{
}
bool isValid() const { return (!id.empty() && !path.empty()); }
void checkAndLog() const;
};
SubgameSpec findSubgame(const std::string &id);
SubgameSpec findWorldSubgame(const std::string &world_path);
std::set<std::string> getAvailableGameIds();
std::vector<SubgameSpec> getAvailableGames();
// Get the list of paths to mods in the environment variable $MINETEST_MOD_PATH
std::vector<std::string> getEnvModPaths();
bool getWorldExists(const std::string &world_path);
//! Try to get the displayed name of a world
std::string getWorldName(const std::string &world_path, const std::string &default_name);
std::string getWorldGameId(const std::string &world_path, bool can_be_legacy = false);
struct WorldSpec
{
std::string path;
std::string name;
std::string gameid;
WorldSpec(const std::string &path = "", const std::string &name = "",
const std::string &gameid = "") :
path(path),
name(name), gameid(gameid)
{
}
bool isValid() const
{
return (!name.empty() && !path.empty() && !gameid.empty());
}
};
std::vector<WorldSpec> getAvailableWorlds();
// loads the subgame's config and creates world directory
// and world.mt if they don't exist
void loadGameConfAndInitWorld(const std::string &path, const std::string &name,
const SubgameSpec &gamespec, bool create_world);