From 6af8a34d91582ce15a0341e75d3e55d847a5a47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Doser?= Date: Sat, 8 Dec 2012 18:10:54 +0100 Subject: [PATCH] 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_ = false". if no load_mod_ = ... 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 --- src/CMakeLists.txt | 1 + src/guiConfigureWorld.cpp | 668 ++++++++++++++++++++++++++++++++++++++ src/guiConfigureWorld.h | 110 +++++++ src/guiMainMenu.cpp | 22 +- src/mods.cpp | 282 +++++++++------- src/mods.h | 89 ++++- src/server.cpp | 80 +++-- src/server.h | 2 +- src/settings.h | 20 ++ src/subgame.cpp | 6 +- src/subgame.h | 13 +- 11 files changed, 1136 insertions(+), 157 deletions(-) create mode 100644 src/guiConfigureWorld.cpp create mode 100644 src/guiConfigureWorld.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e7d29ff0..ac39e43e3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -275,6 +275,7 @@ set(minetest_SRCS guiDeathScreen.cpp guiChatConsole.cpp guiCreateWorld.cpp + guiConfigureWorld.cpp guiConfirmMenu.cpp client.cpp filecache.cpp diff --git a/src/guiConfigureWorld.cpp b/src/guiConfigureWorld.cpp new file mode 100644 index 000000000..a77697f8b --- /dev/null +++ b/src/guiConfigureWorld.cpp @@ -0,0 +1,668 @@ +/* +Minetest-c55 +Copyright (C) 2012 celeron55, Perttu Ahola + +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 +#include +#include + +#include "guiConfigureWorld.h" +#include "guiMessageMenu.h" +#include +#include +#include +#include +#include +#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 paths = m_gspec.addon_mods_paths; + for(std::set::iterator it=paths.begin(); + it != paths.end(); ++it) + { + std::map 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::iterator it = m_addonmods.begin(); + it != m_addonmods.end(); ++it) + { + std::string modname = (*it).first; + ModSpec mod = (*it).second; + for(std::set::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 names = m_settings.getNames(); + + // mod_names contains the names of mods mentioned in the world.mt file + std::set mod_names; + for(std::vector::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::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 missing_mods; + for(std::set::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::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 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 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 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 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 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 rect(0, 0, 200, 20); + rect += v2s32(0, 60) + topleft; + Environment->addStaticText(wgettext("depends on:"), + rect, false, false, this, -1); + } + { + core::rect rect(0, 0, 200, 85); + rect += v2s32(0, 80) + topleft; + m_dependencies_listbox = + Environment->addListBox(rect, this, GUI_ID_DEPENDS_LISTBOX, true); + } + { + core::rect rect(0, 0, 200, 20); + rect += v2s32(0, 175) + topleft; + Environment->addStaticText(wgettext("is required by:"), + rect, false, false, this, -1); + } + { + core::rect rect(0, 0, 200, 85); + rect += v2s32(0, 195) + topleft; + m_rdependencies_listbox = + Environment->addListBox(rect,this, GUI_ID_RDEPENDS_LISTBOX,true); + } + { + core::rect 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 rect(0, 0, 120, 30); + rect = rect + v2s32(330, 270) - topleft; + Environment->addButton(rect, this, GUI_ID_CANCEL, + wgettext("Cancel")); + } + { + core::rect 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." + <getID()){ + case GUI_ID_CANCEL: { + quitMenu(); + return true; + } + case GUI_ID_SAVE: { + for(std::set::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::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 mods, + gui::IGUITreeViewNode* node) +{ + for(std::map::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::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::iterator, + std::multimap::iterator > rdep = + m_reverse_depends.equal_range(modname); + for(std::multimap::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 mods,bool enable) +{ + for(std::map::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::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::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::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::iterator, + std::multimap::iterator > rdep = + m_reverse_depends.equal_range(modname); + for(std::multimap::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); + } +} + diff --git a/src/guiConfigureWorld.h b/src/guiConfigureWorld.h new file mode 100644 index 000000000..2280c6dbe --- /dev/null +++ b/src/guiConfigureWorld.h @@ -0,0 +1,110 @@ +/* +Minetest-c55 +Copyright (C) 2012 celeron55, Perttu Ahola + +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 m_addontree; + + // like m_addontree, but modpacks are expanded. + std::map m_addonmods; + + // list of game mods (flattened) + std::map m_gamemods; + + // list of world mods (flattened) + std::map m_worldmods; + + // for each mod, the set of mods depending on it + std::multimap 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 m_new_mod_names; + + // maps modnames to nodes in m_treeview + std::map 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 mods, + gui::IGUITreeViewNode* node); + void adjustSidebar(); + void enableAllMods(std::map 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 diff --git a/src/guiMainMenu.cpp b/src/guiMainMenu.cpp index ca0c1317c..77a5a85b8 100644 --- a/src/guiMainMenu.cpp +++ b/src/guiMainMenu.cpp @@ -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,11 +1034,22 @@ 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")); - menu->drop(); - return true; + 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); diff --git a/src/mods.cpp b/src/mods.cpp index 08e8e276f..9e3c7d5d7 100644 --- a/src/mods.cpp +++ b/src/mods.cpp @@ -18,23 +18,16 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "mods.h" -#include -#include -#include -#include #include "filesys.h" #include "strfnd.h" #include "log.h" +#include "subgame.h" +#include "settings.h" -static void collectMods(const std::string &modspath, - std::queue &mods_satisfied, - core::list &mods_unsorted, - std::map &mod_names, - std::string indentation) +std::map getModsInPath(std::string path) { - TRACESTREAM(< dirlist = fs::GetDirListing(modspath); + std::map result; + std::vector dirlist = fs::GetDirListing(path); for(u32 j=0; j::const_iterator i; - i = mod_names.find(modname); - if(i != mod_names.end()){ - std::string s; - infostream<second< depends; - std::ifstream is((modpath+DIR_DELIM+"depends.txt").c_str(), + else // not a modpack, add the modspec + { + std::set depends; + std::ifstream is((modpath+DIR_DELIM+"depends.txt").c_str(), std::ios_base::binary); - while(is.good()){ - std::string dep; - std::getline(is, dep); - dep = trim(dep); - if(dep != "") - depends.insert(dep); + while(is.good()) + { + std::string dep; + std::getline(is, dep); + dep = trim(dep); + if(dep != "") + depends.insert(dep); + } + + ModSpec spec(modname, modpath, depends); + result.insert(std::make_pair(modname,spec)); } - ModSpec spec(modname, modpath, depends); - mods_unsorted.push_back(spec); - if(depends.empty()) - mods_satisfied.push(spec); - mod_names[modname] = modpath; } + return result; } -// Get a dependency-sorted list of ModSpecs -core::list getMods(core::list &modspaths) - throw(ModError) +std::map flattenModTree(std::map mods) { - std::queue mods_satisfied; - core::list mods_unsorted; - core::list mods_sorted; - // name, path: For detecting name conflicts - std::map mod_names; - for(core::list::Iterator i = modspaths.begin(); - i != modspaths.end(); i++){ - std::string modspath = *i; - collectMods(modspath, mods_satisfied, mods_unsorted, mod_names, ""); - } - // Sort by depencencies - while(!mods_satisfied.empty()){ - ModSpec mod = mods_satisfied.front(); - mods_satisfied.pop(); - mods_sorted.push_back(mod); - for(core::list::Iterator i = mods_unsorted.begin(); - i != mods_unsorted.end(); i++){ - ModSpec &mod2 = *i; - if(mod2.unsatisfied_depends.empty()) - continue; - mod2.unsatisfied_depends.erase(mod.name); - if(!mod2.unsatisfied_depends.empty()) - continue; - mods_satisfied.push(mod2); + std::map result; + for(std::map::iterator it = mods.begin(); + it != mods.end(); ++it) + { + ModSpec mod = (*it).second; + if(!mod.modpack_content.empty()) //is a modpack + { + std::map 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)); } } - std::ostringstream errs(std::ios::binary); - // Check unsatisfied dependencies - for(core::list::Iterator i = mods_unsorted.begin(); - i != mods_unsorted.end(); i++){ - ModSpec &mod = *i; - if(mod.unsatisfied_depends.empty()) - continue; - errs<<"mod \""<::iterator - i = mod.unsatisfied_depends.begin(); - i != mod.unsatisfied_depends.end(); i++){ - errs<<" \""<<(*i)<<"\""; + return result; +} + +std::vector flattenMods(std::map mods) +{ + std::vector result; + for(std::map::iterator it = mods.begin(); + it != mods.end(); ++it) + { + ModSpec mod = (*it).second; + if(!mod.modpack_content.empty()) //is a modpack + { + std::vector 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); } - errs<<"."<::Iterator i = mods_sorted.begin(); - i != mods_sorted.end(); i++){ - ModSpec &mod = *i; - TRACESTREAM(<<"name=\""<::iterator i = mod.depends.begin(); - i != mod.depends.end(); i++) - TRACESTREAM(<<" "<<(*i)); - TRACESTREAM(<<" ] unsatisfied_depends=["); - for(std::set::iterator i = mod.unsatisfied_depends.begin(); - i != mod.unsatisfied_depends.end(); i++) - TRACESTREAM(<<" "<<(*i)); - TRACESTREAM(<<" ]"< filterMods(std::vector mods, + std::set exclude_mod_names) +{ + std::vector result; + for(std::vector::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 exclude_mods) +{ + addMods(filterMods(flattenMods(getModsInPath(path)),exclude_mods)); } +void ModConfiguration::addMods(std::vector 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 satisfied_mods; + for(std::vector::iterator it = m_sorted_mods.begin(); + it != m_sorted_mods.end(); ++it) + { + ModSpec mod = *it; + for(std::vector::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::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::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()) + { + satisfied_mods.push_back(mod2); + it = m_unsatisfied_mods.erase(it); + } + else + ++it; + } + } +} + +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_ = ... line. + std::string worldmt = worldpath+DIR_DELIM+"world.mt"; + Settings worldmt_settings; + worldmt_settings.readConfigFile(worldmt.c_str()); + std::vector names = worldmt_settings.getNames(); + std::set exclude_mod_names; + for(std::vector::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::const_iterator i = gamespec.addon_mods_paths.begin(); + i != gamespec.addon_mods_paths.end(); ++i) + addModsInPathFiltered((*i),exclude_mod_names); +} diff --git a/src/mods.h b/src/mods.h index d55dd6c92..37e2cd2db 100644 --- a/src/mods.h +++ b/src/mods.h @@ -22,8 +22,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "irrlichttypes.h" #include +#include #include +#include #include +#include #include class ModError : public std::exception @@ -47,21 +50,93 @@ struct ModSpec { std::string name; std::string path; + //if normal mod: std::set depends; std::set unsatisfied_depends; - - ModSpec(const std::string &name_="", const std::string path_="", - const std::set &depends_=std::set()): + // if modpack: + std::map modpack_content; + ModSpec(const std::string name_="", const std::string path_="", + const std::set depends_=std::set()): name(name_), path(path_), depends(depends_), - unsatisfied_depends(depends_) + unsatisfied_depends(depends_), + modpack_content() {} }; -// Get a dependency-sorted list of ModSpecs -core::list getMods(core::list &modspaths) - throw(ModError); + +std::map getModsInPath(std::string path); + +// expands modpack contents, but does not replace them. +std::map flattenModTree(std::map mods); + +// replaces modpack Modspecs with their content +std::vector flattenMods(std::map mods); + +// removes Mods mentioned in exclude_mod_names +std::vector filterMods(std::vector mods, + std::set 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 exclude_mods); + + // adds all mods in the set. + void addMods(std::vector mods); + + // checks if all dependencies are fullfilled. + bool isConsistent() + { + return m_unsatisfied_mods.empty(); + } + + std::vector getMods() + { + return m_sorted_mods; + } + + std::list 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 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 m_sorted_mods; + +}; + #endif diff --git a/src/server.cpp b/src/server.cpp index 0a8288324..853d0a486 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -980,33 +980,65 @@ 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::const_iterator i = m_gamespec.mods_paths.begin(); - i != m_gamespec.mods_paths.end(); i++) - m_modspaths.push_front((*i)); - - // Print out mod search paths - for(core::list::Iterator i = m_modspaths.begin(); - i != m_modspaths.end(); i++){ - std::string modspath = *i; - infostream<<"- mods: "< modlist = modconf.getUnsatisfiedMods(); + for(std::list::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 names = worldmt_settings.getNames(); + std::set exclude_mod_names; + std::set load_mod_names; + for(std::vector::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::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::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"; + // Lock environment JMutexAutoLock envlock(m_env_mutex); JMutexAutoLock conlock(m_con_mutex); // Initialize scripting - + infostream<<"Server: Initializing Lua"<::Iterator i = m_mods.begin(); + for(std::vector::iterator i = m_mods.begin(); i != m_mods.end(); i++){ const ModSpec &mod = *i; infostream<::Iterator i = m_mods.begin(); + for(std::vector::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 paths; - for(core::list::Iterator i = m_mods.begin(); + for(std::vector::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::Iterator i = m_mods.begin(); + for(std::vector::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 &modlist) { - for(core::list::Iterator i = m_mods.begin(); i != m_mods.end(); i++) + for(std::vector::iterator i = m_mods.begin(); i != m_mods.end(); i++) { modlist.push_back((*i).name); } diff --git a/src/server.h b/src/server.h index 43beb167a..26e47d36c 100644 --- a/src/server.h +++ b/src/server.h @@ -756,7 +756,7 @@ private: EventManager *m_event; // Mods - core::list m_mods; + std::vector m_mods; /* Threads diff --git a/src/settings.h b/src/settings.h index 1ab6fcc6b..1a29ef00a 100644 --- a/src/settings.h +++ b/src/settings.h @@ -71,6 +71,26 @@ public: os< getNames(){ + std::vector names; + for(core::map::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) { diff --git a/src/subgame.cpp b/src/subgame.cpp index c36189933..2f5340373 100644 --- a/src/subgame.cpp +++ b/src/subgame.cpp @@ -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 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"; diff --git a/src/subgame.h b/src/subgame.h index bffa86e28..dd725caf7 100644 --- a/src/subgame.h +++ b/src/subgame.h @@ -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 mods_paths; + std::string path; // path to game + std::string gamemods_path; //path to mods of the game + std::set 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 &mods_paths_=std::set(), + const std::string &path_="", + const std::string &gamemods_path_="", + const std::set &addon_mods_paths_=std::set(), const std::string &name_=""): id(id_), path(path_), - mods_paths(mods_paths_), + gamemods_path(gamemods_path_), + addon_mods_paths(addon_mods_paths_), name(name_) {}