mirror of
https://github.com/geode-sdk/geode.git
synced 2024-12-12 00:51:13 -05:00
347 lines
13 KiB
C++
347 lines
13 KiB
C++
#include "DownloadManager.hpp"
|
|
|
|
using namespace server;
|
|
|
|
ModDownloadEvent::ModDownloadEvent(std::string const& id) : id(id) {}
|
|
|
|
ListenerResult ModDownloadFilter::handle(MiniFunction<Callback> fn, ModDownloadEvent* event) {
|
|
if (m_id.empty() || m_id == event->id) {
|
|
fn(event);
|
|
}
|
|
return ListenerResult::Propagate;
|
|
}
|
|
ModDownloadFilter::ModDownloadFilter() {}
|
|
ModDownloadFilter::ModDownloadFilter(std::string const& id) : m_id(id) {}
|
|
|
|
class ModDownload::Impl final {
|
|
public:
|
|
std::string m_id;
|
|
std::optional<VersionInfo> m_version;
|
|
std::optional<std::string> m_dependencyFor;
|
|
DownloadStatus m_status;
|
|
EventListener<ServerRequest<ServerModVersion>> m_infoListener;
|
|
EventListener<web::WebTask> m_downloadListener;
|
|
|
|
Impl(
|
|
std::string const& id,
|
|
std::optional<VersionInfo> const& version,
|
|
std::optional<std::string> const& dependencyFor
|
|
)
|
|
: m_id(id),
|
|
m_version(version),
|
|
m_dependencyFor(dependencyFor),
|
|
m_status(DownloadStatusFetching {
|
|
.percentage = 0,
|
|
})
|
|
{
|
|
m_infoListener.bind([this](ServerRequest<ServerModVersion>::Event* event) {
|
|
if (auto result = event->getValue()) {
|
|
if (result->isOk()) {
|
|
auto data = result->unwrap();
|
|
m_version = data.metadata.getVersion();
|
|
|
|
// Start downloads for any missing dependencies
|
|
for (auto dep : data.metadata.getDependencies()) {
|
|
if (!dep.mod) {
|
|
ModDownloadManager::get()->startDownload(
|
|
dep.id, dep.version.getUnderlyingVersion(), m_id
|
|
);
|
|
}
|
|
}
|
|
|
|
m_status = DownloadStatusConfirm {
|
|
.version = data,
|
|
};
|
|
}
|
|
else {
|
|
m_status = DownloadStatusError {
|
|
.details = result->unwrapErr().details,
|
|
};
|
|
}
|
|
|
|
// Clear the listener to free up the memory
|
|
m_infoListener.setFilter(ServerRequest<ServerModVersion>());
|
|
}
|
|
else if (auto progress = event->getProgress()) {
|
|
m_status = DownloadStatusFetching {
|
|
.percentage = progress->percentage.value_or(0),
|
|
};
|
|
}
|
|
else if (event->isCancelled()) {
|
|
m_status = DownloadStatusCancelled();
|
|
m_infoListener.setFilter(ServerRequest<ServerModVersion>());
|
|
}
|
|
|
|
if (!ModDownloadManager::get()->checkAutoConfirm()) {
|
|
ModDownloadEvent(m_id).post();
|
|
}
|
|
});
|
|
auto fetchVersion = version.has_value() ? ModVersion(*version) : ModVersion(ModVersionLatest());
|
|
m_infoListener.setFilter(getModVersion(id, fetchVersion));
|
|
Loader::get()->queueInMainThread([id = m_id] {
|
|
ModDownloadEvent(id).post();
|
|
});
|
|
}
|
|
|
|
void confirm() {
|
|
auto confirm = std::get_if<DownloadStatusConfirm>(&m_status);
|
|
if (!confirm) return;
|
|
|
|
auto version = confirm->version;
|
|
m_status = DownloadStatusDownloading {
|
|
.percentage = 0,
|
|
};
|
|
|
|
m_downloadListener.bind([this, hash = version.hash](web::WebTask::Event* event) {
|
|
if (auto value = event->getValue()) {
|
|
if (value->ok()) {
|
|
bool removingInstalledWasError = false;
|
|
// If this was an update, delete the old file first
|
|
if (auto mod = Loader::get()->getInstalledMod(m_id)) {
|
|
std::error_code ec;
|
|
ghc::filesystem::remove(mod->getPackagePath(), ec);
|
|
if (ec) {
|
|
removingInstalledWasError = true;
|
|
m_status = DownloadStatusError {
|
|
.details = fmt::format("Unable to delete existing .geode package (code {})", ec),
|
|
};
|
|
}
|
|
}
|
|
if (!removingInstalledWasError) {
|
|
auto ok = file::writeBinary(dirs::getModsDir() / (m_id + ".geode"), value->data());
|
|
if (!ok) {
|
|
m_status = DownloadStatusError {
|
|
.details = ok.unwrapErr(),
|
|
};
|
|
}
|
|
else {
|
|
m_status = DownloadStatusDone();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
m_status = DownloadStatusError {
|
|
.details = value->string().unwrapOr("Unknown error"),
|
|
};
|
|
}
|
|
}
|
|
else if (auto progress = event->getProgress()) {
|
|
m_status = DownloadStatusDownloading {
|
|
.percentage = static_cast<uint8_t>(progress->downloadProgress().value_or(0)),
|
|
};
|
|
}
|
|
else if (event->isCancelled()) {
|
|
m_status = DownloadStatusCancelled();
|
|
}
|
|
ModDownloadEvent(m_id).post();
|
|
});
|
|
|
|
auto req = web::WebRequest();
|
|
m_downloadListener.setFilter(req.get(version.downloadURL));
|
|
ModDownloadEvent(m_id).post();
|
|
}
|
|
};
|
|
|
|
ModDownload::ModDownload(
|
|
std::string const& id,
|
|
std::optional<VersionInfo> const& version,
|
|
std::optional<std::string> const& dependencyFor
|
|
) : m_impl(std::make_shared<Impl>(id, version, dependencyFor)) {}
|
|
|
|
void ModDownload::confirm() {
|
|
m_impl->confirm();
|
|
}
|
|
|
|
std::optional<std::string> ModDownload::getDependencyFor() const {
|
|
return m_impl->m_dependencyFor;
|
|
}
|
|
bool ModDownload::isDone() const {
|
|
return std::holds_alternative<DownloadStatusDone>(m_impl->m_status);
|
|
}
|
|
bool ModDownload::isActive() const {
|
|
return !(
|
|
std::holds_alternative<DownloadStatusDone>(m_impl->m_status) ||
|
|
std::holds_alternative<DownloadStatusError>(m_impl->m_status) ||
|
|
std::holds_alternative<DownloadStatusCancelled>(m_impl->m_status)
|
|
);
|
|
}
|
|
bool ModDownload::canRetry() const {
|
|
return
|
|
std::holds_alternative<DownloadStatusError>(m_impl->m_status) ||
|
|
std::holds_alternative<DownloadStatusCancelled>(m_impl->m_status);
|
|
}
|
|
std::string ModDownload::getID() const {
|
|
return m_impl->m_id;
|
|
}
|
|
DownloadStatus ModDownload::getStatus() const {
|
|
return m_impl->m_status;
|
|
}
|
|
std::optional<VersionInfo> ModDownload::getVersion() const {
|
|
return m_impl->m_version;
|
|
}
|
|
|
|
class ModDownloadManager::Impl {
|
|
public:
|
|
std::unordered_map<std::string, ModDownload> m_downloads;
|
|
Task<std::monostate> m_updateAllTask;
|
|
|
|
void cancelOrphanedDependencies() {
|
|
// "This doesn't handle circular dependencies!!!!"
|
|
// Well OK and the human skull doesn't handle the 5000 newtons
|
|
// of force from this anvil I'm about to drop on your head
|
|
|
|
for (auto& [_, d] : m_downloads) {
|
|
if (auto depFor = d.m_impl->m_dependencyFor) {
|
|
if (
|
|
!m_downloads.contains(*depFor) ||
|
|
!(m_downloads.at(*depFor).isActive() || m_downloads.at(*depFor).isDone())
|
|
) {
|
|
// d.cancel() will cause cancelOrphanedDependencies() to be called again
|
|
// We want that anyway because cancelling one dependency might cause
|
|
// dependencies down the chain to become orphaned
|
|
return d.cancel();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
void ModDownload::cancel() {
|
|
if (!std::holds_alternative<DownloadStatusDone>(m_impl->m_status)) {
|
|
m_impl->m_status = DownloadStatusCancelled();
|
|
m_impl->m_infoListener.getFilter().cancel();
|
|
m_impl->m_infoListener.setFilter(ServerRequest<ServerModVersion>());
|
|
|
|
// Cancel any dependencies of this mod left over (unless some other
|
|
// installation depends on them still)
|
|
ModDownloadManager::get()->m_impl->cancelOrphanedDependencies();
|
|
ModDownloadEvent(m_impl->m_id).post();
|
|
}
|
|
}
|
|
|
|
std::optional<ModDownload> ModDownloadManager::startDownload(
|
|
std::string const& id,
|
|
std::optional<VersionInfo> const& version,
|
|
std::optional<std::string> const& dependencyFor
|
|
) {
|
|
// If this mod has already been succesfully downloaded or is currently
|
|
// being downloaded, return as you can't download multiple versions of the
|
|
// same mod simultaniously, since that wouldn't make sense. I mean the new
|
|
// version would just immediately override to the other one
|
|
if (m_impl->m_downloads.contains(id)) {
|
|
// If the download errored last time, then we can try again
|
|
if (m_impl->m_downloads.at(id).canRetry()) {
|
|
m_impl->m_downloads.erase(id);
|
|
}
|
|
// Otherwise return
|
|
else return std::nullopt;
|
|
}
|
|
|
|
// Start a new download by constructing a ModDownload (which starts the
|
|
// download)
|
|
m_impl->m_downloads.emplace(id, ModDownload(id, version, dependencyFor));
|
|
return m_impl->m_downloads.at(id);
|
|
}
|
|
void ModDownloadManager::cancelAll() {
|
|
for (auto& [_, d] : m_impl->m_downloads) {
|
|
d.cancel();
|
|
}
|
|
}
|
|
void ModDownloadManager::confirmAll() {
|
|
for (auto& [_, d] : m_impl->m_downloads) {
|
|
d.confirm();
|
|
}
|
|
}
|
|
void ModDownloadManager::startUpdateAll() {
|
|
m_impl->m_updateAllTask = checkAllUpdates().map(
|
|
[this](Result<std::vector<ServerModUpdate>, ServerError>* result) {
|
|
if (result->isOk()) {
|
|
for (auto& mod : result->unwrap()) {
|
|
if (mod.hasUpdateForInstalledMod()) {
|
|
this->startDownload(mod.id, std::nullopt);
|
|
}
|
|
}
|
|
}
|
|
return std::monostate();
|
|
},
|
|
[](auto) { return std::monostate(); }
|
|
);
|
|
}
|
|
void ModDownloadManager::dismissAll() {
|
|
std::erase_if(m_impl->m_downloads, [](auto const& d) {
|
|
return d.second.canRetry();
|
|
});
|
|
ModDownloadEvent("").post();
|
|
}
|
|
bool ModDownloadManager::checkAutoConfirm() {
|
|
for (auto& [_, download] : m_impl->m_downloads) {
|
|
auto status = download.getStatus();
|
|
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
|
|
for (auto dep : confirm->version.metadata.getDependencies()) {
|
|
// If some mod has an optional dependency that isn't installed,
|
|
// we need to ask for confirmation
|
|
if (!dep.mod && dep.importance != ModMetadata::Dependency::Importance::Required) {
|
|
return false;
|
|
}
|
|
}
|
|
for (auto inc : confirm->version.metadata.getIncompatibilities()) {
|
|
// If some mod has an incompatability that is installed,
|
|
// we need to ask for confirmation
|
|
if (inc.mod) {
|
|
return false;
|
|
}
|
|
}
|
|
// If some installed mod is incompatible with this one,
|
|
// we need to ask for confirmation
|
|
for (auto mod : Loader::get()->getAllMods()) {
|
|
for (auto inc : mod->getMetadata().getIncompatibilities()) {
|
|
if (inc.id == download.getID()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If there are mods we aren't sure about yet, we can't auto-confirm
|
|
else if (std::holds_alternative<DownloadStatusFetching>(status)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If we have reached this point, we can auto-confirm
|
|
this->confirmAll();
|
|
return true;
|
|
}
|
|
|
|
std::vector<ModDownload> ModDownloadManager::getDownloads() const {
|
|
return map::values(m_impl->m_downloads);
|
|
}
|
|
std::optional<ModDownload> ModDownloadManager::getDownload(std::string const& id) const {
|
|
if (m_impl->m_downloads.contains(id)) {
|
|
return m_impl->m_downloads.at(id);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
bool ModDownloadManager::hasActiveDownloads() const {
|
|
for (auto& [_, download] : m_impl->m_downloads) {
|
|
if (download.isActive()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ModDownloadManager::wantsRestart() const {
|
|
for (auto& [key, v] : m_impl->m_downloads) {
|
|
if (v.isDone()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ModDownloadManager* ModDownloadManager::get() {
|
|
static auto inst = new ModDownloadManager();
|
|
return inst;
|
|
}
|
|
ModDownloadManager::ModDownloadManager() : m_impl(std::make_unique<Impl>()) {}
|
|
ModDownloadManager::~ModDownloadManager() = default;
|