mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-11 19:21:36 -04:00
Cleanup & simplify
This commit is contained in:
+31
-19
@@ -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
@@ -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 =
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,7 +14,7 @@ public:
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
void rebuild_rows();
|
||||
void refresh_queue();
|
||||
|
||||
std::string mFocusId;
|
||||
std::vector<std::string> mItemIds;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user