index work

- installing mods works again
 - add EventListener::setFilter
 - fix loader panicking if some mods failed to load
This commit is contained in:
HJfod 2022-12-09 15:13:08 +02:00
parent 92c22d25e4
commit 3b1a11e11f
7 changed files with 412 additions and 58 deletions

View file

@ -60,9 +60,11 @@ namespace geode {
using MemberFn = typename to_member<C, Callback>::value;
ListenerResult passThrough(Event* e) override {
// it is so silly to use dynamic cast in an interbinary context
if (auto myev = cast::typeinfo_cast<typename T::Event*>(e)) {
return m_filter.handle(m_callback, myev);
if (m_callback) {
// it is so silly to use dynamic cast in an interbinary context
if (auto myev = cast::typeinfo_cast<typename T::Event*>(e)) {
return m_filter.handle(m_callback, myev);
}
}
return ListenerResult::Propagate;
}
@ -100,12 +102,16 @@ namespace geode {
m_callback = std::bind(fn, cls, std::placeholders::_1);
}
void setFilter(T filter) {
m_filter = filter;
}
protected:
std::function<Callback> m_callback = nullptr;
T m_filter;
};
class GEODE_DLL Event {
class GEODE_DLL [[nodiscard]] Event {
private:
static std::unordered_set<EventListenerProtocol*> s_listeners;
friend EventListenerProtocol;

View file

@ -4,6 +4,7 @@
#include "ModInfo.hpp"
#include "Event.hpp"
#include "../utils/Result.hpp"
#include "../utils/web.hpp"
#include <unordered_set>
namespace geode {
@ -15,6 +16,7 @@ namespace geode {
struct GEODE_DLL ModInstallEvent : public Event {
const std::string modID;
const UpdateStatus status;
ModInstallEvent(std::string const& id, const UpdateStatus status);
};
class GEODE_DLL ModInstallFilter : public EventFilter<ModInstallEvent> {
@ -70,6 +72,17 @@ namespace geode {
};
using IndexItemHandle = std::shared_ptr<IndexItem>;
struct IndexInstallList {
/**
* Mod being installed
*/
IndexItemHandle target;
/**
* The mod, its dependencies, everything needed to install it
*/
std::vector<IndexItemHandle> list;
};
class GEODE_DLL Index final {
protected:
// for once, the fact that std::map is ordered is useful (this makes
@ -78,6 +91,10 @@ namespace geode {
std::vector<IndexSourcePtr> m_sources;
std::unordered_map<std::string, UpdateStatus> m_sourceStatuses;
std::unordered_map<
IndexItemHandle,
utils::web::SentAsyncWebRequestHandle
> m_runningInstallations;
std::atomic<bool> m_triedToUpdate = false;
std::unordered_map<std::string, ItemVersions> m_items;
@ -89,6 +106,8 @@ namespace geode {
void updateSourceFromLocal(IndexSourceImpl* src);
void cleanupItems();
void installNext(size_t index, IndexInstallList const& list);
public:
static Index* get();
@ -96,27 +115,107 @@ namespace geode {
void removeSource(std::string const& repository);
std::vector<std::string> getSources() const;
/**
* Get all index items available on this platform
*/
std::vector<IndexItemHandle> getItems() const;
bool isKnownItem(std::string const& id, std::optional<VersionInfo> version) const;
/**
* Get all index items regardless of platform
*/
std::vector<IndexItemHandle> getAllItems() const;
/**
* Check if an item with this ID is found on the index, and optionally
* provide the version sought after
*/
bool isKnownItem(
std::string const& id,
std::optional<VersionInfo> version
) const;
/**
* Get an item from the index by its ID and optionally version
* @param id ID of the mod
* @param version Version to match exactly; if you need to match a range
* of versions, use the getItem overload that takes a
* ComparableVersionInfo
* @returns The item, or nullptr if the item was not found
*/
IndexItemHandle getItem(
std::string const& id,
std::optional<VersionInfo> version
) const;
/**
* Get an item from the index by its ID and version range
* @param id ID of the mod
* @param version Version to match
* @returns The item, or nullptr if the item was not found
*/
IndexItemHandle getItem(
std::string const& id,
ComparableVersionInfo version
) const;
/**
* Get an item from the index by its mod.json
* @param info The mod's info
* @returns The item, or nullptr if the item was not found
*/
IndexItemHandle getItem(ModInfo const& info) const;
/**
* Get an item from the index that corresponds to an installed mod
* @param mod An installed mod
* @returns The item, or nullptr if the item was not found
*/
IndexItemHandle getItem(Mod* mod) const;
bool isUpdateAvailable(IndexItemHandle item) const;
bool areUpdatesAvailable() const;
void install(IndexItemHandle item);
Result<std::vector<IndexItemHandle>> getInstallList(
IndexItemHandle item
) const;
/**
* Check if an item has updates available
* @param item Item to check updates for
* @returns True if the version of the item on the index is newer than
* its installed counterpart
*/
bool isUpdateAvailable(IndexItemHandle item) const;
/**
* Check if any of the mods on the index have updates available
*/
bool areUpdatesAvailable() const;
/**
* Get the list of items needed to install this item (dependencies, etc.)
* @param item Item to get the list for
* @returns The list, or an error if some items on the list cannot be installed
*/
Result<IndexInstallList> getInstallList(IndexItemHandle item) const;
/**
* Install an index item. Add an event listener for the ModInstallEvent
* class to track the installation progress
* @param item Item to install
*/
void install(IndexItemHandle item);
/**
* Install a list of index items. Add an event listener for the
* ModInstallEvent class to track the installation progress
* @param list List of items to install
*/
void install(IndexInstallList const& list);
/**
* Cancel an installation in progress
* @param item Installation to cancel
*/
void cancelInstall(IndexItemHandle item);
/**
* Check if it has been attempted to update the index. You can check
* for errors by doing hasTriedToUpdate() && !isUpToDate()
*/
bool hasTriedToUpdate() const;
/**
* Whether the index is up-to-date, i.e. all sources are up-to-date
*/
bool isUpToDate() const;
/**
* Update the index. Add an event listener for the IndexUpdateEvent
* class to track updating progress
* @param force Forcefully update all sources, even if some have are
* already up-to-date
*/
void update(bool force = false);
};
}

View file

@ -5,9 +5,39 @@
#include <Geode/utils/web.hpp>
#include <Geode/utils/string.hpp>
#include <Geode/utils/map.hpp>
#include <hash/hash.hpp>
USE_GEODE_NAMESPACE();
// Save data
struct IndexSourceSaveData {
std::string downloadedCommitSHA;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSourceSaveData, downloadedCommitSHA);
struct IndexSaveData {
std::unordered_map<std::string, IndexSourceSaveData> sources;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSaveData, sources);
// ModInstallEvent
ModInstallEvent::ModInstallEvent(
std::string const& id, const UpdateStatus status
) : modID(id), status(status) {}
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
if (m_id == event->modID) {
fn(event);
}
return ListenerResult::Propagate;
}
ModInstallFilter::ModInstallFilter(std::string const& id) : m_id(id) {}
// IndexUpdateEvent implementation
// The reason sources have private implementation events that are
// turned into the global IndexUpdateEvent is because it makes it much
// simpler to keep track of progress, what errors were received, etc.
@ -48,36 +78,9 @@ public:
SourceUpdateFilter() {}
};
// Save data
struct IndexSourceSaveData {
std::string downloadedCommitSHA;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSourceSaveData, downloadedCommitSHA);
struct IndexSaveData {
std::unordered_map<std::string, IndexSourceSaveData> sources;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSaveData, sources);
// ModInstallEvent
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
if (m_id == event->modID) {
fn(event);
}
return ListenerResult::Propagate;
}
ModInstallFilter::ModInstallFilter(
std::string const& id
) : m_id(id) {}
// IndexUpdateEvent
IndexUpdateEvent::IndexUpdateEvent(
const UpdateStatus status
) : status(status) {}
IndexUpdateEvent::IndexUpdateEvent(const UpdateStatus status) : status(status) {}
ListenerResult IndexUpdateFilter::handle(
std::function<Callback> fn,
@ -153,7 +156,7 @@ static Result<> flattenGithubRepo(ghc::filesystem::path const& dir) {
return Ok();
}
// Index
// Index globals
Index::Index() {
new EventListener(
@ -168,6 +171,8 @@ Index* Index::get() {
return inst;
}
// Sources
void Index::addSource(std::string const& repository) {
m_sources.emplace_back(new IndexSourceImpl {
.repository = repository
@ -188,6 +193,8 @@ std::vector<std::string> Index::getSources() const {
return res;
}
// Updating
void Index::onSourceUpdate(SourceUpdateEvent* event) {
// save status for aggregating SourceUpdateEvents to a single global
// IndexUpdateEvent
@ -424,7 +431,9 @@ void Index::update(bool force) {
m_triedToUpdate = true;
// update all sources in GD thread
// update all sources in GD thread for synchronization (m_sourceStatuses
// and every other member access happens in AsyncWebRequest callbacks
// which are always run in the GD thread aswell)
Loader::get()->queueInGDThread([force, this]() {
// check if some sources are already being updated
if (m_sourceStatuses.size()) {
@ -442,7 +451,22 @@ void Index::update(bool force) {
});
}
// Items
std::vector<IndexItemHandle> Index::getItems() const {
std::vector<IndexItemHandle> res;
for (auto& items : map::values(m_items)) {
if (items.size()) {
auto item = items.rbegin()->second;
if (item->download.platforms.count(GEODE_PLATFORM_TARGET)) {
res.push_back(item);
}
}
}
return res;
}
std::vector<IndexItemHandle> Index::getAllItems() const {
std::vector<IndexItemHandle> res;
for (auto& items : map::values(m_items)) {
if (items.size()) {
@ -466,9 +490,11 @@ IndexItemHandle Index::getItem(
if (m_items.count(id)) {
auto versions = m_items.at(id);
if (version) {
auto major = version.value().getMajor();
if (versions.count(major)) {
return versions.at(major);
// prefer most major version
for (auto& [_, item] : ranges::reverse(m_items.at(id))) {
if (version.value() == item->info.m_version) {
return item;
}
}
} else {
if (versions.size()) {
@ -513,24 +539,31 @@ bool Index::isUpdateAvailable(IndexItemHandle item) const {
bool Index::areUpdatesAvailable() const {
for (auto& mod : Loader::get()->getAllMods()) {
auto item = this->getItem(mod);
if (item->info.m_version > mod->getVersion()) {
if (item && item->info.m_version > mod->getVersion()) {
return true;
}
}
return false;
}
Result<std::vector<IndexItemHandle>> Index::getInstallList(
IndexItemHandle item
) const {
std::vector<IndexItemHandle> list;
// Item installation
Result<IndexInstallList> Index::getInstallList(IndexItemHandle item) const {
IndexInstallList list;
list.target = item;
for (auto& dep : item->info.m_dependencies) {
if (!dep.isResolved()) {
// check if this dep is available in the index
if (auto depItem = this->getItem(dep.id, dep.version)) {
if (!depItem->download.platforms.count(GEODE_PLATFORM_TARGET)) {
return Err(
"Dependency {} is not available on {}",
dep.id, GEODE_PLATFORM_NAME
);
}
// recursively add dependencies
GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem));
ranges::push(list, deps);
ranges::push(list.list, deps.list);
}
// otherwise user must get this dependency manually from somewhere
// else
@ -548,10 +581,146 @@ Result<std::vector<IndexItemHandle>> Index::getInstallList(
// already in order for that to have happened
}
// add this item to the end of the list
list.push_back(item);
list.list.push_back(item);
return Ok(list);
}
void Index::install(IndexItemHandle item) {
// todo
void Index::installNext(size_t index, IndexInstallList const& list) {
auto postError = [this, list](std::string const& error) {
m_runningInstallations.erase(list.target);
ModInstallEvent(list.target->info.m_id, error).post();
};
// If we're at the end of the list, move the downloaded items to mods
if (index >= list.list.size()) {
m_runningInstallations.erase(list.target);
// Move all downloaded files
for (auto& item : list.list) {
// If the mod is already installed, delete the old .geode file
if (auto mod = Loader::get()->getInstalledMod(item->info.m_id)) {
auto res = mod->uninstall();
if (!res) {
return postError(fmt::format(
"Unable to uninstall old version of {}: {}",
item->info.m_id, res.unwrapErr()
));
}
}
// Move the temp file
try {
ghc::filesystem::rename(
dirs::getTempDir() / (item->info.m_id + ".index"),
dirs::getModsDir() / (item->info.m_id + ".geode")
);
} catch(std::exception& e) {
return postError(fmt::format(
"Unable to install {}: {}",
item->info.m_id, e.what()
));
}
}
// load mods
(void)Loader::get()->refreshModsList();
ModInstallEvent(list.target->info.m_id, UpdateFinished()).post();
return;
}
auto scaledProgress = [index, list](double progress) -> uint8_t {
return static_cast<uint8_t>(
progress * (static_cast<double>(index + 1) / list.list.size())
);
};
auto item = list.list.at(index);
auto tempFile = dirs::getTempDir() / (item->info.m_id + ".index");
m_runningInstallations[list.target] = web::AsyncWebRequest()
.join("install_item_" + item->info.m_id)
.fetch(item->download.url)
.into(tempFile)
.then([=](auto) {
// Check for 404
auto notFound = utils::file::readString(tempFile);
if (notFound && notFound.unwrap() == "Not Found") {
return postError(fmt::format(
"Binary file download for {} returned \"404 Not found\". "
"Report this to the Geode development team.",
item->info.m_id
));
}
// Verify checksum
ModInstallEvent(
list.target->info.m_id,
UpdateProgress(
scaledProgress(100),
fmt::format("Verifying {}", item->info.m_id)
)
).post();
if (::calculateHash(tempFile) != item->download.hash) {
return postError(fmt::format(
"Checksum mismatch with {}! (Downloaded file did not match what "
"was expected. Try again, and if the download fails another time, "
"report this to the Geode development team.)",
item->info.m_id
));
}
// Install next item in queue
this->installNext(index + 1, list);
})
.expect([postError, list, item](std::string const& err) {
postError(fmt::format(
"Unable to download {}: {}",
item->info.m_id, err
));
})
.progress([this, item, list, scaledProgress](auto&, double now, double total) {
ModInstallEvent(
list.target->info.m_id,
UpdateProgress(
scaledProgress(now / total * 100.0),
fmt::format("Downloading {}", item->info.m_id)
)
).post();
})
.cancelled([postError](auto&) {
postError("Download cancelled");
})
.send();
}
void Index::cancelInstall(IndexItemHandle item) {
Loader::get()->queueInGDThread([this, item]() {
if (m_runningInstallations.count(item)) {
m_runningInstallations.at(item)->cancel();
m_runningInstallations.erase(item);
}
});
}
void Index::install(IndexInstallList const& list) {
Loader::get()->queueInGDThread([this, list]() {
this->installNext(0, list);
});
}
void Index::install(IndexItemHandle item) {
Loader::get()->queueInGDThread([this, item]() {
if (m_runningInstallations.count(item)) {
return;
}
auto list = this->getInstallList(item);
if (list) {
this->install(list.unwrap());
} else {
ModInstallEvent(
item->info.m_id,
UpdateFailed(list.unwrapErr())
).post();
}
});
}

View file

@ -114,7 +114,7 @@ Result<> Loader::setup() {
if (!sett) {
log::warn("Unable to load loader settings: {}", sett.unwrapErr());
}
GEODE_UNWRAP(this->refreshModsList());
(void)this->refreshModsList();
this->queueInGDThread([]() {
Loader::get()->addSearchPaths();

View file

@ -303,11 +303,14 @@ Result<> Mod::disable() {
Result<> Mod::uninstall() {
if (m_info.m_supportsDisabling) {
GEODE_UNWRAP(this->disable());
if (m_info.m_supportsUnloading) GEODE_UNWRAP(this->unloadBinary());
if (m_info.m_supportsUnloading) {
GEODE_UNWRAP(this->unloadBinary());
}
}
if (!ghc::filesystem::remove(m_info.m_path)) {
try {
ghc::filesystem::remove(m_info.m_path);
} catch(std::exception& e) {
return Err(
"Unable to delete mod's .geode file! "
"This might be due to insufficient permissions - "

View file

@ -273,6 +273,16 @@ void ModInfoPopup::onClose(CCObject* pSender) {
this->removeFromParentAndCleanup(true);
};
void ModInfoPopup::setInstallStatus(std::optional<UpdateProgress> const& progress) {
if (progress) {
m_installStatus->setVisible(true);
m_installStatus->setStatus(progress.value().second);
m_installStatus->setProgress(progress.value().first);
} else {
m_installStatus->setVisible(false);
}
}
// LocalModInfoPopup
bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
@ -569,8 +579,15 @@ LocalModInfoPopup* LocalModInfoPopup::create(Mod* mod, ModListLayer* list) {
// IndexItemInfoPopup
IndexItemInfoPopup::IndexItemInfoPopup()
: m_installListener(
this, &IndexItemInfoPopup::onInstallProgress,
ModInstallFilter("")
) {}
bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) {
m_item = item;
m_installListener.setFilter(m_item->info.m_id);
auto winSize = CCDirector::sharedDirector()->getWinSize();
@ -600,6 +617,44 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) {
return true;
}
void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
std::visit(makeVisitor {
[&](UpdateFinished) {
this->setInstallStatus(std::nullopt);
FLAlertLayer::create(
"Install complete",
"Mod succesfully installed! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect)",
"OK"
)->show();
if (m_layer) {
m_layer->reloadList();
}
this->onClose(nullptr);
},
[&](UpdateProgress const& progress) {
this->setInstallStatus(progress);
},
[&](UpdateFailed const& info) {
this->setInstallStatus(std::nullopt);
FLAlertLayer::create(
"Installation failed :(", info, "OK"
)->show();
m_installBtn->setEnabled(true);
m_installBtn->setTarget(
this, menu_selector(IndexItemInfoPopup::onInstall)
);
m_installBtnSpr->setString("Install");
m_installBtnSpr->setBG("GE_button_01.png"_spr, false);
}
}, event->status);
}
void IndexItemInfoPopup::onInstall(CCObject*) {
auto list = Index::get()->getInstallList(m_item);
if (!list) {
@ -617,7 +672,7 @@ void IndexItemInfoPopup::onInstall(CCObject*) {
// le nest
ranges::join(
ranges::map<std::vector<std::string>>(
list.unwrap(),
list.unwrap().list,
[](IndexItemHandle handle) {
return fmt::format(
" - <cr>{}</c> (<cy>{}</c>)",
@ -632,7 +687,22 @@ void IndexItemInfoPopup::onInstall(CCObject*) {
)->show();
}
void IndexItemInfoPopup::onCancel(CCObject*) {
Index::get()->cancelInstall(m_item);
}
void IndexItemInfoPopup::doInstall() {
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(false);
}
this->setInstallStatus(UpdateProgress(0, "Starting install"));
m_installBtn->setTarget(
this, menu_selector(IndexItemInfoPopup::onCancel)
);
m_installBtnSpr->setString("Cancel");
m_installBtnSpr->setBG("GJ_button_06.png", false);
Index::get()->install(m_item);
}

View file

@ -48,6 +48,8 @@ protected:
void keyDown(cocos2d::enumKeyCodes) override;
void onClose(cocos2d::CCObject*);
void setInstallStatus(std::optional<UpdateProgress> const& progress);
virtual CCNode* createLogo(CCSize const& size) = 0;
virtual ModInfo getModInfo() const = 0;
};
@ -80,10 +82,13 @@ public:
class IndexItemInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
protected:
IndexItemHandle m_item;
EventListener<ModInstallFilter> m_installListener;
bool init(IndexItemHandle item, ModListLayer* list);
void onInstallProgress(ModInstallEvent* event);
void onInstall(CCObject*);
void onCancel(CCObject*);
void doInstall();
void FLAlert_Clicked(FLAlertLayer*, bool) override;
@ -91,6 +96,8 @@ protected:
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
IndexItemInfoPopup();
public:
static IndexItemInfoPopup* create(IndexItemHandle item, ModListLayer* list);
};