diff --git a/source/server/lua/ServerModLoader.cpp b/source/server/lua/ServerModLoader.cpp index 73ec7b000..4a27ee1c4 100644 --- a/source/server/lua/ServerModLoader.cpp +++ b/source/server/lua/ServerModLoader.cpp @@ -38,7 +38,12 @@ namespace fs = ghc::filesystem; struct ModEntry { fs::path path; std::string id; + std::vector dependencies; + std::vector requiredBy; + + bool isLoaded = false; + bool isValid = true; }; void ServerModLoader::loadMods() { @@ -93,58 +98,75 @@ void ServerModLoader::loadMods() { gkError() << ("The mod at '" + entry.path().string() + "' doesn't contain a 'config.lua' file.").c_str(); } - { - // Small BFS to check cyclic dependencies - for (auto &modit : mods) { - std::queue queue; - queue.emplace(modit.first); - while (!queue.empty()) { - std::string modID = queue.front(); - queue.pop(); - - auto it = mods.find(modID); - if (it == mods.end()) - break; + std::queue dependencyTree; + + // Small BFS to check cyclic dependencies and build the dependency tree + for (auto &modit : mods) { + std::queue queue; + queue.emplace(&modit.second); + while (!queue.empty()) { + ModEntry *mod = queue.front(); + queue.pop(); - for (auto &dep : it->second.dependencies) { - if (dep == modit.first) - throw EXCEPTION("Cyclic dependency detected for mod '" - + modit.second.id + "' in mod '" + it->second.id + "'"); + if (mod->dependencies.empty()) + dependencyTree.emplace(mod); + + for (const std::string &dependencyID : mod->dependencies) { + if (dependencyID == modit.second.id) { + gkError() << ("Cyclic dependency detected for mod '" + modit.second.id + "' in mod '" + mod->id + "'").c_str(); + mod->isValid = false; + break; + } - queue.emplace(dep); + auto it = mods.find(dependencyID); + if (it != mods.end()) { + it->second.requiredBy.emplace_back(mod); + queue.emplace(&it->second); + } + else { + gkError() << ("Mod '" + mod->id + "' cannot be loaded: Missing dependency '" + dependencyID + "'").c_str(); + mod->isValid = false; } } } } - // TODO: Build a dependency graph - // Two types of dependencies should be handled + // TODO: Two types of dependencies should be handled // - Required (cyclic depedencies not handled) // - Optional (cyclic dependencies handled) - // TODO: Load 'mods/config.lua' to let user define a preferred mod path (see #82) - m_scriptEngine.init(); m_scriptEngine.luaCore().setModLoader(this); - for (auto &it : mods) { - if (fs::exists(it.second.path.string() + "/init.lua")) { - fs::current_path(it.second.path.string()); + while (!dependencyTree.empty()) { + ModEntry *mod = dependencyTree.front(); + dependencyTree.pop(); + + if (mod->isLoaded || !mod->isValid) continue; + + if (fs::exists(mod->path.string() + "/init.lua")) { + fs::current_path(mod->path.string()); try { m_scriptEngine.lua().safe_script_file("init.lua"); } catch (const sol::error &e) { - gkError() << "Error: Failed to load mod at" << it.second.path.string(); + gkError() << "Error: Failed to load mod at" << mod->path.string(); gkError() << e.what(); } fs::current_path(basePath); - gkInfo() << "Mod" << it.first << "loaded"; + gkInfo() << "Mod" << mod->id << "loaded"; } else - gkError() << ("The mod at '" + it.second.path.string() + "' doesn't contain an 'init.lua' file.").c_str(); + gkError() << ("The mod at '" + mod->path.string() + "' doesn't contain an 'init.lua' file.").c_str(); + + mod->isLoaded = true; + + for (ModEntry *entry : mod->requiredBy) { + dependencyTree.emplace(entry); + } } for (auto &it : m_mods) {