Skip to content

Commit

Permalink
[CSM] implement client side mod loading (#5123)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nerzhul committed Mar 13, 2017
1 parent c42c53f commit 92b45b2
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 148 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions builtin/client/chatcommands.lua
Expand Up @@ -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)
1 change: 0 additions & 1 deletion builtin/client/init.lua
Expand Up @@ -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.")
Expand Down
File renamed without changes.
44 changes: 40 additions & 4 deletions src/client.cpp
Expand Up @@ -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);

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);
}
}

m_script->loadMod(script_path, BUILTIN_MOD_NAME);
const std::string &Client::getBuiltinLuaPath()
{
static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin";
return builtin_dir;
}

const std::string Client::getBuiltinLuaPath()
const std::string &Client::getClientModsLuaPath()
{
return porting::path_share + DIR_DELIM + "builtin";
static const std::string clientmods_dir = porting::path_share + DIR_DELIM + "clientmods";
return clientmods_dir;
}

const std::vector<ModSpec>& Client::getMods() const
Expand Down
3 changes: 2 additions & 1 deletion src/client.h
Expand Up @@ -433,7 +433,8 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
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;
Expand Down
197 changes: 104 additions & 93 deletions src/mods.cpp
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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);
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;
}

// get the mods in order
resolveDependencies();
}

void ModConfiguration::addModsInPath(std::string path)
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.
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down

0 comments on commit 92b45b2

Please sign in to comment.