[CSM] implement client side mod loading (#5123)

* client side mods are located in clientmods/
* move builtin/preview.lua to clientmods/preview/init.lua as a preview mod
* refactor ModConfiguration class to work properly with client and server using child objects
* move some Server constructor mod load code to ModConfiguration to reduce code duplication between client and server
* remove mods.{cpp,h} unused functions
* use UNORDERED_SET instead of std::set in some modspec storages
This commit is contained in:
Loïc Blot 2017-01-27 07:41:10 +01:00
parent c42c53fccf
commit 92b45b2a18
9 changed files with 196 additions and 150 deletions

@ -152,6 +152,7 @@ endif()
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/builtin" DESTINATION "${SHAREDIR}")
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/client" DESTINATION "${SHAREDIR}")
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/clientmods" DESTINATION "${SHAREDIR}")
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/games" DESTINATION "${SHAREDIR}" PATTERN ".git*" EXCLUDE)
if(BUILD_CLIENT)

@ -5,24 +5,24 @@ core.register_on_sending_chat_messages(function(message)
if not (message:sub(1,1) == "/") then
return false
end
core.display_chat_message("issued command: " .. message)
local cmd, param = string.match(message, "^/([^ ]+) *(.*)")
if not param then
param = ""
end
local cmd_def = core.registered_chatcommands[cmd]
if cmd_def then
core.set_last_run_mod(cmd_def.mod_origin)
local success, message = cmd_def.func(param)
local _, message = cmd_def.func(param)
if message then
core.display_chat_message(message)
end
return true
end
return false
end)
end)

