Cleanup & simplify

This commit is contained in:
Luke Street
2026-09-02 00:24:56 -06:00
parent 212fca3e41
commit a38cd47ce2
16 changed files with 613 additions and 842 deletions
+31 -19
View File
@@ -6,6 +6,7 @@
#include <ranges>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#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<ConfigVar<bool>> cvarIsEnabled;
@@ -243,6 +237,11 @@ struct LoadedMod {
// Mods this mod imports services from, and mods importing services from this mod.
std::vector<ModDependencyEdge> dependencies;
std::vector<ModDependencyEdge> 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<ModOperation> operation;
bool force = false;
bool remove = false;
};
struct SyncResult {
struct InstallRequest {
std::filesystem::path stagedPath;
std::shared_ptr<ModOperation> operation;
};
struct ReloadRequest {
std::string modId;
std::shared_ptr<ModOperation> operation;
};
struct UninstallRequest {
std::string modId;
std::shared_ptr<ModOperation> operation;
};
using Request = std::variant<LifecycleRequest, InstallRequest, ReloadRequest, UninstallRequest>;
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);
+193 -194
View File
@@ -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<ModOperation>& 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<NativeRuntimeLocation>(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<uint32_t>(dirIndex));
(void)try_load_mod(entry.path(), entry.is_directory(), static_cast<uint32_t>(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<ModOperation>();
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<ModOperation>();
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<ModOperation>();
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<ModOperation>();
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<ModBundle> bundle;
std::optional<ModMetadata> 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<ModBundle> bundle;
std::optional<ModMetadata> 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<Request> coalesced;
std::vector<LifecycleRequest> 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<InstallRequest>(&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<ReloadRequest>(&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<UninstallRequest>(&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<LifecycleRequest>(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 =
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <algorithm>
#include <string>
#include <string_view>
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
+115 -266
View File
@@ -2,6 +2,7 @@
#include "dusk/hash.hpp"
#include "dusk/mod_loader.hpp"
#include "dusk/mods/path.hpp"
#include "dusk/ui/ui.hpp"
#include <borealis/http.hpp>
@@ -20,6 +21,7 @@
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
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<borealis::http::Result> task;
borealis::Task<VerifyResult> verification;
ModOperationHandle operation;
bool packagePublished = false;
bool removeAfterOperation = false;
bool pauseRequested = false;
bool resumeRequested = false;
bool cancelRequested = false;
PendingIntent pendingIntent = PendingIntent::None;
};
std::vector<QueueItem> 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<LocalFile>(&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<char>(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<borealis::http::Result> 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<Item> 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<size_t>(std::ranges::count_if(
queueItems, [](const QueueItem& item) { return !is_terminal(item.state); }));
}
std::optional<Item> 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<Item>{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
+10 -7
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <optional>
@@ -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<Item> find(std::string_view key);
[[nodiscard]] std::optional<Item> 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<Item> 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);
+5 -15
View File
@@ -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<DropPackage>& packages) {
return std::ranges::count(packages, true, &DropPackage::valid);
}
@@ -36,14 +28,12 @@ std::vector<DropPackage> prepare_packages(std::vector<DropPackage> 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) {
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <fmt/format.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
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<uint64_t>(giB)) {
return fmt::format(
"{:.{}f} GiB", static_cast<double>(bytes) / giB, options.gibFractionDigits);
}
if (bytes >= static_cast<uint64_t>(miB)) {
return fmt::format(
"{:.{}f} MiB", static_cast<double>(bytes) / miB, options.mibFractionDigits);
}
if (bytes >= static_cast<uint64_t>(kiB)) {
return fmt::format("{:.0f} KiB", static_cast<double>(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<unsigned char>(text[end]) & 0xC0) == 0x80) {
--end;
}
return fmt::format("{}...", text.substr(0, end));
}
} // namespace dusk::ui
+52 -149
View File
@@ -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<uint64_t>(giB)) {
return fmt::format("{:.1f} GiB", static_cast<double>(bytes) / giB);
}
if (bytes >= static_cast<uint64_t>(miB)) {
return fmt::format("{:.1f} MiB", static_cast<double>(bytes) / miB);
}
if (bytes >= static_cast<uint64_t>(kiB)) {
return fmt::format("{:.0f} KiB", static_cast<double>(bytes) / kiB);
}
return fmt::format("{} B", bytes);
}
std::string display_date(std::string_view timestamp) {
return std::string{timestamp.substr(0, std::min<size_t>(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<unsigned char>(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<float>(queued->completed) /
static_cast<float>(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<mods::queue::Item> 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<mods::queue::Url>(mRequest.source).size; }
};
+22 -43
View File
@@ -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 <algorithm>
#include <cstddef>
#include <memory>
#include <ranges>
#include <string>
@@ -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<unsigned char>(text[end]) & 0xC0) == 0x80) {
--end;
}
return fmt::format("{}...", text.substr(0, end));
}
class ModListEntry : public FluentComponent<ModListEntry> {
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<float>(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;
+1 -36
View File
@@ -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<uint64_t>(giB)) {
return fmt::format("{:.1f} GiB", static_cast<double>(bytes) / giB);
}
if (bytes >= static_cast<uint64_t>(miB)) {
return fmt::format("{:.1f} MiB", static_cast<double>(bytes) / miB);
}
if (bytes >= static_cast<uint64_t>(kiB)) {
return fmt::format("{:.0f} KiB", static_cast<double>(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";
}
+1 -2
View File
@@ -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 {
+5 -18
View File
@@ -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<size_t>(GiB)) {
return fmt::format("{:.2f} GiB", static_cast<double>(bytes) / GiB);
}
if (bytes >= static_cast<size_t>(MiB)) {
return fmt::format("{:.0f} MiB", static_cast<double>(bytes) / MiB);
}
if (bytes >= static_cast<size_t>(KiB)) {
return fmt::format("{:.0f} KiB", static_cast<double>(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));
}
}
+28 -55
View File
@@ -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<std::string> 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<QueueRow>(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<QueueRow>(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
+1 -1
View File
@@ -14,7 +14,7 @@ public:
void update() override;
private:
void rebuild_rows();
void refresh_queue();
std::string mFocusId;
std::vector<std::string> mItemIds;
+75 -35
View File
@@ -1,5 +1,16 @@
#include "remote_texture_provider.hpp"
#include <fmt/format.h>
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 <algorithm>
#include <array>
#include <charconv>
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -21,6 +33,7 @@
#include <span>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
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<std::string, Entry>& image_cache() {
static auto* cache = new std::unordered_map<std::string, Entry>();
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<uint32_t>(
(static_cast<uint64_t>(width) * kPlaceholderMaxDimension + maxDimension / 2) /
maxDimension));
height = std::max(
1u, static_cast<uint32_t>(
(static_cast<uint64_t>(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<borealis::http::Result> start_request(std::string source) {
}
std::optional<aurora::rmlui::RuntimeTexture> 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<aurora::rmlui::RuntimeTexture> 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<uint32_t>(
(static_cast<uint64_t>(width) * kPlaceholderMaxDimension + maxDimension / 2) /
maxDimension));
height = std::max(
1u, static_cast<uint32_t>(
(static_cast<uint64_t>(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
+4 -2
View File
@@ -1,14 +1,16 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
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