mirror of
https://github.com/geode-sdk/geode.git
synced 2024-11-23 07:57:51 -05:00
Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
commit
1b77582242
29 changed files with 786 additions and 430 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,5 +1,17 @@
|
|||
# Geode Changelog
|
||||
|
||||
## v1.3.0
|
||||
* Completely remove runtime enabling & disabling of mods (d817749)
|
||||
* Patches auto enabling can be disabled (69821f3)
|
||||
* Move ModEventType::Loaded to after hooks & patches are enabled (23c3095)
|
||||
* Update index to be able to store multiple versions of the same mod (5572f9c)
|
||||
* Implement UI for selection of downloading specific mod versions (5d15eb0)
|
||||
* Change install & uninstall popups to reflect the new changes (d40f467)
|
||||
* Keep the scroll when enabling, installing etc. a mod (b3d444a)
|
||||
* Update MacOS crashlog to include base and offset (7816c43)
|
||||
* Add user agent to AsyncWebRequest (c256207)
|
||||
* Add post and custom requests to AsyncWebRequest (c256207)
|
||||
|
||||
## v1.2.1
|
||||
* Mods now target macOS 10.13 instead of 10.14 (7cc1cd4)
|
||||
* Fix CustomizeObjectLayer ids moving around when multiple objects are selected (9ee0994, 87749d4)
|
||||
|
|
|
@ -96,7 +96,7 @@ if (PROJECT_IS_TOP_LEVEL AND NOT GEODE_BUILDING_DOCS)
|
|||
set(TULIP_LINK_SOURCE ON)
|
||||
endif()
|
||||
set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE)
|
||||
CPMAddPackage("gh:geode-sdk/TulipHook#f77ccbe")
|
||||
CPMAddPackage("gh:geode-sdk/TulipHook#2e4cb5a")
|
||||
set(CMAKE_WARN_DEPRECATED ON CACHE BOOL "" FORCE)
|
||||
|
||||
# Silence warnings from dependencies
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.2.1
|
||||
1.3.0
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace geode {
|
|||
class Mod;
|
||||
class Loader;
|
||||
|
||||
class GEODE_DLL Hook {
|
||||
class GEODE_DLL Hook final {
|
||||
private:
|
||||
class Impl;
|
||||
std::shared_ptr<Impl> m_impl;
|
||||
|
@ -143,20 +143,23 @@ namespace geode {
|
|||
void setAutoEnable(bool autoEnable);
|
||||
};
|
||||
|
||||
class GEODE_DLL Patch {
|
||||
class GEODE_DLL Patch final {
|
||||
// Change to private in 2.0.0
|
||||
protected:
|
||||
Mod* m_owner;
|
||||
void* m_address;
|
||||
ByteVector m_original;
|
||||
ByteVector m_patch;
|
||||
bool m_applied;
|
||||
bool m_autoEnable;
|
||||
|
||||
// Only allow friend classes to create
|
||||
// patches. Whatever method created the
|
||||
// patches should take care of populating
|
||||
// m_owner, m_address, m_original and
|
||||
// m_patch.
|
||||
Patch() : m_applied(false) {}
|
||||
Patch();
|
||||
~Patch();
|
||||
|
||||
// no copying
|
||||
Patch(Patch const&) = delete;
|
||||
|
@ -170,17 +173,13 @@ namespace geode {
|
|||
* Get the address of the patch.
|
||||
* @returns Address
|
||||
*/
|
||||
uintptr_t getAddress() const {
|
||||
return reinterpret_cast<uintptr_t>(m_address);
|
||||
}
|
||||
uintptr_t getAddress() const;
|
||||
|
||||
/**
|
||||
* Get whether the patch is applied or not.
|
||||
* @returns True if applied, false if not.
|
||||
*/
|
||||
bool isApplied() const {
|
||||
return m_applied;
|
||||
}
|
||||
bool isApplied() const;
|
||||
|
||||
bool apply();
|
||||
bool restore();
|
||||
|
@ -189,14 +188,24 @@ namespace geode {
|
|||
* Get the owner of this patch.
|
||||
* @returns Pointer to the owner's Mod handle.
|
||||
*/
|
||||
Mod* getOwner() const {
|
||||
return m_owner;
|
||||
}
|
||||
Mod* getOwner() const;
|
||||
|
||||
/**
|
||||
* Get info about the patch as JSON
|
||||
* @note For IPC
|
||||
*/
|
||||
json::Value getRuntimeInfo() const;
|
||||
|
||||
/**
|
||||
* Get whether the patch should be auto enabled or not.
|
||||
* @returns Auto enable
|
||||
*/
|
||||
bool getAutoEnable() const;
|
||||
|
||||
/**
|
||||
* Set whether the patch should be auto enabled or not.
|
||||
* @param autoEnable Auto enable
|
||||
*/
|
||||
void setAutoEnable(bool autoEnable);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -107,6 +107,14 @@ namespace geode {
|
|||
std::unique_ptr<Impl> m_impl;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Returns the path that contains all the versions
|
||||
*/
|
||||
ghc::filesystem::path getRootPath() const;
|
||||
|
||||
/**
|
||||
* Returns the path to the specific version
|
||||
*/
|
||||
ghc::filesystem::path getPath() const;
|
||||
[[deprecated("use getMetadata instead")]] ModInfo getModInfo() const;
|
||||
ModMetadata getMetadata() const;
|
||||
|
@ -124,10 +132,14 @@ namespace geode {
|
|||
void setAvailablePlatforms(std::unordered_set<PlatformID> const& value);
|
||||
void setIsFeatured(bool const& value);
|
||||
void setTags(std::unordered_set<std::string> const& value);
|
||||
void setIsInstalled(bool const& value);
|
||||
#endif
|
||||
|
||||
IndexItem();
|
||||
~IndexItem();
|
||||
|
||||
friend class ModMetadata;
|
||||
friend class Index;
|
||||
};
|
||||
using IndexItemHandle = std::shared_ptr<IndexItem>;
|
||||
|
||||
|
@ -168,12 +180,22 @@ namespace geode {
|
|||
* Get all featured index items
|
||||
*/
|
||||
std::vector<IndexItemHandle> getFeaturedItems() const;
|
||||
/**
|
||||
* Get all latest index items
|
||||
*/
|
||||
std::vector<IndexItemHandle> getLatestItems() const;
|
||||
/**
|
||||
* Get all index items by a developer
|
||||
*/
|
||||
std::vector<IndexItemHandle> getItemsByDeveloper(
|
||||
std::string const& name
|
||||
) const;
|
||||
/**
|
||||
* Get all index items for a specific mod
|
||||
*/
|
||||
std::vector<IndexItemHandle> getItemsByModID(
|
||||
std::string const& modID
|
||||
) const;
|
||||
/**
|
||||
* Check if an item with this ID is found on the index, and optionally
|
||||
* provide the version sought after
|
||||
|
|
|
@ -33,6 +33,13 @@ namespace geode {
|
|||
~HandleToSaved();
|
||||
};
|
||||
|
||||
enum class ModRequestedAction {
|
||||
None,
|
||||
Enable,
|
||||
Disable,
|
||||
Uninstall,
|
||||
};
|
||||
|
||||
GEODE_HIDDEN Mod* takeNextLoaderMod();
|
||||
|
||||
class ModImpl;
|
||||
|
@ -337,6 +344,8 @@ namespace geode {
|
|||
Result<> uninstall();
|
||||
bool isUninstalled() const;
|
||||
|
||||
ModRequestedAction getRequestedAction() const;
|
||||
|
||||
/**
|
||||
* Check whether or not this Mod
|
||||
* depends on another mod
|
||||
|
|
|
@ -240,6 +240,7 @@ namespace geode {
|
|||
friend class Loader;
|
||||
|
||||
friend class ModMetadataImpl;
|
||||
friend class IndexItem;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,16 @@ namespace geode::utils::web {
|
|||
template <class T>
|
||||
using DataConverter = Result<T> (*)(ByteVector const&);
|
||||
|
||||
// Hack until 2.0.0 to store extra members in AsyncWebRequest
|
||||
struct AsyncWebRequestData {
|
||||
std::string m_userAgent;
|
||||
std::string m_customRequest;
|
||||
bool m_isPostRequest = false;
|
||||
std::string m_postFields;
|
||||
bool m_isJsonRequest = false;
|
||||
bool m_sent = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* An asynchronous, thread-safe web request. Downloads data from the
|
||||
* internet without slowing the main thread. All callbacks are run in the
|
||||
|
@ -111,16 +121,22 @@ namespace geode::utils::web {
|
|||
*/
|
||||
class GEODE_DLL AsyncWebRequest {
|
||||
private:
|
||||
// i want to cry whose idea was to not make this pimpl
|
||||
// For 2.0.0: make this pimpl
|
||||
|
||||
std::optional<std::string> m_joinID;
|
||||
std::string m_url;
|
||||
AsyncThen m_then = nullptr;
|
||||
AsyncExpectCode m_expect = nullptr;
|
||||
AsyncProgress m_progress = nullptr;
|
||||
AsyncCancelled m_cancelled = nullptr;
|
||||
bool m_sent = false;
|
||||
mutable AsyncWebRequestData* m_extra = nullptr;
|
||||
std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target;
|
||||
std::vector<std::string> m_httpHeaders;
|
||||
|
||||
AsyncWebRequestData& extra();
|
||||
AsyncWebRequestData const& extra() const;
|
||||
|
||||
template <class T>
|
||||
friend class AsyncWebResult;
|
||||
friend class SentAsyncWebRequest;
|
||||
|
@ -151,6 +167,29 @@ namespace geode::utils::web {
|
|||
* Can be called more than once.
|
||||
*/
|
||||
AsyncWebRequest& header(std::string const& header);
|
||||
/**
|
||||
* In order to specify an user agent to the request, give it here.
|
||||
*/
|
||||
AsyncWebRequest& userAgent(std::string const& userAgent);
|
||||
/**
|
||||
* Specify that the request is a POST request.
|
||||
*/
|
||||
AsyncWebRequest& postRequest();
|
||||
/**
|
||||
* Specify that the request is a custom request like PUT and DELETE.
|
||||
*/
|
||||
AsyncWebRequest& customRequest(std::string const& request);
|
||||
/**
|
||||
* Specify the post fields to send with the request. Only valid if
|
||||
* `postRequest` or `customRequest` was called before.
|
||||
*/
|
||||
AsyncWebRequest& postFields(std::string const& fields);
|
||||
/**
|
||||
* Specify the post fields to send with the request. Only valid if
|
||||
* `postRequest` or `customRequest` was called before. Additionally
|
||||
* sets the content type to application/json.
|
||||
*/
|
||||
AsyncWebRequest& postFields(json::Value const& fields);
|
||||
/**
|
||||
* URL to fetch from the internet asynchronously
|
||||
* @param url URL of the data to download. Redirects will be
|
||||
|
|
|
@ -12,6 +12,7 @@ using namespace geode::prelude;
|
|||
Hook::Hook(std::shared_ptr<Impl>&& impl) : m_impl(std::move(impl)) {}
|
||||
Hook::~Hook() {}
|
||||
|
||||
// These classes (Hook and Patch) are nasty using new and delete, change them in 2.0.0
|
||||
Hook* Hook::create(
|
||||
Mod* owner,
|
||||
void* address,
|
||||
|
|
|
@ -47,12 +47,14 @@ IndexUpdateFilter::IndexUpdateFilter() {}
|
|||
|
||||
class IndexItem::Impl final {
|
||||
private:
|
||||
ghc::filesystem::path m_rootPath;
|
||||
ghc::filesystem::path m_path;
|
||||
ModMetadata m_metadata;
|
||||
std::string m_downloadURL;
|
||||
std::string m_downloadHash;
|
||||
std::unordered_set<PlatformID> m_platforms;
|
||||
bool m_isFeatured;
|
||||
bool m_isFeatured = false;
|
||||
bool m_isInstalled = false;
|
||||
std::unordered_set<std::string> m_tags;
|
||||
|
||||
friend class IndexItem;
|
||||
|
@ -62,6 +64,7 @@ public:
|
|||
* Create IndexItem from a directory
|
||||
*/
|
||||
static Result<std::shared_ptr<IndexItem>> create(
|
||||
ghc::filesystem::path const& rootDir,
|
||||
ghc::filesystem::path const& dir
|
||||
);
|
||||
|
||||
|
@ -71,6 +74,10 @@ public:
|
|||
IndexItem::IndexItem() : m_impl(std::make_unique<Impl>()) {}
|
||||
IndexItem::~IndexItem() = default;
|
||||
|
||||
ghc::filesystem::path IndexItem::getRootPath() const {
|
||||
return m_impl->m_rootPath;
|
||||
}
|
||||
|
||||
ghc::filesystem::path IndexItem::getPath() const {
|
||||
return m_impl->m_path;
|
||||
}
|
||||
|
@ -131,9 +138,13 @@ void IndexItem::setIsFeatured(bool const& value) {
|
|||
void IndexItem::setTags(std::unordered_set<std::string> const& value) {
|
||||
m_impl->m_tags = value;
|
||||
}
|
||||
|
||||
void IndexItem::setIsInstalled(bool const& value) {
|
||||
m_impl->m_isInstalled = value;
|
||||
}
|
||||
#endif
|
||||
|
||||
Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir) {
|
||||
Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& rootDir, ghc::filesystem::path const& dir) {
|
||||
GEODE_UNWRAP_INTO(
|
||||
auto entry, file::readJson(dir / "entry.json")
|
||||
.expect("Unable to read entry.json")
|
||||
|
@ -142,6 +153,10 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir
|
|||
auto metadata, ModMetadata::createFromFile(dir / "mod.json")
|
||||
.expect("Unable to read mod.json: {error}")
|
||||
);
|
||||
auto metadataRes = metadata.addSpecialFiles(rootDir);
|
||||
if (!metadataRes) {
|
||||
log::warn("Unable to add special files from {}: {}", rootDir, metadataRes.unwrapErr());
|
||||
}
|
||||
|
||||
JsonChecker checker(entry);
|
||||
auto root = checker.root("[entry.json]").obj();
|
||||
|
@ -157,6 +172,7 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir
|
|||
}
|
||||
|
||||
auto item = std::make_shared<IndexItem>();
|
||||
item->m_impl->m_rootPath = rootDir;
|
||||
item->m_impl->m_path = dir;
|
||||
item->m_impl->m_metadata = metadata;
|
||||
item->m_impl->m_platforms = platforms;
|
||||
|
@ -172,7 +188,17 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir
|
|||
}
|
||||
|
||||
bool IndexItem::Impl::isInstalled() const {
|
||||
return ghc::filesystem::exists(dirs::getModsDir() / (m_metadata.getID() + ".geode"));
|
||||
if (m_isInstalled) {
|
||||
return true;
|
||||
}
|
||||
if (!Loader::get()->isModInstalled(m_metadata.getID())) {
|
||||
return false;
|
||||
}
|
||||
auto installed = Loader::get()->getInstalledMod(m_metadata.getID());
|
||||
if (installed->getVersion() != m_metadata.getVersion()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
@ -209,7 +235,7 @@ class Index::Impl final {
|
|||
public:
|
||||
// for once, the fact that std::map is ordered is useful (this makes
|
||||
// getting the latest version of a mod as easy as items.rbegin())
|
||||
using ItemVersions = std::map<size_t, IndexItemHandle>;
|
||||
using ItemVersions = std::map<VersionInfo, IndexItemHandle>;
|
||||
|
||||
private:
|
||||
std::unordered_map<
|
||||
|
@ -335,6 +361,7 @@ void Index::Impl::checkForUpdates() {
|
|||
auto oldSHA = file::readString(checksum).unwrapOr("");
|
||||
web::AsyncWebRequest()
|
||||
.join("index-update")
|
||||
.userAgent("github_api/1.0")
|
||||
.header(fmt::format("If-None-Match: \"{}\"", oldSHA))
|
||||
.header("Accept: application/vnd.github.sha")
|
||||
.fetch("https://api.github.com/repos/geode-sdk/mods/commits/main")
|
||||
|
@ -369,34 +396,36 @@ void Index::Impl::updateFromLocalTree() {
|
|||
// delete old items
|
||||
m_items.clear();
|
||||
|
||||
// read directory and add new items
|
||||
try {
|
||||
for (auto& dir : ghc::filesystem::directory_iterator(dirs::getIndexDir() / "v0" / "mods")) {
|
||||
auto addRes = IndexItem::Impl::create(dir);
|
||||
auto indexRoot = dirs::getIndexDir() / "v0";
|
||||
auto entriesRoot = indexRoot / "mods-v2";
|
||||
|
||||
auto configRes = file::readJson(indexRoot / "config.json");
|
||||
if (!configRes) {
|
||||
IndexUpdateEvent("Unable to read index config").post();
|
||||
return;
|
||||
}
|
||||
auto config = configRes.unwrap();
|
||||
|
||||
JsonChecker checker(config);
|
||||
auto root = checker.root("[config.json]").obj();
|
||||
|
||||
for (auto& [modID, entry] : root.has("entries").items()) {
|
||||
for (auto& version : entry.obj().has("versions").iterate()) {
|
||||
auto rootDir = entriesRoot / modID;
|
||||
auto dir = rootDir / version.get<std::string>();
|
||||
|
||||
auto addRes = IndexItem::Impl::create(rootDir, dir);
|
||||
if (!addRes) {
|
||||
log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr());
|
||||
continue;
|
||||
}
|
||||
auto add = addRes.unwrap();
|
||||
auto metadata = add->getMetadata();
|
||||
// check if this major version of this item has already been added
|
||||
if (m_items[metadata.getID()].count(metadata.getVersion().getMajor())) {
|
||||
log::warn(
|
||||
"Item {}@{} has already been added, skipping",
|
||||
metadata.getID(),
|
||||
metadata.getVersion()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// add new major version of this item
|
||||
m_items[metadata.getID()].insert({metadata.getVersion().getMajor(),
|
||||
|
||||
m_items[modID].insert({metadata.getVersion(),
|
||||
add
|
||||
});
|
||||
}
|
||||
} catch(std::exception& e) {
|
||||
log::error("Unable to read local index tree: {}", e.what());
|
||||
IndexUpdateEvent("Unable to read local index tree").post();
|
||||
return;
|
||||
}
|
||||
|
||||
// mark source as finished
|
||||
|
@ -436,6 +465,14 @@ std::vector<IndexItemHandle> Index::getItems() const {
|
|||
return res;
|
||||
}
|
||||
|
||||
std::vector<IndexItemHandle> Index::getLatestItems() const {
|
||||
std::vector<IndexItemHandle> res;
|
||||
for (auto& [modID, versions] : m_impl->m_items) {
|
||||
res.push_back(this->getMajorItem(modID));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<IndexItemHandle> Index::getFeaturedItems() const {
|
||||
std::vector<IndexItemHandle> res;
|
||||
for (auto& items : map::values(m_impl->m_items)) {
|
||||
|
@ -462,6 +499,18 @@ std::vector<IndexItemHandle> Index::getItemsByDeveloper(
|
|||
return res;
|
||||
}
|
||||
|
||||
std::vector<IndexItemHandle> Index::getItemsByModID(
|
||||
std::string const& modID
|
||||
) const {
|
||||
std::vector<IndexItemHandle> res;
|
||||
if (m_impl->m_items.count(modID)) {
|
||||
for (auto& [versionStr, item] : m_impl->m_items[modID]) {
|
||||
res.push_back(item);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
bool Index::isKnownItem(
|
||||
std::string const& id,
|
||||
std::optional<VersionInfo> version
|
||||
|
@ -485,19 +534,14 @@ IndexItemHandle Index::getItem(
|
|||
if (m_impl->m_items.count(id)) {
|
||||
auto versions = m_impl->m_items.at(id);
|
||||
if (version) {
|
||||
// prefer most major version
|
||||
for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) {
|
||||
if (version.value() == item->getMetadata().getVersion()) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!versions.empty()) {
|
||||
return m_impl->m_items.at(id).rbegin()->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
return this->getMajorItem(id);
|
||||
}
|
||||
|
||||
IndexItemHandle Index::getItem(
|
||||
|
@ -718,6 +762,8 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
|
|||
));
|
||||
}
|
||||
|
||||
item->setIsInstalled(true);
|
||||
|
||||
// Install next item in queue
|
||||
this->installNext(index + 1, list);
|
||||
})
|
||||
|
@ -752,6 +798,10 @@ void Index::cancelInstall(IndexItemHandle item) {
|
|||
}
|
||||
|
||||
void Index::install(IndexInstallList const& list) {
|
||||
if (list.list.empty()) {
|
||||
ModInstallEvent(list.target->getMetadata().getID(), UpdateFinished()).post();
|
||||
return;
|
||||
}
|
||||
Loader::get()->queueInMainThread([this, list]() {
|
||||
m_impl->installNext(0, list);
|
||||
});
|
||||
|
|
|
@ -166,7 +166,6 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) {
|
|||
Result<> Loader::Impl::saveData() {
|
||||
// save mods' data
|
||||
for (auto& [id, mod] : m_mods) {
|
||||
Mod::get()->setSavedValue("should-load-" + id, mod->isUninstalled() || mod->isEnabled());
|
||||
auto r = mod->saveData();
|
||||
if (!r) {
|
||||
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
|
||||
|
@ -411,31 +410,16 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) {
|
|||
return;
|
||||
}
|
||||
|
||||
log::debug("Load");
|
||||
auto res = node->m_impl->loadBinary();
|
||||
if (!res) {
|
||||
m_problems.push_back({
|
||||
LoadProblem::Type::LoadFailed,
|
||||
node,
|
||||
res.unwrapErr()
|
||||
});
|
||||
log::error("Failed to load binary: {}", res.unwrapErr());
|
||||
log::popNest();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mod::get()->getSavedValue<bool>("should-load-" + node->getID(), true)) {
|
||||
log::debug("Enable");
|
||||
res = node->m_impl->enable();
|
||||
log::debug("Load");
|
||||
auto res = node->m_impl->loadBinary();
|
||||
if (!res) {
|
||||
node->m_impl->m_enabled = true;
|
||||
(void)node->m_impl->disable();
|
||||
m_problems.push_back({
|
||||
LoadProblem::Type::EnableFailed,
|
||||
LoadProblem::Type::LoadFailed,
|
||||
node,
|
||||
res.unwrapErr()
|
||||
});
|
||||
log::error("Failed to enable: {}", res.unwrapErr());
|
||||
log::error("Failed to load binary: {}", res.unwrapErr());
|
||||
log::popNest();
|
||||
return;
|
||||
}
|
||||
|
@ -509,19 +493,19 @@ void Loader::Impl::findProblems() {
|
|||
|
||||
Mod* myEpicMod = mod; // clang fix
|
||||
// if the mod is not loaded but there are no problems related to it
|
||||
if (!mod->isLoaded() && !std::any_of(m_problems.begin(), m_problems.end(), [myEpicMod](auto& item) {
|
||||
return std::holds_alternative<ModMetadata>(item.cause) &&
|
||||
std::get<ModMetadata>(item.cause).getID() == myEpicMod->getID() ||
|
||||
std::holds_alternative<Mod*>(item.cause) &&
|
||||
std::get<Mod*>(item.cause) == myEpicMod;
|
||||
})) {
|
||||
m_problems.push_back({
|
||||
LoadProblem::Type::Unknown,
|
||||
mod,
|
||||
""
|
||||
});
|
||||
log::error("{} failed to load for an unknown reason", id);
|
||||
}
|
||||
// if (!mod->isLoaded() && !std::any_of(m_problems.begin(), m_problems.end(), [myEpicMod](auto& item) {
|
||||
// return std::holds_alternative<ModMetadata>(item.cause) &&
|
||||
// std::get<ModMetadata>(item.cause).getID() == myEpicMod->getID() ||
|
||||
// std::holds_alternative<Mod*>(item.cause) &&
|
||||
// std::get<Mod*>(item.cause) == myEpicMod;
|
||||
// })) {
|
||||
// m_problems.push_back({
|
||||
// LoadProblem::Type::Unknown,
|
||||
// mod,
|
||||
// ""
|
||||
// });
|
||||
// log::error("{} failed to load for an unknown reason", id);
|
||||
// }
|
||||
|
||||
log::popNest();
|
||||
}
|
||||
|
@ -533,7 +517,7 @@ void Loader::Impl::refreshModGraph() {
|
|||
|
||||
auto begin = std::chrono::high_resolution_clock::now();
|
||||
|
||||
if (m_mods.size() > 1) {
|
||||
if (m_isSetup) {
|
||||
log::error("Cannot refresh mod graph after startup");
|
||||
log::popNest();
|
||||
return;
|
||||
|
@ -564,7 +548,7 @@ void Loader::Impl::refreshModGraph() {
|
|||
m_loadingState = LoadingState::EarlyMods;
|
||||
log::debug("Loading early mods");
|
||||
log::pushNest();
|
||||
for (auto const& dep : Mod::get()->m_impl->m_dependants) {
|
||||
for (auto const& dep : ModImpl::get()->m_dependants) {
|
||||
this->loadModGraph(dep, true);
|
||||
}
|
||||
log::popNest();
|
||||
|
@ -716,6 +700,7 @@ void Loader::Impl::fetchLatestGithubRelease(
|
|||
// TODO: add header to not get rate limited
|
||||
web::AsyncWebRequest()
|
||||
.join("loader-auto-update-check")
|
||||
.userAgent("github_api/1.0")
|
||||
.fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest")
|
||||
.json()
|
||||
.then([this, then](json::Value const& json) {
|
||||
|
@ -784,6 +769,7 @@ void Loader::Impl::downloadLoaderResources(bool useLatestRelease) {
|
|||
if (!useLatestRelease) {
|
||||
web::AsyncWebRequest()
|
||||
.join("loader-tag-exists-check")
|
||||
.userAgent("github_api/1.0")
|
||||
.fetch(fmt::format(
|
||||
"https://api.github.com/repos/geode-sdk/geode/git/ref/tags/{}",
|
||||
this->getVersion().toString()
|
||||
|
|
|
@ -198,6 +198,10 @@ bool Mod::isUninstalled() const {
|
|||
return m_impl->isUninstalled();
|
||||
}
|
||||
|
||||
ModRequestedAction Mod::getRequestedAction() const {
|
||||
return m_impl->getRequestedAction();
|
||||
}
|
||||
|
||||
bool Mod::depends(std::string const& id) const {
|
||||
return m_impl->depends(id);
|
||||
}
|
||||
|
|
|
@ -117,23 +117,15 @@ bool Mod::Impl::isLoaded() const {
|
|||
}
|
||||
|
||||
bool Mod::Impl::supportsDisabling() const {
|
||||
return m_metadata.getID() != "geode.loader" && !m_metadata.isAPI();
|
||||
return m_metadata.getID() != "geode.loader";
|
||||
}
|
||||
|
||||
bool Mod::Impl::canDisable() const {
|
||||
auto deps = m_dependants;
|
||||
return this->supportsDisabling() &&
|
||||
(deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) {
|
||||
return item->canDisable();
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Mod::Impl::canEnable() const {
|
||||
auto deps = m_metadata.getDependencies();
|
||||
return !this->isUninstalled() &&
|
||||
(deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) {
|
||||
return item.isResolved();
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Mod::Impl::needsEarlyLoad() const {
|
||||
|
@ -345,119 +337,74 @@ Result<> Mod::Impl::loadBinary() {
|
|||
|
||||
LoaderImpl::get()->releaseNextMod();
|
||||
|
||||
ModStateEvent(m_self, ModEventType::Loaded).post();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::enable() {
|
||||
if (!m_binaryLoaded)
|
||||
return Err("Tried to enable {} but its binary is not loaded", m_metadata.getID());
|
||||
|
||||
bool enabledDependencies = true;
|
||||
for (auto const& item : m_metadata.getDependencies()) {
|
||||
if (!item.isResolved() || !item.mod)
|
||||
continue;
|
||||
auto res = item.mod->enable();
|
||||
if (!res) {
|
||||
enabledDependencies = false;
|
||||
log::error("Failed to enable {}: {}", item.id, res.unwrapErr());
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledDependencies)
|
||||
return Err("Mod cannot be enabled because one or more of its dependencies cannot be enabled.");
|
||||
|
||||
if (!this->canEnable())
|
||||
return Err("Mod cannot be enabled because it has unresolved dependencies.");
|
||||
|
||||
for (auto const& hook : m_hooks) {
|
||||
if (!hook) {
|
||||
log::warn("Hook is null in mod \"{}\"", m_metadata.getName());
|
||||
continue;
|
||||
}
|
||||
if (hook->getAutoEnable()) {
|
||||
GEODE_UNWRAP(this->enableHook(hook));
|
||||
auto res = this->enableHook(hook);
|
||||
if (!res) {
|
||||
log::error("Can't enable hook {} for mod {}: {}", hook->getDisplayName(), m_metadata.getID(), res.unwrapErr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& patch : m_patches) {
|
||||
if (!patch->apply()) {
|
||||
log::warn("Unable to apply patch at {}", patch->getAddress());
|
||||
if (!patch) {
|
||||
log::warn("Patch is null in mod \"{}\"", m_metadata.getName());
|
||||
continue;
|
||||
}
|
||||
if (patch->getAutoEnable()) {
|
||||
if (!patch->apply()) {
|
||||
log::warn("Unable to apply patch at {}", patch->getAddress());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
|
||||
ModStateEvent(m_self, ModEventType::Loaded).post();
|
||||
ModStateEvent(m_self, ModEventType::Enabled).post();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::enable() {
|
||||
if (m_requestedAction != ModRequestedAction::None) {
|
||||
return Err("Mod already has a requested action");
|
||||
}
|
||||
|
||||
m_requestedAction = ModRequestedAction::Enable;
|
||||
Mod::get()->setSavedValue("should-load-" + m_metadata.getID(), true);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::disable() {
|
||||
if (!m_enabled)
|
||||
return Ok();
|
||||
|
||||
if (!this->supportsDisabling())
|
||||
return Err("Mod does not support disabling.");
|
||||
|
||||
if (!this->canDisable())
|
||||
return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled.");
|
||||
|
||||
// disable dependants
|
||||
bool disabledDependants = true;
|
||||
for (auto& item : m_dependants) {
|
||||
auto res = item->disable();
|
||||
if (res)
|
||||
continue;
|
||||
disabledDependants = false;
|
||||
log::error("Failed to disable {}: {}", item->getID(), res.unwrapErr());
|
||||
if (m_requestedAction != ModRequestedAction::None) {
|
||||
return Err("Mod already has a requested action");
|
||||
}
|
||||
|
||||
if (!disabledDependants)
|
||||
return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled.");
|
||||
|
||||
std::vector<std::string> errors;
|
||||
for (auto const& hook : m_hooks) {
|
||||
auto res = this->disableHook(hook);
|
||||
if (!res)
|
||||
errors.push_back(res.unwrapErr());
|
||||
}
|
||||
for (auto const& patch : m_patches) {
|
||||
auto res = this->unpatch(patch);
|
||||
if (!res)
|
||||
errors.push_back(res.unwrapErr());
|
||||
}
|
||||
|
||||
m_enabled = false;
|
||||
ModStateEvent(m_self, ModEventType::Disabled).post();
|
||||
|
||||
if (!errors.empty())
|
||||
return Err(utils::string::join(errors, "\n"));
|
||||
m_requestedAction = ModRequestedAction::Disable;
|
||||
Mod::get()->setSavedValue("should-load-" + m_metadata.getID(), false);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::uninstall() {
|
||||
if (supportsDisabling()) {
|
||||
GEODE_UNWRAP(this->disable());
|
||||
}
|
||||
else {
|
||||
for (auto& item : m_dependants) {
|
||||
if (!item->canDisable())
|
||||
continue;
|
||||
GEODE_UNWRAP(item->disable());
|
||||
}
|
||||
if (m_requestedAction != ModRequestedAction::None) {
|
||||
return Err("Mod already has a requested action");
|
||||
}
|
||||
|
||||
try {
|
||||
ghc::filesystem::remove(m_metadata.getPath());
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
m_requestedAction = ModRequestedAction::Uninstall;
|
||||
|
||||
std::error_code ec;
|
||||
ghc::filesystem::remove(m_metadata.getPath(), ec);
|
||||
if (ec) {
|
||||
return Err(
|
||||
"Unable to delete mod's .geode file! "
|
||||
"This might be due to insufficient permissions - "
|
||||
"try running GD as administrator."
|
||||
"Unable to delete mod's .geode file: " + ec.message()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -465,7 +412,11 @@ Result<> Mod::Impl::uninstall() {
|
|||
}
|
||||
|
||||
bool Mod::Impl::isUninstalled() const {
|
||||
return m_self != Mod::get() && !ghc::filesystem::exists(m_metadata.getPath());
|
||||
return m_requestedAction == ModRequestedAction::Uninstall;
|
||||
}
|
||||
|
||||
ModRequestedAction Mod::Impl::getRequestedAction() const {
|
||||
return m_requestedAction;
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
|
@ -661,7 +612,7 @@ ModJson Mod::Impl::getRuntimeInfo() const {
|
|||
for (auto patch : m_patches) {
|
||||
obj["patches"].as_array().push_back(ModJson(patch->getRuntimeInfo()));
|
||||
}
|
||||
obj["enabled"] = m_enabled;
|
||||
// obj["enabled"] = m_enabled;
|
||||
obj["loaded"] = m_binaryLoaded;
|
||||
obj["temp-dir"] = this->getTempDir();
|
||||
obj["save-dir"] = this->getSaveDir();
|
||||
|
|
|
@ -62,6 +62,9 @@ namespace geode {
|
|||
*/
|
||||
bool m_resourcesLoaded = false;
|
||||
|
||||
|
||||
ModRequestedAction m_requestedAction = ModRequestedAction::None;
|
||||
|
||||
Impl(Mod* self, ModMetadata const& metadata);
|
||||
~Impl();
|
||||
|
||||
|
@ -122,6 +125,10 @@ namespace geode {
|
|||
Result<> disable();
|
||||
Result<> uninstall();
|
||||
bool isUninstalled() const;
|
||||
|
||||
// 1.3.0 additions
|
||||
ModRequestedAction getRequestedAction() const;
|
||||
|
||||
bool depends(std::string const& id) const;
|
||||
Result<> updateDependencies();
|
||||
bool hasUnresolvedDependencies() const;
|
||||
|
|
|
@ -4,13 +4,43 @@
|
|||
using namespace geode::prelude;
|
||||
|
||||
bool Patch::apply() {
|
||||
return bool(tulip::hook::writeMemory(m_address, m_patch.data(), m_patch.size()));
|
||||
bool res = bool(tulip::hook::writeMemory(m_address, m_patch.data(), m_patch.size()));
|
||||
if (res)
|
||||
m_applied = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
bool Patch::restore() {
|
||||
return bool(tulip::hook::writeMemory(m_address, m_original.data(), m_original.size()));
|
||||
bool res = bool(tulip::hook::writeMemory(m_address, m_original.data(), m_original.size()));
|
||||
if (res)
|
||||
m_applied = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
Patch::Patch() : m_owner(nullptr), m_address(nullptr), m_applied(false), m_autoEnable(true) {}
|
||||
|
||||
void Patch::setAutoEnable(bool autoEnable) {
|
||||
m_autoEnable = autoEnable;
|
||||
}
|
||||
|
||||
bool Patch::getAutoEnable() const {
|
||||
return m_autoEnable;
|
||||
}
|
||||
|
||||
uintptr_t Patch::getAddress() const {
|
||||
return reinterpret_cast<uintptr_t>(m_address);
|
||||
}
|
||||
|
||||
bool Patch::isApplied() const {
|
||||
return m_applied;
|
||||
}
|
||||
|
||||
Mod* Patch::getOwner() const {
|
||||
return m_owner;
|
||||
}
|
||||
|
||||
Patch::~Patch() {}
|
||||
|
||||
template <>
|
||||
struct json::Serialize<ByteVector> {
|
||||
static json::Value to_json(ByteVector const& bytes) {
|
||||
|
|
|
@ -251,7 +251,20 @@ static std::string getStacktrace() {
|
|||
stream >> std::hex >> address >> std::dec;
|
||||
|
||||
if (!line.empty()) {
|
||||
stacktrace << " - " << std::showbase << std::hex << address << std::dec;
|
||||
// log::debug("address: {}", address);
|
||||
auto image = imageFromAddress(reinterpret_cast<void*>(address));
|
||||
// log::debug("image: {}", image);
|
||||
stacktrace << " - " << std::showbase << std::hex;
|
||||
|
||||
if (image) {
|
||||
auto baseAddress = image->imageLoadAddress;
|
||||
auto imageName = getImageName(image);
|
||||
stacktrace << imageName << " + " << (address - (uintptr_t)baseAddress);
|
||||
}
|
||||
else {
|
||||
stacktrace << address;
|
||||
}
|
||||
stacktrace << std::dec;
|
||||
stacktrace << ": " << line << "\n";
|
||||
}
|
||||
else {
|
||||
|
@ -319,14 +332,15 @@ static void handlerThread() {
|
|||
s_cv.wait(lock, [] { return s_signal != 0; });
|
||||
|
||||
auto signalAddress = reinterpret_cast<void*>(s_context->uc_mcontext->__ss.__rip);
|
||||
Mod* faultyMod = nullptr;
|
||||
for (int i = 1; i < s_backtraceSize; ++i) {
|
||||
auto mod = modFromAddress(s_backtrace[i]);
|
||||
if (mod != nullptr) {
|
||||
faultyMod = mod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Mod* faultyMod = nullptr;
|
||||
// for (int i = 1; i < s_backtraceSize; ++i) {
|
||||
// auto mod = modFromAddress(s_backtrace[i]);
|
||||
// if (mod != nullptr) {
|
||||
// faultyMod = mod;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
Mod* faultyMod = modFromAddress(signalAddress);
|
||||
|
||||
auto text = crashlog::writeCrashlog(faultyMod, getInfo(signalAddress, faultyMod), getStacktrace(), getRegisters());
|
||||
|
||||
|
@ -334,6 +348,8 @@ static void handlerThread() {
|
|||
|
||||
s_signal = 0;
|
||||
s_cv.notify_all();
|
||||
|
||||
log::debug("Notified");
|
||||
}
|
||||
|
||||
static bool s_lastLaunchCrashed;
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace gd {
|
|||
return std::string(*this) == std::string(other);
|
||||
}
|
||||
|
||||
// TODO: these need to stay for old mods linking against geode <1.2.0
|
||||
// TODO: these need to stay for old mods linking against geode <1.2.0, remove in 2.0.0
|
||||
template class map<int, int>;
|
||||
template class map<gd::string, gd::string>;
|
||||
template class map<gd::string, bool>;
|
||||
|
|
|
@ -83,7 +83,7 @@ CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
|
|||
}
|
||||
|
||||
CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) {
|
||||
auto logoPath = ghc::filesystem::absolute(item->getPath() / "logo.png");
|
||||
auto logoPath = ghc::filesystem::absolute(item->getRootPath() / "logo.png");
|
||||
CCNode* spr = CCSprite::create(logoPath.string().c_str());
|
||||
if (!spr) {
|
||||
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
|
||||
|
|
|
@ -277,6 +277,95 @@ void ModInfoPopup::setInstallStatus(std::optional<UpdateProgress> const& progres
|
|||
}
|
||||
}
|
||||
|
||||
void ModInfoPopup::popupInstallItem(IndexItemHandle item) {
|
||||
auto deps = item->getMetadata().getDependencies();
|
||||
enum class DepState {
|
||||
None,
|
||||
HasOnlyRequired,
|
||||
HasOptional
|
||||
} depState = DepState::None;
|
||||
for (auto const& dep : deps) {
|
||||
// resolved means it's already installed, so
|
||||
// no need to ask the user whether they want to install it
|
||||
if (Loader::get()->isModLoaded(dep.id))
|
||||
continue;
|
||||
if (dep.importance != ModMetadata::Dependency::Importance::Required) {
|
||||
depState = DepState::HasOptional;
|
||||
break;
|
||||
}
|
||||
depState = DepState::HasOnlyRequired;
|
||||
}
|
||||
|
||||
std::string content;
|
||||
char const* btn1;
|
||||
char const* btn2;
|
||||
switch (depState) {
|
||||
case DepState::None:
|
||||
content = fmt::format(
|
||||
"Are you sure you want to install <cg>{}</c>?",
|
||||
item->getMetadata().getName()
|
||||
);
|
||||
btn1 = "Info";
|
||||
btn2 = "Install";
|
||||
break;
|
||||
case DepState::HasOnlyRequired:
|
||||
content =
|
||||
"Installing this mod requires other mods to be installed. "
|
||||
"Would you like to <cy>proceed</c> with the installation or "
|
||||
"<cb>view</c> which mods are going to be installed?";
|
||||
btn1 = "View";
|
||||
btn2 = "Proceed";
|
||||
break;
|
||||
case DepState::HasOptional:
|
||||
content =
|
||||
"This mod recommends installing other mods alongside it. "
|
||||
"Would you like to continue with <cy>recommended settings</c> or "
|
||||
"<cb>customize</c> which mods to install?";
|
||||
btn1 = "Customize";
|
||||
btn2 = "Recommended";
|
||||
break;
|
||||
}
|
||||
|
||||
createQuickPopup("Confirm Install", content, btn1, btn2, 320.f, [&](FLAlertLayer*, bool btn2) {
|
||||
if (btn2) {
|
||||
auto canInstall = Index::get()->canInstall(m_item);
|
||||
if (!canInstall) {
|
||||
FLAlertLayer::create(
|
||||
"Unable to Install",
|
||||
canInstall.unwrapErr(),
|
||||
"OK"
|
||||
)->show();
|
||||
return;
|
||||
}
|
||||
this->preInstall();
|
||||
Index::get()->install(m_item);
|
||||
}
|
||||
else {
|
||||
InstallListPopup::create(m_item, [&](IndexInstallList const& list) {
|
||||
this->preInstall();
|
||||
Index::get()->install(list);
|
||||
})->show();
|
||||
}
|
||||
}, true, true);
|
||||
}
|
||||
|
||||
void ModInfoPopup::preInstall() {
|
||||
if (m_latestVersionLabel) {
|
||||
m_latestVersionLabel->setVisible(false);
|
||||
}
|
||||
this->setInstallStatus(UpdateProgress(0, "Starting install"));
|
||||
|
||||
m_installBtn->setTarget(
|
||||
this, menu_selector(ModInfoPopup::onCancelInstall)
|
||||
);
|
||||
m_installBtnSpr->setString("Cancel");
|
||||
m_installBtnSpr->setBG("GJ_button_06.png", false);
|
||||
}
|
||||
|
||||
void ModInfoPopup::onCancelInstall(CCObject*) {
|
||||
Index::get()->cancelInstall(m_item);
|
||||
}
|
||||
|
||||
// LocalModInfoPopup
|
||||
LocalModInfoPopup::LocalModInfoPopup()
|
||||
: m_installListener(
|
||||
|
@ -462,8 +551,8 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) {
|
|||
FLAlertLayer::create(
|
||||
"Update complete",
|
||||
"Mod successfully updated! :) "
|
||||
"(You may need to <cy>restart the game</c> "
|
||||
"for the mod to take full effect)",
|
||||
"(You have to <cy>restart the game</c> "
|
||||
"for the mod to take effect)",
|
||||
"OK"
|
||||
)->show();
|
||||
|
||||
|
@ -493,61 +582,7 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) {
|
|||
}
|
||||
|
||||
void LocalModInfoPopup::onUpdate(CCObject*) {
|
||||
auto list = Index::get()->getInstallList(m_item);
|
||||
if (!list) {
|
||||
return FLAlertLayer::create(
|
||||
"Unable to Update",
|
||||
list.unwrapErr(),
|
||||
"OK"
|
||||
)->show();
|
||||
}
|
||||
auto layer = FLAlertLayer::create(
|
||||
this,
|
||||
"Confirm Update",
|
||||
fmt::format(
|
||||
"The following mods will be updated:\n {}",
|
||||
// le nest
|
||||
ranges::join(
|
||||
ranges::map<std::vector<std::string>>(
|
||||
list.unwrap().list,
|
||||
[](IndexItemHandle handle) {
|
||||
return fmt::format(
|
||||
" - <cr>{}</c> (<cy>{}</c>)",
|
||||
handle->getMetadata().getName(),
|
||||
handle->getMetadata().getID()
|
||||
);
|
||||
}
|
||||
),
|
||||
"\n "
|
||||
)
|
||||
),
|
||||
"Cancel", "OK"
|
||||
);
|
||||
layer->setTag(TAG_CONFIRM_UPDATE);
|
||||
layer->show();
|
||||
}
|
||||
|
||||
void LocalModInfoPopup::onCancel(CCObject*) {
|
||||
Index::get()->cancelInstall(m_item);
|
||||
}
|
||||
|
||||
void LocalModInfoPopup::doUpdate() {
|
||||
if (m_latestVersionLabel) {
|
||||
m_latestVersionLabel->setVisible(false);
|
||||
}
|
||||
|
||||
if (m_minorVersionLabel) {
|
||||
m_minorVersionLabel->setVisible(false);
|
||||
}
|
||||
this->setInstallStatus(UpdateProgress(0, "Starting update"));
|
||||
|
||||
m_installBtn->setTarget(
|
||||
this, menu_selector(LocalModInfoPopup::onCancel)
|
||||
);
|
||||
m_installBtnSpr->setString("Cancel");
|
||||
m_installBtnSpr->setBG("GJ_button_06.png", false);
|
||||
|
||||
Index::get()->install(m_item);
|
||||
this->popupInstallItem(m_item);
|
||||
}
|
||||
|
||||
void LocalModInfoPopup::onUninstall(CCObject*) {
|
||||
|
@ -631,12 +666,6 @@ void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
|
|||
}
|
||||
this->onClose(nullptr);
|
||||
} break;
|
||||
|
||||
case TAG_CONFIRM_UPDATE: {
|
||||
if (btn2) {
|
||||
this->doUpdate();
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -649,8 +678,8 @@ void LocalModInfoPopup::doUninstall() {
|
|||
this,
|
||||
"Uninstall complete",
|
||||
"Mod was successfully uninstalled! :) "
|
||||
"(You may need to <cy>restart the game</c> "
|
||||
"for the mod to take full effect). "
|
||||
"(You have to <cy>restart the game</c> "
|
||||
"for the mod to take effect). "
|
||||
"<co>Would you also like to delete the mod's "
|
||||
"save data?</c>",
|
||||
"Keep",
|
||||
|
@ -687,7 +716,8 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) {
|
|||
|
||||
if (!ModInfoPopup::init(item->getMetadata(), list)) return false;
|
||||
|
||||
if (item->isInstalled()) return true;
|
||||
// bruh why is this here if we are allowing for browsing already installed mods
|
||||
// if (item->isInstalled()) return true;
|
||||
|
||||
m_installBtnSpr = IconButtonSprite::create(
|
||||
"GE_button_01.png"_spr,
|
||||
|
@ -719,8 +749,8 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
|
|||
FLAlertLayer::create(
|
||||
"Install complete",
|
||||
"Mod successfully installed! :) "
|
||||
"(You may need to <cy>restart the game</c> "
|
||||
"for the mod to take full effect)",
|
||||
"(You have to <cy>restart the game</c> "
|
||||
"for the mod to take effect)",
|
||||
"OK"
|
||||
)->show();
|
||||
|
||||
|
@ -750,92 +780,7 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
|
|||
}
|
||||
|
||||
void IndexItemInfoPopup::onInstall(CCObject*) {
|
||||
auto deps = m_item->getMetadata().getDependencies();
|
||||
enum class DepState {
|
||||
None,
|
||||
HasOnlyRequired,
|
||||
HasOptional
|
||||
} depState = DepState::None;
|
||||
for (auto const& item : deps) {
|
||||
// resolved means it's already installed, so
|
||||
// no need to ask the user whether they want to install it
|
||||
if (Loader::get()->isModLoaded(item.id))
|
||||
continue;
|
||||
if (item.importance != ModMetadata::Dependency::Importance::Required) {
|
||||
depState = DepState::HasOptional;
|
||||
break;
|
||||
}
|
||||
depState = DepState::HasOnlyRequired;
|
||||
}
|
||||
|
||||
std::string content;
|
||||
char const* btn1;
|
||||
char const* btn2;
|
||||
switch (depState) {
|
||||
case DepState::None:
|
||||
content = fmt::format(
|
||||
"Are you sure you want to install <cg>{}</c>?",
|
||||
m_item->getMetadata().getName()
|
||||
);
|
||||
btn1 = "Info";
|
||||
btn2 = "Install";
|
||||
break;
|
||||
case DepState::HasOnlyRequired:
|
||||
content =
|
||||
"Installing this mod requires other mods to be installed. "
|
||||
"Would you like to <cy>proceed</c> with the installation or "
|
||||
"<cb>view</c> which mods are going to be installed?";
|
||||
btn1 = "View";
|
||||
btn2 = "Proceed";
|
||||
break;
|
||||
case DepState::HasOptional:
|
||||
content =
|
||||
"This mod recommends installing other mods alongside it. "
|
||||
"Would you like to continue with <cy>recommended settings</c> or "
|
||||
"<cb>customize</c> which mods to install?";
|
||||
btn1 = "Customize";
|
||||
btn2 = "Recommended";
|
||||
break;
|
||||
}
|
||||
|
||||
createQuickPopup("Confirm Install", content, btn1, btn2, 320.f, [&](FLAlertLayer*, bool btn2) {
|
||||
if (btn2) {
|
||||
auto canInstall = Index::get()->canInstall(m_item);
|
||||
if (!canInstall) {
|
||||
FLAlertLayer::create(
|
||||
"Unable to Install",
|
||||
canInstall.unwrapErr(),
|
||||
"OK"
|
||||
)->show();
|
||||
return;
|
||||
}
|
||||
this->preInstall();
|
||||
Index::get()->install(m_item);
|
||||
}
|
||||
else {
|
||||
InstallListPopup::create(m_item, [&](IndexInstallList const& list) {
|
||||
this->preInstall();
|
||||
Index::get()->install(list);
|
||||
})->show();
|
||||
}
|
||||
}, true, true);
|
||||
}
|
||||
|
||||
void IndexItemInfoPopup::preInstall() {
|
||||
if (m_latestVersionLabel) {
|
||||
m_latestVersionLabel->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);
|
||||
}
|
||||
|
||||
void IndexItemInfoPopup::onCancel(CCObject*) {
|
||||
Index::get()->cancelInstall(m_item);
|
||||
this->popupInstallItem(m_item);
|
||||
}
|
||||
|
||||
CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {
|
||||
|
|
|
@ -38,6 +38,7 @@ protected:
|
|||
MDTextArea* m_detailsArea;
|
||||
MDTextArea* m_changelogArea = nullptr;
|
||||
Scrollbar* m_scrollbar;
|
||||
IndexItemHandle m_item;
|
||||
|
||||
void onChangelog(CCObject*);
|
||||
void onRepository(CCObject*);
|
||||
|
@ -51,13 +52,16 @@ protected:
|
|||
|
||||
void setInstallStatus(std::optional<UpdateProgress> const& progress);
|
||||
|
||||
void popupInstallItem(IndexItemHandle item);
|
||||
void preInstall();
|
||||
void onCancelInstall(CCObject*);
|
||||
|
||||
virtual CCNode* createLogo(CCSize const& size) = 0;
|
||||
virtual ModMetadata getMetadata() const = 0;
|
||||
};
|
||||
|
||||
class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
|
||||
protected:
|
||||
IndexItemHandle m_item;
|
||||
EventListener<ModInstallFilter> m_installListener;
|
||||
Mod* m_mod;
|
||||
|
||||
|
@ -74,8 +78,6 @@ protected:
|
|||
|
||||
void onUpdateProgress(ModInstallEvent* event);
|
||||
void onUpdate(CCObject*);
|
||||
void onCancel(CCObject*);
|
||||
void doUpdate();
|
||||
|
||||
|
||||
void FLAlert_Clicked(FLAlertLayer*, bool) override;
|
||||
|
@ -91,16 +93,12 @@ public:
|
|||
|
||||
class IndexItemInfoPopup : public ModInfoPopup {
|
||||
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 preInstall();
|
||||
|
||||
CCNode* createLogo(CCSize const& size) override;
|
||||
ModMetadata getMetadata() const override;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
#include <Geode/ui/GeodeUI.hpp>
|
||||
#include <loader/LoaderImpl.hpp>
|
||||
#include <utility>
|
||||
#include "../info/TagNode.hpp"
|
||||
#include "../info/DevProfilePopup.hpp"
|
||||
|
||||
// InstallListCell
|
||||
|
@ -27,8 +26,11 @@ void InstallListCell::setupInfo(
|
|||
std::variant<VersionInfo, ComparableVersionInfo> version,
|
||||
bool inactive
|
||||
) {
|
||||
m_inactive = inactive;
|
||||
m_menu = CCMenu::create();
|
||||
m_menu->setPosition(m_width - 10.f, m_height / 2);
|
||||
m_menu->setPosition(0, 0);
|
||||
m_menu->setAnchorPoint({ .0f, .0f });
|
||||
m_menu->setContentSize({m_width, m_height});
|
||||
this->addChild(m_menu);
|
||||
|
||||
auto logoSize = this->getLogoSize();
|
||||
|
@ -41,15 +43,15 @@ void InstallListCell::setupInfo(
|
|||
}
|
||||
this->addChild(logoSpr);
|
||||
|
||||
auto titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
|
||||
titleLabel->setAnchorPoint({ .0f, .5f });
|
||||
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
|
||||
titleLabel->setPositionY(m_height / 2);
|
||||
titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f);
|
||||
m_titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
|
||||
m_titleLabel->setAnchorPoint({ .0f, .5f });
|
||||
m_titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
|
||||
m_titleLabel->setPositionY(m_height / 2);
|
||||
m_titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f);
|
||||
if (inactive) {
|
||||
titleLabel->setColor({ 163, 163, 163 });
|
||||
m_titleLabel->setColor({ 163, 163, 163 });
|
||||
}
|
||||
this->addChild(titleLabel);
|
||||
this->addChild(m_titleLabel);
|
||||
|
||||
m_developerBtn = nullptr;
|
||||
if (developer) {
|
||||
|
@ -64,43 +66,55 @@ void InstallListCell::setupInfo(
|
|||
creatorLabel, this, menu_selector(InstallListCell::onViewDev)
|
||||
);
|
||||
m_developerBtn->setPosition(
|
||||
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f +
|
||||
creatorLabel->getScaledContentSize().width / 2 -
|
||||
m_menu->getPositionX(),
|
||||
-0.5f
|
||||
m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f +
|
||||
creatorLabel->getScaledContentSize().width / 2,
|
||||
m_height / 2
|
||||
);
|
||||
m_menu->addChild(m_developerBtn);
|
||||
}
|
||||
|
||||
auto versionLabel = CCLabelBMFont::create(
|
||||
this->setupVersion(version);
|
||||
}
|
||||
|
||||
void InstallListCell::setupVersion(std::variant<VersionInfo, ComparableVersionInfo> version) {
|
||||
if (m_versionLabel) {
|
||||
m_versionLabel->removeFromParent();
|
||||
m_versionLabel = nullptr;
|
||||
}
|
||||
if (m_tagLabel) {
|
||||
m_tagLabel->removeFromParent();
|
||||
m_tagLabel = nullptr;
|
||||
}
|
||||
|
||||
m_versionLabel = CCLabelBMFont::create(
|
||||
std::holds_alternative<VersionInfo>(version) ?
|
||||
std::get<VersionInfo>(version).toString(false).c_str() :
|
||||
std::get<ComparableVersionInfo>(version).toString().c_str(),
|
||||
"bigFont.fnt"
|
||||
);
|
||||
versionLabel->setAnchorPoint({ .0f, .5f });
|
||||
versionLabel->setScale(.2f);
|
||||
versionLabel->setPosition(
|
||||
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f +
|
||||
m_versionLabel->setAnchorPoint({ .0f, .5f });
|
||||
m_versionLabel->setScale(.2f);
|
||||
m_versionLabel->setPosition(
|
||||
m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f +
|
||||
(m_developerBtn ? m_developerBtn->getScaledContentSize().width + 3.f : 0.f),
|
||||
titleLabel->getPositionY() - 1.f
|
||||
m_titleLabel->getPositionY() - 1.f
|
||||
);
|
||||
versionLabel->setColor({ 0, 255, 0 });
|
||||
if (inactive) {
|
||||
versionLabel->setColor({ 0, 163, 0 });
|
||||
m_versionLabel->setColor({ 0, 255, 0 });
|
||||
if (m_inactive) {
|
||||
m_versionLabel->setColor({ 0, 163, 0 });
|
||||
}
|
||||
this->addChild(versionLabel);
|
||||
this->addChild(m_versionLabel);
|
||||
|
||||
if (!std::holds_alternative<VersionInfo>(version)) return;
|
||||
if (auto tag = std::get<VersionInfo>(version).getTag()) {
|
||||
auto tagLabel = TagNode::create(tag->toString());
|
||||
tagLabel->setAnchorPoint({.0f, .5f});
|
||||
tagLabel->setScale(.2f);
|
||||
tagLabel->setPosition(
|
||||
versionLabel->getPositionX() + versionLabel->getScaledContentSize().width + 3.f,
|
||||
versionLabel->getPositionY()
|
||||
m_tagLabel = TagNode::create(tag->toString());
|
||||
m_tagLabel->setAnchorPoint({.0f, .5f});
|
||||
m_tagLabel->setScale(.2f);
|
||||
m_tagLabel->setPosition(
|
||||
m_versionLabel->getPositionX() + m_versionLabel->getScaledContentSize().width + 3.f,
|
||||
m_versionLabel->getPositionY()
|
||||
);
|
||||
this->addChild(tagLabel);
|
||||
this->addChild(m_tagLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,7 +148,7 @@ bool ModInstallListCell::init(Mod* mod, InstallListPopup* list, CCSize const& si
|
|||
this->setupInfo(mod->getMetadata(), true);
|
||||
auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
|
||||
message->setAnchorPoint({ 1.f, .5f });
|
||||
message->setPositionX(m_menu->getPositionX());
|
||||
message->setPositionX(m_width - 10.0f);
|
||||
message->setPositionY(16.f);
|
||||
message->setScale(0.4f);
|
||||
message->setColor({ 163, 163, 163 });
|
||||
|
@ -174,23 +188,38 @@ bool IndexItemInstallListCell::init(
|
|||
return false;
|
||||
m_item = item;
|
||||
this->setupInfo(item->getMetadata(), item->isInstalled());
|
||||
if (item->isInstalled()) {
|
||||
auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
|
||||
message->setAnchorPoint({ 1.f, .5f });
|
||||
message->setPositionX(m_menu->getPositionX());
|
||||
message->setPositionY(16.f);
|
||||
message->setScale(0.4f);
|
||||
message->setColor({ 163, 163, 163 });
|
||||
this->addChild(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: show installed label properly
|
||||
// if (item->isInstalled()) {
|
||||
// auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
|
||||
// message->setAnchorPoint({ 1.f, .5f });
|
||||
// message->setPositionX(m_width - 10.0f);
|
||||
// message->setPositionY(16.f);
|
||||
// message->setScale(0.4f);
|
||||
// message->setColor({ 163, 163, 163 });
|
||||
// this->addChild(message);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
m_toggle = CCMenuItemToggler::createWithStandardSprites(
|
||||
m_layer,
|
||||
menu_selector(InstallListPopup::onCellToggle),
|
||||
.6f
|
||||
);
|
||||
m_toggle->setPosition(-m_toggle->getScaledContentSize().width / 2, 0.f);
|
||||
m_toggle->setAnchorPoint({1.f, .5f});
|
||||
m_toggle->setPosition(m_width - 5, m_height / 2);
|
||||
|
||||
// recycling sprites in my Geode?? noo never
|
||||
auto versionSelectSpr = EditorButtonSprite::createWithSpriteFrameName(
|
||||
"filters.png"_spr, 1.0f, EditorBaseColor::Gray
|
||||
);
|
||||
versionSelectSpr->setScale(.7f);
|
||||
|
||||
auto versionSelectBtn =
|
||||
CCMenuItemSpriteExtra::create(versionSelectSpr, this, menu_selector(IndexItemInstallListCell::onSelectVersion));
|
||||
versionSelectBtn->setAnchorPoint({1.f, .5f});
|
||||
versionSelectBtn->setPosition({m_toggle->getPositionX() - m_toggle->getContentSize().width - 5, m_height / 2});
|
||||
m_menu->addChild(versionSelectBtn);
|
||||
|
||||
switch (importance) {
|
||||
case ModMetadata::Dependency::Importance::Required:
|
||||
|
@ -207,13 +236,18 @@ bool IndexItemInstallListCell::init(
|
|||
break;
|
||||
}
|
||||
|
||||
if (item->isInstalled()) {
|
||||
m_toggle->setClickable(false);
|
||||
m_toggle->toggle(true);
|
||||
}
|
||||
|
||||
if (m_item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) {
|
||||
m_toggle->setClickable(false);
|
||||
m_toggle->toggle(false);
|
||||
|
||||
auto message = CCLabelBMFont::create("N/A", "bigFont.fnt");
|
||||
message->setAnchorPoint({ 1.f, .5f });
|
||||
message->setPositionX(m_menu->getPositionX() - m_toggle->getScaledContentSize().width - 5.f);
|
||||
message->setPositionX(m_width - 5.f);
|
||||
message->setPositionY(16.f);
|
||||
message->setScale(0.4f);
|
||||
message->setColor({ 240, 31, 31 });
|
||||
|
@ -269,6 +303,15 @@ IndexItemHandle IndexItemInstallListCell::getItem() {
|
|||
return m_item;
|
||||
}
|
||||
|
||||
void IndexItemInstallListCell::setVersionFromItem(IndexItemHandle item) {
|
||||
this->setupVersion(item->getMetadata().getVersion());
|
||||
m_item = item;
|
||||
}
|
||||
|
||||
void IndexItemInstallListCell::onSelectVersion(CCObject*) {
|
||||
SelectVersionPopup::create(m_item->getMetadata().getID(), this)->show();
|
||||
}
|
||||
|
||||
// UnknownInstallListCell
|
||||
|
||||
bool UnknownInstallListCell::init(
|
||||
|
@ -317,3 +360,50 @@ std::string UnknownInstallListCell::getID() const {
|
|||
std::string UnknownInstallListCell::getDeveloper() const {
|
||||
return "";
|
||||
}
|
||||
|
||||
// SelectVersionCell
|
||||
|
||||
bool SelectVersionCell::init(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size) {
|
||||
if (!InstallListCell::init(nullptr, size))
|
||||
return false;
|
||||
m_item = item;
|
||||
m_versionPopup = versionPopup;
|
||||
this->setupInfo(item->getMetadata(), item->isInstalled());
|
||||
|
||||
auto selectSpr = ButtonSprite::create(
|
||||
"Select", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .6f
|
||||
);
|
||||
selectSpr->setScale(.6f);
|
||||
|
||||
auto selectBtn = CCMenuItemSpriteExtra::create(
|
||||
selectSpr, this, menu_selector(SelectVersionCell::onSelected)
|
||||
);
|
||||
selectBtn->setAnchorPoint({1.f, .5f});
|
||||
selectBtn->setPosition({m_width - 5, m_height / 2});
|
||||
m_menu->addChild(selectBtn);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SelectVersionCell::onSelected(CCObject*) {
|
||||
m_versionPopup->selectItem(m_item);
|
||||
}
|
||||
|
||||
SelectVersionCell* SelectVersionCell::create(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size) {
|
||||
auto ret = new SelectVersionCell();
|
||||
if (ret->init(item, versionPopup, size)) {
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CCNode* SelectVersionCell::createLogo(CCSize const& size) {
|
||||
return geode::createIndexItemLogo(m_item, size);
|
||||
}
|
||||
std::string SelectVersionCell::getID() const {
|
||||
return m_item->getMetadata().getID();
|
||||
}
|
||||
std::string SelectVersionCell::getDeveloper() const {
|
||||
return m_item->getMetadata().getDeveloper();
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
#include <Geode/loader/ModMetadata.hpp>
|
||||
#include <Geode/loader/Index.hpp>
|
||||
|
||||
#include "../info/TagNode.hpp"
|
||||
|
||||
using namespace geode::prelude;
|
||||
|
||||
class InstallListPopup;
|
||||
|
@ -17,10 +19,14 @@ class InstallListCell : public CCLayer {
|
|||
protected:
|
||||
float m_width;
|
||||
float m_height;
|
||||
InstallListPopup* m_layer;
|
||||
CCMenu* m_menu;
|
||||
CCMenuItemSpriteExtra* m_developerBtn;
|
||||
InstallListPopup* m_layer = nullptr;
|
||||
CCMenu* m_menu = nullptr;
|
||||
CCMenuItemSpriteExtra* m_developerBtn = nullptr;
|
||||
CCLabelBMFont* m_titleLabel = nullptr;
|
||||
CCLabelBMFont* m_versionLabel = nullptr;
|
||||
TagNode* m_tagLabel = nullptr;
|
||||
CCMenuItemToggler* m_toggle = nullptr;
|
||||
bool m_inactive = false;
|
||||
|
||||
void setupInfo(
|
||||
std::string name,
|
||||
|
@ -29,6 +35,8 @@ protected:
|
|||
bool inactive
|
||||
);
|
||||
|
||||
void setupVersion(std::variant<VersionInfo, ComparableVersionInfo> version);
|
||||
|
||||
bool init(InstallListPopup* list, CCSize const& size);
|
||||
void setupInfo(ModMetadata const& metadata, bool inactive);
|
||||
void draw() override;
|
||||
|
@ -90,6 +98,9 @@ public:
|
|||
[[nodiscard]] std::string getDeveloper() const override;
|
||||
|
||||
IndexItemHandle getItem();
|
||||
void setVersionFromItem(IndexItemHandle item);
|
||||
|
||||
void onSelectVersion(CCObject*);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -112,3 +123,23 @@ public:
|
|||
[[nodiscard]] std::string getID() const override;
|
||||
[[nodiscard]] std::string getDeveloper() const override;
|
||||
};
|
||||
|
||||
class SelectVersionPopup;
|
||||
/**
|
||||
* Select version list item
|
||||
*/
|
||||
class SelectVersionCell : public InstallListCell {
|
||||
protected:
|
||||
IndexItemHandle m_item;
|
||||
SelectVersionPopup* m_versionPopup;
|
||||
|
||||
bool init(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size);
|
||||
|
||||
void onSelected(CCObject*);
|
||||
public:
|
||||
static SelectVersionCell* create(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size);
|
||||
|
||||
CCNode* createLogo(CCSize const& size) override;
|
||||
[[nodiscard]] std::string getID() const override;
|
||||
[[nodiscard]] std::string getDeveloper() const override;
|
||||
};
|
|
@ -112,7 +112,8 @@ CCArray* InstallListPopup::createCells(std::unordered_map<std::string, InstallLi
|
|||
queued.insert(item.id);
|
||||
|
||||
// installed
|
||||
if (item.mod && !item.mod->isUninstalled()) {
|
||||
// TODO: we should be able to select a different version even if its installed
|
||||
if (/*item.mod && !item.mod->isUninstalled()*/item.mod->getMetadata().getID() == "geode.loader") {
|
||||
bottom.push_back(ModInstallListCell::create(item.mod, this, this->getCellSize()));
|
||||
for (auto const& dep : item.mod->getMetadata().getDependencies()) {
|
||||
queue.push(dep);
|
||||
|
@ -204,7 +205,7 @@ void InstallListPopup::onInstall(cocos2d::CCObject* obj) {
|
|||
CCArray* entries = m_list->m_entries;
|
||||
for (size_t i = entries->count(); i > 0; i--) {
|
||||
auto* itemCell = typeinfo_cast<IndexItemInstallListCell*>(entries->objectAtIndex(i - 1));
|
||||
if (!itemCell || !itemCell->isIncluded())
|
||||
if (!itemCell || !itemCell->isIncluded() || itemCell->getItem()->isInstalled())
|
||||
continue;
|
||||
IndexItemHandle item = itemCell->getItem();
|
||||
list.list.push_back(item);
|
||||
|
@ -227,3 +228,70 @@ InstallListPopup* InstallListPopup::create(
|
|||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// SelectVersionPopup
|
||||
|
||||
bool SelectVersionPopup::setup(std::string const& modID, IndexItemInstallListCell* installCell) {
|
||||
m_modID = modID;
|
||||
m_installCell = installCell;
|
||||
|
||||
this->setTitle("Select Version");
|
||||
|
||||
this->createList();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SelectVersionPopup::createList() {
|
||||
auto winSize = CCDirector::sharedDirector()->getWinSize();
|
||||
|
||||
if (m_listParent) {
|
||||
m_listParent->removeFromParent();
|
||||
}
|
||||
|
||||
m_listParent = CCNode::create();
|
||||
m_mainLayer->addChild(m_listParent);
|
||||
|
||||
auto items = this->createCells();
|
||||
m_list = ListView::create(
|
||||
items,
|
||||
this->getCellSize().height,
|
||||
this->getListSize().width,
|
||||
this->getListSize().height
|
||||
);
|
||||
m_list->setPosition(winSize / 2 - m_list->getScaledContentSize() / 2);
|
||||
m_listParent->addChild(m_list);
|
||||
|
||||
addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize());
|
||||
}
|
||||
|
||||
CCArray* SelectVersionPopup::createCells() {
|
||||
auto cells = CCArray::create();
|
||||
for (auto& item : ranges::reverse(Index::get()->getItemsByModID(m_modID))) {
|
||||
cells->addObject(SelectVersionCell::create(item, this, this->getCellSize()));
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
CCSize SelectVersionPopup::getCellSize() const {
|
||||
return { getListSize().width, 30.f };
|
||||
}
|
||||
CCSize SelectVersionPopup::getListSize() const {
|
||||
return { 340.f, 170.f };
|
||||
}
|
||||
|
||||
void SelectVersionPopup::selectItem(IndexItemHandle item) {
|
||||
this->onBtn2(nullptr);
|
||||
|
||||
m_installCell->setVersionFromItem(item);
|
||||
}
|
||||
|
||||
SelectVersionPopup* SelectVersionPopup::create(std::string const& modID, IndexItemInstallListCell* installCell) {
|
||||
auto ret = new SelectVersionPopup();
|
||||
if (!ret->init(380.f, 250.f, modID, installCell)) {
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
|
@ -28,3 +28,24 @@ public:
|
|||
|
||||
static InstallListPopup* create(IndexItemHandle item, MiniFunction<void(IndexInstallList const&)> onInstall);
|
||||
};
|
||||
|
||||
class SelectVersionPopup : public Popup<std::string const&, IndexItemInstallListCell*> {
|
||||
protected:
|
||||
std::string m_modID;
|
||||
CCNode* m_listParent;
|
||||
ListView* m_list;
|
||||
IndexItemInstallListCell* m_installCell;
|
||||
|
||||
bool setup(std::string const& modID, IndexItemInstallListCell* installCell) override;
|
||||
|
||||
void createList();
|
||||
CCArray* createCells();
|
||||
CCSize getCellSize() const;
|
||||
CCSize getListSize() const;
|
||||
|
||||
|
||||
public:
|
||||
void selectItem(IndexItemHandle item);
|
||||
|
||||
static SelectVersionPopup* create(std::string const& modID, IndexItemInstallListCell* installCell);
|
||||
};
|
||||
|
|
|
@ -227,11 +227,7 @@ void ModCell::onEnable(CCObject* sender) {
|
|||
else {
|
||||
tryOrAlert(m_mod->disable(), "Error disabling mod");
|
||||
}
|
||||
Loader::get()->queueInMainThread([this]() {
|
||||
if (m_layer) {
|
||||
m_layer->updateAllStates();
|
||||
}
|
||||
});
|
||||
m_layer->reloadList();
|
||||
}
|
||||
|
||||
void ModCell::onUnresolvedInfo(CCObject*) {
|
||||
|
@ -278,9 +274,9 @@ bool ModCell::init(
|
|||
return false;
|
||||
m_mod = mod;
|
||||
|
||||
this->setupInfo(mod->getMetadata(), false, display, m_mod->isUninstalled());
|
||||
this->setupInfo(mod->getMetadata(), false, display, mod->getRequestedAction() != ModRequestedAction::None);
|
||||
|
||||
if (mod->isUninstalled()) {
|
||||
if (mod->getRequestedAction() != ModRequestedAction::None) {
|
||||
auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f);
|
||||
restartSpr->setScale(.65f);
|
||||
|
||||
|
@ -317,14 +313,16 @@ bool ModCell::init(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_mod->wasSuccessfullyLoaded() && m_mod->getMetadata().getID() != "geode.loader") {
|
||||
m_enableToggle =
|
||||
CCMenuItemToggler::createWithStandardSprites(this, menu_selector(ModCell::onEnable), .7f);
|
||||
m_enableToggle->setPosition(-45.f, 0.f);
|
||||
m_menu->addChild(m_enableToggle);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_mod->wasSuccessfullyLoaded() && m_mod->supportsDisabling() && !m_mod->isUninstalled()) {
|
||||
m_enableToggle =
|
||||
CCMenuItemToggler::createWithStandardSprites(this, menu_selector(ModCell::onEnable), .7f);
|
||||
m_enableToggle->setPosition(-45.f, 0.f);
|
||||
m_menu->addChild(m_enableToggle);
|
||||
}
|
||||
|
||||
|
||||
auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png");
|
||||
exMark->setScale(.5f);
|
||||
|
|
|
@ -198,7 +198,7 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer
|
|||
std::multimap<int, IndexItemHandle> sorted;
|
||||
|
||||
auto index = Index::get();
|
||||
for (auto const& item : index->getItems()) {
|
||||
for (auto const& item : index->getLatestItems()) {
|
||||
if (auto match = queryMatch(query, item)) {
|
||||
sorted.insert({ match.value(), item });
|
||||
}
|
||||
|
@ -444,13 +444,18 @@ void ModListLayer::createSearchControl() {
|
|||
this->addChild(m_searchInput);
|
||||
}
|
||||
|
||||
void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
|
||||
void ModListLayer::reloadList(bool keepScroll, std::optional<ModListQuery> const& query) {
|
||||
auto winSize = CCDirector::sharedDirector()->getWinSize();
|
||||
|
||||
if (query) {
|
||||
m_query = query.value();
|
||||
}
|
||||
|
||||
float scroll = 0.0f;
|
||||
if (keepScroll && m_list) {
|
||||
scroll = m_list->m_listView->m_tableView->m_contentLayer->getPositionY();
|
||||
}
|
||||
|
||||
// set search query
|
||||
m_query.keywords =
|
||||
m_searchInput &&
|
||||
|
@ -489,6 +494,12 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
|
|||
m_listLabel->setVisible(false);
|
||||
}
|
||||
|
||||
if (keepScroll) {
|
||||
list->m_tableView->m_contentLayer->setPosition(
|
||||
{ 0.0f, scroll }
|
||||
);
|
||||
}
|
||||
|
||||
// update index if needed
|
||||
if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) {
|
||||
m_listLabel->setVisible(true);
|
||||
|
@ -658,7 +669,7 @@ void ModListLayer::onTab(CCObject* pSender) {
|
|||
if (pSender) {
|
||||
g_tab = static_cast<ModListType>(pSender->getTag());
|
||||
}
|
||||
this->reloadList();
|
||||
this->reloadList(false);
|
||||
|
||||
auto toggleTab = [this](CCMenuItemToggler* member) -> void {
|
||||
auto isSelected = member->getTag() == static_cast<int>(g_tab);
|
||||
|
@ -670,7 +681,7 @@ void ModListLayer::onTab(CCObject* pSender) {
|
|||
targetMenu->addChild(member);
|
||||
member->release();
|
||||
}
|
||||
if (isSelected)
|
||||
if (isSelected && m_tabsGradientStencil)
|
||||
m_tabsGradientStencil->setPosition(member->m_onButton->convertToWorldSpace({0.f, -1.f}));
|
||||
};
|
||||
|
||||
|
@ -686,7 +697,7 @@ void ModListLayer::keyDown(enumKeyCodes key) {
|
|||
}
|
||||
|
||||
void ModListLayer::textChanged(CCTextInputNode* input) {
|
||||
this->reloadList();
|
||||
this->reloadList(false);
|
||||
}
|
||||
|
||||
// Constructors etc.
|
||||
|
|
|
@ -94,5 +94,5 @@ public:
|
|||
ModListDisplay getDisplay() const;
|
||||
ModListQuery& getQuery();
|
||||
|
||||
void reloadList(std::optional<ModListQuery> const& query = std::nullopt);
|
||||
void reloadList(bool keepScroll = true, std::optional<ModListQuery> const& query = std::nullopt);
|
||||
};
|
||||
|
|
|
@ -175,7 +175,7 @@ void SearchFilterPopup::onPlatformToggle(CCObject* sender) {
|
|||
|
||||
void SearchFilterPopup::onClose(CCObject* sender) {
|
||||
Popup::onClose(sender);
|
||||
m_modLayer->reloadList();
|
||||
m_modLayer->reloadList(false);
|
||||
}
|
||||
|
||||
SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) {
|
||||
|
|
|
@ -48,7 +48,6 @@ Result<> web::fetchFile(
|
|||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
|
||||
if (prog) {
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, utils::fetch::progress);
|
||||
|
@ -81,7 +80,6 @@ Result<ByteVector> web::fetchBytes(std::string const& url) {
|
|||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBytes);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
|
||||
auto res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
curl_easy_cleanup(curl);
|
||||
|
@ -120,7 +118,6 @@ Result<std::string> web::fetch(std::string const& url) {
|
|||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeString);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
|
||||
auto res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
curl_easy_cleanup(curl);
|
||||
|
@ -162,9 +159,10 @@ private:
|
|||
SentAsyncWebRequest* m_self;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target =
|
||||
std::monostate();
|
||||
AsyncWebRequestData m_extra;
|
||||
std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target;
|
||||
std::vector<std::string> m_httpHeaders;
|
||||
|
||||
|
||||
template <class T>
|
||||
friend class AsyncWebResult;
|
||||
|
@ -187,7 +185,7 @@ static std::unordered_map<std::string, SentAsyncWebRequestHandle> RUNNING_REQUES
|
|||
static std::mutex RUNNING_REQUESTS_MUTEX;
|
||||
|
||||
SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const& req, std::string const& id) :
|
||||
m_id(id), m_url(req.m_url), m_target(req.m_target), m_httpHeaders(req.m_httpHeaders) {
|
||||
m_id(id), m_url(req.m_url), m_target(req.m_target), m_extra(req.extra()), m_httpHeaders(req.m_httpHeaders) {
|
||||
|
||||
#define AWAIT_RESUME() \
|
||||
{\
|
||||
|
@ -242,8 +240,30 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
|
|||
// No need to verify SSL, we trust our domains :-)
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
// Github User Agent
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
|
||||
// User Agent
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, m_extra.m_userAgent.c_str());
|
||||
|
||||
// Headers
|
||||
curl_slist* headers = nullptr;
|
||||
for (auto& header : m_httpHeaders) {
|
||||
headers = curl_slist_append(headers, header.c_str());
|
||||
}
|
||||
|
||||
// Post request
|
||||
if (m_extra.m_isPostRequest || m_extra.m_customRequest.size()) {
|
||||
if (m_extra.m_isPostRequest) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
}
|
||||
else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, m_extra.m_customRequest.c_str());
|
||||
}
|
||||
if (m_extra.m_isJsonRequest) {
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, m_extra.m_postFields.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, m_extra.m_postFields.size());
|
||||
}
|
||||
|
||||
// Track progress
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
|
||||
// Follow redirects
|
||||
|
@ -251,10 +271,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
|
|||
// Fail if response code is 4XX or 5XX
|
||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
|
||||
|
||||
curl_slist* headers = nullptr;
|
||||
for (auto& header : m_httpHeaders) {
|
||||
headers = curl_slist_append(headers, header.c_str());
|
||||
}
|
||||
// Headers end
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
struct ProgressData {
|
||||
|
@ -410,11 +427,50 @@ void SentAsyncWebRequest::error(std::string const& error, int code) {
|
|||
return m_impl->error(error, code);
|
||||
}
|
||||
|
||||
AsyncWebRequestData& AsyncWebRequest::extra() {
|
||||
if (!m_extra) {
|
||||
m_extra = new AsyncWebRequestData();
|
||||
}
|
||||
return *m_extra;
|
||||
}
|
||||
|
||||
AsyncWebRequestData const& AsyncWebRequest::extra() const {
|
||||
if (!m_extra) {
|
||||
m_extra = new AsyncWebRequestData();
|
||||
}
|
||||
return *m_extra;
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) {
|
||||
m_joinID = requestID;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::userAgent(std::string const& userAgent) {
|
||||
this->extra().m_userAgent = userAgent;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::postRequest() {
|
||||
this->extra().m_isPostRequest = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::customRequest(std::string const& request) {
|
||||
this->extra().m_customRequest = request;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::postFields(std::string const& fields) {
|
||||
this->extra().m_postFields = fields;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::postFields(json::Value const& fields) {
|
||||
this->extra().m_isJsonRequest = true;
|
||||
return this->postFields(fields.dump());
|
||||
}
|
||||
|
||||
AsyncWebRequest& AsyncWebRequest::header(std::string const& header) {
|
||||
m_httpHeaders.push_back(header);
|
||||
return *this;
|
||||
|
@ -448,8 +504,8 @@ AsyncWebRequest& AsyncWebRequest::cancelled(AsyncCancelled cancelledFunc) {
|
|||
}
|
||||
|
||||
SentAsyncWebRequestHandle AsyncWebRequest::send() {
|
||||
if (m_sent) return nullptr;
|
||||
m_sent = true;
|
||||
if (this->extra().m_sent) return nullptr;
|
||||
this->extra().m_sent = true;
|
||||
|
||||
std::lock_guard __(RUNNING_REQUESTS_MUTEX);
|
||||
|
||||
|
@ -486,6 +542,7 @@ SentAsyncWebRequestHandle AsyncWebRequest::send() {
|
|||
|
||||
AsyncWebRequest::~AsyncWebRequest() {
|
||||
this->send();
|
||||
delete m_extra;
|
||||
}
|
||||
|
||||
AsyncWebResult<std::monostate> AsyncWebResponse::into(std::ostream& stream) {
|
||||
|
|
Loading…
Reference in a new issue