@ -7,7 +7,6 @@ dofile(clientpath .. "register.lua")
dofile(commonpath .. "after.lua")
dofile(commonpath .. "chatcommands.lua")
dofile(clientpath .. "chatcommands.lua")
dofile(clientpath .. "preview.lua")
core.register_on_death(function()
core.display_chat_message("You died.")

@ -268,14 +268,50 @@ Client::Client(
void Client::initMods()
{
std::string script_path = getBuiltinLuaPath() + DIR_DELIM "init.lua";
m_script->loadMod(getBuiltinLuaPath() + DIR_DELIM "init.lua", BUILTIN_MOD_NAME);
m_script->loadMod(script_path, BUILTIN_MOD_NAME);
ClientModConfiguration modconf(getClientModsLuaPath());
std::vector<ModSpec> mods = modconf.getMods();
std::vector<ModSpec> unsatisfied_mods = modconf.getUnsatisfiedMods();
// complain about mods with unsatisfied dependencies
if (!modconf.isConsistent()) {
modconf.printUnsatisfiedModsError();
}
// Print mods
infostream << "Client Loading mods: ";
for (std::vector<ModSpec>::const_iterator i = mods.begin();
i != mods.end(); ++i) {
infostream << (*i).name << " ";
}
infostream << std::endl;
// Load and run "mod" scripts
for (std::vector<ModSpec>::const_iterator it = mods.begin();
it != mods.end(); ++it) {
const ModSpec &mod = *it;
if (!string_allowed(mod.name, MODNAME_ALLOWED_CHARS)) {
throw ModError("Error loading mod \"" + mod.name +
"\": Mod name does not follow naming conventions: "
"Only chararacters [a-z0-9_] are allowed.");
}
std::string script_path = mod.path + DIR_DELIM + "init.lua";
infostream << " [" << padStringRight(mod.name, 12) << "] [\""
<< script_path << "\"]" << std::endl;
m_script->loadMod(script_path, mod.name);
}
}
const std::string Client::getBuiltinLuaPath()
const std::string &Client::getBuiltinLuaPath()
{
return porting::path_share + DIR_DELIM + "builtin";
static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin";
return builtin_dir;
}
const std::string &Client::getClientModsLuaPath()
{
static const std::string clientmods_dir = porting::path_share + DIR_DELIM + "clientmods";
return clientmods_dir;
}
const std::vector<ModSpec>& Client::getMods() const

@ -433,7 +433,8 @@ public:
ClientEnvironment& getEnv() { return m_env; }
ITextureSource *tsrc() { return getTextureSource(); }
ISoundManager *sound() { return getSoundManager(); }
static const std::string getBuiltinLuaPath();
static const std::string &getBuiltinLuaPath();
static const std::string &getClientModsLuaPath();
virtual const std::vector<ModSpec> &getMods() const;
virtual const ModSpec* getModSpec(const std::string &modname) const;

@ -25,6 +25,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "subgame.h"
#include "settings.h"
#include "convert_json.h"
#include "exceptions.h"
#include "porting.h"
static bool parseDependsLine(std::istream &is,
std::string &dep, std::set<char> &symbols)
@ -107,28 +109,6 @@ std::map<std::string, ModSpec> getModsInPath(std::string path, bool part_of_modp
return result;
}
std::map<std::string, ModSpec> flattenModTree(std::map<std::string, ModSpec> mods)
{
std::map<std::string, ModSpec> result;
for(std::map<std::string,ModSpec>::iterator it = mods.begin();
it != mods.end(); ++it)
{
ModSpec mod = (*it).second;
if(mod.is_modpack)
{
std::map<std::string, ModSpec> content =
flattenModTree(mod.modpack_content);
result.insert(content.begin(),content.end());
result.insert(std::make_pair(mod.name,mod));
}
else //not a modpack
{
result.insert(std::make_pair(mod.name,mod));
}
}
return result;
}
std::vector<ModSpec> flattenMods(std::map<std::string, ModSpec> mods)
{
std::vector<ModSpec> result;
@ -151,78 +131,32 @@ std::vector<ModSpec> flattenMods(std::map<std::string, ModSpec> mods)
return result;
}
ModConfiguration::ModConfiguration(std::string worldpath)
ModConfiguration::ModConfiguration(const std::string &worldpath):
m_unsatisfied_mods(),
m_sorted_mods(),
m_name_conflicts()
{
SubgameSpec gamespec = findWorldSubgame(worldpath);
// Add all game mods and all world mods
addModsInPath(gamespec.gamemods_path);
addModsInPath(worldpath + DIR_DELIM + "worldmods");
// check world.mt file for mods explicitely declared to be
// loaded or not by a load_mod_<modname> = ... line.
std::string worldmt = worldpath+DIR_DELIM+"world.mt";
Settings worldmt_settings;
worldmt_settings.readConfigFile(worldmt.c_str());
std::vector<std::string> names = worldmt_settings.getNames();
std::set<std::string> include_mod_names;
for(std::vector<std::string>::iterator it = names.begin();
it != names.end(); ++it)
{
std::string name = *it;
// for backwards compatibility: exclude only mods which are
// explicitely excluded. if mod is not mentioned at all, it is
// enabled. So by default, all installed mods are enabled.
if (name.compare(0,9,"load_mod_") == 0 &&
worldmt_settings.getBool(name))
{
include_mod_names.insert(name.substr(9));
}
}
// Collect all mods that are also in include_mod_names
std::vector<ModSpec> addon_mods;
for(std::set<std::string>::const_iterator it_path = gamespec.addon_mods_paths.begin();
it_path != gamespec.addon_mods_paths.end(); ++it_path)
{
std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(*it_path));
for(std::vector<ModSpec>::iterator it = addon_mods_in_path.begin();
it != addon_mods_in_path.end(); ++it)
{
ModSpec& mod = *it;
if(include_mod_names.count(mod.name) != 0)
addon_mods.push_back(mod);
else
worldmt_settings.setBool("load_mod_" + mod.name, false);
}
}
worldmt_settings.updateConfigFile(worldmt.c_str());
addMods(addon_mods);
// report on name conflicts
if(!m_name_conflicts.empty()){
std::string s = "Unresolved name conflicts for mods ";
for(std::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::addModsInPath(std::string path)
void ModConfiguration::printUnsatisfiedModsError() const
{
for (std::vector<ModSpec>::const_iterator it = m_unsatisfied_mods.begin();
it != m_unsatisfied_mods.end(); ++it) {
ModSpec mod = *it;
errorstream << "mod \"" << mod.name << "\" has unsatisfied dependencies: ";
for (UNORDERED_SET<std::string>::iterator dep_it = mod.unsatisfied_depends.begin();
dep_it != mod.unsatisfied_depends.end(); ++dep_it)
errorstream << " \"" << *dep_it << "\"";
errorstream << std::endl;
}
}
void ModConfiguration::addModsInPath(const std::string &path)
{
addMods(flattenMods(getModsInPath(path)));
}
void ModConfiguration::addMods(std::vector<ModSpec> new_mods)
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.
@ -240,8 +174,8 @@ void ModConfiguration::addMods(std::vector<ModSpec> new_mods)
std::set<std::string> seen_this_iteration;
for(std::vector<ModSpec>::const_iterator it = new_mods.begin();
it != new_mods.end(); ++it){
for (std::vector<ModSpec>::const_iterator it = new_mods.begin();
it != new_mods.end(); ++it) {
const ModSpec &mod = *it;
if(mod.part_of_modpack != (bool)want_from_modpack)
continue;
@ -280,6 +214,24 @@ void ModConfiguration::addMods(std::vector<ModSpec> new_mods)
}
}
void ModConfiguration::checkConflictsAndDeps()
{
// report on name conflicts
if (!m_name_conflicts.empty()) {
std::string s = "Unresolved name conflicts for mods ";
for (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
@ -293,19 +245,19 @@ void ModConfiguration::resolveDependencies()
// of each mod, split mods into satisfied and unsatisfied
std::list<ModSpec> satisfied;
std::list<ModSpec> unsatisfied;
for(std::vector<ModSpec>::iterator it = m_unsatisfied_mods.begin();
it != m_unsatisfied_mods.end(); ++it){
for (std::vector<ModSpec>::iterator it = m_unsatisfied_mods.begin();
it != m_unsatisfied_mods.end(); ++it) {
ModSpec mod = *it;
mod.unsatisfied_depends = mod.depends;
// check which optional dependencies actually exist
for(std::set<std::string>::iterator it_optdep = mod.optdepends.begin();
it_optdep != mod.optdepends.end(); ++it_optdep){
for (UNORDERED_SET<std::string>::iterator it_optdep = mod.optdepends.begin();
it_optdep != mod.optdepends.end(); ++it_optdep) {
std::string optdep = *it_optdep;
if(modnames.count(optdep) != 0)
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())
if (mod.unsatisfied_depends.empty())
satisfied.push_back(mod);
else
unsatisfied.push_back(mod);
@ -335,6 +287,65 @@ void ModConfiguration::resolveDependencies()
m_unsatisfied_mods.assign(unsatisfied.begin(), unsatisfied.end());
}
ServerModConfiguration::ServerModConfiguration(const std::string &worldpath):
ModConfiguration(worldpath)
{
SubgameSpec gamespec = findWorldSubgame(worldpath);
// Add all game mods and all world mods
addModsInPath(gamespec.gamemods_path);
addModsInPath(worldpath + DIR_DELIM + "worldmods");
// check world.mt file for mods explicitely declared to be
// loaded or not by a load_mod_<modname> = ... line.
std::string worldmt = worldpath+DIR_DELIM+"world.mt";
Settings worldmt_settings;
worldmt_settings.readConfigFile(worldmt.c_str());
std::vector<std::string> names = worldmt_settings.getNames();
std::set<std::string> include_mod_names;
for (std::vector<std::string>::const_iterator it = names.begin();
it != names.end(); ++it) {
std::string name = *it;
// for backwards compatibility: exclude only mods which are
// explicitely excluded. if mod is not mentioned at all, it is
// enabled. So by default, all installed mods are enabled.
if (name.compare(0,9,"load_mod_") == 0 &&
worldmt_settings.getBool(name)) {
include_mod_names.insert(name.substr(9));
}
}
// Collect all mods that are also in include_mod_names
std::vector<ModSpec> addon_mods;
for (std::set<std::string>::const_iterator it_path = gamespec.addon_mods_paths.begin();
it_path != gamespec.addon_mods_paths.end(); ++it_path) {
std::vector<ModSpec> addon_mods_in_path = flattenMods(getModsInPath(*it_path));
for (std::vector<ModSpec>::const_iterator it = addon_mods_in_path.begin();
it != addon_mods_in_path.end(); ++it) {
const ModSpec& mod = *it;
if (include_mod_names.count(mod.name) != 0)
addon_mods.push_back(mod);
else
worldmt_settings.setBool("load_mod_" + mod.name, false);
}
}
worldmt_settings.updateConfigFile(worldmt.c_str());
addMods(addon_mods);
checkConflictsAndDeps();
}
#ifndef SERVER
ClientModConfiguration::ClientModConfiguration(const std::string &path):
ModConfiguration(path)
{
addModsInPath(path);
addModsInPath(porting::path_user + DIR_DELIM + "clientmods");
checkConflictsAndDeps();
}
#endif
#if USE_CURL
Json::Value getModstoreUrl(std::string url)
{

@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <string>
#include <map>
#include <json/json.h>
#include "util/cpp11_container.h"
#include "config.h"
#include "metadata.h"
@ -37,9 +38,9 @@ struct ModSpec
std::string name;
std::string path;
//if normal mod:
std::set<std::string> depends;
std::set<std::string> optdepends;
std::set<std::string> unsatisfied_depends;
UNORDERED_SET<std::string> depends;
UNORDERED_SET<std::string> optdepends;
UNORDERED_SET<std::string> unsatisfied_depends;
bool part_of_modpack;
bool is_modpack;
@ -62,12 +63,6 @@ void parseModContents(ModSpec &mod);
std::map<std::string,ModSpec> getModsInPath(std::string path, bool part_of_modpack = false);
// If failed, returned modspec has name==""
ModSpec findCommonMod(const std::string &modname);
// expands modpack contents, but does not replace them.
std::map<std::string, ModSpec> flattenModTree(std::map<std::string, ModSpec> mods);
// replaces modpack Modspecs with their content
std::vector<ModSpec> flattenMods(std::map<std::string,ModSpec> mods);
@ -77,17 +72,8 @@ std::vector<ModSpec> flattenMods(std::map<std::string,ModSpec> mods);
class ModConfiguration
{
public:
ModConfiguration():
m_unsatisfied_mods(),
m_sorted_mods(),
m_name_conflicts()
{}
ModConfiguration(std::string worldpath);
// checks if all dependencies are fullfilled.
bool isConsistent()
bool isConsistent() const
{
return m_unsatisfied_mods.empty();
}
@ -97,19 +83,24 @@ public:
return m_sorted_mods;
}
std::vector<ModSpec> getUnsatisfiedMods()
const std::vector<ModSpec> &getUnsatisfiedMods() const
{
return m_unsatisfied_mods;
}
private:
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(std::string path);
void addModsInPath(const std::string &path);
// adds all mods in the set.
void addMods(std::vector<ModSpec> new_mods);
void addMods(const std::vector<ModSpec> &new_mods);
void checkConflictsAndDeps();
private:
// move mods from m_unsatisfied_mods to m_sorted_mods
// in an order that satisfies dependencies
void resolveDependencies();
@ -132,10 +123,28 @@ private:
// 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::set<std::string> m_name_conflicts;
UNORDERED_SET<std::string> m_name_conflicts;
// Deleted default constructor
ModConfiguration() {}
};
class ServerModConfiguration: public ModConfiguration
{
public:
ServerModConfiguration(const std::string &worldpath);
};
#ifndef SERVER
class ClientModConfiguration: public ModConfiguration
{
public:
ClientModConfiguration(const std::string &path);
};
#endif
#if USE_CURL
Json::Value getModstoreUrl(std::string url);
#else

@ -218,20 +218,12 @@ Server::Server(
std::string ban_path = m_path_world + DIR_DELIM "ipban.txt";
m_banmanager = new BanManager(ban_path);
ModConfiguration modconf(m_path_world);
ServerModConfiguration modconf(m_path_world);
m_mods = modconf.getMods();
std::vector<ModSpec> unsatisfied_mods = modconf.getUnsatisfiedMods();
// complain about mods with unsatisfied dependencies
if(!modconf.isConsistent()) {
for(std::vector<ModSpec>::iterator it = unsatisfied_mods.begin();
it != unsatisfied_mods.end(); ++it) {
ModSpec mod = *it;
errorstream << "mod \"" << mod.name << "\" has unsatisfied dependencies: ";
for(std::set<std::string>::iterator dep_it = mod.unsatisfied_depends.begin();
dep_it != mod.unsatisfied_depends.end(); ++dep_it)
errorstream << " \"" << *dep_it << "\"";
errorstream << std::endl;
}
if (!modconf.isConsistent()) {
modconf.printUnsatisfiedModsError();
}
Settings worldmt_settings;
@ -271,20 +263,17 @@ Server::Server(
m_script = new ServerScripting(this);
std::string script_path = getBuiltinLuaPath() + DIR_DELIM "init.lua";
m_script->loadMod(script_path, BUILTIN_MOD_NAME);
m_script->loadMod(getBuiltinLuaPath() + DIR_DELIM "init.lua", BUILTIN_MOD_NAME);
// Print mods
infostream << "Server: Loading mods: ";
for(std::vector<ModSpec>::iterator i = m_mods.begin();
for (std::vector<ModSpec>::const_iterator i = m_mods.begin();
i != m_mods.end(); ++i) {
const ModSpec &mod = *i;
infostream << mod.name << " ";
infostream << (*i).name << " ";
}
infostream << std::endl;
// Load and run "mod" scripts
for (std::vector<ModSpec>::iterator it = m_mods.begin();
for (std::vector<ModSpec>::const_iterator it = m_mods.begin();
it != m_mods.end(); ++it) {
const ModSpec &mod = *it;
if (!string_allowed(mod.name, MODNAME_ALLOWED_CHARS)) {