1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-07-02 16:38:41 +00:00

Add online content repository

Replaces mods and texture pack tabs with a single content tab
This commit is contained in:
rubenwardy 2018-04-17 14:54:50 +01:00
parent 36eb823b1c
commit 87ad4d8e7f
No known key found for this signature in database
GPG key ID: A1E29D52FF81513C
46 changed files with 1696 additions and 860 deletions

View file

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

108
src/content/content.cpp Normal file
View file

@ -0,0 +1,108 @@
/*
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 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 + "mod.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("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 (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>());
}
}

33
src/content/content.h Normal file
View file

@ -0,0 +1,33 @@
/*
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"
struct ContentSpec
{
std::string type;
std::string author;
std::string name;
std::string desc;
std::string path;
};
void parseContentInfo(ContentSpec &spec);

469
src/content/mods.cpp Normal file
View file

@ -0,0 +1,469 @@
/*
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 "filesys.h"
#include "log.h"
#include "content/subgames.h"
#include "settings.h"
#include "porting.h"
#include "convert_json.h"
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();
}
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");
if (info.exists("author"))
spec.author = info.get("author");
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 {
// 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());
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 {
std::ifstream is((spec.path + DIR_DELIM + "description.txt")
.c_str());
spec.desc = std::string((std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>());
}
}
}
std::map<std::string, ModSpec> getModsInPath(
const std::string &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 modpath;
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;
modpath.clear();
modpath.append(path).append(DIR_DELIM).append(modname);
ModSpec spec(modname, modpath, part_of_modpack);
parseModContents(spec);
result.insert(std::make_pair(modname, spec));
}
return result;
}
std::vector<ModSpec> flattenMods(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;
}
ModConfiguration::ModConfiguration(const std::string &worldpath)
{
}
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)
{
addMods(flattenMods(getModsInPath(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::addModsFromConfig(
const std::string &settings_path, const std::set<std::string> &mods)
{
Settings conf;
std::set<std::string> load_mod_names;
conf.readConfigFile(settings_path.c_str());
std::vector<std::string> 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<ModSpec> addon_mods;
for (const std::string &i : mods) {
std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(i));
for (std::vector<ModSpec>::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<ModSpec> 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<std::string>::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<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());
}
#ifndef SERVER
ClientModConfiguration::ClientModConfiguration(const std::string &path) :
ModConfiguration(path)
{
std::set<std::string> paths;
std::string path_user = porting::path_user + DIR_DELIM + "clientmods";
paths.insert(path);
paths.insert(path_user);
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 (!fs::PathExists(root_path)) {
if (!fs::CreateAllDirs(root_path)) {
errorstream << "ModMetadata[" << m_mod_name
<< "]: Unable to save. '" << root_path
<< "' tree cannot be created." << std::endl;
return false;
}
} else if (!fs::IsDir(root_path)) {
errorstream << "ModMetadata[" << m_mod_name << "]: Unable to save. '"
<< root_path << "' is not a directory." << std::endl;
return false;
}
bool w_ok = fs::safeWriteToFile(
root_path + DIR_DELIM + m_mod_name, fastWriteJson(json));
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;
}
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;
}

162
src/content/mods.h Normal file
View file

@ -0,0 +1,162 @@
/*
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"
#define MODNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_"
struct ModSpec
{
std::string name;
std::string author;
std::string path;
std::string desc;
// 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;
// if modpack:
std::map<std::string, ModSpec> modpack_content;
ModSpec(const std::string &name = "", const std::string &path = "") :
name(name), path(path)
{
}
ModSpec(const std::string &name, const std::string &path, bool part_of_modpack) :
name(name), path(path), part_of_modpack(part_of_modpack)
{
}
};
// Retrieves depends, optdepends, is_modpack and modpack_content
void parseModContents(ModSpec &mod);
std::map<std::string, ModSpec> getModsInPath(
const std::string &path, bool part_of_modpack = false);
// replaces modpack Modspecs with their content
std::vector<ModSpec> flattenMods(std::map<std::string, ModSpec> mods);
// 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.
class ModConfiguration
{
public:
// checks if all dependencies are fullfilled.
bool isConsistent() const { return m_unsatisfied_mods.empty(); }
const std::vector<ModSpec> &getMods() const { return m_sorted_mods; }
const std::vector<ModSpec> &getUnsatisfiedMods() const
{
return m_unsatisfied_mods;
}
void printUnsatisfiedModsError() const;
protected:
ModConfiguration(const std::string &worldpath);
// adds all mods in the given path. used for games, modpacks
// and world-specific mods (worldmods-folders)
void addModsInPath(const std::string &path);
// adds all mods in the set.
void addMods(const std::vector<ModSpec> &new_mods);
void addModsFromConfig(const std::string &settings_path,
const std::set<std::string> &mods);
void checkConflictsAndDeps();
protected:
// list of mods sorted such that they can be loaded in the
// given order with all dependencies being fullfilled. I.e.,
// every mod in this list has only dependencies on mods which
// appear earlier in the vector.
std::vector<ModSpec> m_sorted_mods;
private:
// 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;
// Deleted default constructor
ModConfiguration() = default;
};
#ifndef SERVER
class ClientModConfiguration : public ModConfiguration
{
public:
ClientModConfiguration(const std::string &path);
};
#endif
class ModMetadata : public Metadata
{
public:
ModMetadata() = delete;
ModMetadata(const std::string &mod_name);
~ModMetadata() = default;
virtual void clear();
bool save(const std::string &root_path);
bool load(const std::string &root_path);
bool isModified() const { return m_modified; }
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;
bool m_modified = false;
};

68
src/content/packages.cpp Normal file
View file

@ -0,0 +1,68 @@
/*
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 "content/packages.h"
#include "log.h"
#include "filesys.h"
#include "porting.h"
#include "settings.h"
#include "content/mods.h"
#include "content/subgames.h"
#if USE_CURL
std::vector<Package> getPackagesFromURL(const std::string &url)
{
std::vector<std::string> extra_headers;
extra_headers.emplace_back("Accept: application/json");
Json::Value json = fetchJsonValue(url, &extra_headers);
if (!json.isArray()) {
errorstream << "Invalid JSON download " << std::endl;
return std::vector<Package>();
}
std::vector<Package> packages;
// Note: `unsigned int` is required to index JSON
for (unsigned int i = 0; i < json.size(); ++i) {
Package package;
package.name = json[i]["name"].asString();
package.title = json[i]["title"].asString();
package.author = json[i]["author"].asString();
package.type = json[i]["type"].asString();
package.shortDesc = json[i]["shortDesc"].asString();
package.url = json[i]["url"].asString();
Json::Value jScreenshots = json[i]["screenshots"];
for (unsigned int j = 0; j < jScreenshots.size(); ++j) {
package.screenshots.push_back(jScreenshots[j].asString());
}
if (package.valid()) {
packages.push_back(package);
} else {
errorstream << "Invalid package at " << i << std::endl;
}
}
return packages;
}
#endif

49
src/content/packages.h Normal file
View file

@ -0,0 +1,49 @@
/*
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"
struct Package
{
std::string name; // Technical name
std::string title;
std::string author;
std::string type; // One of "mod", "game", or "txp"
std::string shortDesc;
std::string url; // download URL
std::vector<std::string> screenshots;
bool valid()
{
return !(name.empty() || title.empty() || author.empty() ||
type.empty() || url.empty());
}
};
#if USE_CURL
std::vector<Package> getPackagesFromURL(const std::string &url);
#else
inline std::vector<Package> getPackagesFromURL(const std::string &url)
{
return std::vector<Package>();
}
#endif

332
src/content/subgames.cpp Normal file
View file

@ -0,0 +1,332 @@
/*
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 "content/subgames.h"
#include "porting.h"
#include "filesys.h"
#include "settings.h"
#include "log.h"
#include "util/strfnd.h"
#include "defaultsettings.h" // for override_default_settings
#include "mapgen/mapgen.h" // for MapgenParams
#include "util/string.h"
#ifndef SERVER
#include "client/tile.h" // getImagePath
#endif
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());
}
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);
find_paths.emplace_back(path + DIR_DELIM + id, false);
find_paths.emplace_back(path + DIR_DELIM + id + "_game", false);
}
find_paths.emplace_back(
user + DIR_DELIM + "games" + DIR_DELIM + id + "_game", true);
find_paths.emplace_back(user + DIR_DELIM + "games" + DIR_DELIM + id, true);
find_paths.emplace_back(
share + DIR_DELIM + "games" + DIR_DELIM + id + "_game", false);
find_paths.emplace_back(share + DIR_DELIM + "games" + DIR_DELIM + id, 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::set<std::string> mods_paths;
if (!user_game)
mods_paths.insert(share + DIR_DELIM + "mods");
if (user != share || user_game)
mods_paths.insert(user + DIR_DELIM + "mods");
// Get meta
std::string conf_path = game_path + DIR_DELIM + "game.conf";
Settings conf;
conf.readConfigFile(conf_path.c_str());
std::string game_name;
if (conf.exists("name"))
game_name = conf.get("name");
else
game_name = id;
std::string game_author;
if (conf.exists("author"))
game_author = conf.get("author");
std::string menuicon_path;
#ifndef SERVER
menuicon_path = getImagePath(
game_path + DIR_DELIM + "menu" + DIR_DELIM + "icon.png");
#endif
return SubgameSpec(id, game_path, gamemod_path, mods_paths, game_name,
menuicon_path, game_author);
}
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("name"))
gamespec.name = conf.get("name");
else
gamespec.name = 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();
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"));
}
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::endl;
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 = 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;
}
bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamespec)
{
// Override defaults with those provided by the game.
// We clear and reload the defaults because the defaults
// might have been overridden by other subgame config
// files that were loaded before.
g_settings->clearDefaults();
set_default_settings(g_settings);
Settings game_defaults;
getGameMinetestConfig(gamespec.path, game_defaults);
override_default_settings(g_settings, &game_defaults);
infostream << "Initializing world at " << path << std::endl;
fs::CreateAllDirs(path);
// Create world.mt if does not already exist
std::string worldmt_path = path + DIR_DELIM "world.mt";
if (!fs::PathExists(worldmt_path)) {
Settings conf;
conf.set("gameid", gamespec.id);
conf.set("backend", "sqlite3");
conf.set("player_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()))
return false;
}
// Create map_meta.txt if does not already exist
std::string map_meta_path = path + DIR_DELIM + "map_meta.txt";
if (!fs::PathExists(map_meta_path)) {
verbosestream << "Creating map_meta.txt (" << map_meta_path << ")"
<< std::endl;
fs::CreateAllDirs(path);
std::ostringstream oss(std::ios_base::binary);
Settings conf;
MapgenParams params;
params.readParams(g_settings);
params.writeParams(&conf);
conf.writeLines(oss);
oss << "[end_of_params]\n";
fs::safeWriteToFile(map_meta_path, oss.str());
}
return true;
}

90
src/content/subgames.h Normal file
View file

@ -0,0 +1,90 @@
/*
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 <vector>
class Settings;
struct SubgameSpec
{
std::string id;
std::string name;
std::string author;
std::string path;
std::string gamemods_path;
std::set<std::string> addon_mods_paths;
std::string menuicon_path;
SubgameSpec(const std::string &id = "", const std::string &path = "",
const std::string &gamemods_path = "",
const std::set<std::string> &addon_mods_paths =
std::set<std::string>(),
const std::string &name = "",
const std::string &menuicon_path = "",
const std::string &author = "") :
id(id),
name(name), author(author), path(path),
gamemods_path(gamemods_path), addon_mods_paths(addon_mods_paths),
menuicon_path(menuicon_path)
{
}
bool isValid() const { return (!id.empty() && !path.empty()); }
};
// minetest.conf
bool getGameMinetestConfig(const std::string &game_path, Settings &conf);
SubgameSpec findSubgame(const std::string &id);
SubgameSpec findWorldSubgame(const std::string &world_path);
std::set<std::string> getAvailableGameIds();
std::vector<SubgameSpec> getAvailableGames();
bool getWorldExists(const std::string &world_path);
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
bool loadGameConfAndInitWorld(const std::string &path, const SubgameSpec &gamespec);