Basic support for configuring which mods to load for each world

settings.h: added function to return all keys used in settings, and a
function to remove a setting

mods.{h,cpp}: added class ModConfiguration that represents a subset of the installed mods.

server.{h,cpp}: server does not load add-on mods that are disabled in
the world.mt file. mods are disabled by a setting of the form
"load_mod_<modname> = false". if no load_mod_<modname> = ... setting
is found, the mod is loaded anyways for backwards compatibilty. server
also complains to errorstream about mods with unstatisfied
dependencies and about mods that are not installed.

guiConfigureWorld.{h,cpp}: shows a treeview of installed add-on mods
and modpacks with little icons in front of their name indicating their
status: a checkmark for enabled mods, a cross for disabled mods, a
question mark for "new" mods

Mods can be enabled/disabled by a checkbox. Mods also show a list of
dependencies and reverse dependencies. double-click on a mod in
dependency or reverse dependency listbox selects the corresponding
mod. Enabling a mod also enables all its dependencies. Disabling a mod
also disables all its reverse dependencies.

For modpacks, show buttons to enable/disable all mods (recursively,
including their dependencies) in it.

Button "Save" saves the current settings to the world.mt file and
returns to the main menu. Button "Cancel" returns to main menu without
saving.

basic keyboard controls (if the proper widget has keyboard focus):

up/down: scroll through tree of mods
left/right: collaps/expand a modpack
space: enable/disable the selected mod
This commit is contained in:
Jürgen Doser 2012-12-08 18:10:54 +01:00 committed by Perttu Ahola
parent 6f93c01af9
commit 6af8a34d91
11 changed files with 1136 additions and 157 deletions

