diff --git a/src/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp index 98c0075290..f8c97521d6 100644 --- a/src/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "dusk/config.hpp" @@ -69,12 +70,6 @@ struct ModSearchDir { std::filesystem::path nativeLibDir; }; -enum class ModOrigin : u8 { - User, - Bundled, - BundledInPlace, -}; - struct ModOperation { enum class State : u8 { Pending, @@ -199,9 +194,8 @@ struct LoadedMod { std::string dataDirUtf8; uint32_t searchDirIndex = 0; - ModOrigin origin = ModOrigin::User; // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. - bool inPlace = false; + bool nativeInPlace = false; FileIdentity fileIdentity; std::unique_ptr> cvarIsEnabled; @@ -243,6 +237,11 @@ struct LoadedMod { // Mods this mod imports services from, and mods importing services from this mod. std::vector dependencies; std::vector dependents; + + [[nodiscard]] bool is_enabled() const { + return cvarIsEnabled != nullptr && cvarIsEnabled->getValue(); + } + [[nodiscard]] bool activation_failed() const { return loadFailed || (is_enabled() && !active); } }; class ModLoader { @@ -265,6 +264,8 @@ public: [[nodiscard]] std::filesystem::path user_mods_dir() const; [[nodiscard]] bool can_uninstall(const LoadedMod& mod) const; + [[nodiscard]] LoadedMod* find_mod(std::string_view id); + [[nodiscard]] const LoadedMod* find_mod(std::string_view id) const; [[nodiscard]] uint64_t generation() const noexcept { return m_generation; } [[nodiscard]] auto mods() const { @@ -276,16 +277,27 @@ public: } private: - enum class RequestKind : u8 { Enable, Disable, Sync, Install, Reactivate }; - struct Request { + enum class LifecycleAction : u8 { Enable, Disable, Reactivate }; + struct LifecycleRequest { std::string modId; - RequestKind kind; - std::filesystem::path path; + LifecycleAction action; std::shared_ptr operation; - bool force = false; - bool remove = false; }; - struct SyncResult { + struct InstallRequest { + std::filesystem::path stagedPath; + std::shared_ptr operation; + }; + struct ReloadRequest { + std::string modId; + std::shared_ptr operation; + }; + struct UninstallRequest { + std::string modId; + std::shared_ptr operation; + }; + using Request = std::variant; + + struct OperationResult { bool success = true; std::string message; LoadedMod* mod = nullptr; @@ -329,12 +341,12 @@ private: [[nodiscard]] std::string describe_missing_import( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const; - LoadedMod* find_mod(std::string_view id) const; void drain_retired_natives(); void apply_pending_requests(); - [[nodiscard]] SyncResult install_staged(const std::filesystem::path& path); - [[nodiscard]] SyncResult sync_path( - const std::filesystem::path& path, bool force, uint32_t searchDirIndex = 0); + [[nodiscard]] OperationResult install_staged(const std::filesystem::path& path); + [[nodiscard]] OperationResult load_runtime_mod(const std::filesystem::path& path); + [[nodiscard]] OperationResult reload_runtime_mod(LoadedMod& mod); + [[nodiscard]] OperationResult runtime_result(LoadedMod& mod); void forget_mod(LoadedMod& mod); void flush_toasts(); void on_enabled_changed(LoadedMod& mod); diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index da056a09cd..aa8ff4ad1b 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -21,6 +21,7 @@ #include "dusk/data.hpp" #include "dusk/io.hpp" #include "dusk/mods/log_buffer.hpp" +#include "dusk/mods/path.hpp" #include "dusk/mods/svc/config.hpp" #include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" @@ -204,20 +205,6 @@ void complete_operation(const std::shared_ptr& operation, const bo operation->message = std::move(message); } -std::string safe_filename(std::string_view id) { - std::string result{id}; - std::ranges::replace_if( - result, - [](char character) { - return !((character >= 'a' && character <= 'z') || - (character >= 'A' && character <= 'Z') || - (character >= '0' && character <= '9') || character == '.' || - character == '_' || character == '-'); - }, - '_'); - return result; -} - LoadedMod::FileIdentity file_identity(const fs::path& path) { std::error_code error; const bool isDirectory = fs::is_directory(path, error); @@ -235,13 +222,6 @@ LoadedMod::FileIdentity file_identity(const fs::path& path) { return {.size = size, .modified = modified, .valid = true}; } -bool same_path(const fs::path& lhs, const fs::path& rhs) { - std::error_code error; - if (fs::equivalent(lhs, rhs, error)) { - return true; - } - return fs::absolute(lhs).lexically_normal() == fs::absolute(rhs).lexically_normal(); -} } // namespace ModLoader& ModLoader::instance() { @@ -617,7 +597,7 @@ void ModLoader::load_native( fs::path libPath; fs::path runtimeDir; DirectoryRollback runtimeDirRollback; - if (mod.inPlace) { + if (mod.nativeInPlace) { if (!dllEntry.empty()) { libPath = mod.modPath / dllEntry; } else if (auto external = external_native_lib_path(mod); !external.empty()) { @@ -750,7 +730,7 @@ bool ModLoader::load_native_if_present(LoadedMod& mod) { } const auto& native = std::get(result); - if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { + if (!native.anyLibs && !(mod.nativeInPlace && !external_native_lib_path(mod).empty())) { mod.nativeStatus = NativeModStatus::None; return true; } @@ -765,7 +745,7 @@ bool ModLoader::load_native_if_present(LoadedMod& mod) { } void ModLoader::unload_native(LoadedMod& mod) { - if (!mod.native || mod.inPlace) { + if (!mod.native || mod.nativeInPlace) { return; } // Deferred dlclose: this mod's code may still be on the stack below the current tick @@ -900,10 +880,7 @@ LoadedMod* ModLoader::try_load_mod(const fs::path& modPath, bool fromDir, uint32 mod.active = true; mod.modPath = fs::absolute(modPath); mod.searchDirIndex = searchDirIndex; - mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; - mod.origin = searchDirIndex == 0 ? ModOrigin::User : - mod.inPlace ? ModOrigin::BundledInPlace : - ModOrigin::Bundled; + mod.nativeInPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; mod.fileIdentity = file_identity(modPath); mod.metadata = std::move(*metadata); mod.bundle = std::move(bundle); @@ -1093,7 +1070,7 @@ void ModLoader::init() { }); for (auto& entry : entries) { - (void)sync_path(entry.path(), false, static_cast(dirIndex)); + (void)try_load_mod(entry.path(), entry.is_directory(), static_cast(dirIndex)); } } @@ -1166,10 +1143,19 @@ void ModLoader::init() { m_startupComplete = true; } -LoadedMod* ModLoader::find_mod(std::string_view id) const { - for (auto& mod : mods()) { - if (mod.metadata.id == id) { - return &mod; +LoadedMod* ModLoader::find_mod(std::string_view id) { + for (auto& mod : m_mods) { + if (mod->metadata.id == id) { + return mod.get(); + } + } + return nullptr; +} + +const LoadedMod* ModLoader::find_mod(std::string_view id) const { + for (const auto& mod : m_mods) { + if (mod->metadata.id == id) { + return mod.get(); } } return nullptr; @@ -1189,13 +1175,10 @@ void ModLoader::request_disable(std::string_view id) { ModOperationHandle ModLoader::request_reload(std::string_view id) { auto operation = std::make_shared(); - if (auto* mod = find_mod(id)) { - m_pendingRequests.push_back({ + if (find_mod(id) != nullptr) { + m_pendingRequests.push_back(ReloadRequest{ .modId = std::string{id}, - .kind = RequestKind::Sync, - .path = mod->modPath, .operation = operation, - .force = true, }); } else { complete_operation(operation, false, "The mod is no longer installed"); @@ -1205,9 +1188,8 @@ ModOperationHandle ModLoader::request_reload(std::string_view id) { ModOperationHandle ModLoader::request_install(fs::path path) { auto operation = std::make_shared(); - m_pendingRequests.push_back({ - .kind = RequestKind::Install, - .path = std::move(path), + m_pendingRequests.push_back(InstallRequest{ + .stagedPath = std::move(path), .operation = operation, }); return operation; @@ -1215,14 +1197,10 @@ ModOperationHandle ModLoader::request_install(fs::path path) { ModOperationHandle ModLoader::request_uninstall(std::string_view id) { auto operation = std::make_shared(); - if (auto* mod = find_mod(id)) { - m_pendingRequests.push_back({ + if (find_mod(id) != nullptr) { + m_pendingRequests.push_back(UninstallRequest{ .modId = std::string{id}, - .kind = RequestKind::Sync, - .path = mod->modPath, .operation = operation, - .force = false, - .remove = true, }); } else { complete_operation(operation); @@ -1232,9 +1210,9 @@ ModOperationHandle ModLoader::request_uninstall(std::string_view id) { ModOperationHandle ModLoader::request_reactivate(std::string_view id) { auto operation = std::make_shared(); - m_pendingRequests.push_back({ + m_pendingRequests.push_back(LifecycleRequest{ .modId = std::string{id}, - .kind = RequestKind::Reactivate, + .action = LifecycleAction::Reactivate, .operation = operation, }); return operation; @@ -1245,7 +1223,7 @@ fs::path ModLoader::user_mods_dir() const { } bool ModLoader::can_uninstall(const LoadedMod& mod) const { - return mod.origin == ModOrigin::User && !mod.inPlace && mod.modPath.extension() == ".dusk"; + return mod.searchDirIndex == 0 && mod.modPath.extension() == ".dusk"; } void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { @@ -1256,7 +1234,10 @@ void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { if (!m_startupComplete) { return; } - m_pendingRequests.push_back({mod.metadata.id, RequestKind::Disable}); + m_pendingRequests.push_back(LifecycleRequest{ + .modId = mod.metadata.id, + .action = LifecycleAction::Disable, + }); } void ModLoader::flush_toasts() { @@ -1473,8 +1454,11 @@ void ModLoader::on_enabled_changed(LoadedMod& mod) { } return; } - m_pendingRequests.push_back({mod.metadata.id, - mod.cvarIsEnabled->getValue() ? RequestKind::Enable : RequestKind::Disable}); + m_pendingRequests.push_back(LifecycleRequest{ + .modId = mod.metadata.id, + .action = + mod.cvarIsEnabled->getValue() ? LifecycleAction::Enable : LifecycleAction::Disable, + }); } void ModLoader::forget_mod(LoadedMod& mod) { @@ -1542,121 +1526,99 @@ void ModLoader::forget_mod(LoadedMod& mod) { log::write(modId, LOG_LEVEL_INFO, "forgot removed package"); } -ModLoader::SyncResult ModLoader::sync_path( - const fs::path& requestedPath, const bool force, const uint32_t searchDirIndex) { +ModLoader::OperationResult ModLoader::load_runtime_mod(const fs::path& requestedPath) { const auto path = fs::absolute(requestedPath).lexically_normal(); - auto loaded = std::ranges::find_if( - m_mods, [&](const auto& candidate) { return same_path(candidate->modPath, path); }); - LoadedMod* mod = loaded == m_mods.end() ? nullptr : loaded->get(); - std::error_code error; const auto status = fs::status(path, error); - const bool exists = !error && fs::exists(status); - if (!exists) { - const bool isGone = error == std::errc::no_such_file_or_directory -#if defined(_WIN32) - || error == std::errc::permission_denied -#endif - ; - if (error && !isGone) { - return { - .success = false, - .message = fmt::format("Could not inspect the package: {}", error.message()), - }; - } - if (mod != nullptr) { - forget_mod(*mod); - } - return {}; - } - - const auto identity = file_identity(path); - if (mod != nullptr) { - if (!force && identity.valid && identity == mod->fileIdentity) { - return {.mod = mod}; - } - if (mod->inPlace) { - return { - .success = false, - .message = "Built-in mods cannot be updated in-game", - .mod = mod, - }; - } - apply_lifecycle_change(*mod, true); - ++m_generation; - } else { - const bool fromDir = fs::is_directory(status); - std::unique_ptr bundle; - std::optional metadata; - try { - bundle = load_bundle(path, fromDir); - metadata = load_metadata(path, *bundle); - } catch (const std::exception& exception) { - return { - .success = false, - .message = fmt::format("Invalid mod package: {}", exception.what()), - }; - } - if (const auto* duplicate = find_mod(metadata->id)) { - return { - .success = false, - .message = fmt::format("A mod with this ID is already loaded from {}", - data::abbreviated_path_string(duplicate->modPath)), - }; - } - mod = try_load_mod(path, fromDir, searchDirIndex, std::move(bundle), std::move(metadata)); - if (mod == nullptr) { - return { - .success = false, - .message = "The mod could not be loaded", - }; - } - - if (m_startupComplete) { - mod->enabledSubscription = Register(*mod->cvarIsEnabled, - [this, mod](const bool&, const bool&) { on_enabled_changed(*mod); }); - loader::sort_mods(m_mods); - if (!mod->cvarIsEnabled->getValue()) { - mod->active = false; - mod->suspendedByProvider = false; - log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed disabled by config"); - } else if (!mod->loadFailed) { - mod->active = false; - apply_lifecycle_change(*mod, false); - } - ++m_generation; - log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed at runtime"); - } - } - - if (!m_startupComplete) { - return {.mod = mod}; - } - if (mod->loadFailed) { + if (error || !fs::exists(status)) { return { .success = false, - .message = - mod->failureReason.empty() ? "The mod failed to activate" : mod->failureReason, - .mod = mod, + .message = error ? fmt::format("Could not inspect the package: {}", error.message()) : + "The package was not found", }; } - if (mod->cvarIsEnabled->getValue() && !mod->active) { + + const bool fromDir = fs::is_directory(status); + std::unique_ptr bundle; + std::optional metadata; + try { + bundle = load_bundle(path, fromDir); + metadata = load_metadata(path, *bundle); + } catch (const std::exception& exception) { + return { + .success = false, + .message = fmt::format("Invalid mod package: {}", exception.what()), + }; + } + if (const auto* duplicate = find_mod(metadata->id)) { + return { + .success = false, + .message = fmt::format("A mod with this ID is already loaded from {}", + data::abbreviated_path_string(duplicate->modPath)), + }; + } + auto* mod = try_load_mod(path, fromDir, 0, std::move(bundle), std::move(metadata)); + if (mod == nullptr) { + return { + .success = false, + .message = "The mod could not be loaded", + }; + } + + mod->enabledSubscription = Register( + *mod->cvarIsEnabled, [this, mod](const bool&, const bool&) { on_enabled_changed(*mod); }); + loader::sort_mods(m_mods); + if (!mod->cvarIsEnabled->getValue()) { + mod->active = false; + mod->suspendedByProvider = false; + log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed disabled by config"); + } else if (!mod->loadFailed) { + mod->active = false; + apply_lifecycle_change(*mod, false); + } + ++m_generation; + log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed at runtime"); + return runtime_result(*mod); +} + +ModLoader::OperationResult ModLoader::reload_runtime_mod(LoadedMod& mod) { + if (mod.nativeInPlace) { + return { + .success = false, + .message = "Built-in mods cannot be updated in-game", + .mod = &mod, + }; + } + apply_lifecycle_change(mod, true); + ++m_generation; + return runtime_result(mod); +} + +ModLoader::OperationResult ModLoader::runtime_result(LoadedMod& mod) { + if (mod.loadFailed) { + return { + .success = false, + .message = mod.failureReason.empty() ? "The mod failed to activate" : mod.failureReason, + .mod = &mod, + }; + } + if (mod.cvarIsEnabled->getValue() && !mod.active) { return { .success = false, .message = "A required provider is unavailable", - .mod = mod, + .mod = &mod, }; } - if (!mod->cvarIsEnabled->getValue()) { + if (!mod.cvarIsEnabled->getValue()) { return { .message = "Installed, disabled by config", - .mod = mod, + .mod = &mod, }; } - return {.mod = mod}; + return {.mod = &mod}; } -ModLoader::SyncResult ModLoader::install_staged(const fs::path& requestedPath) { +ModLoader::OperationResult ModLoader::install_staged(const fs::path& requestedPath) { if (m_searchDirs.empty()) { return { .success = false, @@ -1695,7 +1657,8 @@ ModLoader::SyncResult ModLoader::install_staged(const fs::path& requestedPath) { } fs::path destination; - if (auto* installed = find_mod(metadata.id)) { + auto* installed = find_mod(metadata.id); + if (installed != nullptr) { if (!can_uninstall(*installed)) { return { .success = false, @@ -1734,7 +1697,8 @@ ModLoader::SyncResult ModLoader::install_staged(const fs::path& requestedPath) { .message = std::move(replaceError), }; } - auto result = sync_path(destination, true); + auto result = + installed != nullptr ? reload_runtime_mod(*installed) : load_runtime_mod(destination); if (hadDestination) { fs::remove(aside, error); } @@ -1746,7 +1710,7 @@ ModLoader::SyncResult ModLoader::install_staged(const fs::path& requestedPath) { .message = std::move(replaceError), }; } - return sync_path(destination, true); + return installed != nullptr ? reload_runtime_mod(*installed) : load_runtime_mod(destination); #endif } @@ -1758,55 +1722,90 @@ void ModLoader::apply_pending_requests() { return; } - // Path mutations retain queue order. Enable/disable/reactivate can still coalesce per mod. + // Package mutations retain request order. Enable/disable/reactivate can still coalesce per mod. const auto requests = std::exchange(m_pendingRequests, {}); - std::vector coalesced; + std::vector coalesced; for (const auto& request : requests) { - if (request.kind == RequestKind::Install) { - auto result = install_staged(request.path); - complete_operation(request.operation, result.success, std::move(result.message)); - continue; - } - if (request.kind == RequestKind::Sync) { - std::string removedName; - if (request.remove) { - auto* mod = find_mod(request.modId); - if (mod == nullptr) { - complete_operation(request.operation); - continue; - } - if (!can_uninstall(*mod)) { - complete_operation( - request.operation, false, "The mod is part of this Dusklight installation"); - continue; - } - removedName = mod->metadata.name; - std::error_code error; - if (!fs::remove(request.path, error)) { - complete_operation(request.operation, false, - error ? error.message() : "The package was not found"); - continue; - } - } - auto result = sync_path(request.path, request.force); - complete_operation(request.operation, result.success, std::move(result.message)); - if (request.remove && result.success) { + if (const auto* install = std::get_if(&request)) { + auto result = install_staged(install->stagedPath); + if (result.success && result.mod != nullptr) { ui::push_toast({ - .title = "Mod uninstalled", - .content = std::move(removedName), + .title = "Mod installed", + .content = fmt::format( + "{} {}", result.mod->metadata.name, result.mod->metadata.version), .duration = std::chrono::seconds{4}, }); + } else if (!result.success && result.mod == nullptr) { + ui::push_toast({ + .type = "warning", + .title = "Mod install failed", + .content = + result.message.empty() ? "The loader rejected the package" : result.message, + .duration = std::chrono::seconds{6}, + }); } + if (!result.success && !m_searchDirs.empty()) { + // request_install owns only files under the configured staging directory. + std::error_code error; + const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); + if (!error) { + const auto stagedPath = fs::weakly_canonical(install->stagedPath, error); + if (!error && stagedPath.parent_path() == userDir / ".staging") { + fs::remove(stagedPath, error); + } + } + } + complete_operation(install->operation, result.success, std::move(result.message)); continue; } - const auto existing = std::ranges::find_if( - coalesced, [&](const Request& r) { return r.modId == request.modId; }); + if (const auto* reload = std::get_if(&request)) { + auto* mod = find_mod(reload->modId); + if (mod == nullptr) { + complete_operation(reload->operation, false, "The mod is no longer installed"); + continue; + } + auto result = reload_runtime_mod(*mod); + complete_operation(reload->operation, result.success, std::move(result.message)); + continue; + } + if (const auto* uninstall = std::get_if(&request)) { + auto* mod = find_mod(uninstall->modId); + if (mod == nullptr) { + complete_operation(uninstall->operation); + continue; + } + if (!can_uninstall(*mod)) { + complete_operation( + uninstall->operation, false, "The mod is part of this Dusklight installation"); + continue; + } + + const auto removedName = mod->metadata.name; + std::error_code error; + if (!fs::remove(mod->modPath, error)) { + complete_operation(uninstall->operation, false, + error ? error.message() : "The package was not found"); + continue; + } + forget_mod(*mod); + complete_operation(uninstall->operation); + ui::push_toast({ + .title = "Mod uninstalled", + .content = removedName, + .duration = std::chrono::seconds{4}, + }); + continue; + } + + const auto& lifecycle = std::get(request); + const auto existing = + std::ranges::find(coalesced, lifecycle.modId, &LifecycleRequest::modId); if (existing != coalesced.end()) { complete_operation( existing->operation, false, "Superseded by a newer lifecycle request"); - *existing = request; + *existing = lifecycle; } else { - coalesced.push_back(request); + coalesced.push_back(lifecycle); } } @@ -1817,14 +1816,14 @@ void ModLoader::apply_pending_requests() { complete_operation(request.operation, false, "The mod is no longer installed"); continue; } - if (request.kind == RequestKind::Enable && mod->enabledApplied) { + if (request.action == LifecycleAction::Enable && mod->enabledApplied) { continue; } - if (request.kind == RequestKind::Disable && !mod->enabledApplied && !mod->active) { + if (request.action == LifecycleAction::Disable && !mod->enabledApplied && !mod->active) { continue; } - if (request.kind == RequestKind::Reactivate) { + if (request.action == LifecycleAction::Reactivate) { mod->loadFailed = false; mod->failureReason.clear(); mod->suspendedByProvider = false; @@ -1834,7 +1833,7 @@ void ModLoader::apply_pending_requests() { } apply_lifecycle_change(*mod, false); - if (request.kind == RequestKind::Reactivate) { + if (request.action == LifecycleAction::Reactivate) { std::string error; if (mod->loadFailed) { error = diff --git a/src/dusk/mods/path.hpp b/src/dusk/mods/path.hpp new file mode 100644 index 0000000000..4213c3fbf6 --- /dev/null +++ b/src/dusk/mods/path.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace dusk::mods { + +inline std::string safe_filename(std::string_view value) { + std::string result{value}; + std::ranges::replace_if( + result, + [](char character) { + return !((character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z') || + (character >= '0' && character <= '9') || character == '.' || + character == '_' || character == '-'); + }, + '_'); + return result; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/queue.cpp b/src/dusk/mods/queue.cpp index 3707396ca3..12005fbf7f 100644 --- a/src/dusk/mods/queue.cpp +++ b/src/dusk/mods/queue.cpp @@ -2,6 +2,7 @@ #include "dusk/hash.hpp" #include "dusk/mod_loader.hpp" +#include "dusk/mods/path.hpp" #include "dusk/ui/ui.hpp" #include @@ -20,6 +21,7 @@ #include #include #include +#include #include namespace dusk::mods::queue { @@ -34,12 +36,14 @@ struct VerifyResult { bool canceled = false; }; +enum class PendingIntent { None, Pause, Cancel }; + struct QueueItem { std::string key; Request request; State state = State::Queued; std::filesystem::path partialPath; - std::filesystem::path installPath; + std::filesystem::path stagedPath; uint64_t completed = 0; uint64_t total = 0; std::string message; @@ -47,19 +51,12 @@ struct QueueItem { clock::time_point retryAt{}; borealis::Task task; borealis::Task verification; - ModOperationHandle operation; - bool packagePublished = false; - bool removeAfterOperation = false; - bool pauseRequested = false; - bool resumeRequested = false; - bool cancelRequested = false; + PendingIntent pendingIntent = PendingIntent::None; }; std::vector queueItems; uint64_t nextLocalKey = 1; -bool terminal(State state); - QueueItem* find_queue_item(std::string_view key) { const auto item = std::ranges::find(queueItems, key, [](const QueueItem& candidate) { return std::string_view{candidate.key}; }); @@ -80,15 +77,6 @@ const LocalFile* local_source(const QueueItem& item) { return std::get_if(&item.request.source); } -const LoadedMod* find_loaded_mod(std::string_view id) { - for (const auto& mod : ModLoader::instance().mods()) { - if (mod.metadata.id == id) { - return &mod; - } - } - return nullptr; -} - std::string lowercase(std::string value) { std::ranges::transform(value, value.begin(), [](char character) { return character >= 'A' && character <= 'Z' ? static_cast(character + ('a' - 'A')) : @@ -104,20 +92,6 @@ bool valid_sha256(std::string_view value) { }); } -std::string safe_filename(std::string_view id) { - std::string result{id}; - std::ranges::replace_if( - result, - [](char character) { - return !((character >= 'a' && character <= 'z') || - (character >= 'A' && character <= 'Z') || - (character >= '0' && character <= '9') || character == '.' || - character == '_' || character == '-'); - }, - '_'); - return result; -} - std::string sha256_file( const std::filesystem::path& path, borealis::TaskContext& context, std::string& error) { std::ifstream input{path, std::ios::binary}; @@ -273,29 +247,23 @@ void remove_partial(const QueueItem& item) { metadataPath += ".borealis-resume.json"; std::filesystem::remove(metadataPath, ec); } - if (!item.installPath.empty()) { - std::filesystem::remove(item.installPath, ec); + if (!item.stagedPath.empty()) { + std::filesystem::remove(item.stagedPath, ec); } } -void fail(QueueItem& item, State state, std::string message, bool discardPartial) { +void fail(QueueItem& item, std::string message, bool discardPartial) { item.task = {}; item.verification = {}; - item.operation.reset(); - item.state = state; + item.state = State::Failed; item.message = std::move(message); - item.pauseRequested = false; - item.resumeRequested = false; - item.cancelRequested = false; - item.removeAfterOperation = false; + item.pendingIntent = PendingIntent::None; if (discardPartial) { remove_partial(item); item.completed = 0; } - const char* title = state == State::ActivationFailed ? "Mod activation failed" : - state == State::InstallFailed ? "Mod install failed" : - local_source(item) != nullptr ? "Mod package failed" : - "Mod download failed"; + const char* title = + local_source(item) != nullptr ? "Mod package failed" : "Mod download failed"; ui::push_toast({ .type = "warning", .title = title, @@ -327,12 +295,12 @@ void schedule_retry(QueueItem& item, std::string message) { void start_download(QueueItem& item) { const auto* source = url_source(item); if (source == nullptr) { - fail(item, State::Failed, "The install source is not a URL", false); + fail(item, "The install source is not a URL", false); return; } const auto userDir = ModLoader::instance().user_mods_dir(); if (userDir.empty()) { - fail(item, State::Failed, "No writable mods directory is configured", false); + fail(item, "No writable mods directory is configured", false); return; } @@ -341,14 +309,11 @@ void start_download(QueueItem& item) { std::error_code ec; std::filesystem::create_directories(item.partialPath.parent_path(), ec); if (ec) { - fail(item, State::Failed, - fmt::format("Could not create the download directory: {}", ec.message()), false); + fail(item, fmt::format("Could not create the download directory: {}", ec.message()), false); return; } - item.pauseRequested = false; - item.resumeRequested = false; - item.cancelRequested = false; + item.pendingIntent = PendingIntent::None; item.message.clear(); item.total = source->size; item.state = State::Downloading; @@ -364,12 +329,12 @@ void start_download(QueueItem& item) { void start_local_verification(QueueItem& item) { const auto* source = local_source(item); if (source == nullptr) { - fail(item, State::Failed, "The install source is not a local file", false); + fail(item, "The install source is not a local file", false); return; } const auto userDir = ModLoader::instance().user_mods_dir(); if (userDir.empty()) { - fail(item, State::Failed, "No writable mods directory is configured", false); + fail(item, "No writable mods directory is configured", false); return; } item.state = State::Verifying; @@ -390,39 +355,36 @@ void finish_download(QueueItem& item) { item.completed = std::max(item.completed, progress.completed); std::optional completed; + std::string taskError; + bool taskFailed = false; try { completed = item.task.try_take(); } catch (const std::exception& exception) { - item.task = {}; - if (item.cancelRequested) { - remove_partial(item); - item.state = State::Canceled; - return; - } - if (item.pauseRequested) { - item.state = item.resumeRequested ? State::Queued : State::Paused; - item.pauseRequested = false; - item.resumeRequested = false; - return; - } - schedule_retry(item, exception.what()); - return; + taskError = exception.what(); + taskFailed = true; + } catch (...) { + taskError = "The download failed"; + taskFailed = true; } - if (!completed) { + if (!completed && !taskFailed) { return; } item.task = {}; - if (item.cancelRequested) { + switch (std::exchange(item.pendingIntent, PendingIntent::None)) { + case PendingIntent::Cancel: remove_partial(item); item.completed = 0; item.state = State::Canceled; return; + case PendingIntent::Pause: + return; + case PendingIntent::None: + break; } - if (item.pauseRequested) { - item.state = item.resumeRequested ? State::Queued : State::Paused; - item.pauseRequested = false; - item.resumeRequested = false; + + if (taskFailed) { + schedule_retry(item, std::move(taskError)); return; } @@ -437,14 +399,14 @@ void finish_download(QueueItem& item) { if (retryable(*completed)) { schedule_retry(item, message); } else { - fail(item, State::Failed, message, true); + fail(item, message, true); } return; } const auto* source = url_source(item); if (source == nullptr) { - fail(item, State::Failed, "The install source changed", true); + fail(item, "The install source changed", true); return; } item.completed = source->size; @@ -458,23 +420,12 @@ void finish_download(QueueItem& item) { }); } -void start_install(QueueItem& item) { - auto& loader = ModLoader::instance(); - item.state = State::Installing; - item.message.clear(); - item.operation = loader.request_install(item.installPath); -} - -void publish_verified(QueueItem& item) { - start_install(item); -} - -void finish_verification(QueueItem& item) { +bool finish_verification(QueueItem& item) { VerifyResult result; try { auto completed = item.verification.try_take(); if (!completed) { - return; + return false; } result = std::move(*completed); } catch (const std::exception& exception) { @@ -483,7 +434,8 @@ void finish_verification(QueueItem& item) { result.error = "Package verification failed"; } item.verification = {}; - if (result.canceled || item.cancelRequested) { + if (result.canceled || item.pendingIntent == PendingIntent::Cancel) { + item.pendingIntent = PendingIntent::None; if (!result.stagedPath.empty()) { std::error_code error; std::filesystem::remove(result.stagedPath, error); @@ -491,81 +443,31 @@ void finish_verification(QueueItem& item) { remove_partial(item); item.state = State::Canceled; item.completed = 0; - return; + return false; } if (!result.error.empty()) { - fail(item, State::Failed, std::move(result.error), true); - return; + fail(item, std::move(result.error), true); + return false; } if (const auto duplicate = find_queue_item_by_mod_id(result.metadata.id); - duplicate != nullptr && duplicate != &item && !terminal(duplicate->state)) + duplicate != nullptr && duplicate != &item && !is_terminal(duplicate->state)) { - fail(item, State::InstallFailed, "This mod already has an active install", true); - return; + fail(item, "This mod already has an active install", true); + return false; } if (local_source(item) != nullptr && !item.request.id.empty() && (item.request.id != result.metadata.id || item.request.version != result.metadata.version)) { - fail(item, State::InstallFailed, "The local package changed after confirmation", true); - return; + fail(item, "The local package changed after confirmation", true); + return false; } item.request.id = result.metadata.id; item.request.name = result.metadata.name; item.request.version = result.metadata.version; - item.installPath = std::move(result.stagedPath); + item.stagedPath = std::move(result.stagedPath); item.completed = item.total; - publish_verified(item); -} - -void update_install(QueueItem& item) { - if (item.operation == nullptr || item.operation->state == ModOperation::State::Pending) { - return; - } - - const auto* mod = find_loaded_mod(item.request.id); - item.packagePublished = mod != nullptr && mod->metadata.version == item.request.version; - if (item.operation->state == ModOperation::State::Failed) { - const bool activationFailed = - item.packagePublished && - (mod->loadFailed || (mod->cvarIsEnabled->getValue() && !mod->active)); - fail(item, activationFailed ? State::ActivationFailed : State::InstallFailed, - item.operation->message.empty() ? "The mod could not be installed" : - item.operation->message, - false); - return; - } - - item.message = item.operation->message; - item.operation.reset(); - if (mod == nullptr || mod->metadata.version != item.request.version) { - fail(item, State::InstallFailed, "The installed package was not loaded", false); - return; - } - - item.state = State::Installed; - ui::push_toast({ - .title = "Mod installed", - .content = fmt::format("{} {}", item.request.name, item.request.version), - .duration = std::chrono::seconds{4}, - }); -} - -void update_uninstall(QueueItem& item) { - if (item.operation == nullptr || item.operation->state == ModOperation::State::Pending) { - return; - } - if (item.operation->state == ModOperation::State::Failed) { - fail(item, State::ActivationFailed, - item.operation->message.empty() ? - "The mod could not be uninstalled" : - fmt::format("Uninstall failed: {}", item.operation->message), - false); - return; - } - - item.operation.reset(); - item.state = State::Canceled; - item.message.clear(); + ModLoader::instance().request_install(std::exchange(item.stagedPath, std::filesystem::path{})); + return true; } Item snapshot(const QueueItem& item) { @@ -598,11 +500,6 @@ Item snapshot(const QueueItem& item) { return result; } -bool terminal(State state) { - return state == State::Installed || state == State::Failed || state == State::InstallFailed || - state == State::ActivationFailed || state == State::Canceled; -} - } // namespace bool enqueue(Request request, std::string* keyOut) { @@ -623,7 +520,7 @@ bool enqueue(Request request, std::string* keyOut) { } if (auto* existing = find_queue_item_by_mod_id(request.id)) { - if (!terminal(existing->state)) { + if (!is_terminal(existing->state)) { return false; } existing->request = std::move(request); @@ -631,15 +528,10 @@ bool enqueue(Request request, std::string* keyOut) { existing->completed = 0; existing->total = total; existing->partialPath.clear(); - existing->installPath.clear(); + existing->stagedPath.clear(); existing->message.clear(); existing->retryCount = 0; - existing->operation.reset(); - existing->packagePublished = false; - existing->removeAfterOperation = false; - existing->pauseRequested = false; - existing->resumeRequested = false; - existing->cancelRequested = false; + existing->pendingIntent = PendingIntent::None; if (keyOut != nullptr) { *keyOut = existing->key; } @@ -655,52 +547,27 @@ bool enqueue(Request request, std::string* keyOut) { } void update() { - for (auto& item : queueItems) { - if (item.task && item.task.ready()) { - finish_download(item); + for (auto item = queueItems.begin(); item != queueItems.end();) { + if (item->task && item->task.ready()) { + finish_download(*item); } - if (item.state == State::Verifying && item.verification && item.verification.ready()) { - finish_verification(item); - } - if (item.state == State::Installing) { - update_install(item); - } - if (item.state == State::Activating) { - update_install(item); - } - if (item.state == State::Uninstalling) { - update_uninstall(item); - } - if (item.state == State::ActivationFailed && item.packagePublished) { - const auto* mod = find_loaded_mod(item.request.id); - if (mod != nullptr && mod->active && mod->metadata.version == item.request.version) { - item.state = State::Installed; - item.message.clear(); - } + if (item->state == State::Verifying && item->verification && item->verification.ready() && + finish_verification(*item)) + { + item = queueItems.erase(item); + } else { + ++item; } } - std::erase_if(queueItems, [](const QueueItem& item) { - if (item.removeAfterOperation && item.state == State::Canceled) { - return true; - } - const bool trackedInstalledState = - item.state == State::Installed || item.state == State::ActivationFailed; - return item.packagePublished && trackedInstalledState && - find_loaded_mod(item.request.id) == nullptr; - }); - for (auto& item : queueItems) { if (item.task || item.verification) { return; } - if (terminal(item.state) || item.state == State::Paused) { + if (is_terminal(item.state) || item.state == State::Paused) { continue; } - if (item.state == State::Downloading || item.state == State::Verifying || - item.state == State::Installing || item.state == State::Activating || - item.state == State::Uninstalling) - { + if (item.state == State::Downloading || item.state == State::Verifying) { return; } if (item.state == State::Retrying && clock::now() < item.retryAt) { @@ -750,12 +617,42 @@ std::optional find_by_mod_id(std::string_view id) { bool has_active_items() { return std::ranges::any_of( - queueItems, [](const QueueItem& item) { return !terminal(item.state); }); + queueItems, [](const QueueItem& item) { return !is_terminal(item.state); }); +} + +size_t item_count() noexcept { + return queueItems.size(); +} + +size_t active_count() noexcept { + return static_cast(std::ranges::count_if( + queueItems, [](const QueueItem& item) { return !is_terminal(item.state); })); +} + +std::optional first_active() { + const auto item = std::ranges::find_if( + queueItems, [](const QueueItem& candidate) { return !is_terminal(candidate.state); }); + return item == queueItems.end() ? std::nullopt : std::optional{snapshot(*item)}; +} + +size_t active_items_ahead(std::string_view id) noexcept { + size_t result = 0; + for (const auto& item : queueItems) { + if (item.request.id == id) { + break; + } + if (!is_terminal(item.state)) { + ++result; + } + } + return result; } void pause(std::string_view id) { auto* item = find_queue_item(id); - if (item == nullptr || local_source(*item) != nullptr) { + if (item == nullptr || local_source(*item) != nullptr || + item->pendingIntent == PendingIntent::Cancel) + { return; } if (item->state == State::Queued || item->state == State::Retrying) { @@ -764,7 +661,7 @@ void pause(std::string_view id) { } if (item->state == State::Downloading && item->task) { item->completed = std::max(item->completed, item->task.progress().completed); - item->pauseRequested = true; + item->pendingIntent = PendingIntent::Pause; item->state = State::Paused; item->task.cancel(); } @@ -772,91 +669,45 @@ void pause(std::string_view id) { void resume(std::string_view id) { auto* item = find_queue_item(id); - if (item == nullptr || item->state != State::Paused) { + if (item == nullptr || item->state != State::Paused || + item->pendingIntent == PendingIntent::Cancel) + { return; } - if (item->task) { - item->resumeRequested = true; - } else { - item->state = State::Queued; - } + item->state = State::Queued; } void retry(std::string_view id) { auto* item = find_queue_item(id); - if (item == nullptr) { + if (item == nullptr || item->state != State::Failed) { return; } - - if (item->state == State::ActivationFailed) { - item->message.clear(); - item->state = State::Activating; - item->operation = ModLoader::instance().request_reactivate(item->request.id); - return; - } - if (item->state == State::InstallFailed) { - item->message.clear(); - std::error_code ec; - if (!item->installPath.empty() && std::filesystem::is_regular_file(item->installPath, ec)) { - start_install(*item); - } else { - item->completed = 0; - item->state = State::Queued; - } - return; - } - if (item->state == State::Failed) { - remove_partial(*item); - item->completed = 0; - item->retryCount = 0; - item->message.clear(); - item->state = State::Queued; - } + remove_partial(*item); + item->completed = 0; + item->retryCount = 0; + item->message.clear(); + item->state = State::Queued; } void cancel(std::string_view id) { auto* item = find_queue_item(id); - if (item == nullptr || item->state == State::Installing || item->state == State::Activating || - item->state == State::Uninstalling) - { - return; - } - if ((item->state == State::ActivationFailed || item->state == State::InstallFailed) && - item->packagePublished) - { - const auto* mod = find_loaded_mod(item->request.id); - if (mod == nullptr) { - std::error_code ec; - if (!std::filesystem::remove(item->installPath, ec) && ec) { - fail(*item, State::InstallFailed, fmt::format("Uninstall failed: {}", ec.message()), - false); - return; - } - item->state = State::Canceled; - item->message.clear(); - return; - } - item->message.clear(); - item->state = State::Uninstalling; - item->removeAfterOperation = true; - item->operation = ModLoader::instance().request_uninstall(item->request.id); + if (item == nullptr) { return; } if (item->task) { - item->cancelRequested = true; - item->pauseRequested = false; - item->resumeRequested = false; + item->pendingIntent = PendingIntent::Cancel; item->message = "Canceling..."; item->task.cancel(); return; } if (item->verification) { - item->cancelRequested = true; + item->pendingIntent = PendingIntent::Cancel; item->message = "Canceling..."; item->verification.cancel(); return; } remove_partial(*item); + item->pendingIntent = PendingIntent::None; item->completed = 0; item->state = State::Canceled; } @@ -876,9 +727,7 @@ void pause_all() { } void clear_finished() { - std::erase_if(queueItems, [](const QueueItem& item) { - return item.state == State::Installed || item.state == State::Canceled; - }); + std::erase_if(queueItems, [](const QueueItem& item) { return item.state == State::Canceled; }); } } // namespace dusk::mods::queue diff --git a/src/dusk/mods/queue.hpp b/src/dusk/mods/queue.hpp index e19278863d..8394b9f71a 100644 --- a/src/dusk/mods/queue.hpp +++ b/src/dusk/mods/queue.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -16,16 +17,14 @@ enum class State { Paused, Retrying, Verifying, - Installing, - Activating, - Installed, Failed, - InstallFailed, - ActivationFailed, - Uninstalling, Canceled, }; +[[nodiscard]] constexpr bool is_terminal(State state) noexcept { + return state == State::Failed || state == State::Canceled; +} + struct Url { std::string url; std::string sha256; @@ -59,7 +58,7 @@ struct Item { bool local = false; }; -/** Adds an install, replacing terminal URL history for the same package ID. */ +/** Adds an install, replacing failed or canceled work for the same package ID. */ bool enqueue(Request request, std::string* key = nullptr); /** Polls transfer and verification work. Call once per UI frame on the main thread. */ @@ -70,6 +69,10 @@ void shutdown() noexcept; [[nodiscard]] std::optional find(std::string_view key); [[nodiscard]] std::optional find_by_mod_id(std::string_view id); [[nodiscard]] bool has_active_items(); +[[nodiscard]] size_t item_count() noexcept; +[[nodiscard]] size_t active_count() noexcept; +[[nodiscard]] std::optional first_active(); +[[nodiscard]] size_t active_items_ahead(std::string_view id) noexcept; void pause(std::string_view id); void resume(std::string_view id); diff --git a/src/dusk/ui/drop_install_modal.cpp b/src/dusk/ui/drop_install_modal.cpp index 41a47298f5..e21e63c77e 100644 --- a/src/dusk/ui/drop_install_modal.cpp +++ b/src/dusk/ui/drop_install_modal.cpp @@ -2,6 +2,7 @@ #include "dusk/mods/loader/loader.hpp" #include "dusk/mods/queue.hpp" +#include "format.hpp" #include "package_row.hpp" #include "queue_window.hpp" @@ -13,15 +14,6 @@ namespace dusk::ui { namespace { -const mods::LoadedMod* installed_mod(std::string_view id) { - for (const auto& mod : mods::ModLoader::instance().mods()) { - if (mod.metadata.id == id) { - return &mod; - } - } - return nullptr; -} - size_t valid_count(const std::vector& packages) { return std::ranges::count(packages, true, &DropPackage::valid); } @@ -36,14 +28,12 @@ std::vector prepare_packages(std::vector packages) { } else if (package.hasNative && !mods::EnableCodeMods) { package.status = "Native mods cannot be installed on this platform"; } else if (const auto queued = mods::queue::find_by_mod_id(package.metadata.id); - queued && queued->state != mods::queue::State::Installed && - queued->state != mods::queue::State::Failed && - queued->state != mods::queue::State::InstallFailed && - queued->state != mods::queue::State::ActivationFailed && - queued->state != mods::queue::State::Canceled) + queued && !mods::queue::is_terminal(queued->state)) { package.status = "Already in the install queue"; - } else if (const auto* installed = installed_mod(package.metadata.id)) { + } else if (const auto* installed = + mods::ModLoader::instance().find_mod(package.metadata.id)) + { if (!mods::ModLoader::instance().can_uninstall(*installed)) { package.status = "Bundled mods cannot be updated in-game"; } else if (installed->metadata.version == package.metadata.version) { diff --git a/src/dusk/ui/format.hpp b/src/dusk/ui/format.hpp new file mode 100644 index 0000000000..28aa5efe3e --- /dev/null +++ b/src/dusk/ui/format.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace dusk::ui { + +struct ByteFormat { + int gibFractionDigits = 1; + int mibFractionDigits = 1; +}; + +inline std::string format_bytes(uint64_t bytes, ByteFormat options = {}) { + constexpr double kiB = 1024.0; + constexpr double miB = kiB * 1024.0; + constexpr double giB = miB * 1024.0; + if (bytes >= static_cast(giB)) { + return fmt::format( + "{:.{}f} GiB", static_cast(bytes) / giB, options.gibFractionDigits); + } + if (bytes >= static_cast(miB)) { + return fmt::format( + "{:.{}f} MiB", static_cast(bytes) / miB, options.mibFractionDigits); + } + if (bytes >= static_cast(kiB)) { + return fmt::format("{:.0f} KiB", static_cast(bytes) / kiB); + } + return fmt::format("{} B", bytes); +} + +// Truncates without splitting a UTF-8 sequence. +inline std::string snippet(std::string_view text, size_t maxBytes) { + if (text.size() <= maxBytes) { + return std::string{text}; + } + size_t end = maxBytes; + while (end > 0 && (static_cast(text[end]) & 0xC0) == 0x80) { + --end; + } + return fmt::format("{}...", text.substr(0, end)); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/mod_browser.cpp b/src/dusk/ui/mod_browser.cpp index 13944c92b3..2146b924dd 100644 --- a/src/dusk/ui/mod_browser.cpp +++ b/src/dusk/ui/mod_browser.cpp @@ -6,7 +6,9 @@ #include "dusk/mods/queue.hpp" #include "dusk/mods/svc/registry.hpp" #include "fmt/format.h" +#include "format.hpp" #include "nav_group.hpp" +#include "package_row.hpp" #include "queue_window.hpp" #include "remote_texture_provider.hpp" #include "string_button.hpp" @@ -55,22 +57,6 @@ std::string format_count(uint64_t value) { return fmt::format("{}", value); } -std::string format_bytes(uint64_t bytes) { - constexpr double kiB = 1024.0; - constexpr double miB = kiB * 1024.0; - constexpr double giB = miB * 1024.0; - if (bytes >= static_cast(giB)) { - return fmt::format("{:.1f} GiB", static_cast(bytes) / giB); - } - if (bytes >= static_cast(miB)) { - return fmt::format("{:.1f} MiB", static_cast(bytes) / miB); - } - if (bytes >= static_cast(kiB)) { - return fmt::format("{:.0f} KiB", static_cast(bytes) / kiB); - } - return fmt::format("{} B", bytes); -} - std::string display_date(std::string_view timestamp) { return std::string{timestamp.substr(0, std::min(timestamp.size(), 10))}; } @@ -120,17 +106,6 @@ std::string relative_date(std::string_view timestamp) { return fmt::format("{} years ago", age / 365); } -std::string snippet(std::string_view text, size_t maxBytes) { - if (text.size() <= maxBytes) { - return std::string{text}; - } - size_t end = maxBytes; - while (end > 0 && (static_cast(text[end]) & 0xc0) == 0x80) { - --end; - } - return fmt::format("{}...", text.substr(0, end)); -} - void add_list_markers(Rml::Element* fragment) { Rml::ElementList lists; fragment->QuerySelectorAll(lists, "ul, ol"); @@ -168,32 +143,25 @@ std::string image_source(const mods::catalog::Image& image, uint32_t preferredWi return image.sources.empty() ? std::string{} : image.sources.back().pngUrl; } -void set_image(Rml::Element* element, const mods::catalog::Image& image, - uint32_t preferredWidth, std::string_view fit = "cover") { +void set_image(Rml::Element* element, const mods::catalog::Image& image, uint32_t preferredWidth, + std::string_view fit = "cover") { if (element == nullptr) { return; } - const auto source = image_source(image, preferredWidth); + auto source = image_source(image, preferredWidth); if (!source.empty()) { - set_remote_texture_dimensions(source, image.width, image.height); + source = remote_image_source(source, image.width, image.height); element->SetProperty( "decorator", fmt::format(R"(image("{}" {} center center))", escape(source), fit)); element->SetClass("has-image", true); } } -bool installed(std::string_view id) { - return std::ranges::any_of(mods::ModLoader::instance().mods(), - [id](const mods::LoadedMod& mod) { return mod.metadata.id == id; }); -} - -const mods::LoadedMod* installed_mod(std::string_view id) { - for (const auto& mod : mods::ModLoader::instance().mods()) { - if (mod.metadata.id == id) { - return &mod; - } +std::string_view activation_failure(const mods::LoadedMod& mod) { + if (!mod.failureReason.empty()) { + return mod.failureReason; } - return nullptr; + return mod.suspendedByProvider ? "A required provider is unavailable" : "Activation failed"; } bool safe_web_url(std::string_view url) { @@ -240,7 +208,7 @@ public: : Button{parent, Props{}} { mRoot->SetClass("catalog-card", true); const auto category = mod.category ? mod.category->name : "Uncategorized"; - const auto isInstalled = installed(mod.id); + const auto isInstalled = mods::ModLoader::instance().find_mod(mod.id) != nullptr; const auto installedLabel = isInstalled ? "Installed" : format_bytes(mod.packageSize); auto* art = append(mRoot, "catalog-card-art"); @@ -476,13 +444,13 @@ public: } void update() override { - auto queued = mods::queue::find_by_mod_id(mRequest.id); - const auto* local = installed_mod(mRequest.id); - if (queued && (queued->version != mRequest.version || - (queued->state == mods::queue::State::Installed && - (local == nullptr || local->metadata.version != mRequest.version)))) - { - queued.reset(); + auto queued = matching_queue_item(); + const auto* local = mods::ModLoader::instance().find_mod(mRequest.id); + const bool activationPending = + mActivationOperation != nullptr && + mActivationOperation->state == mods::ModOperation::State::Pending; + if (mActivationOperation != nullptr && !activationPending) { + mActivationOperation.reset(); } std::string glyph = "\uE2C4"; std::string label; @@ -493,7 +461,7 @@ public: if (queued && queued->state != mods::queue::State::Canceled) { using enum mods::queue::State; - state = state_class(queued->state); + state = queue_state_class(queued->state); progress = queued->total == 0 ? 0.0f : std::clamp(static_cast(queued->completed) / static_cast(queued->total), @@ -502,7 +470,7 @@ public: case Queued: glyph = "\uE8B5"; label = "Queued"; - if (const auto ahead = queue_items_ahead(mRequest.id); ahead != 0) { + if (const auto ahead = mods::queue::active_items_ahead(mRequest.id); ahead != 0) { caption = fmt::format("{} ahead · opens the queue", ahead); } else { caption = "Next · opens the queue"; @@ -528,46 +496,10 @@ public: label = "Verifying…"; caption = "Checking package integrity"; break; - case Installing: - glyph = "\uE8B5"; - label = "Installing…"; - caption = "Installing and activating"; - break; - case Activating: - glyph = "\uE8B5"; - label = "Activating…"; - caption = "Retrying mod activation"; - break; - case Installed: - glyph = "\uE86C"; - label = "Installed"; - caption = fmt::format("Installed · {} · {}", format_bytes(queued->total), - local != nullptr && local->active ? "enabled" : "disabled"); - progress = 1.0f; - disabled = true; - break; case Failed: glyph = "\uE5D5"; - label = "Retry download"; - caption = queued->message.empty() ? "Download failed" : queued->message; - progress = 1.0f; - break; - case InstallFailed: - glyph = "\uE5D5"; - label = "Retry install"; - caption = queued->message.empty() ? "Install failed" : queued->message; - progress = 1.0f; - break; - case ActivationFailed: - glyph = "\uE5D5"; - label = "Retry activation"; - caption = queued->message.empty() ? "Activation failed" : queued->message; - progress = 1.0f; - break; - case Uninstalling: - glyph = "\uE8B5"; - label = "Uninstalling…"; - caption = "Removing the installed package"; + label = queued->local ? "Retry package" : "Retry download"; + caption = queued->message.empty() ? "Package preparation failed" : queued->message; progress = 1.0f; break; case Canceled: @@ -577,9 +509,22 @@ public: if (label.empty()) { const bool current = local != nullptr && local->metadata.version == mRequest.version; - const bool updateable = local != nullptr && !current && - local->origin == mods::ModOrigin::User && !local->inPlace; - if (current || (local != nullptr && !updateable)) { + const bool updateable = + local != nullptr && !current && mods::ModLoader::instance().can_uninstall(*local); + if (activationPending) { + glyph = "\uE8B5"; + label = "Activating…"; + caption = "Retrying mod activation"; + state = "installing"; + progress = 1.0f; + disabled = true; + } else if (current && local->activation_failed()) { + glyph = "\uE5D5"; + label = "Retry activation"; + caption = activation_failure(*local); + state = "failed"; + progress = 1.0f; + } else if (current || (local != nullptr && !updateable)) { glyph = "\uE86C"; label = "Installed"; caption = fmt::format("Installed · {} · {}", format_bytes(package_size()), @@ -593,7 +538,7 @@ public: } if (mLabel != label || mGlyph != glyph) { - ::dusk::ui::clear_children(mRoot); + ui::clear_children(mRoot); append_text(append(mRoot, "icon"), glyph); append_text_element(mRoot, "catalog-action-label", label); mProgress = append(mRoot, "progress"); @@ -617,67 +562,24 @@ public: } private: - static size_t queue_items_ahead(std::string_view id) { - size_t result = 0; - for (const auto& item : mods::queue::items()) { - if (item.modId == id) { - break; - } - if (item.state != mods::queue::State::Installed && - item.state != mods::queue::State::Failed && - item.state != mods::queue::State::InstallFailed && - item.state != mods::queue::State::ActivationFailed && - item.state != mods::queue::State::Canceled) - { - ++result; - } - } - return result; - } - - static std::string state_class(mods::queue::State state) { - using enum mods::queue::State; - switch (state) { - case Queued: - return "queued"; - case Downloading: - return "downloading"; - case Paused: - return "paused"; - case Retrying: - return "retrying"; - case Verifying: - case Installing: - case Activating: - case Uninstalling: - return "installing"; - case Installed: - return "installed"; - case Failed: - case InstallFailed: - case ActivationFailed: - return "failed"; - case Canceled: - return "idle"; - } - return "idle"; + std::optional matching_queue_item() const { + auto item = mods::queue::find_by_mod_id(mRequest.id); + return item && item->version == mRequest.version ? item : std::nullopt; } void press() { - auto queued = mods::queue::find_by_mod_id(mRequest.id); - const auto* local = installed_mod(mRequest.id); - if (queued && (queued->version != mRequest.version || - (queued->state == mods::queue::State::Installed && - (local == nullptr || local->metadata.version != mRequest.version)))) - { - queued.reset(); - } - if (queued && queued->state != mods::queue::State::Canceled && - queued->state != mods::queue::State::Installed) - { + auto queued = matching_queue_item(); + const auto* local = mods::ModLoader::instance().find_mod(mRequest.id); + if (queued && queued->state != mods::queue::State::Canceled) { mWindow.show_downloads(queued->id); return; } + if (local != nullptr && local->metadata.version == mRequest.version && + local->activation_failed()) + { + mActivationOperation = mods::ModLoader::instance().request_reactivate(mRequest.id); + return; + } if (!mods::queue::enqueue(mRequest)) { push_toast({ .type = "warning", @@ -694,6 +596,7 @@ private: Rml::Element* mProgress = nullptr; std::string mLabel; std::string mGlyph; + mods::ModOperationHandle mActivationOperation; uint64_t package_size() const { return std::get(mRequest.source).size; } }; diff --git a/src/dusk/ui/mods_window.cpp b/src/dusk/ui/mods_window.cpp index d0fbcc0841..63dad37d89 100644 --- a/src/dusk/ui/mods_window.cpp +++ b/src/dusk/ui/mods_window.cpp @@ -5,6 +5,7 @@ #include "dusk/mods/svc/ui.hpp" #include "fmt/format.h" #include "fmt/ranges.h" +#include "format.hpp" #include "logs_window.hpp" #include "mod_browser.hpp" #include "mod_texture_provider.hpp" @@ -19,6 +20,7 @@ #include "m_Do/m_Do_audio.h" #include +#include #include #include #include @@ -33,10 +35,6 @@ struct ModStatus { const char* text = ""; }; -bool mod_enabled(const mods::LoadedMod& mod) { - return mod.cvarIsEnabled != nullptr && mod.cvarIsEnabled->getValue(); -} - ModStatus mod_status(const mods::LoadedMod& mod) { if (mod.loadFailed) { return {"failed", "Failed"}; @@ -57,18 +55,6 @@ bool mod_uses_network(const mods::LoadedMod& mod) { }); } -// Truncates to at most maxBytes without splitting a UTF-8 sequence. -std::string snippet(std::string_view text, size_t maxBytes) { - if (text.size() <= maxBytes) { - return std::string{text}; - } - size_t end = maxBytes; - while (end > 0 && (static_cast(text[end]) & 0xC0) == 0x80) { - --end; - } - return fmt::format("{}...", text.substr(0, end)); -} - class ModListEntry : public FluentComponent { public: ModListEntry(Rml::Element* parent, const mods::LoadedMod& mod) @@ -163,26 +149,12 @@ public: } void update() override { - const auto queueItems = mods::queue::items(); - const mods::queue::Item* current = nullptr; - size_t active = 0; - for (const auto& item : queueItems) { - if (item.state == mods::queue::State::Installed || - item.state == mods::queue::State::Failed || - item.state == mods::queue::State::InstallFailed || - item.state == mods::queue::State::ActivationFailed || - item.state == mods::queue::State::Canceled) - { - continue; - } - ++active; - if (current == nullptr) { - current = &item; - } - } + const auto current = mods::queue::first_active(); + const auto activeCount = mods::queue::active_count(); + const auto totalCount = mods::queue::item_count(); - if (current == nullptr) { - set_text_content(mSummary, fmt::format("{} finished", queueItems.size())); + if (!current) { + set_text_content(mSummary, fmt::format("0 active · {} total", totalCount)); mProgress->SetProperty("display", "none"); } else { const float progress = current->total == 0 ? @@ -191,7 +163,7 @@ public: static_cast(current->total), 0.0f, 1.0f); set_text_content( - mSummary, fmt::format("{} in queue · {:.0f}%", active, progress * 100.0f)); + mSummary, fmt::format("{} in queue · {:.0f}%", activeCount, progress * 100.0f)); mProgress->SetAttribute("value", progress); mProgress->SetProperty("display", "block"); } @@ -217,8 +189,15 @@ public: auto* actions = append(mRoot, "mod-actions"); const std::string modId = mod.metadata.id; - if (mod_enabled(mod)) { - if (!mod.inPlace) { + if (mod.activation_failed()) { + make_button(actions, "Retry").on_pressed([modId] { + mods::ModLoader::instance().request_reactivate(modId); + }); + make_button(actions, "Disable").on_pressed([modId] { + mods::ModLoader::instance().request_disable(modId); + }); + } else if (mod.is_enabled()) { + if (!mod.nativeInPlace) { make_button(actions, "Reload").on_pressed([modId] { mods::ModLoader::instance().request_reload(modId); }); @@ -286,7 +265,7 @@ ModsWindow::ModsWindow() : Window{Props{.tabBar = false, .styleSheets = {"res/rm mRoot->SetClass("mods", true); refresh_snapshot(); - mQueueItemCount = mods::queue::items().size(); + mQueueItemCount = mods::queue::item_count(); set_content([this](Rml::Element* content) { build_content(content); }); } @@ -485,7 +464,7 @@ void ModsWindow::refresh_snapshot() { .mod = &trackedMod, .active = trackedMod.active, .loadFailed = trackedMod.loadFailed, - .enabled = mod_enabled(trackedMod), + .enabled = trackedMod.is_enabled(), .suspended = trackedMod.suspendedByProvider, .cacheGeneration = trackedMod.cacheGeneration, }); @@ -511,20 +490,20 @@ void ModsWindow::update() { for (auto& snapshot : mSnapshot) { const auto& mod = *snapshot.mod; if (mod.active != snapshot.active || mod.loadFailed != snapshot.loadFailed || - mod_enabled(mod) != snapshot.enabled || + mod.is_enabled() != snapshot.enabled || mod.suspendedByProvider != snapshot.suspended || mod.cacheGeneration != snapshot.cacheGeneration) { snapshot.active = mod.active; snapshot.loadFailed = mod.loadFailed; - snapshot.enabled = mod_enabled(mod); + snapshot.enabled = mod.is_enabled(); snapshot.suspended = mod.suspendedByProvider; snapshot.cacheGeneration = mod.cacheGeneration; dirty = true; } } } - const auto queueItemCount = mods::queue::items().size(); + const auto queueItemCount = mods::queue::item_count(); if (queueItemCount != mQueueItemCount) { mQueueItemCount = queueItemCount; dirty = true; diff --git a/src/dusk/ui/package_row.cpp b/src/dusk/ui/package_row.cpp index 62644b4cfa..e02c286481 100644 --- a/src/dusk/ui/package_row.cpp +++ b/src/dusk/ui/package_row.cpp @@ -12,23 +12,7 @@ Rml::Element* create_row(Rml::Element* parent) { } // namespace -std::string format_bytes(uint64_t bytes) { - constexpr double kiB = 1024.0; - constexpr double miB = kiB * 1024.0; - constexpr double giB = miB * 1024.0; - if (bytes >= static_cast(giB)) { - return fmt::format("{:.1f} GiB", static_cast(bytes) / giB); - } - if (bytes >= static_cast(miB)) { - return fmt::format("{:.1f} MiB", static_cast(bytes) / miB); - } - if (bytes >= static_cast(kiB)) { - return fmt::format("{:.0f} KiB", static_cast(bytes) / kiB); - } - return fmt::format("{} B", bytes); -} - -const char* state_class(mods::queue::State state) { +const char* queue_state_class(mods::queue::State state) { using enum mods::queue::State; switch (state) { case Downloading: @@ -38,15 +22,8 @@ const char* state_class(mods::queue::State state) { case Retrying: return "retrying"; case Verifying: - case Installing: - case Activating: - case Uninstalling: return "installing"; - case Installed: - return "installed"; case Failed: - case InstallFailed: - case ActivationFailed: return "failed"; case Canceled: return "canceled"; @@ -69,20 +46,8 @@ std::string state_label(const mods::queue::Item& item) { return fmt::format("Retrying in {}s", item.retrySeconds); case Verifying: return "Verifying"; - case Installing: - return "Installing"; - case Activating: - return "Activating"; - case Installed: - return "Installed"; case Failed: return item.local ? "Package failed" : "Download failed"; - case InstallFailed: - return "Install failed"; - case ActivationFailed: - return "Activation failed"; - case Uninstalling: - return "Uninstalling"; case Canceled: return "Canceled"; } diff --git a/src/dusk/ui/package_row.hpp b/src/dusk/ui/package_row.hpp index 6d9168231e..d9662559cc 100644 --- a/src/dusk/ui/package_row.hpp +++ b/src/dusk/ui/package_row.hpp @@ -9,8 +9,7 @@ namespace dusk::ui { -std::string format_bytes(uint64_t bytes); -const char* state_class(mods::queue::State state); +const char* queue_state_class(mods::queue::State state); std::string state_label(const mods::queue::Item& item); class PackageRow : public Component { diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 92034a4e10..4a6361a4ef 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -8,6 +8,7 @@ #include "dusk/language.hpp" #include "dusk/main.h" #include "dusk/settings.h" +#include "dusk/ui/format.hpp" #include "dusk/ui/menu_bar.hpp" #include "modal.hpp" #include "mods_window.hpp" @@ -37,6 +38,7 @@ namespace dusk::ui { namespace { constexpr borealis::Log PrelaunchLog{"dusk::ui::prelaunch"}; +constexpr ByteFormat verificationByteFormat{.gibFractionDigits = 2, .mibFractionDigits = 0}; PrelaunchState sPrelaunchState; @@ -163,22 +165,6 @@ DiscVerificationState verification_to_config(iso::ValidationError validation) { } } -std::string format_bytes(size_t bytes) { - constexpr double KiB = 1024.0; - constexpr double MiB = KiB * 1024.0; - constexpr double GiB = MiB * 1024.0; - if (bytes >= static_cast(GiB)) { - return fmt::format("{:.2f} GiB", static_cast(bytes) / GiB); - } - if (bytes >= static_cast(MiB)) { - return fmt::format("{:.0f} MiB", static_cast(bytes) / MiB); - } - if (bytes >= static_cast(KiB)) { - return fmt::format("{:.0f} KiB", static_cast(bytes) / KiB); - } - return fmt::format("{} B", bytes); -} - void begin_disc_verification(std::string path) noexcept { if (path.empty()) { return; @@ -466,8 +452,9 @@ private: mProgress->SetAttribute("value", fraction); } if (mDetail != nullptr) { - set_text_content(mDetail, fmt::format("{} / {} ({:.0f}%)", format_bytes(bytesRead), - format_bytes(bytesTotal), fraction * 100.0f)); + set_text_content(mDetail, + fmt::format("{} / {} ({:.0f}%)", format_bytes(bytesRead, verificationByteFormat), + format_bytes(bytesTotal, verificationByteFormat), fraction * 100.0f)); } } diff --git a/src/dusk/ui/queue_window.cpp b/src/dusk/ui/queue_window.cpp index 02a9fdfe32..65150b5c88 100644 --- a/src/dusk/ui/queue_window.cpp +++ b/src/dusk/ui/queue_window.cpp @@ -3,6 +3,7 @@ #include "button.hpp" #include "dusk/mods/queue.hpp" #include "fmt/format.h" +#include "format.hpp" #include "package_row.hpp" #include "pane.hpp" @@ -32,8 +33,6 @@ public: mods::queue::resume(mId); break; case mods::queue::State::Failed: - case mods::queue::State::InstallFailed: - case mods::queue::State::ActivationFailed: mods::queue::retry(mId); break; default: @@ -51,11 +50,7 @@ public: return; } mods::queue::cancel(mId); - if (item->state == mods::queue::State::Installed || - item->state == mods::queue::State::Failed || - item->state == mods::queue::State::InstallFailed || - item->state == mods::queue::State::Canceled) - { + if (mods::queue::is_terminal(item->state)) { mods::queue::clear_finished(); } }); @@ -89,38 +84,24 @@ public: } const auto name = item->version.empty() ? item->name : fmt::format("{} {}", item->name, item->version); - set_package(name, state_label(*item), detail, state_class(item->state), progress); + set_package(name, state_label(*item), detail, queue_state_class(item->state), progress); const bool pauseVisible = (!item->local && item->state == mods::queue::State::Queued) || item->state == mods::queue::State::Downloading || item->state == mods::queue::State::Paused || item->state == mods::queue::State::Retrying || - item->state == mods::queue::State::Failed || - item->state == mods::queue::State::InstallFailed || - item->state == mods::queue::State::ActivationFailed; + item->state == mods::queue::State::Failed; mPause->root()->SetProperty("display", pauseVisible ? "block" : "none"); if (item->state == mods::queue::State::Paused) { mPause->set_text("Resume"); - } else if (item->state == mods::queue::State::Failed || - item->state == mods::queue::State::InstallFailed || - item->state == mods::queue::State::ActivationFailed) - { + } else if (item->state == mods::queue::State::Failed) { mPause->set_text("Retry"); } else { mPause->set_text("Pause"); } - const bool cancelVisible = item->state != mods::queue::State::Installing && - item->state != mods::queue::State::Activating && - item->state != mods::queue::State::Uninstalling; - mCancel->root()->SetProperty("display", cancelVisible ? "block" : "none"); - mCancel->set_text(item->state == mods::queue::State::Installed || - item->state == mods::queue::State::Failed || - item->state == mods::queue::State::InstallFailed || - item->state == mods::queue::State::ActivationFailed || - item->state == mods::queue::State::Canceled ? - "Clear" : - "Cancel"); + mCancel->root()->SetProperty("display", "block"); + mCancel->set_text(mods::queue::is_terminal(item->state) ? "Clear" : "Cancel"); Component::update(); } @@ -147,50 +128,42 @@ QueueWindow::QueueWindow(std::string focusId) }}, mFocusId{std::move(focusId)} { content_pane(); - rebuild_rows(); + refresh_queue(); } void QueueWindow::update() { + refresh_queue(); + Modal::update(); +} + +void QueueWindow::refresh_queue() { const auto queueItems = mods::queue::items(); std::vector ids; ids.reserve(queueItems.size()); size_t active = 0; for (const auto& item : queueItems) { ids.push_back(item.id); - if (item.state != mods::queue::State::Installed && - item.state != mods::queue::State::Failed && - item.state != mods::queue::State::InstallFailed && - item.state != mods::queue::State::ActivationFailed && - item.state != mods::queue::State::Canceled) - { + if (!mods::queue::is_terminal(item.state)) { ++active; } } if (ids != mItemIds) { - rebuild_rows(); - } - set_body_text(fmt::format("{} active · {} total", active, queueItems.size())); - Modal::update(); -} - -void QueueWindow::rebuild_rows() { - auto& pane = content_pane(); - pane.clear(); - mItemIds.clear(); - - const auto queueItems = mods::queue::items(); - if (queueItems.empty()) { - pane.add_text("No installs."); - return; - } - for (const auto& item : queueItems) { - mItemIds.push_back(item.id); - auto& row = pane.add_child(item.id); - if (!mFocusId.empty() && item.id == mFocusId) { - row.focus(); - mFocusId.clear(); + auto& pane = content_pane(); + pane.clear(); + mItemIds = std::move(ids); + if (queueItems.empty()) { + pane.add_text("No installs."); + } else { + for (const auto& item : queueItems) { + auto& row = pane.add_child(item.id); + if (!mFocusId.empty() && item.id == mFocusId) { + row.focus(); + mFocusId.clear(); + } + } } } + set_body_text(fmt::format("{} active · {} total", active, queueItems.size())); } } // namespace dusk::ui diff --git a/src/dusk/ui/queue_window.hpp b/src/dusk/ui/queue_window.hpp index 7d8aa69592..8e2a2499b7 100644 --- a/src/dusk/ui/queue_window.hpp +++ b/src/dusk/ui/queue_window.hpp @@ -14,7 +14,7 @@ public: void update() override; private: - void rebuild_rows(); + void refresh_queue(); std::string mFocusId; std::vector mItemIds; diff --git a/src/dusk/ui/remote_texture_provider.cpp b/src/dusk/ui/remote_texture_provider.cpp index 04b91467a9..4345068f3f 100644 --- a/src/dusk/ui/remote_texture_provider.cpp +++ b/src/dusk/ui/remote_texture_provider.cpp @@ -1,5 +1,16 @@ #include "remote_texture_provider.hpp" +#include + +namespace dusk::ui { + +std::string remote_image_source(std::string_view url, uint32_t width, uint32_t height) { + url = url.substr(0, url.find('#')); + return fmt::format("{}#size={}x{}", url, width == 0 ? 1 : width, height == 0 ? 1 : height); +} + +} // namespace dusk::ui + #ifdef AURORA_ENABLE_RMLUI #include "dusk/app_info.hpp" @@ -12,6 +23,7 @@ #include #include +#include #include #include #include @@ -21,6 +33,7 @@ #include #include #include +#include #include namespace dusk::ui { @@ -55,6 +68,12 @@ struct Entry { uint32_t placeholderHeight = 1; }; +struct RemoteSource { + std::string_view requestUrl; + uint32_t placeholderWidth = 1; + uint32_t placeholderHeight = 1; +}; + std::unordered_map& image_cache() { static auto* cache = new std::unordered_map(); return *cache; @@ -65,6 +84,48 @@ uint64_t& use_counter() { return *counter; } +RemoteSource parse_remote_source(std::string_view source) noexcept { + constexpr std::string_view marker{"#size="}; + const auto fragment = source.find('#'); + RemoteSource result{.requestUrl = source.substr(0, fragment)}; + if (fragment == std::string_view::npos || !source.substr(fragment).starts_with(marker)) { + return result; + } + + const auto dimensions = source.substr(fragment + marker.size()); + const auto separator = dimensions.find('x'); + if (separator == std::string_view::npos) { + return result; + } + uint32_t width = 0; + uint32_t height = 0; + const auto widthResult = + std::from_chars(dimensions.data(), dimensions.data() + separator, width); + const auto heightResult = std::from_chars( + dimensions.data() + separator + 1, dimensions.data() + dimensions.size(), height); + if (widthResult.ec != std::errc{} || widthResult.ptr != dimensions.data() + separator || + heightResult.ec != std::errc{} || + heightResult.ptr != dimensions.data() + dimensions.size() || width == 0 || height == 0) + { + return result; + } + + const auto maxDimension = std::max(width, height); + if (maxDimension > kPlaceholderMaxDimension) { + width = std::max( + 1u, static_cast( + (static_cast(width) * kPlaceholderMaxDimension + maxDimension / 2) / + maxDimension)); + height = std::max( + 1u, static_cast( + (static_cast(height) * kPlaceholderMaxDimension + maxDimension / 2) / + maxDimension)); + } + result.placeholderWidth = width; + result.placeholderHeight = height; + return result; +} + bool make_cache_room() { auto& cache = image_cache(); if (cache.size() < kMaxCachedImages) { @@ -108,7 +169,8 @@ borealis::Task start_request(std::string source) { } std::optional remote_texture_provider(std::string_view source) { - if (!source.starts_with(kAllowedPrefix)) { + const auto parsed = parse_remote_source(source); + if (!parsed.requestUrl.starts_with(kAllowedPrefix)) { return std::nullopt; } @@ -118,12 +180,21 @@ std::optional remote_texture_provider(std::string if (iter == cache.end()) { if (!make_cache_room()) { Log.warn("Remote image cache is full; skipping '{}'", source); - return transparent_texture(Entry{}); + return transparent_texture(Entry{ + .placeholderWidth = parsed.placeholderWidth, + .placeholderHeight = parsed.placeholderHeight, + }); } - iter = cache.emplace(key, Entry{}).first; + iter = cache + .emplace(key, + Entry{ + .placeholderWidth = parsed.placeholderWidth, + .placeholderHeight = parsed.placeholderHeight, + }) + .first; } if (iter->second.state == State::Unrequested) { - iter->second.request = start_request(key); + iter->second.request = start_request(std::string{parsed.requestUrl}); iter->second.state = State::Pending; } iter->second.lastUsed = ++use_counter(); @@ -197,36 +268,6 @@ void update_remote_texture_provider() noexcept { } } -void set_remote_texture_dimensions( - std::string_view source, uint32_t width, uint32_t height) noexcept { - if (!source.starts_with(kAllowedPrefix) || width == 0 || height == 0) { - return; - } - - auto& cache = image_cache(); - auto iter = cache.find(std::string{source}); - if (iter == cache.end()) { - if (!make_cache_room()) { - return; - } - iter = cache.emplace(std::string{source}, Entry{}).first; - } - - const auto maxDimension = std::max(width, height); - if (maxDimension > kPlaceholderMaxDimension) { - width = std::max( - 1u, static_cast( - (static_cast(width) * kPlaceholderMaxDimension + maxDimension / 2) / - maxDimension)); - height = std::max( - 1u, static_cast( - (static_cast(height) * kPlaceholderMaxDimension + maxDimension / 2) / - maxDimension)); - } - iter->second.placeholderWidth = width; - iter->second.placeholderHeight = height; -} - } // namespace dusk::ui #else @@ -236,7 +277,6 @@ namespace dusk::ui { void register_remote_texture_provider() noexcept {} void unregister_remote_texture_provider() noexcept {} void update_remote_texture_provider() noexcept {} -void set_remote_texture_dimensions(std::string_view, uint32_t, uint32_t) noexcept {} } // namespace dusk::ui diff --git a/src/dusk/ui/remote_texture_provider.hpp b/src/dusk/ui/remote_texture_provider.hpp index 3a994d9af9..21ceb1162b 100644 --- a/src/dusk/ui/remote_texture_provider.hpp +++ b/src/dusk/ui/remote_texture_provider.hpp @@ -1,14 +1,16 @@ #pragma once #include +#include #include namespace dusk::ui { +[[nodiscard]] std::string remote_image_source( + std::string_view url, uint32_t width, uint32_t height); + void register_remote_texture_provider() noexcept; void unregister_remote_texture_provider() noexcept; void update_remote_texture_provider() noexcept; -void set_remote_texture_dimensions( - std::string_view source, uint32_t width, uint32_t height) noexcept; } // namespace dusk::ui