From bc11bf356359fa5883ecd5ed84a98fe76fcecb63 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 8 Jul 2026 17:51:13 -0600 Subject: [PATCH] Mods manager UI & logs viewer --- docs/modding.md | 6 +- files.cmake | 12 +- include/dusk/mod_loader.hpp | 8 +- res/rml/logs.rcss | 97 +++++++ res/rml/mods.rcss | 218 ++++++++++++++++ res/rml/overlay.rcss | 8 + res/rml/prelaunch.rcss | 4 + res/rml/tabbing.rcss | 15 +- src/dusk/mods/loader/context.cpp | 21 +- src/dusk/mods/loader/depgraph.cpp | 13 +- src/dusk/mods/loader/loader.cpp | 163 +++++++----- src/dusk/mods/log_buffer.cpp | 103 ++++++++ src/dusk/mods/log_buffer.hpp | 55 ++++ src/dusk/mods/{loader => }/manifest.cpp | 0 src/dusk/mods/{loader => }/manifest.hpp | 0 src/dusk/mods/svc/config.cpp | 4 +- src/dusk/mods/svc/hook.cpp | 2 +- src/dusk/mods/svc/host.cpp | 8 +- src/dusk/mods/svc/log.cpp | 18 +- src/dusk/mods/svc/registry.cpp | 38 ++- src/dusk/mods/svc/registry.hpp | 1 - src/dusk/ui/logs_window.cpp | 301 ++++++++++++++++++++++ src/dusk/ui/logs_window.hpp | 44 ++++ src/dusk/ui/menu_bar.cpp | 18 +- src/dusk/ui/menu_bar.hpp | 2 + src/dusk/ui/mod_texture_provider.cpp | 210 +++++++++++++++ src/dusk/ui/mod_texture_provider.hpp | 19 ++ src/dusk/ui/mods_window.cpp | 325 ++++++++++++++++++++++++ src/dusk/ui/mods_window.hpp | 38 +++ src/dusk/ui/overlay.cpp | 3 + src/dusk/ui/prelaunch.cpp | 14 +- src/dusk/ui/settings.cpp | 17 +- src/dusk/ui/ui.cpp | 5 +- src/dusk/ui/window.cpp | 82 +++++- src/dusk/ui/window.hpp | 13 +- 35 files changed, 1709 insertions(+), 176 deletions(-) create mode 100644 res/rml/logs.rcss create mode 100644 res/rml/mods.rcss create mode 100644 src/dusk/mods/log_buffer.cpp create mode 100644 src/dusk/mods/log_buffer.hpp rename src/dusk/mods/{loader => }/manifest.cpp (100%) rename src/dusk/mods/{loader => }/manifest.hpp (100%) create mode 100644 src/dusk/ui/logs_window.cpp create mode 100644 src/dusk/ui/logs_window.hpp create mode 100644 src/dusk/ui/mod_texture_provider.cpp create mode 100644 src/dusk/ui/mod_texture_provider.hpp create mode 100644 src/dusk/ui/mods_window.cpp create mode 100644 src/dusk/ui/mods_window.hpp diff --git a/docs/modding.md b/docs/modding.md index 0df1fe6dfc..9208f550e6 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -131,9 +131,9 @@ A service is a struct of C function pointers with a version header. You declare loader resolves it before your mod initializes: ```cpp -IMPORT_SERVICE(LogService, svc_log); // required, any minor version -IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2 -IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null +IMPORT_SERVICE(LogService, svc_log); // required, any minor version +IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2 +IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null ``` Each service is individually versioned, and there may be multiple major versions of a service provided at once, diff --git a/files.cmake b/files.cmake index 524c53943f..1cbef1694b 100644 --- a/files.cmake +++ b/files.cmake @@ -1496,6 +1496,10 @@ set(DUSK_FILES src/dusk/ui/input.hpp src/dusk/ui/icon_provider.cpp src/dusk/ui/icon_provider.hpp + src/dusk/ui/logs_window.cpp + src/dusk/ui/logs_window.hpp + src/dusk/ui/mod_texture_provider.cpp + src/dusk/ui/mod_texture_provider.hpp src/dusk/ui/modal.cpp src/dusk/ui/modal.hpp src/dusk/ui/nav_types.hpp @@ -1507,6 +1511,8 @@ set(DUSK_FILES src/dusk/ui/pane.hpp src/dusk/ui/menu_bar.cpp src/dusk/ui/menu_bar.hpp + src/dusk/ui/mods_window.cpp + src/dusk/ui/mods_window.hpp src/dusk/ui/prelaunch.cpp src/dusk/ui/prelaunch.hpp src/dusk/ui/preset.cpp @@ -1541,6 +1547,10 @@ set(DUSK_FILES src/dusk/OSReport.cpp src/dusk/OSThread.cpp src/dusk/OSMutex.cpp + src/dusk/mods/log_buffer.cpp + src/dusk/mods/log_buffer.hpp + src/dusk/mods/manifest.cpp + src/dusk/mods/manifest.hpp src/dusk/mods/loader/bundle_disk.cpp src/dusk/mods/loader/bundle_zip.cpp src/dusk/mods/loader/context.cpp @@ -1550,8 +1560,6 @@ set(DUSK_FILES src/dusk/mods/loader/loader.hpp src/dusk/mods/loader/native_module.cpp src/dusk/mods/loader/native_module.hpp - src/dusk/mods/loader/manifest.cpp - src/dusk/mods/loader/manifest.hpp src/dusk/mods/svc/config.cpp src/dusk/mods/svc/config.hpp src/dusk/mods/svc/game.cpp diff --git a/include/dusk/mod_loader.hpp b/include/dusk/mod_loader.hpp index 7f9d1ad340..9d77f6984a 100644 --- a/include/dusk/mod_loader.hpp +++ b/include/dusk/mod_loader.hpp @@ -176,7 +176,7 @@ public: void request_enable(std::string_view id); void request_disable(std::string_view id); void request_reload(std::string_view id); - void notify_mod_failure(LoadedMod& mod); + void notify_mod_failure(LoadedMod& mod, bool firstFailure); [[nodiscard]] auto mods() const { return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; }); @@ -204,6 +204,7 @@ private: std::vector m_searchDirs; std::filesystem::path m_cacheDir; std::vector m_pendingRequests; + std::vector m_pendingFailures; std::vector m_retiredNatives; bool m_initialized = false; bool m_startupComplete = false; @@ -224,12 +225,11 @@ private: bool resolve_service_imports(LoadedMod& mod); [[nodiscard]] std::string describe_missing_import( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const; - void clear_services(); - void fail_mod(LoadedMod& mod, ModResult code, std::string_view message); - LoadedMod* find_mod(std::string_view id); + LoadedMod* find_mod(std::string_view id) const; void drain_retired_natives(); void apply_pending_requests(); + void flush_toasts(); void on_enabled_changed(LoadedMod& mod); // Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the // bundle from disk, then reactivates whatever the current cvar/provider state allows. diff --git a/res/rml/logs.rcss b/res/rml/logs.rcss new file mode 100644 index 0000000000..126684f9d2 --- /dev/null +++ b/res/rml/logs.rcss @@ -0,0 +1,97 @@ +window.logs content { + flex-flow: column; +} + +window.logs .log-toolbar { + display: flex; + flex-flow: row; + flex: 0 0 64dp; + height: 64dp; + align-items: center; + gap: 8dp; + padding-right: 72dp; + background-color: rgba(217, 217, 217, 10%); + border-bottom: 2dp #92875B; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; +} + +window.logs > close { + top: 8dp; + right: 8dp; +} + +window.logs .log-title { + align-self: stretch; + flex: 0 0 auto; + padding: 0 24dp; + line-height: 64dp; + text-transform: uppercase; + border-bottom: 4dp #C2A42D; + font-effect: glow(0dp 4dp 0dp 4dp black); +} + +window.logs .log-title-mod { + flex: 0 1 auto; + min-width: 0; + white-space: nowrap; + overflow: hidden; + font-family: "Fira Sans"; + font-weight: normal; + font-size: 15dp; + color: rgba(224, 219, 200, 55%); +} + +.log-toolbar-spacer { + flex: 1 1 0; +} + +.log-toolbar button { + flex: 0 0 auto; + font-family: "Fira Sans"; + font-weight: normal; + font-size: 15dp; + padding: 5dp 12dp; +} + +window.logs content pane.log-view { + flex: 1 1 0; + padding: 12dp 16dp; + padding-bottom: 0dp; + gap: 0dp; +} + +.log-lines { + display: block; +} + +.log-line { + display: block; + font-family: "Noto Mono"; + font-size: 13dp; + line-height: 1.5; + word-break: break-word; + white-space: pre-wrap; +} + +.log-line .log-time { + color: rgba(224, 219, 200, 45%); +} + +.log-line .log-mod { + color: rgba(194, 164, 45, 80%); +} + +.log-line.lvl-trace, +.log-line.lvl-debug { + opacity: 0.55; +} + +.log-line.lvl-warn .log-msg { + color: #ffa826; +} + +.log-line.lvl-error .log-msg { + color: #cc4444; +} diff --git a/res/rml/mods.rcss b/res/rml/mods.rcss new file mode 100644 index 0000000000..99aa33d236 --- /dev/null +++ b/res/rml/mods.rcss @@ -0,0 +1,218 @@ +window.mods content pane.mod-list { + flex: 0 0 360dp; + padding: 16dp; + padding-bottom: 0dp; + gap: 4dp; +} + +@media (max-height: 640dp) { + window.mods content pane.mod-list { + flex: 1 1 0; + } +} + +window.mods content pane.mod-detail { + gap: 12dp; +} + +.mod-info-row { + display: flex; + align-items: center; + gap: 12dp; + padding: 4dp 0; +} + +.mod-info-label { + font-family: "Fira Sans Condensed"; + font-weight: bold; + opacity: 0.55; + flex: 0 0 auto; +} + +.mod-info-value { + flex: 1 1 0; +} + +.mod-path { + font-size: 14dp; + word-break: break-all; + opacity: 0.7; +} + +mod-entry { + display: flex; + flex-flow: row; + gap: 12dp; + padding: 10dp; + border-radius: 10dp; + decorator: vertical-gradient(#c2a42d00 #c2a42d00); + transition: decorator 0.1s linear-in-out; + cursor: pointer; + focus: auto; +} + +mod-entry.current { + box-shadow: rgba(146, 135, 91, 40%) 0 0 0 1dp; +} + +mod-entry:hover, +mod-entry:focus-visible { + decorator: vertical-gradient(#c2a42d00 #c2a42d26); +} + +mod-entry:selected { + decorator: vertical-gradient(#c2a42d10 #c2a42d40); +} + +mod-entry .mod-icon { + flex: 0 0 auto; + width: 56dp; + height: 56dp; + border-radius: 8dp; +} + +mod-entry icon.mod-icon { + font-size: 36dp; + background-color: rgba(17, 16, 10, 20%); + color: rgba(224, 219, 200, 45%); + decorator: text("" center center); +} + +mod-entry .mod-entry-info { + display: flex; + flex-flow: column; + flex: 1 1 0; + min-width: 0; + gap: 2dp; +} + +mod-entry .mod-entry-name { + display: flex; + flex-flow: row; + align-items: baseline; + gap: 6dp; +} + +mod-entry .mod-entry-name-text { + flex: 0 1 auto; + min-width: 0; + font-weight: bold; + white-space: nowrap; + overflow: hidden; +} + +mod-entry .mod-entry-version { + flex: 0 0 auto; + font-size: 13dp; + color: rgba(224, 219, 200, 50%); +} + +mod-entry .mod-entry-status.active { + color: #44cc55; +} + +mod-entry .mod-entry-status.failed { + color: #cc4444; +} + +mod-entry .mod-entry-desc { + font-size: 14dp; + line-height: 1.3; + color: rgba(224, 219, 200, 65%); + max-height: 2.6em; + overflow: hidden; +} + +mod-entry .mod-entry-sub { + font-size: 13dp; + color: rgba(224, 219, 200, 50%); +} + +mod-entry.inactive .mod-icon { + filter: grayscale(1); +} + +mod-entry.inactive .mod-entry-info { + opacity: 0.5; +} + +mod-header { + position: relative; + flex: 0 0 auto; +} + +mod-header.has-banner { + height: 180dp; + margin: -24dp -24dp 0dp -24dp; +} + +mod-header .mod-actions { + position: absolute; + top: 24dp; + left: 24dp; + display: flex; + flex-flow: row; + gap: 8dp; +} + +mod-header .mod-actions button { + font-size: 16dp; + padding: 6dp 14dp; + background-color: rgba(21, 22, 16, 80%); + box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp; +} + +mod-header.no-banner { + display: flex; + flex-flow: row; + align-items: center; +} + +mod-header.no-banner .mod-actions { + position: static; +} + +window.mods .mod-title { + display: block; + font-size: 28dp; + font-weight: bold; +} + +window.mods .mod-title .mod-title-version { + font-weight: normal; + font-size: 16dp; + color: rgba(224, 219, 200, 55%); +} + +window.mods .mod-author { + display: block; + font-size: 15dp; + color: rgba(224, 219, 200, 55%); +} + +window.mods .mod-restart-note { + font-size: 15dp; + color: #ffa826; + opacity: 0.85; +} + +window.mods .mod-description { + line-height: 1.5; +} + +.status-badge { + font-size: 14dp; + opacity: 0.7; +} + +.status-badge.active, +.mod-info-label.active { + color: #44cc55; + opacity: 1; +} + +.status-badge.failed, +.mod-info-label.failed { + color: #cc4444; + opacity: 1; +} \ No newline at end of file diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 384c7a2a9b..98f8bfa27a 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -159,6 +159,14 @@ toast.achievement heading { color: #C2A42D; } +toast.warning { + border: 1dp #C2A42D; +} + +toast.warning heading { + color: #C2A42D; +} + toast.controller-warning { top: auto; right: auto; diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 73efc18086..054b6e0183 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -362,6 +362,10 @@ body.animate-in .intro-item { transition: opacity transform 0.3s 0.6s cubic-in-out; } +.delay-6 { + transition: opacity transform 0.3s 0.7s cubic-in-out; +} + /* Mobile layout */ @media (max-height: 640dp) { .gradient { diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss index 194ca89f05..8f42dd84d5 100644 --- a/res/rml/tabbing.rcss +++ b/res/rml/tabbing.rcss @@ -50,7 +50,8 @@ tab-bar[closable] tab-end-spacer { pointer-events: none; } -tab-bar[closable] close { +tab-bar[closable] close, +window > close { display: block; position: fixed; top: 8dp; @@ -70,12 +71,20 @@ tab-bar[closable] close { } tab-bar[closable] close:hover, -tab-bar[closable] close:focus-visible { +tab-bar[closable] close:focus-visible, +window > close:hover, +window > close:focus-visible { color: #fff; background-color: rgba(194, 164, 45, 24%); } -tab-bar[closable] close:active { +window > close { + top: 16dp; + right: 16dp; +} + +tab-bar[closable] close:active, +window > close:active { color: #fff; background-color: rgba(194, 164, 45, 40%); } diff --git a/src/dusk/mods/loader/context.cpp b/src/dusk/mods/loader/context.cpp index 99417117d6..9c8fe71db4 100644 --- a/src/dusk/mods/loader/context.cpp +++ b/src/dusk/mods/loader/context.cpp @@ -1,11 +1,7 @@ #include "loader.hpp" -#include "dusk/logging.h" +#include "dusk/mods/log_buffer.hpp" #include "dusk/mods/svc/registry.hpp" -#include "dusk/ui/ui.hpp" -#include "fmt/format.h" - -#include namespace dusk::mods { @@ -55,16 +51,9 @@ void fail_mod(LoadedMod& mod, ModResult code, std::string_view message) { // callback), and full teardown happens via deactivate_mod at a safe point. svc::remove_services_for_provider(mod); mod.servicesRegistered = false; - ModLoader::instance().notify_mod_failure(mod); - DuskLog.error("[{}] disabled: {} ({})", mod.metadata.id, message, static_cast(code)); - if (firstFailure) { - ui::push_toast({ - .type = "warning", - .title = "Mod Disabled", - .content = ui::escape(fmt::format("{}: {}", mod.metadata.name, message)), - .duration = std::chrono::seconds(5), - }); - } + ModLoader::instance().notify_mod_failure(mod, firstFailure); + log::write( + mod.metadata.id, LOG_LEVEL_ERROR, "failed: {} ({})", message, static_cast(code)); } -} // namespace dusk::mods::loader +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/depgraph.cpp b/src/dusk/mods/loader/depgraph.cpp index 20c831075a..d1748b56c4 100644 --- a/src/dusk/mods/loader/depgraph.cpp +++ b/src/dusk/mods/loader/depgraph.cpp @@ -5,13 +5,11 @@ #include "dusk/logging.h" #include "loader.hpp" -#include "native_module.hpp" - -static aurora::Module Log("dusk::modLoader"); +#include "native_module.hpp" // IWYU pragma: keep namespace dusk::mods::loader { - namespace { +aurora::Module Log{"dusk::mods::loader"}; struct Edge { size_t provider; @@ -31,7 +29,7 @@ std::vector collect_edges(const std::vector>& m for (size_t i = 0; i < mods.size(); ++i) { const auto matches = [&](const ModManifestInfo::Export& serviceExport) { return serviceExport.major == serviceImport.major && - serviceExport.id == serviceImport.id; + serviceExport.id == serviceImport.id; }; if (std::ranges::any_of(mods[i]->manifestInfo.exports, matches)) { return i; @@ -165,7 +163,7 @@ void sort_mods(std::vector>& mods) { } for (const size_t index : cycleMods) { fail_mod(*mods[index], MOD_CONFLICT, - "required service import cycle between mods: " + names); + "Required service import cycle between mods: " + names); place(index); } continue; @@ -174,8 +172,7 @@ void sort_mods(std::vector>& mods) { // Only optional imports left in the cycle: drop one edge and retry. The // import still resolves, but without any initialization-order guarantee. const auto optionalEdge = std::ranges::find_if(edges, [&](const Edge& edge) { - return edge.alive && !edge.required && !placed[edge.provider] && - !placed[edge.importer]; + return edge.alive && !edge.required && !placed[edge.provider] && !placed[edge.importer]; }); if (optionalEdge == edges.end()) { // Unreachable: a stall with no required cycle implies an optional edge. diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index fb293a55b4..77b6e7f1a9 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -3,24 +3,26 @@ #include "dusk/mod_loader.hpp" #include +#include #include #include #include #include #include +#include "../manifest.hpp" #include "depgraph.hpp" #include "dusk/config.hpp" #include "dusk/io.hpp" +#include "dusk/mods/log_buffer.hpp" #include "dusk/mods/svc/config.hpp" #include "dusk/mods/svc/registry.hpp" -#include "manifest.hpp" +#include "dusk/ui/mods_window.hpp" +#include "dusk/ui/ui.hpp" #include "miniz.h" #include "native_module.hpp" #include "nlohmann/json.hpp" -static aurora::Module Log("dusk::modLoader"); - using namespace std::string_literals; using namespace std::string_view_literals; @@ -71,6 +73,7 @@ static constexpr std::string_view k_nativeLibName = ""sv; namespace dusk::mods { namespace { +aurora::Module Log{"dusk::mods::loader"}; ModLoader g_modLoader; std::unique_ptr load_bundle(const std::filesystem::path& modPath, bool fromDir) { @@ -163,9 +166,11 @@ static std::string resolve_image_path(ModBundle& bundle, const std::string& modI std::string_view key, const std::string& manifestPath, const std::string& defaultPath) { if (!manifestPath.empty()) { if (!is_safe_resource_path(manifestPath)) { - Log.warn("{}: invalid {} path '{}' in mod.json", modId, key, manifestPath); + log::write( + modId, LOG_LEVEL_WARN, "invalid {} path '{}' in mod.json", key, manifestPath); } else if (!bundle_has_file(bundle, manifestPath)) { - Log.warn("{}: {} path '{}' not found in bundle", modId, key, manifestPath); + log::write( + modId, LOG_LEVEL_WARN, "{} path '{}' not found in bundle", key, manifestPath); } else { return manifestPath; } @@ -217,18 +222,18 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) { if (manifest == nullptr) { - Log.error("{} returned a null mod manifest", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "returned a null mod manifest"); mod.nativeStatus = NativeModStatus::MissingExport; return false; } if (manifest->struct_size != sizeof(ModManifest)) { - Log.error("{} manifest has invalid size {} (expected {})", mod.metadata.id, + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid size {} (expected {})", manifest->struct_size, sizeof(ModManifest)); mod.nativeStatus = NativeModStatus::ApiVersionMismatch; return false; } if (manifest->abi_version != MOD_ABI_VERSION) { - Log.error("{} expects ABI v{} but engine is v{}, skipping", mod.metadata.id, + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", manifest->abi_version, MOD_ABI_VERSION); mod.nativeStatus = NativeModStatus::ApiVersionMismatch; return false; @@ -236,7 +241,7 @@ static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) { if ((manifest->import_count > 0 && manifest->imports == nullptr) || (manifest->export_count > 0 && manifest->exports == nullptr)) { - Log.error("{} manifest has invalid import/export arrays", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid import/export arrays"); mod.nativeStatus = NativeModStatus::MissingExport; return false; } @@ -245,7 +250,7 @@ static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) { static bool validate_context_symbol(ModContext** contextSymbol, LoadedMod& mod) { if (contextSymbol == nullptr) { - Log.error("{} missing required mod_ctx export", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); mod.nativeStatus = NativeModStatus::MissingExport; return false; } @@ -289,8 +294,8 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod) if (libDir.empty()) { return {}; } - fs::path path = libDir / fs::path(mod.metadata.id + io::fs_path_to_string( - fs::path(k_nativeLibName).extension())); + fs::path path = libDir / fs::path(mod.metadata.id + + io::fs_path_to_string(fs::path(k_nativeLibName).extension())); std::error_code ec; if (!fs::is_regular_file(path, ec)) { return {}; @@ -300,7 +305,7 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod) void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { if (!EnableCodeMods) { - Log.error("Code mods are not available in this build"); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); mod.nativeStatus = NativeModStatus::BuildDisabled; return; } @@ -318,15 +323,15 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { } else if (auto external = external_native_lib_path(mod); !external.empty()) { libPath = std::move(external); } else { - Log.error("no native library named {} found in {}; skipping", k_nativeLibName, - mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "no native library named {} found; skipping", k_nativeLibName); mod.nativeStatus = NativeModStatus::ModMissingPlatform; return; } } else { if (dllEntry.empty()) { - Log.error("no native library named {} found in {}; skipping", k_nativeLibName, - mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "no native library named {} found; skipping", k_nativeLibName); mod.nativeStatus = NativeModStatus::ModMissingPlatform; return; } @@ -342,14 +347,15 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { try { dllData = mod.bundle->readFile(dllEntry); } catch (const std::exception& e) { - Log.error("failed to extract {} from {}", dllEntry, mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}", dllEntry); return; } { std::ofstream out(dllCachePath, std::ios::binary | std::ios::out); if (!out) { - Log.error("failed to write {}", io::fs_path_to_string(dllCachePath)); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", + io::fs_path_to_string(dllCachePath)); return; } @@ -364,7 +370,8 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { try { nativeMod->handle = std::make_unique(libPath); } catch (const std::runtime_error& e) { - Log.error("failed to open {}: {}", io::fs_path_to_string(libPath), e.what()); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", + io::fs_path_to_string(libPath), e.what()); return; } @@ -377,7 +384,8 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { if (!getManifest || !nativeMod->contextSymbol || !nativeMod->fn_initialize || !nativeMod->fn_update || !nativeMod->fn_shutdown) { - Log.error("{} missing required mod API exports; skipping", + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "{} missing required mod API exports; skipping", io::fs_path_to_string(libPath.filename())); mod.nativeStatus = NativeModStatus::MissingExport; return; @@ -486,9 +494,9 @@ static void warn_unpublished_deferred_exports(const LoadedMod& mod) { const auto* record = svc::find_service_record(serviceExport.service_id, serviceExport.major_version); if (record != nullptr && record->service == nullptr) { - Log.warn("'{}' declared deferred service '{}@{}' but never published it during " - "initialization", - mod.metadata.id, serviceExport.service_id, serviceExport.major_version); + log::write(mod.metadata.id, LOG_LEVEL_WARN, + "declared deferred service '{}@{}' but never published it during initialization", + serviceExport.service_id, serviceExport.major_version); } } } @@ -516,10 +524,10 @@ void ModLoader::try_load_mod( if (const auto* existing = find_mod(metadata.id)) { if (existing->searchDirIndex < searchDirIndex) { - Log.info("'{}' ({}) shadowed by higher-priority copy {}", metadata.id, + log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority copy {}", io::fs_path_to_string(modPath.filename()), existing->modPath); } else { - Log.error("mod with id '{}' already exists, not loading {}", metadata.id, + log::write(metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}", io::fs_path_to_string(modPath.filename())); } return; @@ -537,7 +545,7 @@ void ModLoader::try_load_mod( mod.context = std::make_unique(); mod.context->mod = &mod; mod.cvarIsEnabled = - std::make_unique >(mod_enabled_cvar_name(mod.metadata.id), true); + std::make_unique>(mod_enabled_cvar_name(mod.metadata.id), true); const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); if (anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())) { @@ -545,19 +553,18 @@ void ModLoader::try_load_mod( load_native(mod, dllEntry); if (mod.nativeStatus != NativeModStatus::Loaded) { Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id); - mod.active = false; - mod.loadFailed = true; - mod.failureReason = native_status_message(mod.nativeStatus); + fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); } else { mod.manifestInfo = build_manifest_info(*mod.native->manifest); } } - Log.info("found '{}' ('{}') v{} by {} ({})", mod.metadata.name, mod.metadata.id, + log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name, mod.metadata.version, mod.metadata.author, io::fs_path_to_string(modPath.filename())); } bool ModLoader::activate_mod(LoadedMod& mod) { + log::write(mod.metadata.id, LOG_LEVEL_INFO, "activating mod"); mod.active = true; // Asset-only mods have no lifecycle beyond their overlay files. @@ -568,33 +575,33 @@ bool ModLoader::activate_mod(LoadedMod& mod) { if (!mod.servicesRegistered) { if (!register_static_service_exports(mod)) { - Log.error("'{}' failed to register service exports", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); return false; } mod.servicesRegistered = true; } if (!resolve_service_imports(mod)) { - Log.error("'{}' failed to resolve service imports", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to resolve service imports"); return false; } *mod.native->contextSymbol = mod.context.get(); - Log.debug("Initializing '{}'", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize"); try { ModError error = MOD_ERROR_INIT; const auto result = mod.native->fn_initialize(&error); if (result == MOD_OK && !mod.loadFailed) { mod.initialized = true; - Log.info("'{}' initialized", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_TRACE, "mod_initialize succeeded"); } else { fail_mod(mod, result, lifecycle_error_message("mod_initialize", result, error)); } } catch (const std::exception& e) { - fail_mod(mod, MOD_ERROR, fmt::format("exception in mod_initialize: {}", e.what())); + fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod_initialize: {}", e.what())); } catch (...) { - fail_mod(mod, MOD_ERROR, "unknown exception in mod_initialize"); + fail_mod(mod, MOD_ERROR, "Unknown exception in mod_initialize"); } warn_unpublished_deferred_exports(mod); @@ -611,11 +618,14 @@ bool ModLoader::activate_mod(LoadedMod& mod) { void ModLoader::deactivate_mod(LoadedMod& mod) { if (mod.initialized && mod.native && mod.native->fn_shutdown) { + log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_shutdown"); try { ModError error = MOD_ERROR_INIT; const auto result = mod.native->fn_shutdown(&error); - if (result != MOD_OK) { - Log.error("mod_shutdown failed for '{}': {}", mod.metadata.id, + if (result == MOD_OK) { + log::write(mod.metadata.id, LOG_LEVEL_TRACE, "mod_shutdown succeeded"); + } else { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_shutdown failed: {}", lifecycle_error_message("mod_shutdown", result, error)); } } catch (...) { @@ -719,7 +729,7 @@ void ModLoader::init() { if (register_static_service_exports(mod)) { mod.servicesRegistered = true; } else { - Log.error("'{}' failed to register service exports", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); } } @@ -730,7 +740,7 @@ void ModLoader::init() { // Config-disabled mods keep their dependency edges but must not resolve until enabled. for (auto& mod : mods()) { if (!mod.cvarIsEnabled->getValue()) { - Log.info("Mod '{}' is disabled by config", mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_INFO, "disabled by config"); mod.active = false; if (mod.servicesRegistered) { svc::remove_services_for_provider(mod); @@ -753,7 +763,7 @@ void ModLoader::init() { m_startupComplete = true; } -LoadedMod* ModLoader::find_mod(std::string_view id) { +LoadedMod* ModLoader::find_mod(std::string_view id) const { for (auto& mod : mods()) { if (mod.metadata.id == id) { return &mod; @@ -778,23 +788,55 @@ void ModLoader::request_reload(std::string_view id) { m_pendingRequests.push_back({std::string{id}, RequestKind::Reload}); } -void ModLoader::notify_mod_failure(LoadedMod& mod) { - // Startup failures are handled inline by activate_mod; the queue only exists for failures - // raised while mod code may be on the stack (mod_update, hook callbacks, UI callbacks). +void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { + if (firstFailure) { + m_pendingFailures.push_back(mod.metadata.name); + } + // Startup failures are handled inline by activate_mod if (!m_startupComplete) { return; } m_pendingRequests.push_back({mod.metadata.id, RequestKind::Disable}); } +void ModLoader::flush_toasts() { + if (m_pendingFailures.empty()) { + return; + } + + const auto names = std::exchange(m_pendingFailures, {}); + + // Skip displaying toasts if the mods window is currently open + if (const auto* window = dynamic_cast(ui::top_document())) { + if (window->visible()) { + return; + } + } + + ui::Toast toast{.type = "warning", .duration = std::chrono::seconds{5}}; + if (names.size() == 1) { + toast.title = "Mod failed"; + toast.content = + fmt::format("
{} failed and was disabled.
Check Mods for " + "more information.
", + ui::escape(names.front())); + } else { + toast.title = "Mods failed"; + toast.content = fmt::format("
{} mods failed and were disabled.
Check " + "Mods for more information.
", + names.size()); + } + ui::push_toast(std::move(toast)); +} + static bool requiredDepsActive(const LoadedMod& mod) { return std::ranges::all_of(mod.dependencies, [](const ModDependencyEdge& edge) { return !edge.required || edge.mod->active; }); } std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { - std::vector included{&target}; - std::vector pending{&target}; + std::vector included{&target}; + std::vector pending{&target}; while (!pending.empty()) { auto* current = pending.back(); pending.pop_back(); @@ -843,7 +885,7 @@ bool ModLoader::ensure_native_loaded(LoadedMod& mod) { bool ModLoader::reload_bundle(LoadedMod& mod) { namespace fs = std::filesystem; - Log.info("reloading '{}' from {}", mod.metadata.id, mod.modPath); + log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", mod.modPath); std::shared_ptr newBundle; ModMetadata newMetadata; @@ -852,13 +894,13 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { newBundle = load_bundle(mod.modPath, fs::is_directory(mod.modPath, ec)); newMetadata = load_metadata(mod.modPath, *newBundle); } catch (const std::exception& e) { - fail_mod(mod, MOD_ERROR, fmt::format("reload failed: {}", e.what())); + fail_mod(mod, MOD_ERROR, fmt::format("Reload failed: {}", e.what())); return false; } if (newMetadata.id != mod.metadata.id) { fail_mod(mod, MOD_CONFLICT, - fmt::format("mod id changed on reload ('{}'); restart the game", newMetadata.id)); + fmt::format("Mod ID changed on reload ('{}'); restart required", newMetadata.id)); return false; } @@ -885,8 +927,8 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { if (newInfo != mod.manifestInfo) { // The reload changes the mod's imports/exports; rebuild the dependency graph so edges, // init/tick/shutdown order and cascade sets reflect the new manifest. - Log.info("'{}' changed its service imports/exports; rebuilding mod dependency graph", - mod.metadata.id); + log::write(mod.metadata.id, LOG_LEVEL_INFO, + "changed its service imports/exports; rebuilding mod dependency graph"); mod.manifestInfo = std::move(newInfo); loader::sort_mods(m_mods); } @@ -906,7 +948,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { continue; } const bool wasActive = mod->active; - Log.info("deactivating '{}'", mod->metadata.id); + log::write(mod->metadata.id, LOG_LEVEL_INFO, "deactivating mod"); deactivate_mod(*mod); if (mod != &target && wasActive) { // Provisional; cleared below if the mod comes straight back up. @@ -953,7 +995,8 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { } if (!requiredDepsActive(*mod)) { mod->suspendedByProvider = true; - Log.info("'{}' suspended: a required provider is disabled", mod->metadata.id); + log::write( + mod->metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); continue; } mod->suspendedByProvider = false; @@ -972,6 +1015,10 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { void ModLoader::on_enabled_changed(LoadedMod& mod) { svc::config_mark_dirty(); if (mod.loadFailed) { + if (!mod.cvarIsEnabled->getValue()) { + mod.loadFailed = false; + mod.failureReason.clear(); + } return; } if (mod.suspendedByProvider) { @@ -1013,7 +1060,7 @@ void ModLoader::apply_pending_requests() { continue; } if (request.kind == RequestKind::Reload && mod->inPlace) { - Log.warn("'{}' is a built-in mod and can't be reloaded", mod->metadata.id); + log::write(mod->metadata.id, LOG_LEVEL_WARN, "is a built-in mod and can't be reloaded"); continue; } if (request.kind == RequestKind::Enable && mod->enabledApplied) { @@ -1046,13 +1093,14 @@ void ModLoader::tick() { fail_mod(mod, result, lifecycle_error_message("mod_update", result, error)); } } catch (const std::exception& e) { - fail_mod(mod, MOD_ERROR, fmt::format("exception in mod_update: {}", e.what())); + fail_mod(mod, MOD_ERROR, fmt::format("Exception in mod_update: {}", e.what())); } catch (...) { - fail_mod(mod, MOD_ERROR, "unknown exception in mod_update"); + fail_mod(mod, MOD_ERROR, "Unknown exception in mod_update"); } } svc::modules_frame_end(); + flush_toasts(); } void ModLoader::shutdown() { @@ -1069,7 +1117,6 @@ void ModLoader::shutdown() { m_mods.clear(); drain_retired_natives(); svc::modules_shutdown(); - clear_services(); Log.info("all mods unloaded"); } diff --git a/src/dusk/mods/log_buffer.cpp b/src/dusk/mods/log_buffer.cpp new file mode 100644 index 0000000000..bfee810767 --- /dev/null +++ b/src/dusk/mods/log_buffer.cpp @@ -0,0 +1,103 @@ +#include "log_buffer.hpp" + +#include +#include + +#include "dusk/logging.h" + +namespace dusk::mods::log { +namespace { + +struct BufferState { + std::mutex mutex; + std::vector ring; + size_t head = 0; + size_t count = 0; + uint64_t nextSeq = 0; + std::vector modIds; + + uint16_t intern(std::string_view modId) { + for (size_t i = 0; i < modIds.size(); ++i) { + if (modIds[i] == modId) { + return static_cast(i); + } + } + modIds.emplace_back(modId); + return static_cast(modIds.size() - 1); + } +}; +BufferState g_buffer; + +} // namespace + +void emit(Source source, const std::string& modId, LogLevel level, const std::string& message) { + const auto timeMs = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + { + std::lock_guard lock{g_buffer.mutex}; + if (g_buffer.ring.empty()) { + g_buffer.ring.resize(k_capacity); + } + auto& slot = g_buffer.ring[(g_buffer.head + g_buffer.count) % k_capacity]; + if (g_buffer.count == k_capacity) { + g_buffer.head = (g_buffer.head + 1) % k_capacity; + } else { + ++g_buffer.count; + } + slot.seq = g_buffer.nextSeq++; + slot.timeMs = timeMs; + slot.level = level; + slot.modIndex = g_buffer.intern(modId); + slot.source = source; + // assign() reuses the slot's capacity once the ring has wrapped + slot.message.assign(message); + } + + AuroraLogLevel auroraLevel = LOG_INFO; + switch (level) { + case LOG_LEVEL_TRACE: + case LOG_LEVEL_DEBUG: + auroraLevel = LOG_DEBUG; + break; + case LOG_LEVEL_INFO: + auroraLevel = LOG_INFO; + break; + case LOG_LEVEL_WARN: + auroraLevel = LOG_WARNING; + break; + case LOG_LEVEL_ERROR: + auroraLevel = LOG_ERROR; + break; + } + if (aurora::g_config.logLevel <= auroraLevel) { + aurora::log_internal(auroraLevel, modId.c_str(), message.c_str(), + static_cast(message.length())); + } +} + +Range copy_since(uint64_t sinceSeq, std::vector& out) { + std::lock_guard lock{g_buffer.mutex}; + const Range range{ + .firstSeq = g_buffer.nextSeq - g_buffer.count, + .nextSeq = g_buffer.nextSeq, + }; + for (uint64_t seq = std::max(sinceSeq, range.firstSeq); seq < range.nextSeq; ++seq) { + out.push_back(g_buffer.ring[(g_buffer.head + (seq - range.firstSeq)) % k_capacity]); + } + return range; +} + +std::vector ids() { + std::lock_guard lock{g_buffer.mutex}; + return g_buffer.modIds; +} + +void clear() { + std::lock_guard lock{g_buffer.mutex}; + g_buffer.head = 0; + g_buffer.count = 0; +} + +} // namespace dusk::mods::log diff --git a/src/dusk/mods/log_buffer.hpp b/src/dusk/mods/log_buffer.hpp new file mode 100644 index 0000000000..33a143705d --- /dev/null +++ b/src/dusk/mods/log_buffer.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "mods/svc/log.h" + +namespace dusk::mods::log { + +constexpr size_t k_capacity = 2048; + +enum class Source : uint8_t { + Loader, + Mod, +}; + +struct Line { + uint64_t seq = 0; // monotonic, never reset (survives clear) + int64_t timeMs = 0; + LogLevel level = LOG_LEVEL_INFO; + uint16_t modIndex = 0; // index into ids() + Source source = Source::Loader; + std::string message; +}; + +struct Range { + uint64_t firstSeq = 0; // oldest retained entry + uint64_t nextSeq = 0; // one past the newest entry +}; + +// Appends to the mod log buffer and emits through aurora logging with the mod ID as the +// module tag. Console/file output honors the configured log level; the buffer captures +// every level. Thread-safe; mods may log from worker threads. +void emit(Source source, const std::string& modId, LogLevel level, const std::string& message); + +// Appends entries with seq >= sinceSeq to `out` and returns the retained range, +// so callers diffing by seq can detect both new entries and truncation. +Range copy_since(uint64_t sinceSeq, std::vector& out); + +// Append-only intern table of mod ids; indices are stable for the session. +std::vector ids(); + +void clear(); + +// Formatting helper for loader messages. +template +void write(const std::string& modId, LogLevel level, fmt::format_string format, T&&... args) { + emit(Source::Loader, modId, level, fmt::format(format, std::forward(args)...)); +} + +} // namespace dusk::mods::log diff --git a/src/dusk/mods/loader/manifest.cpp b/src/dusk/mods/manifest.cpp similarity index 100% rename from src/dusk/mods/loader/manifest.cpp rename to src/dusk/mods/manifest.cpp diff --git a/src/dusk/mods/loader/manifest.hpp b/src/dusk/mods/manifest.hpp similarity index 100% rename from src/dusk/mods/loader/manifest.hpp rename to src/dusk/mods/manifest.hpp diff --git a/src/dusk/mods/svc/config.cpp b/src/dusk/mods/svc/config.cpp index b7ea9c9e9d..cae6bf840e 100644 --- a/src/dusk/mods/svc/config.cpp +++ b/src/dusk/mods/svc/config.cpp @@ -385,9 +385,9 @@ ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChang callback(modPtr->context.get(), varHandle, ¤tValue, &previousValue, userData); } catch (const std::exception& e) { fail_mod(*modPtr, MOD_ERROR, - fmt::format("exception in config change callback: {}", e.what())); + fmt::format("Exception in config change callback: {}", e.what())); } catch (...) { - fail_mod(*modPtr, MOD_ERROR, "unknown exception in config change callback"); + fail_mod(*modPtr, MOD_ERROR, "Unknown exception in config change callback"); } }); if (outHandle != nullptr) { diff --git a/src/dusk/mods/svc/hook.cpp b/src/dusk/mods/svc/hook.cpp index f565776183..1439a80619 100644 --- a/src/dusk/mods/svc/hook.cpp +++ b/src/dusk/mods/svc/hook.cpp @@ -1,7 +1,7 @@ #include "registry.hpp" #include "dusk/mods/loader/loader.hpp" -#include "dusk/mods/loader/manifest.hpp" +#include "dusk/mods/manifest.hpp" #include "mods/svc/hook.h" #if DUSK_CODE_MODS diff --git a/src/dusk/mods/svc/host.cpp b/src/dusk/mods/svc/host.cpp index 05971d5be1..5904903ee7 100644 --- a/src/dusk/mods/svc/host.cpp +++ b/src/dusk/mods/svc/host.cpp @@ -1,7 +1,7 @@ #include "registry.hpp" #include "dusk/mods/loader/loader.hpp" -#include "dusk/mods/loader/manifest.hpp" +#include "dusk/mods/manifest.hpp" #include "fmt/format.h" #include @@ -38,7 +38,7 @@ ModResult host_publish_service( void host_fail(ModContext* context, const ModResult code, const char* message) { auto* mod = mod_from_context(context); if (mod != nullptr) { - fail_mod(*mod, code, message != nullptr ? message : "mod reported failure"); + fail_mod(*mod, code, message != nullptr ? message : "Mod reported an unknown failure"); } } @@ -112,9 +112,9 @@ void host_mod_detached(LoadedMod& mod) { MOD_LIFECYCLE_DETACHED, watcher.userData); } catch (const std::exception& e) { fail_mod(*watcher.owner, MOD_ERROR, - fmt::format("exception in mod lifecycle callback: {}", e.what())); + fmt::format("Exception in mod lifecycle callback: {}", e.what())); } catch (...) { - fail_mod(*watcher.owner, MOD_ERROR, "unknown exception in mod lifecycle callback"); + fail_mod(*watcher.owner, MOD_ERROR, "Unknown exception in mod lifecycle callback"); } } } diff --git a/src/dusk/mods/svc/log.cpp b/src/dusk/mods/svc/log.cpp index d9fc88eae7..498a20aeaf 100644 --- a/src/dusk/mods/svc/log.cpp +++ b/src/dusk/mods/svc/log.cpp @@ -1,28 +1,14 @@ #include "registry.hpp" -#include "dusk/logging.h" #include "dusk/mods/loader/loader.hpp" +#include "dusk/mods/log_buffer.hpp" namespace dusk::mods::svc { namespace { void log_write(ModContext* context, const LogLevel level, const char* message) { const char* text = message != nullptr ? message : ""; - switch (level) { - case LOG_LEVEL_TRACE: - case LOG_LEVEL_DEBUG: - DuskLog.debug("[{}] {}", mod_id_from_context(context), text); - break; - case LOG_LEVEL_INFO: - DuskLog.info("[{}] {}", mod_id_from_context(context), text); - break; - case LOG_LEVEL_WARN: - DuskLog.warn("[{}] {}", mod_id_from_context(context), text); - break; - case LOG_LEVEL_ERROR: - DuskLog.error("[{}] {}", mod_id_from_context(context), text); - break; - } + log::emit(log::Source::Mod, mod_id_from_context(context), level, text); } void log_trace(ModContext* context, const char* message) { diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 0a2da3add3..0c7c61276a 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -1,5 +1,6 @@ #include "registry.hpp" +#include "dusk/app_info.hpp" #include "dusk/logging.h" #include "dusk/mods/loader/loader.hpp" @@ -22,7 +23,7 @@ std::string service_key(std::string_view id, const uint16_t majorVersion) { } const char* mod_id(const LoadedMod* mod) { - return mod != nullptr ? mod->metadata.id.c_str() : "dusklight"; + return mod != nullptr ? mod->metadata.id.c_str() : AppName; } bool validate_service_header(const ServiceHeader* header, const char* serviceId, @@ -45,6 +46,11 @@ bool validate_service_header(const ServiceHeader* header, const char* serviceId, return true; } +void clear_services() { + s_services.clear(); + s_modules.clear(); +} + } // namespace bool valid_service_id(const char* serviceId) { @@ -131,11 +137,6 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m return it != s_services.end() ? &it->second : nullptr; } -void clear_services() { - s_services.clear(); - s_modules.clear(); -} - ModResult register_module(const ServiceModule& module) { const auto result = register_service( module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false); @@ -187,6 +188,7 @@ void modules_shutdown() { module->shutdown(); } } + clear_services(); } } // namespace dusk::mods::svc @@ -222,13 +224,13 @@ bool ModLoader::register_static_service_exports(LoadedMod& mod) { if (serviceExport.struct_size != sizeof(ServiceExport) || !svc::valid_service_id(serviceExport.service_id)) { - fail_mod(mod, MOD_INVALID_ARGUMENT, "invalid service export descriptor"); + fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor"); return false; } const bool deferred = (serviceExport.flags & SERVICE_EXPORT_DEFERRED) != 0; if (!deferred && serviceExport.service == nullptr) { - fail_mod(mod, MOD_INVALID_ARGUMENT, "static service export has null service pointer"); + fail_mod(mod, MOD_INVALID_ARGUMENT, "Static service export has null service pointer"); return false; } @@ -236,7 +238,7 @@ bool ModLoader::register_static_service_exports(LoadedMod& mod) { svc::register_service(serviceExport.service_id, serviceExport.major_version, serviceExport.minor_version, serviceExport.service, &mod, deferred); if (result != MOD_OK) { - fail_mod(mod, result, "service export registration failed"); + fail_mod(mod, result, "Service export registration failed"); return false; } } @@ -248,10 +250,10 @@ std::string ModLoader::describe_missing_import( const char* serviceId, const uint16_t majorVersion, const uint16_t minMinorVersion) const { if (const auto* record = svc::find_service_record(serviceId, majorVersion)) { if (record->service == nullptr) { - return fmt::format("required service {}@{} was never published by provider '{}'", + return fmt::format("Required service {}@{} was never published by provider '{}'", serviceId, majorVersion, svc::mod_id(record->provider)); } - return fmt::format("required service {}@{} only provides minor version {} (need >= {})", + return fmt::format("Required service {}@{} only provides minor version {} (need >= {})", serviceId, majorVersion, record->minorVersion, minMinorVersion); } @@ -270,14 +272,14 @@ std::string ModLoader::describe_missing_import( std::string_view{serviceExport.service_id} == serviceId && serviceExport.major_version == majorVersion) { - return fmt::format("required service {}@{} unavailable: provider '{}' {}", + return fmt::format("Required service {}@{} unavailable: provider '{}' {}", serviceId, majorVersion, other.metadata.id, other.loadFailed ? "failed to load" : "is disabled"); } } } - return fmt::format("required service unavailable: {}@{}", serviceId, majorVersion); + return fmt::format("Required service unavailable: {}@{}", serviceId, majorVersion); } bool ModLoader::resolve_service_imports(LoadedMod& mod) { @@ -291,7 +293,7 @@ bool ModLoader::resolve_service_imports(LoadedMod& mod) { if (serviceImport.struct_size != sizeof(ServiceImport) || !svc::valid_service_id(serviceImport.service_id) || serviceImport.slot == nullptr) { - fail_mod(mod, MOD_INVALID_ARGUMENT, "invalid service import descriptor"); + fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor"); return false; } @@ -315,12 +317,4 @@ bool ModLoader::resolve_service_imports(LoadedMod& mod) { return true; } -void ModLoader::clear_services() { - svc::clear_services(); -} - -void ModLoader::fail_mod(LoadedMod& mod, const ModResult code, std::string_view message) { - mods::fail_mod(mod, code, message); -} - } // namespace dusk::mods diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index bdbe2c2e72..e8d613057e 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -53,7 +53,6 @@ const ServiceRecord* find_service( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion); // Unlike find_service, also returns deferred records that have not been published yet. const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion); -void clear_services(); ModResult register_module(const ServiceModule& module); void modules_mod_detached(LoadedMod& mod); diff --git a/src/dusk/ui/logs_window.cpp b/src/dusk/ui/logs_window.cpp new file mode 100644 index 0000000000..9fdd89e50d --- /dev/null +++ b/src/dusk/ui/logs_window.cpp @@ -0,0 +1,301 @@ +#include "logs_window.hpp" + +#include +#include + +#include +#include + +#include "pane.hpp" + +namespace dusk::ui { +namespace { + +const char* level_name(LogLevel level) { + switch (level) { + case LOG_LEVEL_TRACE: + return "Trace"; + case LOG_LEVEL_DEBUG: + return "Debug"; + case LOG_LEVEL_INFO: + return "Info"; + case LOG_LEVEL_WARN: + return "Warn"; + case LOG_LEVEL_ERROR: + return "Error"; + } + return "?"; +} + +const char* level_logger_name(LogLevel level) { + switch (level) { + case LOG_LEVEL_TRACE: + return "TRACE"; + case LOG_LEVEL_DEBUG: + return "DEBUG"; + case LOG_LEVEL_INFO: + return "INFO"; + case LOG_LEVEL_WARN: + return "WARNING"; + case LOG_LEVEL_ERROR: + return "ERROR"; + } + return "?"; +} + +const char* level_class(LogLevel level) { + switch (level) { + case LOG_LEVEL_TRACE: + return "lvl-trace"; + case LOG_LEVEL_DEBUG: + return "lvl-debug"; + case LOG_LEVEL_INFO: + return "lvl-info"; + case LOG_LEVEL_WARN: + return "lvl-warn"; + case LOG_LEVEL_ERROR: + return "lvl-error"; + } + return "lvl-info"; +} + +std::string format_time(int64_t timeMs) { + const auto seconds = static_cast(timeMs / 1000); + std::tm localTime{}; +#if _WIN32 + localtime_s(&localTime, &seconds); +#else + localtime_r(&seconds, &localTime); +#endif + std::array buffer{}; + std::strftime(buffer.data(), buffer.size(), "%H:%M:%S", &localTime); + return fmt::format("{}.{:03}", buffer.data(), timeMs % 1000); +} + +void append_text(Rml::ElementDocument* doc, Rml::Element* parent, const Rml::String& text) { + parent->AppendChild(doc->CreateTextNode(text)); +} + +Rml::Element* append_span(Rml::ElementDocument* doc, Rml::Element* parent, const char* className, + const Rml::String& text) { + auto span = doc->CreateElement("span"); + span->SetClass(className, true); + append_text(doc, span.get(), text); + return parent->AppendChild(std::move(span)); +} + +} // namespace + +LogsWindow::LogsWindow(std::string modFilter) + : Window{Props{.tabBar = false, .styleSheets = {"res/rml/logs.rcss"}}}, + mModFilter{std::move(modFilter)} { + mRoot->SetClass("logs", true); + set_content([this](Rml::Element* content) { build_content(content); }); +} + +void LogsWindow::build_content(Rml::Element* content) { + auto* toolbar = append(content, "div"); + toolbar->SetClass("log-toolbar", true); + + auto* title = append(toolbar, "div"); + title->SetClass("log-title", true); + title->SetInnerRML("Logs"); + + auto* modLabel = append(toolbar, "div"); + modLabel->SetClass("log-title-mod", true); + modLabel->SetInnerRML(mModFilter.empty() ? "All mods" : fmt::format("{}", escape(mModFilter))); + + append(toolbar, "div")->SetClass("log-toolbar-spacer", true); + + for (const LogLevel level : + {LOG_LEVEL_TRACE, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR}) + { + add_child(toolbar, + ControlledButton::Props{ + .text = level_name(level), + .isSelected = [this, level] { return mMinLevel <= level; }, + }) + .on_pressed([this, level] { + mMinLevel = level; + rebuild_lines(); + }); + } + + append(toolbar, "div")->SetClass("log-toolbar-spacer", true); + + add_child