@ -275,6 +275,7 @@ set(minetest_SRCS
guiDeathScreen.cpp
guiChatConsole.cpp
guiCreateWorld.cpp
guiConfigureWorld.cpp
guiConfirmMenu.cpp
client.cpp
filecache.cpp

668
src/guiConfigureWorld.cpp Normal file

@ -0,0 +1,668 @@
/*
Minetest-c55
Copyright (C) 2012 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 <iostream>
#include <string>
#include <map>
#include "guiConfigureWorld.h"
#include "guiMessageMenu.h"
#include <IGUIButton.h>
#include <IGUICheckBox.h>
#include <IGUIListBox.h>
#include <IGUIStaticText.h>
#include <IGUITreeView.h>
#include "gettext.h"
#include "util/string.h"
#include "settings.h"
#include "filesys.h"
enum
{
GUI_ID_MOD_TREEVIEW = 101,
GUI_ID_ENABLED_CHECKBOX,
GUI_ID_ENABLEALL,
GUI_ID_DISABLEALL,
GUI_ID_DEPENDS_LISTBOX,
GUI_ID_RDEPENDS_LISTBOX,
GUI_ID_CANCEL,
GUI_ID_SAVE
};
#define QUESTIONMARK_STR L"?"
#define CHECKMARK_STR L"\411"
#define CROSS_STR L"\403"
GUIConfigureWorld::GUIConfigureWorld(gui::IGUIEnvironment* env,
gui::IGUIElement* parent, s32 id,
IMenuManager *menumgr, WorldSpec wspec):
GUIModalMenu(env, parent, id, menumgr),
m_wspec(wspec),
m_gspec(findWorldSubgame(m_wspec.path)),
m_menumgr(menumgr)
{
//will be initialized in regenerateGUI()
m_treeview=NULL;
// game mods
m_gamemods = flattenModTree(getModsInPath(m_gspec.gamemods_path));
// world mods
std::string worldmods_path = wspec.path + DIR_DELIM + "worldmods";
m_worldmods = flattenModTree(getModsInPath(worldmods_path));
// fill m_addontree with add-on mods
ModSpec addons("Add-Ons");
std::set<std::string> paths = m_gspec.addon_mods_paths;
for(std::set<std::string>::iterator it=paths.begin();
it != paths.end(); ++it)
{
std::map<std::string,ModSpec> mods = getModsInPath(*it);
addons.modpack_content.insert(mods.begin(), mods.end());
}
m_addontree.insert(std::make_pair(addons.name,addons));
// expand modpacks
m_addonmods = flattenModTree(m_addontree);
// collect reverse dependencies
for(std::map<std::string, ModSpec>::iterator it = m_addonmods.begin();
it != m_addonmods.end(); ++it)
{
std::string modname = (*it).first;
ModSpec mod = (*it).second;
for(std::set<std::string>::iterator dep_it = mod.depends.begin();
dep_it != mod.depends.end(); ++dep_it)
{
m_reverse_depends.insert(std::make_pair((*dep_it),modname));
}
}
m_settings.readConfigFile((m_wspec.path + DIR_DELIM + "world.mt").c_str());
std::vector<std::string> names = m_settings.getNames();
// mod_names contains the names of mods mentioned in the world.mt file
std::set<std::string> mod_names;
for(std::vector<std::string>::iterator it = names.begin();
it != names.end(); ++it)
{
std::string name = *it;
if (name.compare(0,9,"load_mod_")==0)
mod_names.insert(name.substr(9));
}
// find new mods (installed but not mentioned in world.mt)
for(std::map<std::string, ModSpec>::iterator it = m_addonmods.begin();
it != m_addonmods.end(); ++it)
{
std::string modname = (*it).first;
ModSpec mod = (*it).second;
// a mod is new if it is not a modpack, and does not occur in
// mod_names
if(mod.modpack_content.empty() &&
mod_names.count(modname) == 0)
m_new_mod_names.insert(modname);
}
if(!m_new_mod_names.empty())
{
GUIMessageMenu *menu =
new GUIMessageMenu(Environment, Parent, -1, m_menumgr,
wgettext("Warning: Some mods are not configured yet.\n"
"They will be enabled by default when you save the configuration. "));
menu->drop();
}
// find missing mods (mentioned in world.mt, but not installed)
std::set<std::string> missing_mods;
for(std::set<std::string>::iterator it = mod_names.begin();
it != mod_names.end(); ++it)
{
std::string modname = *it;
if(m_addonmods.count(modname) == 0)
missing_mods.insert(modname);
}
if(!missing_mods.empty())
{
GUIMessageMenu *menu =
new GUIMessageMenu(Environment, Parent, -1, m_menumgr,
wgettext("Warning: Some configured mods are missing.\n"
"Their setting will be removed when you save the configuration. "));
for(std::set<std::string>::iterator it = missing_mods.begin();
it != missing_mods.end(); ++it)
m_settings.remove("load_mod_"+(*it));
menu->drop();
}
}
void GUIConfigureWorld::drawMenu()
{
gui::IGUISkin* skin = Environment->getSkin();
if (!skin)
return;
video::IVideoDriver* driver = Environment->getVideoDriver();
video::SColor bgcolor(140,0,0,0);
driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect);
gui::IGUIElement::draw();
}
void GUIConfigureWorld::regenerateGui(v2u32 screensize)
{
/*
Remove stuff
*/
removeChildren();
/*
Calculate new sizes and positions
*/
core::rect<s32> rect(
screensize.X/2 - 580/2,
screensize.Y/2 - 300/2,
screensize.X/2 + 580/2,
screensize.Y/2 + 300/2
);
DesiredRect = rect;
recalculateAbsolutePosition(false);
v2s32 size = rect.getSize();
v2s32 topleft = v2s32(10, 10);
/*
Add stuff
*/
changeCtype("");
{
core::rect<s32> rect(0, 0, 200, 20);
rect += topleft;
//proper text is set below, when a mod is selected
m_modname_text = Environment->addStaticText(L"Mod: N/A", rect, false,
false, this, -1);
}
{
core::rect<s32> rect(0, 0, 200, 20);
rect += v2s32(0, 25) + topleft;
m_enabled_checkbox =
Environment->addCheckBox(false, rect, this, GUI_ID_ENABLED_CHECKBOX,
wgettext("enabled"));
m_enabled_checkbox->setVisible(false);
}
{
core::rect<s32> rect(0, 0, 85, 30);
rect = rect + v2s32(0, 25) + topleft;
m_enableall = Environment->addButton(rect, this, GUI_ID_ENABLEALL,
wgettext("Enable All"));
m_enableall->setVisible(false);
}
{
core::rect<s32> rect(0, 0, 85, 30);
rect = rect + v2s32(115, 25) + topleft;
m_disableall = Environment->addButton(rect, this, GUI_ID_DISABLEALL,
wgettext("Disable All"));
m_disableall->setVisible(false);
}
{
core::rect<s32> rect(0, 0, 200, 20);
rect += v2s32(0, 60) + topleft;
Environment->addStaticText(wgettext("depends on:"),
rect, false, false, this, -1);
}
{
core::rect<s32> rect(0, 0, 200, 85);
rect += v2s32(0, 80) + topleft;
m_dependencies_listbox =
Environment->addListBox(rect, this, GUI_ID_DEPENDS_LISTBOX, true);
}
{
core::rect<s32> rect(0, 0, 200, 20);
rect += v2s32(0, 175) + topleft;
Environment->addStaticText(wgettext("is required by:"),
rect, false, false, this, -1);
}
{
core::rect<s32> rect(0, 0, 200, 85);
rect += v2s32(0, 195) + topleft;
m_rdependencies_listbox =
Environment->addListBox(rect,this, GUI_ID_RDEPENDS_LISTBOX,true);
}
{
core::rect<s32> rect(0, 0, 340, 250);
rect += v2s32(220, 0) + topleft;
m_treeview = Environment->addTreeView(rect, this,
GUI_ID_MOD_TREEVIEW,true);
buildTreeView(m_addontree, m_treeview->getRoot());
}
{
core::rect<s32> rect(0, 0, 120, 30);
rect = rect + v2s32(330, 270) - topleft;
Environment->addButton(rect, this, GUI_ID_CANCEL,
wgettext("Cancel"));
}
{
core::rect<s32> rect(0, 0, 120, 30);
rect = rect + v2s32(460, 270) - topleft;
Environment->addButton(rect, this, GUI_ID_SAVE,
wgettext("Save"));
}
changeCtype("C");
// at start, none of the treeview nodes is selected, so we select
// the first element in the treeview of mods manually here.
if(m_treeview->getRoot()->hasChilds())
{
m_treeview->getRoot()->getFirstChild()->setExpanded(true);
m_treeview->getRoot()->getFirstChild()->setSelected(true);
// Because a manual ->setSelected() doesn't cause an event, we
// have to do this here:
adjustSidebar();
}
}
bool GUIConfigureWorld::OnEvent(const SEvent& event)
{
gui::IGUITreeViewNode* selected_node = NULL;
if(m_treeview != NULL)
selected_node = m_treeview->getSelected();
if(event.EventType==EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
{
switch (event.KeyInput.Key) {
case KEY_ESCAPE: {
quitMenu();
return true;
}
// irrlicht's built-in TreeView gui has no keyboard control,
// so we do it here: up/down to select prev/next node,
// left/right to collapse/expand nodes, space to toggle
// enabled/disabled.
case KEY_DOWN: {
if(selected_node != NULL)
{
gui::IGUITreeViewNode* node = selected_node->getNextVisible();
if(node != NULL)
{
node->setSelected(true);
adjustSidebar();
}
}
return true;
}
case KEY_UP: {
if(selected_node != NULL)
{
gui::IGUITreeViewNode* node = selected_node->getPrevSibling();
if(node!=NULL)
{
node->setSelected(true);
adjustSidebar();
}
else
{
gui::IGUITreeViewNode* parent = selected_node->getParent();
if(selected_node == parent->getFirstChild() &&
parent != m_treeview->getRoot())
{
parent->setSelected(true);
adjustSidebar();
}
}
}
return true;
}
case KEY_RIGHT: {
if(selected_node != NULL && selected_node->hasChilds())
selected_node->setExpanded(true);
return true;
}
case KEY_LEFT: {
if(selected_node != NULL && selected_node->hasChilds())
selected_node->setExpanded(false);
return true;
}
case KEY_SPACE: {
if(selected_node != NULL && !selected_node->hasChilds() &&
selected_node->getText() != NULL)
{
std::string modname = wide_to_narrow(selected_node->getText());
bool checked = m_enabled_checkbox->isChecked();
m_enabled_checkbox->setChecked(!checked);
setEnabled(modname,!checked);
}
return true;
}
default: {}
}
}
if(event.EventType==EET_GUI_EVENT)
{
if(event.GUIEvent.EventType==gui::EGET_ELEMENT_FOCUS_LOST
&& isVisible())
{
if(!canTakeFocus(event.GUIEvent.Element))
{
dstream<<"GUIConfigureWorld: Not allowing focus change."
<<std::endl;
// Returning true disables focus change
return true;
}
}
if(event.GUIEvent.EventType==gui::EGET_BUTTON_CLICKED){
switch(event.GUIEvent.Caller->getID()){
case GUI_ID_CANCEL: {
quitMenu();
return true;
}
case GUI_ID_SAVE: {
for(std::set<std::string>::iterator it = m_new_mod_names.begin();
it!= m_new_mod_names.end(); ++it)
{
m_settings.setBool("load_mod_"+(*it),true);
}
std::string worldmtfile = m_wspec.path+DIR_DELIM+"world.mt";
m_settings.updateConfigFile(worldmtfile.c_str());
// The trailing spaces are because there seems to be a
// bug in the text-size calculation. if the trailing
// spaces are removed from the message text, the
// message gets wrapped and parts of it are cut off:
GUIMessageMenu *menu =
new GUIMessageMenu(Environment, Parent, -1, m_menumgr,
wgettext("Configuration saved. "));
menu->drop();
ModConfiguration modconf(m_wspec.path);
if(!modconf.isConsistent())
{
GUIMessageMenu *menu =
new GUIMessageMenu(Environment, Parent, -1, m_menumgr,
wgettext("Warning: Configuration not consistent. "));
menu->drop();
}
quitMenu();
return true;
}
case GUI_ID_ENABLEALL: {
if(selected_node != NULL && selected_node->getText() != NULL)
{
std::string modname = wide_to_narrow(selected_node->getText());
ModSpec mod = m_addonmods[modname];
enableAllMods(mod.modpack_content,true);
}
return true;
}
case GUI_ID_DISABLEALL: {
if(selected_node != NULL && selected_node->getText() != NULL)
{
std::string modname = wide_to_narrow(selected_node->getText());
ModSpec mod = m_addonmods[modname];
enableAllMods(mod.modpack_content,false);
}
return true;
}
}
}
if(event.GUIEvent.EventType==gui::EGET_CHECKBOX_CHANGED &&
event.GUIEvent.Caller->getID() == GUI_ID_ENABLED_CHECKBOX)
{
if(selected_node != NULL && !selected_node->hasChilds() &&
selected_node->getText() != NULL)
{
std::string modname = wide_to_narrow(selected_node->getText());
setEnabled(modname, m_enabled_checkbox->isChecked());
}
return true;
}
if(event.GUIEvent.EventType==gui::EGET_TREEVIEW_NODE_SELECT &&
event.GUIEvent.Caller->getID() == GUI_ID_MOD_TREEVIEW)
{
selecting_dep = -1;
selecting_rdep = -1;
adjustSidebar();
return true;
}
if(event.GUIEvent.EventType==gui::EGET_LISTBOX_CHANGED &&
event.GUIEvent.Caller->getID() == GUI_ID_DEPENDS_LISTBOX)
{
selecting_dep = m_dependencies_listbox->getSelected();
selecting_rdep = -1;
return true;
}
if(event.GUIEvent.EventType==gui::EGET_LISTBOX_CHANGED &&
event.GUIEvent.Caller->getID() == GUI_ID_RDEPENDS_LISTBOX)
{
selecting_dep = -1;
selecting_rdep = m_rdependencies_listbox->getSelected();
return true;
}
//double click in a dependency listbox: find corresponding
//treeviewnode and select it:
if(event.GUIEvent.EventType==gui::EGET_LISTBOX_SELECTED_AGAIN)
{
gui::IGUIListBox* box = NULL;
if(event.GUIEvent.Caller->getID() == GUI_ID_DEPENDS_LISTBOX)
{
box = m_dependencies_listbox;
if(box->getSelected() != selecting_dep)
return true;
}
if(event.GUIEvent.Caller->getID() == GUI_ID_RDEPENDS_LISTBOX)
{
box = m_rdependencies_listbox;
if(box->getSelected() != selecting_rdep)
return true;
}
if(box != NULL && box->getSelected() != -1 &&
box->getListItem(box->getSelected()) != NULL)
{
std::string modname =
wide_to_narrow(box->getListItem(box->getSelected()));
std::map<std::string, gui::IGUITreeViewNode*>::iterator it =
m_nodes.find(modname);
if(it != m_nodes.end())
{
// select node and make sure node is visible by
// expanding all parents
gui::IGUITreeViewNode* node = (*it).second;
node->setSelected(true);
while(!node->isVisible() &&
node->getParent() != m_treeview->getRoot())
{
node = node->getParent();
node->setExpanded(true);
}
adjustSidebar();
}
}
return true;
}
}
return Parent ? Parent->OnEvent(event) : false;
}
void GUIConfigureWorld::buildTreeView(std::map<std::string, ModSpec> mods,
gui::IGUITreeViewNode* node)
{
for(std::map<std::string,ModSpec>::iterator it = mods.begin();
it != mods.end(); ++it)
{
std::string modname = (*it).first;
ModSpec mod = (*it).second;
gui::IGUITreeViewNode* new_node =
node->addChildBack(narrow_to_wide(modname).c_str());
m_nodes.insert(std::make_pair(modname, new_node));
if(!mod.modpack_content.empty())
buildTreeView(mod.modpack_content, new_node);
else
{
// set icon for node: ? for new mods, x for disabled mods,
// checkmark for enabled mods
if(m_new_mod_names.count(modname) > 0)
{
new_node->setIcon(QUESTIONMARK_STR);
}
else
{
bool mod_enabled = true;
if(m_settings.exists("load_mod_"+modname))
mod_enabled = m_settings.getBool("load_mod_"+modname);
if(mod_enabled)
new_node->setIcon(CHECKMARK_STR);
else
new_node->setIcon(CROSS_STR);
}
}
}
}
void GUIConfigureWorld::adjustSidebar()
{
gui::IGUITreeViewNode* node = m_treeview->getSelected();
std::wstring modname_w;
if(node->getText() != NULL)
modname_w = node->getText();
else
modname_w = L"N/A";
std::string modname = wide_to_narrow(modname_w);
// if modpack, show enable/disable all buttons. otherwise, show
// enabled checkbox
if(node->hasChilds())
{
m_enabled_checkbox->setVisible(false);
m_disableall->setVisible(true);
m_enableall->setVisible(true);
m_modname_text->setText((L"Modpack: "+modname_w).c_str());
}
else
{
m_disableall->setVisible(false);
m_enableall->setVisible(false);
m_enabled_checkbox->setVisible(true);
m_modname_text->setText((L"Mod: "+modname_w).c_str());
}
// the mod is enabled unless it is disabled in the world.mt settings.
bool mod_enabled = true;
if(m_settings.exists("load_mod_"+modname))
mod_enabled = m_settings.getBool("load_mod_"+modname);
m_enabled_checkbox->setChecked(mod_enabled);
// dependencies of this mod:
m_dependencies_listbox->clear();
ModSpec mspec = m_addonmods[modname];
for(std::set<std::string>::iterator it=mspec.depends.begin();
it != mspec.depends.end(); ++it)
{
// check if it is an add-on mod or a game/world mod. We only
// want to show add-ons
std::string dependency = (*it);
if(m_gamemods.count(dependency) > 0)
dependency += " (" + m_gspec.id + ")";
else if(m_worldmods.count(dependency) > 0)
dependency += " (" + m_wspec.name + ")";
else if(m_addonmods.count(dependency) == 0)
dependency += " (missing)";
m_dependencies_listbox->addItem(narrow_to_wide(dependency).c_str());
}
// reverse dependencies of this mod:
m_rdependencies_listbox->clear();
std::pair< std::multimap<std::string, std::string>::iterator,
std::multimap<std::string, std::string>::iterator > rdep =
m_reverse_depends.equal_range(modname);
for(std::multimap<std::string,std::string>::iterator it = rdep.first;
it != rdep.second; ++it)
{
// check if it is an add-on mod or a game/world mod. We only
// want to show add-ons
std::string rdependency = (*it).second;
if(m_addonmods.count(rdependency) > 0)
m_rdependencies_listbox->addItem(narrow_to_wide(rdependency).c_str());
}
}
void GUIConfigureWorld::enableAllMods(std::map<std::string, ModSpec> mods,bool enable)
{
for(std::map<std::string, ModSpec>::iterator it = mods.begin();
it != mods.end(); ++it)
{
ModSpec mod = (*it).second;
if(!mod.modpack_content.empty())
// a modpack, recursively enable all mods in it
enableAllMods(mod.modpack_content,enable);
else // not a modpack
setEnabled(mod.name, enable);
}
}
void GUIConfigureWorld::enableMod(std::string modname)
{
m_settings.setBool("load_mod_"+modname,true);
std::map<std::string,gui::IGUITreeViewNode*>::iterator it =
m_nodes.find(modname);
if(it != m_nodes.end())
(*it).second->setIcon(CHECKMARK_STR);
m_new_mod_names.erase(modname);
//also enable all dependencies
ModSpec mspec = m_addonmods[modname];
for(std::set<std::string>::iterator it=mspec.depends.begin();
it != mspec.depends.end(); ++it)
{
std::string dependency = *it;
// only enable it if it is an add-on mod
if(m_addonmods.count(dependency) > 0)
enableMod(dependency);
}
}
void GUIConfigureWorld::disableMod(std::string modname)
{
m_settings.setBool("load_mod_"+modname,false);
std::map<std::string,gui::IGUITreeViewNode*>::iterator it =
m_nodes.find(modname);
if(it != m_nodes.end())
(*it).second->setIcon(CROSS_STR);
m_new_mod_names.erase(modname);
//also disable all mods that depend on this one
std::pair<std::multimap<std::string, std::string>::iterator,
std::multimap<std::string, std::string>::iterator > rdep =
m_reverse_depends.equal_range(modname);
for(std::multimap<std::string,std::string>::iterator it = rdep.first;
it != rdep.second; ++it)
{
std::string rdependency = (*it).second;
// only disable it if it is an add-on mod
if(m_addonmods.count(rdependency) > 0)
disableMod(rdependency);
}
}

110
src/guiConfigureWorld.h Normal file

@ -0,0 +1,110 @@
/*
Minetest-c55
Copyright (C) 2012 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.
*/
#ifndef GUICONFIGUREWORLD_HEADER
#define GUICONFIGUREWORLD_HEADER
#include "irrlichttypes_extrabloated.h"
#include "modalMenu.h"
#include "mods.h"
#include "subgame.h"
#include "settings.h"
namespace irr{
namespace gui{
class IGUITreeViewNode;
}
}
class GUIConfigureWorld : public GUIModalMenu
{
public:
GUIConfigureWorld(gui::IGUIEnvironment* env,
gui::IGUIElement* parent, s32 id,
IMenuManager *menumgr, WorldSpec wspec);
void regenerateGui(v2u32 screensize);
void drawMenu();
bool OnEvent(const SEvent& event);
private:
WorldSpec m_wspec;
SubgameSpec m_gspec;
// tree of installed add-on mods. key is the mod name, modpacks
// are not expanded.
std::map<std::string, ModSpec> m_addontree;
// like m_addontree, but modpacks are expanded.
std::map<std::string, ModSpec> m_addonmods;
// list of game mods (flattened)
std::map<std::string, ModSpec> m_gamemods;
// list of world mods (flattened)
std::map<std::string, ModSpec> m_worldmods;
// for each mod, the set of mods depending on it
std::multimap<std::string, std::string> m_reverse_depends;
// the settings in the world.mt file
Settings m_settings;
// mods that are installed but not mentioned in world.mt file
std::set<std::string> m_new_mod_names;
// maps modnames to nodes in m_treeview
std::map<std::string,gui::IGUITreeViewNode*> m_nodes;
gui::IGUIStaticText* m_modname_text;
gui::IGUITreeView* m_treeview;
gui::IGUIButton* m_enableall;
gui::IGUIButton* m_disableall;
gui::IGUICheckBox* m_enabled_checkbox;
gui::IGUIListBox* m_dependencies_listbox;
gui::IGUIListBox* m_rdependencies_listbox;
void buildTreeView(std::map<std::string,ModSpec> mods,
gui::IGUITreeViewNode* node);
void adjustSidebar();
void enableAllMods(std::map<std::string,ModSpec> mods, bool enable);
void setEnabled(std::string modname, bool enable)
{
if(enable)
enableMod(modname);
else
disableMod(modname);
};
void enableMod(std::string modname);
void disableMod(std::string modname);
// hack to work around wonky handling of double-click in
// irrlicht. store selected index of listbox items here so event
// handling can check whether it was a real double click on the
// same item. (irrlicht also reports a double click if you rapidly
// select two different items.)
int selecting_dep;
int selecting_rdep;
IMenuManager* m_menumgr;
};
#endif

@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "guiMainMenu.h"
#include "guiKeyChangeMenu.h"
#include "guiCreateWorld.h"
#include "guiConfigureWorld.h"
#include "guiMessageMenu.h"
#include "guiConfirmMenu.h"
#include "debug.h"
@ -1033,12 +1034,23 @@ bool GUIMainMenu::OnEvent(const SEvent& event)
return true;
}
case GUI_ID_CONFIGURE_WORLD_BUTTON: {
GUIMessageMenu *menu = new GUIMessageMenu(env, parent,
-1, menumgr,
wgettext("Nothing here"));
MainMenuData cur;
readInput(&cur);
if(cur.selected_world == -1)
{
(new GUIMessageMenu(env, parent, -1, menumgr,
wgettext("Cannot configure world: Nothing selected"))
)->drop();
}
else
{
WorldSpec wspec = m_data->worlds[cur.selected_world];
GUIConfigureWorld *menu = new GUIConfigureWorld(env, parent,
-1, menumgr, wspec);
menu->drop();
return true;
}
}
case GUI_ID_SERVERLIST_DELETE: {
gui::IGUIListBox *serverlist = (gui::IGUIListBox*)getElementFromId(GUI_ID_SERVERLIST);
u16 selected = ((gui::IGUIListBox*)serverlist)->getSelected();

@ -18,23 +18,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/
#include "mods.h"
#include <queue>
#include <fstream>
#include <sstream>
#include <map>
#include "filesys.h"
#include "strfnd.h"
#include "log.h"
#include "subgame.h"
#include "settings.h"
static void collectMods(const std::string &modspath,
std::queue<ModSpec> &mods_satisfied,
core::list<ModSpec> &mods_unsorted,
std::map<std::string, std::string> &mod_names,
std::string indentation)
std::map<std::string, ModSpec> getModsInPath(std::string path)
{
TRACESTREAM(<<indentation<<"collectMods(\""<<modspath<<"\")"<<std::endl);
TRACEDO(indentation += " ");
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(modspath);
std::map<std::string, ModSpec> result;
std::vector<fs::DirListNode> dirlist = fs::GetDirListing(path);
for(u32 j=0; j<dirlist.size(); j++){
if(!dirlist[j].dir)
continue;
@ -43,119 +36,186 @@ static void collectMods(const std::string &modspath,
// VCS directories like ".git" or ".svn"
if(modname[0] == '.')
continue;
std::string modpath = modspath + DIR_DELIM + modname;
TRACESTREAM(<<indentation<<"collectMods: "<<modname<<" at \""<<modpath<<"\""<<std::endl);
std::string modpath = path + DIR_DELIM + modname;
// Handle modpacks (defined by containing modpack.txt)
{
std::ifstream is((modpath+DIR_DELIM+"modpack.txt").c_str(),
std::ifstream modpack_is((modpath+DIR_DELIM+"modpack.txt").c_str(),
std::ios_base::binary);
if(is.good()){
// Is a modpack
is.close(); // We don't actually need the file
// Recurse into it
collectMods(modpath, mods_satisfied, mods_unsorted, mod_names, indentation);
continue;
}
}
// Detect mod name conflicts
if(modpack_is.good()) //a modpack, recursively get the mods in it
{
std::map<std::string, std::string>::const_iterator i;
i = mod_names.find(modname);
if(i != mod_names.end()){
std::string s;
infostream<<indentation<<"WARNING: Mod name conflict detected: "
<<std::endl
<<"Already loaded: "<<i->second<<std::endl
<<"Will not load: "<<modpath<<std::endl;
continue;
}
modpack_is.close(); // We don't actually need the file
ModSpec spec(modname,modpath);
spec.modpack_content = getModsInPath(modpath);
result.insert(std::make_pair(modname,spec));
}
else // not a modpack, add the modspec
{
std::set<std::string> depends;
std::ifstream is((modpath+DIR_DELIM+"depends.txt").c_str(),
std::ios_base::binary);
while(is.good()){
while(is.good())
{
std::string dep;
std::getline(is, dep);
dep = trim(dep);
if(dep != "")
depends.insert(dep);
}
ModSpec spec(modname, modpath, depends);
mods_unsorted.push_back(spec);
if(depends.empty())
mods_satisfied.push(spec);
mod_names[modname] = modpath;
result.insert(std::make_pair(modname,spec));
}
}
return result;
}
// Get a dependency-sorted list of ModSpecs
core::list<ModSpec> getMods(core::list<std::string> &modspaths)
throw(ModError)
std::map<std::string, ModSpec> flattenModTree(std::map<std::string, ModSpec> mods)
{
std::queue<ModSpec> mods_satisfied;
core::list<ModSpec> mods_unsorted;
core::list<ModSpec> mods_sorted;
// name, path: For detecting name conflicts
std::map<std::string, std::string> mod_names;
for(core::list<std::string>::Iterator i = modspaths.begin();
i != modspaths.end(); i++){
std::string modspath = *i;
collectMods(modspath, mods_satisfied, mods_unsorted, mod_names, "");
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.modpack_content.empty()) //is a 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));
}
// Sort by depencencies
while(!mods_satisfied.empty()){
ModSpec mod = mods_satisfied.front();
mods_satisfied.pop();
mods_sorted.push_back(mod);
for(core::list<ModSpec>::Iterator i = mods_unsorted.begin();
i != mods_unsorted.end(); i++){
ModSpec &mod2 = *i;
if(mod2.unsatisfied_depends.empty())
continue;
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;
for(std::map<std::string,ModSpec>::iterator it = mods.begin();
it != mods.end(); ++it)
{
ModSpec mod = (*it).second;
if(!mod.modpack_content.empty()) //is a 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
{
// infostream << "inserting mod " << mod.name << std::endl;
result.push_back(mod);
}
}
return result;
}
std::vector<ModSpec> filterMods(std::vector<ModSpec> mods,
std::set<std::string> exclude_mod_names)
{
std::vector<ModSpec> result;
for(std::vector<ModSpec>::iterator it = mods.begin();
it != mods.end(); ++it)
{
ModSpec& mod = *it;
if(exclude_mod_names.count(mod.name) == 0)
result.push_back(mod);
}
return result;
}
void ModConfiguration::addModsInPathFiltered(std::string path, std::set<std::string> exclude_mods)
{
addMods(filterMods(flattenMods(getModsInPath(path)),exclude_mods));
}
void ModConfiguration::addMods(std::vector<ModSpec> new_mods)
{
// Step 1: remove mods in sorted_mods from unmet dependencies
// of new_mods. new mods without unmet dependencies are
// temporarily stored in satisfied_mods
std::vector<ModSpec> satisfied_mods;
for(std::vector<ModSpec>::iterator it = m_sorted_mods.begin();
it != m_sorted_mods.end(); ++it)
{
ModSpec mod = *it;
for(std::vector<ModSpec>::iterator it_new = new_mods.begin();
it_new != new_mods.end(); ++it_new)
{
ModSpec& mod_new = *it_new;
//infostream << "erasing dependency " << mod.name << " from " << mod_new.name << std::endl;
mod_new.unsatisfied_depends.erase(mod.name);
}
}
// split new mods into satisfied and unsatisfied
for(std::vector<ModSpec>::iterator it = new_mods.begin();
it != new_mods.end(); ++it)
{
ModSpec mod_new = *it;
if(mod_new.unsatisfied_depends.empty())
satisfied_mods.push_back(mod_new);
else
m_unsatisfied_mods.push_back(mod_new);
}
// Step 2: mods without unmet dependencies can be appended to
// the sorted list.
while(!satisfied_mods.empty())
{
ModSpec mod = satisfied_mods.back();
m_sorted_mods.push_back(mod);
satisfied_mods.pop_back();
for(std::list<ModSpec>::iterator it = m_unsatisfied_mods.begin();
it != m_unsatisfied_mods.end(); )
{
ModSpec& mod2 = *it;
mod2.unsatisfied_depends.erase(mod.name);
if(!mod2.unsatisfied_depends.empty())
continue;
mods_satisfied.push(mod2);
if(mod2.unsatisfied_depends.empty())
{
satisfied_mods.push_back(mod2);
it = m_unsatisfied_mods.erase(it);
}
else
++it;
}
}
std::ostringstream errs(std::ios::binary);
// Check unsatisfied dependencies
for(core::list<ModSpec>::Iterator i = mods_unsorted.begin();
i != mods_unsorted.end(); i++){
ModSpec &mod = *i;
if(mod.unsatisfied_depends.empty())
continue;
errs<<"mod \""<<mod.name
<<"\" has unsatisfied dependencies:";
for(std::set<std::string>::iterator
i = mod.unsatisfied_depends.begin();
i != mod.unsatisfied_depends.end(); i++){
errs<<" \""<<(*i)<<"\"";
}
errs<<"."<<std::endl;
mods_sorted.push_back(mod);
}
// If an error occurred, throw an exception
if(errs.str().size() != 0){
throw ModError(errs.str());
}
// Print out some debug info
TRACESTREAM(<<"Detected mods in load order:"<<std::endl);
for(core::list<ModSpec>::Iterator i = mods_sorted.begin();
i != mods_sorted.end(); i++){
ModSpec &mod = *i;
TRACESTREAM(<<"name=\""<<mod.name<<"\" path=\""<<mod.path<<"\"");
TRACESTREAM(<<" depends=[");
for(std::set<std::string>::iterator i = mod.depends.begin();
i != mod.depends.end(); i++)
TRACESTREAM(<<" "<<(*i));
TRACESTREAM(<<" ] unsatisfied_depends=[");
for(std::set<std::string>::iterator i = mod.unsatisfied_depends.begin();
i != mod.unsatisfied_depends.end(); i++)
TRACESTREAM(<<" "<<(*i));
TRACESTREAM(<<" ]"<<std::endl);
}
return mods_sorted;
}
ModConfiguration::ModConfiguration(std::string worldpath)
{
// Add all world mods and all game mods
addModsInPath(worldpath + DIR_DELIM + "worldmods");
SubgameSpec gamespec = findWorldSubgame(worldpath);
addModsInPath(gamespec.gamemods_path);
// 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> exclude_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))
{
exclude_mod_names.insert(name.substr(9));
}
}
for(std::set<std::string>::const_iterator i = gamespec.addon_mods_paths.begin();
i != gamespec.addon_mods_paths.end(); ++i)
addModsInPathFiltered((*i),exclude_mod_names);
}

@ -22,8 +22,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "irrlichttypes.h"
#include <irrList.h>
#include <list>
#include <set>
#include <vector>
#include <string>
#include <map>
#include <exception>
class ModError : public std::exception
@ -47,21 +50,93 @@ struct ModSpec
{
std::string name;
std::string path;
//if normal mod:
std::set<std::string> depends;
std::set<std::string> unsatisfied_depends;
ModSpec(const std::string &name_="", const std::string path_="",
const std::set<std::string> &depends_=std::set<std::string>()):
// if modpack:
std::map<std::string,ModSpec> modpack_content;
ModSpec(const std::string name_="", const std::string path_="",
const std::set<std::string> depends_=std::set<std::string>()):
name(name_),
path(path_),
depends(depends_),
unsatisfied_depends(depends_)
unsatisfied_depends(depends_),
modpack_content()
{}
};
// Get a dependency-sorted list of ModSpecs
core::list<ModSpec> getMods(core::list<std::string> &modspaths)
throw(ModError);
std::map<std::string,ModSpec> getModsInPath(std::string path);
// 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);
// removes Mods mentioned in exclude_mod_names
std::vector<ModSpec> filterMods(std::vector<ModSpec> mods,
std::set<std::string> exclude_mod_names);
// 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:
ModConfiguration():
m_unsatisfied_mods(),
m_sorted_mods()
{}
ModConfiguration(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)
{
addMods(flattenMods(getModsInPath(path)));
}
// adds all mods in the given path whose name does not appear
// in the exclude_mods set.
void addModsInPathFiltered(std::string path, std::set<std::string> exclude_mods);
// adds all mods in the set.
void addMods(std::vector<ModSpec> mods);
// checks if all dependencies are fullfilled.
bool isConsistent()
{
return m_unsatisfied_mods.empty();
}
std::vector<ModSpec> getMods()
{
return m_sorted_mods;
}
std::list<ModSpec> getUnsatisfiedMods()
{
return m_unsatisfied_mods;
}
private:
// mods with unmet dependencies. This is a list and not a
// vector because we want easy removal of elements at every
// position.
std::list<ModSpec> m_unsatisfied_mods;
// 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;
};
#endif

@ -980,27 +980,59 @@ Server::Server(
std::string rollback_path = m_path_world+DIR_DELIM+"rollback.txt";
m_rollback = createRollbackManager(rollback_path, this);
// Add world mod search path
m_modspaths.push_front(m_path_world + DIR_DELIM + "worldmods");
// Add addon mod search path
for(std::set<std::string>::const_iterator i = m_gamespec.mods_paths.begin();
i != m_gamespec.mods_paths.end(); i++)
m_modspaths.push_front((*i));
// Create world if it doesn't exist
if(!initializeWorld(m_path_world, m_gamespec.id))
throw ServerError("Failed to initialize world");
// Print out mod search paths
for(core::list<std::string>::Iterator i = m_modspaths.begin();
i != m_modspaths.end(); i++){
std::string modspath = *i;
infostream<<"- mods: "<<modspath<<std::endl;
ModConfiguration modconf(m_path_world);
m_mods = modconf.getMods();
// complain about mods with unsatisfied dependencies
if(!modconf.isConsistent())
{
errorstream << "The following mods have unsatisfied dependencies: ";
std::list<ModSpec> modlist = modconf.getUnsatisfiedMods();
for(std::list<ModSpec>::iterator it = modlist.begin();
it != modlist.end(); ++it)
{
errorstream << (*it).name << " ";
}
errorstream << std::endl;
}
Settings worldmt_settings;
std::string worldmt = m_path_world + DIR_DELIM + "world.mt";
worldmt_settings.readConfigFile(worldmt.c_str());
std::vector<std::string> names = worldmt_settings.getNames();
std::set<std::string> exclude_mod_names;
std::set<std::string> load_mod_names;
for(std::vector<std::string>::iterator it = names.begin();
it != names.end(); ++it)
{
std::string name = *it;
if (name.compare(0,9,"load_mod_")==0)
{
if(worldmt_settings.getBool(name))
load_mod_names.insert(name.substr(9));
else
exclude_mod_names.insert(name.substr(9));
}
}
// complain about mods declared to be loaded, but not found
for(std::vector<ModSpec>::iterator it = m_mods.begin();
it != m_mods.end(); ++it)
load_mod_names.erase((*it).name);
if(!load_mod_names.empty())
{
errorstream << "The following mods could not be found: ";
for(std::set<std::string>::iterator it = load_mod_names.begin();
it != load_mod_names.end(); ++it)
errorstream << (*it) << " ";
errorstream << std::endl;
}
// Path to builtin.lua
std::string builtinpath = getBuiltinLuaPath() + DIR_DELIM + "builtin.lua";
// Create world if it doesn't exist
if(!initializeWorld(m_path_world, m_gamespec.id))
throw ServerError("Failed to initialize world");
// Lock environment
JMutexAutoLock envlock(m_env_mutex);
JMutexAutoLock conlock(m_con_mutex);
@ -1021,18 +1053,16 @@ Server::Server(
<<builtinpath<<std::endl;
throw ModError("Failed to load and run "+builtinpath);
}
// Find mods in mod search paths
m_mods = getMods(m_modspaths);
// Print 'em
infostream<<"Server: Loading mods: ";
for(core::list<ModSpec>::Iterator i = m_mods.begin();
for(std::vector<ModSpec>::iterator i = m_mods.begin();
i != m_mods.end(); i++){
const ModSpec &mod = *i;
infostream<<mod.name<<" ";
}
infostream<<std::endl;
// Load and run "mod" scripts
for(core::list<ModSpec>::Iterator i = m_mods.begin();
for(std::vector<ModSpec>::iterator i = m_mods.begin();
i != m_mods.end(); i++){
const ModSpec &mod = *i;
std::string scriptpath = mod.path + DIR_DELIM + "init.lua";
@ -4105,7 +4135,7 @@ void Server::fillMediaCache()
// Collect all media file paths
std::list<std::string> paths;
for(core::list<ModSpec>::Iterator i = m_mods.begin();
for(std::vector<ModSpec>::iterator i = m_mods.begin();
i != m_mods.end(); i++){
const ModSpec &mod = *i;
paths.push_back(mod.path + DIR_DELIM + "textures");
@ -4770,7 +4800,7 @@ IWritableCraftDefManager* Server::getWritableCraftDefManager()
const ModSpec* Server::getModSpec(const std::string &modname)
{
for(core::list<ModSpec>::Iterator i = m_mods.begin();
for(std::vector<ModSpec>::iterator i = m_mods.begin();
i != m_mods.end(); i++){
const ModSpec &mod = *i;
if(mod.name == modname)
@ -4780,7 +4810,7 @@ const ModSpec* Server::getModSpec(const std::string &modname)
}
void Server::getModNames(core::list<std::string> &modlist)
{
for(core::list<ModSpec>::Iterator i = m_mods.begin(); i != m_mods.end(); i++)
for(std::vector<ModSpec>::iterator i = m_mods.begin(); i != m_mods.end(); i++)
{
modlist.push_back((*i).name);
}

@ -756,7 +756,7 @@ private:
EventManager *m_event;
// Mods
core::list<ModSpec> m_mods;
std::vector<ModSpec> m_mods;
/*
Threads

@ -72,6 +72,26 @@ public:
}
}
// return all keys used
std::vector<std::string> getNames(){
std::vector<std::string> names;
for(core::map<std::string, std::string>::Iterator
i = m_settings.getIterator();
i.atEnd() == false; i++)
{
std::string name = i.getNode()->getKey();
names.push_back(name);
}
return names;
}
// remove a setting
bool remove(const std::string& name)
{
return m_settings.remove(name);
}
bool parseConfigLine(const std::string &line)
{
JMutexAutoLock lock(m_mutex);

@ -74,9 +74,9 @@ SubgameSpec findSubgame(const std::string &id)
}
if(game_path == "")
return SubgameSpec();
std::string gamemod_path = game_path + DIR_DELIM + "mods";
// Find mod directories
std::set<std::string> mods_paths;
mods_paths.insert(game_path + DIR_DELIM + "mods");
if(!user_game)
mods_paths.insert(share + DIR_DELIM + "mods" + DIR_DELIM + id);
if(user != share || user_game)
@ -84,7 +84,7 @@ SubgameSpec findSubgame(const std::string &id)
std::string game_name = getGameName(game_path);
if(game_name == "")
game_name = id;
return SubgameSpec(id, game_path, mods_paths, game_name);
return SubgameSpec(id, game_path, gamemod_path, mods_paths, game_name);
}
SubgameSpec findWorldSubgame(const std::string &world_path)
@ -96,7 +96,7 @@ SubgameSpec findWorldSubgame(const std::string &world_path)
SubgameSpec gamespec;
gamespec.id = world_gameid;
gamespec.path = world_gamepath;
gamespec.mods_paths.insert(world_gamepath + DIR_DELIM + "mods");
gamespec.gamemods_path= world_gamepath + DIR_DELIM + "mods";
gamespec.name = getGameName(world_gamepath);
if(gamespec.name == "")
gamespec.name = "unknown";

@ -29,17 +29,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
struct SubgameSpec
{
std::string id; // "" = game does not exist
std::string path;
std::set<std::string> mods_paths;
std::string path; // path to game
std::string gamemods_path; //path to mods of the game
std::set<std::string> addon_mods_paths; //paths to addon mods for this game
std::string name;
SubgameSpec(const std::string &id_="",
const std::string &path_="",
const std::set<std::string> &mods_paths_=std::set<std::string>(),
const std::string &gamemods_path_="",
const std::set<std::string> &addon_mods_paths_=std::set<std::string>(),
const std::string &name_=""):
id(id_),
path(path_),
mods_paths(mods_paths_),
gamemods_path(gamemods_path_),
addon_mods_paths(addon_mods_paths_),
name(name_)
{}