diff --git a/include/dusk/mod_loader.hpp b/include/dusk/mod_loader.hpp index 04f896c3ab..e0ec830474 100644 --- a/include/dusk/mod_loader.hpp +++ b/include/dusk/mod_loader.hpp @@ -22,11 +22,16 @@ struct RmlTabUpdateCallback { void* userdata; }; -struct LoadedMod { +struct ModMetadata { + std::string id; std::string name; std::string version; std::string author; std::string description; +}; + +struct LoadedMod { + ModMetadata metadata; std::string mod_path; std::string dir; diff --git a/src/dusk/modding/mod_loader.cpp b/src/dusk/modding/mod_loader.cpp index 18a65043c1..02d6759cc9 100644 --- a/src/dusk/modding/mod_loader.cpp +++ b/src/dusk/modding/mod_loader.cpp @@ -74,6 +74,7 @@ static constexpr const char* k_libExt = ".so"; #endif using namespace dusk::modding; +using namespace std::string_literals; using namespace std::string_view_literals; #if defined(_M_ARM64) || defined(__aarch64__) @@ -106,7 +107,7 @@ struct ModGuard { }; static const char* modName() { - return g_currentMod ? g_currentMod->name.c_str() : "mod"; + return g_currentMod ? g_currentMod->metadata.id.c_str() : "mod"; } static void cb_log_info(const char* fmt, ...) { @@ -156,7 +157,7 @@ static void* cb_load_resource(const char* relative_path, size_t* out_size) { try { data = g_currentMod->bundle->readFile(entry); } catch (const std::runtime_error& e) { - DuskLog.error("[{}] load_resource: '{}' failed: {}", g_currentMod->name, entry, e.what()); + DuskLog.error("[{}] load_resource: '{}' failed: {}", g_currentMod->metadata.id, entry, e.what()); return nullptr; } @@ -415,6 +416,50 @@ static DllLocateResult LocateDllInBundle(ModBundle& bundle) { return DllLocateResult{dllEntry, dllFallback}; } +class InvalidModDataException : public std::runtime_error { +public: + explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {} + explicit InvalidModDataException(const char* msg) : runtime_error(msg) {} +}; + +static ModMetadata loadMetadata(const std::filesystem::path& modPath, ModBundle& bundle) { + const auto metaJson = bundle.readFile("mod.json"); + auto j = nlohmann::json::parse(metaJson); + + std::string metaId = j.value("id", ""); + std::string metaName = j.value("name", ""); + std::string metaVersion = j.value("version", ""); + std::string metaAuthor = j.value("author", ""); + std::string metaDescription = j.value("description", ""); + + if (metaId.empty()) { + throw InvalidModDataException("Missing ID value in mod metadata!"); + } + if (metaName.empty()) { + metaName = io::fs_path_to_string(modPath.stem()); + } + if (metaVersion.empty()) { + metaVersion = "?"s; + } + if (metaAuthor.empty()) { + metaAuthor = "unknown"s; + } + + return ModMetadata{ + std::move(metaId), + std::move(metaName), + std::move(metaVersion), + std::move(metaAuthor), + std::move(metaDescription), + }; +} + +static bool checkDuplicateMod(const ModMetadata& metadata, const std::vector& mods) { + return std::ranges::any_of(mods, [&](const LoadedMod& mod) { + return mod.metadata.id == metadata.id; + }); +} + void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) { namespace fs = std::filesystem; @@ -426,15 +471,10 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) return; } - std::string metaName, metaVersion, metaAuthor, metaDescription; + ModMetadata metadata; try { - const auto metaJson = bundle->readFile("mod.json"); - auto j = nlohmann::json::parse(metaJson); - metaName = j.value("name", ""); - metaVersion = j.value("version", ""); - metaAuthor = j.value("author", ""); - metaDescription = j.value("description", ""); + metadata = loadMetadata(modPath, *bundle); } catch (const std::runtime_error& e) { Log.error( @@ -442,6 +482,14 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) return; } + if (checkDuplicateMod(metadata, m_mods)) { + Log.error( + "ModLoader: mod with id '{}' already exists, not loading {}", + metadata.id, + io::fs_path_to_string(modPath.filename())); + return; + } + auto [dllEntry, dllFallback] = LocateDllInBundle(*bundle); if (dllEntry.empty()) { dllEntry = dllFallback; @@ -509,16 +557,19 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) return; } - mod.name = metaName.empty() ? io::fs_path_to_string(modPath.stem()) : metaName; - mod.version = metaVersion.empty() ? "?" : metaVersion; - mod.author = metaAuthor.empty() ? "unknown" : metaAuthor; - mod.description = metaDescription; + mod.metadata = std::move(metadata); mod.bundle = std::move(bundle); - m_mods.push_back(std::move(mod)); - DuskLog.info("ModLoader: found '{}' v{} by {} ({})", m_mods.back().name, m_mods.back().version, - m_mods.back().author, io::fs_path_to_string(modPath.filename())); + const auto& inserted = m_mods.emplace_back(std::move(mod)); + + DuskLog.info( + "ModLoader: found '{}' ('{}') v{} by {} ({})", + inserted.metadata.name, + inserted.metadata.id, + inserted.metadata.version, + inserted.metadata.author, + io::fs_path_to_string(modPath.filename())); } void ModLoader::init() { @@ -571,14 +622,14 @@ void ModLoader::init() { mod.fn_init(&mod.api); if (!mod.load_failed) { mod.active = true; - DuskLog.info("ModLoader: '{}' initialized", mod.name); + DuskLog.info("ModLoader: '{}' initialized", mod.metadata.id); } else { - DuskLog.error("ModLoader: '{}' failed to load due to hook conflicts", mod.name); + DuskLog.error("ModLoader: '{}' failed to load due to hook conflicts", mod.metadata.id); } } catch (const std::exception& e) { - DuskLog.error("ModLoader: exception in {}.mod_init(): {}", mod.name, e.what()); + DuskLog.error("ModLoader: exception in {}.mod_init(): {}", mod.metadata.id, e.what()); } catch (...) { - DuskLog.error("ModLoader: unknown exception in {}.mod_init()", mod.name); + DuskLog.error("ModLoader: unknown exception in {}.mod_init()", mod.metadata.id); } } @@ -597,10 +648,10 @@ void ModLoader::tick() { mod.fn_tick(&mod.api); } catch (const std::exception& e) { DuskLog.error( - "ModLoader: exception in {}.mod_tick(): {} — disabling", mod.name, e.what()); + "ModLoader: exception in {}.mod_tick(): {} — disabling", mod.metadata.id, e.what()); mod.active = false; } catch (...) { - DuskLog.error("ModLoader: unknown exception in {}.mod_tick() — disabling", mod.name); + DuskLog.error("ModLoader: unknown exception in {}.mod_tick() — disabling", mod.metadata.id); mod.active = false; } } diff --git a/src/dusk/ui/mods_window.cpp b/src/dusk/ui/mods_window.cpp index 6f4c1b70ab..6c7053584b 100644 --- a/src/dusk/ui/mods_window.cpp +++ b/src/dusk/ui/mods_window.cpp @@ -38,8 +38,8 @@ Rml::String build_mod_detail_rml(const dusk::LoadedMod& mod) { R"(Path)" R"({})" R"()", - mod.version, - mod.author, + mod.metadata.version, + mod.metadata.author, statusClass, statusText, mod.mod_path ); @@ -62,7 +62,7 @@ ModsWindow::ModsWindow() { for (size_t i = 0; i < mods.size(); ++i) { mSnapshot.push_back({mods[i].active, mods[i].load_failed}); - add_tab(mods[i].name, [this, i](Rml::Element* content) { + add_tab(mods[i].metadata.name, [this, i](Rml::Element* content) { mActiveModIndex = static_cast(i); const auto& curMods = dusk::ModLoader::instance().mods(); @@ -76,9 +76,9 @@ ModsWindow::ModsWindow() { pane.add_section("Details"); pane.add_rml(build_mod_detail_rml(mod)); - if (!mod.description.empty()) { + if (!mod.metadata.description.empty()) { pane.add_section("Description"); - pane.add_text(mod.description); + pane.add_text(mod.metadata.description); } for (const auto& cb : mod.tab_content) { diff --git a/tools/mod_template/mod.json b/tools/mod_template/mod.json index 0ebe9f5176..44364017ee 100644 --- a/tools/mod_template/mod.json +++ b/tools/mod_template/mod.json @@ -1,4 +1,5 @@ { + "id": "example.template_mod", "name": "Template Mod", "version": "1.0.0", "author": "Maddie", diff --git a/tools/mod_test/mod.json b/tools/mod_test/mod.json index 0a168a6c22..0e36ab47a2 100644 --- a/tools/mod_test/mod.json +++ b/tools/mod_test/mod.json @@ -1,4 +1,5 @@ { + "id": "dev.twilitrealm.test_mod", "name": "API Test Mod", "version": "1.0.0", "author": "dusk",