Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
camila314 2023-09-08 12:41:43 -05:00
commit 1b77582242
29 changed files with 786 additions and 430 deletions

View file

@ -1,5 +1,17 @@
# Geode Changelog # 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 ## v1.2.1
* Mods now target macOS 10.13 instead of 10.14 (7cc1cd4) * Mods now target macOS 10.13 instead of 10.14 (7cc1cd4)
* Fix CustomizeObjectLayer ids moving around when multiple objects are selected (9ee0994, 87749d4) * Fix CustomizeObjectLayer ids moving around when multiple objects are selected (9ee0994, 87749d4)

View file

@ -96,7 +96,7 @@ if (PROJECT_IS_TOP_LEVEL AND NOT GEODE_BUILDING_DOCS)
set(TULIP_LINK_SOURCE ON) set(TULIP_LINK_SOURCE ON)
endif() endif()
set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) 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) set(CMAKE_WARN_DEPRECATED ON CACHE BOOL "" FORCE)
# Silence warnings from dependencies # Silence warnings from dependencies

View file

@ -1 +1 @@
1.2.1 1.3.0

View file

@ -12,7 +12,7 @@ namespace geode {
class Mod; class Mod;
class Loader; class Loader;
class GEODE_DLL Hook { class GEODE_DLL Hook final {
private: private:
class Impl; class Impl;
std::shared_ptr<Impl> m_impl; std::shared_ptr<Impl> m_impl;
@ -143,20 +143,23 @@ namespace geode {
void setAutoEnable(bool autoEnable); void setAutoEnable(bool autoEnable);
}; };
class GEODE_DLL Patch { class GEODE_DLL Patch final {
// Change to private in 2.0.0
protected: protected:
Mod* m_owner; Mod* m_owner;
void* m_address; void* m_address;
ByteVector m_original; ByteVector m_original;
ByteVector m_patch; ByteVector m_patch;
bool m_applied; bool m_applied;
bool m_autoEnable;
// Only allow friend classes to create // Only allow friend classes to create
// patches. Whatever method created the // patches. Whatever method created the
// patches should take care of populating // patches should take care of populating
// m_owner, m_address, m_original and // m_owner, m_address, m_original and
// m_patch. // m_patch.
Patch() : m_applied(false) {} Patch();
~Patch();
// no copying // no copying
Patch(Patch const&) = delete; Patch(Patch const&) = delete;
@ -170,17 +173,13 @@ namespace geode {
* Get the address of the patch. * Get the address of the patch.
* @returns Address * @returns Address
*/ */
uintptr_t getAddress() const { uintptr_t getAddress() const;
return reinterpret_cast<uintptr_t>(m_address);
}
/** /**
* Get whether the patch is applied or not. * Get whether the patch is applied or not.
* @returns True if applied, false if not. * @returns True if applied, false if not.
*/ */
bool isApplied() const { bool isApplied() const;
return m_applied;
}
bool apply(); bool apply();
bool restore(); bool restore();
@ -189,14 +188,24 @@ namespace geode {
* Get the owner of this patch. * Get the owner of this patch.
* @returns Pointer to the owner's Mod handle. * @returns Pointer to the owner's Mod handle.
*/ */
Mod* getOwner() const { Mod* getOwner() const;
return m_owner;
}
/** /**
* Get info about the patch as JSON * Get info about the patch as JSON
* @note For IPC * @note For IPC
*/ */
json::Value getRuntimeInfo() const; 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);
}; };
} }

View file

@ -107,6 +107,14 @@ namespace geode {
std::unique_ptr<Impl> m_impl; std::unique_ptr<Impl> m_impl;
public: 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; ghc::filesystem::path getPath() const;
[[deprecated("use getMetadata instead")]] ModInfo getModInfo() const; [[deprecated("use getMetadata instead")]] ModInfo getModInfo() const;
ModMetadata getMetadata() const; ModMetadata getMetadata() const;
@ -124,10 +132,14 @@ namespace geode {
void setAvailablePlatforms(std::unordered_set<PlatformID> const& value); void setAvailablePlatforms(std::unordered_set<PlatformID> const& value);
void setIsFeatured(bool const& value); void setIsFeatured(bool const& value);
void setTags(std::unordered_set<std::string> const& value); void setTags(std::unordered_set<std::string> const& value);
void setIsInstalled(bool const& value);
#endif #endif
IndexItem(); IndexItem();
~IndexItem(); ~IndexItem();
friend class ModMetadata;
friend class Index;
}; };
using IndexItemHandle = std::shared_ptr<IndexItem>; using IndexItemHandle = std::shared_ptr<IndexItem>;
@ -168,12 +180,22 @@ namespace geode {
* Get all featured index items * Get all featured index items
*/ */
std::vector<IndexItemHandle> getFeaturedItems() const; std::vector<IndexItemHandle> getFeaturedItems() const;
/**
* Get all latest index items
*/
std::vector<IndexItemHandle> getLatestItems() const;
/** /**
* Get all index items by a developer * Get all index items by a developer
*/ */
std::vector<IndexItemHandle> getItemsByDeveloper( std::vector<IndexItemHandle> getItemsByDeveloper(
std::string const& name std::string const& name
) const; ) 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 * Check if an item with this ID is found on the index, and optionally
* provide the version sought after * provide the version sought after

View file

@ -33,6 +33,13 @@ namespace geode {
~HandleToSaved(); ~HandleToSaved();
}; };
enum class ModRequestedAction {
None,
Enable,
Disable,
Uninstall,
};
GEODE_HIDDEN Mod* takeNextLoaderMod(); GEODE_HIDDEN Mod* takeNextLoaderMod();
class ModImpl; class ModImpl;
@ -337,6 +344,8 @@ namespace geode {
Result<> uninstall(); Result<> uninstall();
bool isUninstalled() const; bool isUninstalled() const;
ModRequestedAction getRequestedAction() const;
/** /**
* Check whether or not this Mod * Check whether or not this Mod
* depends on another mod * depends on another mod

View file

@ -240,6 +240,7 @@ namespace geode {
friend class Loader; friend class Loader;
friend class ModMetadataImpl; friend class ModMetadataImpl;
friend class IndexItem;
}; };
} }

View file

@ -104,6 +104,16 @@ namespace geode::utils::web {
template <class T> template <class T>
using DataConverter = Result<T> (*)(ByteVector const&); 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 * An asynchronous, thread-safe web request. Downloads data from the
* internet without slowing the main thread. All callbacks are run in 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 { class GEODE_DLL AsyncWebRequest {
private: 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::optional<std::string> m_joinID;
std::string m_url; std::string m_url;
AsyncThen m_then = nullptr; AsyncThen m_then = nullptr;
AsyncExpectCode m_expect = nullptr; AsyncExpectCode m_expect = nullptr;
AsyncProgress m_progress = nullptr; AsyncProgress m_progress = nullptr;
AsyncCancelled m_cancelled = 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::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target;
std::vector<std::string> m_httpHeaders; std::vector<std::string> m_httpHeaders;
AsyncWebRequestData& extra();
AsyncWebRequestData const& extra() const;
template <class T> template <class T>
friend class AsyncWebResult; friend class AsyncWebResult;
friend class SentAsyncWebRequest; friend class SentAsyncWebRequest;
@ -151,6 +167,29 @@ namespace geode::utils::web {
* Can be called more than once. * Can be called more than once.
*/ */
AsyncWebRequest& header(std::string const& header); 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 * URL to fetch from the internet asynchronously
* @param url URL of the data to download. Redirects will be * @param url URL of the data to download. Redirects will be

View file

@ -12,6 +12,7 @@ using namespace geode::prelude;
Hook::Hook(std::shared_ptr<Impl>&& impl) : m_impl(std::move(impl)) {} Hook::Hook(std::shared_ptr<Impl>&& impl) : m_impl(std::move(impl)) {}
Hook::~Hook() {} Hook::~Hook() {}
// These classes (Hook and Patch) are nasty using new and delete, change them in 2.0.0
Hook* Hook::create( Hook* Hook::create(
Mod* owner, Mod* owner,
void* address, void* address,

View file

@ -47,12 +47,14 @@ IndexUpdateFilter::IndexUpdateFilter() {}
class IndexItem::Impl final { class IndexItem::Impl final {
private: private:
ghc::filesystem::path m_rootPath;
ghc::filesystem::path m_path; ghc::filesystem::path m_path;
ModMetadata m_metadata; ModMetadata m_metadata;
std::string m_downloadURL; std::string m_downloadURL;
std::string m_downloadHash; std::string m_downloadHash;
std::unordered_set<PlatformID> m_platforms; std::unordered_set<PlatformID> m_platforms;
bool m_isFeatured; bool m_isFeatured = false;
bool m_isInstalled = false;
std::unordered_set<std::string> m_tags; std::unordered_set<std::string> m_tags;
friend class IndexItem; friend class IndexItem;
@ -62,6 +64,7 @@ public:
* Create IndexItem from a directory * Create IndexItem from a directory
*/ */
static Result<std::shared_ptr<IndexItem>> create( static Result<std::shared_ptr<IndexItem>> create(
ghc::filesystem::path const& rootDir,
ghc::filesystem::path const& dir ghc::filesystem::path const& dir
); );
@ -71,6 +74,10 @@ public:
IndexItem::IndexItem() : m_impl(std::make_unique<Impl>()) {} IndexItem::IndexItem() : m_impl(std::make_unique<Impl>()) {}
IndexItem::~IndexItem() = default; IndexItem::~IndexItem() = default;
ghc::filesystem::path IndexItem::getRootPath() const {
return m_impl->m_rootPath;
}
ghc::filesystem::path IndexItem::getPath() const { ghc::filesystem::path IndexItem::getPath() const {
return m_impl->m_path; 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) { void IndexItem::setTags(std::unordered_set<std::string> const& value) {
m_impl->m_tags = value; m_impl->m_tags = value;
} }
void IndexItem::setIsInstalled(bool const& value) {
m_impl->m_isInstalled = value;
}
#endif #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( GEODE_UNWRAP_INTO(
auto entry, file::readJson(dir / "entry.json") auto entry, file::readJson(dir / "entry.json")
.expect("Unable to read 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") auto metadata, ModMetadata::createFromFile(dir / "mod.json")
.expect("Unable to read mod.json: {error}") .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); JsonChecker checker(entry);
auto root = checker.root("[entry.json]").obj(); 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>(); auto item = std::make_shared<IndexItem>();
item->m_impl->m_rootPath = rootDir;
item->m_impl->m_path = dir; item->m_impl->m_path = dir;
item->m_impl->m_metadata = metadata; item->m_impl->m_metadata = metadata;
item->m_impl->m_platforms = platforms; 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 { 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 // Helpers
@ -209,7 +235,7 @@ class Index::Impl final {
public: public:
// for once, the fact that std::map is ordered is useful (this makes // 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()) // 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: private:
std::unordered_map< std::unordered_map<
@ -335,6 +361,7 @@ void Index::Impl::checkForUpdates() {
auto oldSHA = file::readString(checksum).unwrapOr(""); auto oldSHA = file::readString(checksum).unwrapOr("");
web::AsyncWebRequest() web::AsyncWebRequest()
.join("index-update") .join("index-update")
.userAgent("github_api/1.0")
.header(fmt::format("If-None-Match: \"{}\"", oldSHA)) .header(fmt::format("If-None-Match: \"{}\"", oldSHA))
.header("Accept: application/vnd.github.sha") .header("Accept: application/vnd.github.sha")
.fetch("https://api.github.com/repos/geode-sdk/mods/commits/main") .fetch("https://api.github.com/repos/geode-sdk/mods/commits/main")
@ -369,34 +396,36 @@ void Index::Impl::updateFromLocalTree() {
// delete old items // delete old items
m_items.clear(); m_items.clear();
// read directory and add new items auto indexRoot = dirs::getIndexDir() / "v0";
try { auto entriesRoot = indexRoot / "mods-v2";
for (auto& dir : ghc::filesystem::directory_iterator(dirs::getIndexDir() / "v0" / "mods")) {
auto addRes = IndexItem::Impl::create(dir); 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) { if (!addRes) {
log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr()); log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr());
continue; continue;
} }
auto add = addRes.unwrap(); auto add = addRes.unwrap();
auto metadata = add->getMetadata(); 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())) { m_items[modID].insert({metadata.getVersion(),
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(),
add 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 // mark source as finished
@ -436,6 +465,14 @@ std::vector<IndexItemHandle> Index::getItems() const {
return res; 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> Index::getFeaturedItems() const {
std::vector<IndexItemHandle> res; std::vector<IndexItemHandle> res;
for (auto& items : map::values(m_impl->m_items)) { for (auto& items : map::values(m_impl->m_items)) {
@ -462,6 +499,18 @@ std::vector<IndexItemHandle> Index::getItemsByDeveloper(
return res; 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( bool Index::isKnownItem(
std::string const& id, std::string const& id,
std::optional<VersionInfo> version std::optional<VersionInfo> version
@ -485,19 +534,14 @@ IndexItemHandle Index::getItem(
if (m_impl->m_items.count(id)) { if (m_impl->m_items.count(id)) {
auto versions = m_impl->m_items.at(id); auto versions = m_impl->m_items.at(id);
if (version) { if (version) {
// prefer most major version
for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) { for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) {
if (version.value() == item->getMetadata().getVersion()) { if (version.value() == item->getMetadata().getVersion()) {
return item; return item;
} }
} }
} else {
if (!versions.empty()) {
return m_impl->m_items.at(id).rbegin()->second;
}
} }
} }
return nullptr; return this->getMajorItem(id);
} }
IndexItemHandle Index::getItem( 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 // Install next item in queue
this->installNext(index + 1, list); this->installNext(index + 1, list);
}) })
@ -752,6 +798,10 @@ void Index::cancelInstall(IndexItemHandle item) {
} }
void Index::install(IndexInstallList const& list) { void Index::install(IndexInstallList const& list) {
if (list.list.empty()) {
ModInstallEvent(list.target->getMetadata().getID(), UpdateFinished()).post();
return;
}
Loader::get()->queueInMainThread([this, list]() { Loader::get()->queueInMainThread([this, list]() {
m_impl->installNext(0, list); m_impl->installNext(0, list);
}); });

View file

@ -166,7 +166,6 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) {
Result<> Loader::Impl::saveData() { Result<> Loader::Impl::saveData() {
// save mods' data // save mods' data
for (auto& [id, mod] : m_mods) { for (auto& [id, mod] : m_mods) {
Mod::get()->setSavedValue("should-load-" + id, mod->isUninstalled() || mod->isEnabled());
auto r = mod->saveData(); auto r = mod->saveData();
if (!r) { if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr()); 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; 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)) { if (Mod::get()->getSavedValue<bool>("should-load-" + node->getID(), true)) {
log::debug("Enable"); log::debug("Load");
res = node->m_impl->enable(); auto res = node->m_impl->loadBinary();
if (!res) { if (!res) {
node->m_impl->m_enabled = true;
(void)node->m_impl->disable();
m_problems.push_back({ m_problems.push_back({
LoadProblem::Type::EnableFailed, LoadProblem::Type::LoadFailed,
node, node,
res.unwrapErr() res.unwrapErr()
}); });
log::error("Failed to enable: {}", res.unwrapErr()); log::error("Failed to load binary: {}", res.unwrapErr());
log::popNest(); log::popNest();
return; return;
} }
@ -509,19 +493,19 @@ void Loader::Impl::findProblems() {
Mod* myEpicMod = mod; // clang fix Mod* myEpicMod = mod; // clang fix
// if the mod is not loaded but there are no problems related to it // 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) { // if (!mod->isLoaded() && !std::any_of(m_problems.begin(), m_problems.end(), [myEpicMod](auto& item) {
return std::holds_alternative<ModMetadata>(item.cause) && // return std::holds_alternative<ModMetadata>(item.cause) &&
std::get<ModMetadata>(item.cause).getID() == myEpicMod->getID() || // std::get<ModMetadata>(item.cause).getID() == myEpicMod->getID() ||
std::holds_alternative<Mod*>(item.cause) && // std::holds_alternative<Mod*>(item.cause) &&
std::get<Mod*>(item.cause) == myEpicMod; // std::get<Mod*>(item.cause) == myEpicMod;
})) { // })) {
m_problems.push_back({ // m_problems.push_back({
LoadProblem::Type::Unknown, // LoadProblem::Type::Unknown,
mod, // mod,
"" // ""
}); // });
log::error("{} failed to load for an unknown reason", id); // log::error("{} failed to load for an unknown reason", id);
} // }
log::popNest(); log::popNest();
} }
@ -533,7 +517,7 @@ void Loader::Impl::refreshModGraph() {
auto begin = std::chrono::high_resolution_clock::now(); 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::error("Cannot refresh mod graph after startup");
log::popNest(); log::popNest();
return; return;
@ -564,7 +548,7 @@ void Loader::Impl::refreshModGraph() {
m_loadingState = LoadingState::EarlyMods; m_loadingState = LoadingState::EarlyMods;
log::debug("Loading early mods"); log::debug("Loading early mods");
log::pushNest(); log::pushNest();
for (auto const& dep : Mod::get()->m_impl->m_dependants) { for (auto const& dep : ModImpl::get()->m_dependants) {
this->loadModGraph(dep, true); this->loadModGraph(dep, true);
} }
log::popNest(); log::popNest();
@ -716,6 +700,7 @@ void Loader::Impl::fetchLatestGithubRelease(
// TODO: add header to not get rate limited // TODO: add header to not get rate limited
web::AsyncWebRequest() web::AsyncWebRequest()
.join("loader-auto-update-check") .join("loader-auto-update-check")
.userAgent("github_api/1.0")
.fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest") .fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest")
.json() .json()
.then([this, then](json::Value const& json) { .then([this, then](json::Value const& json) {
@ -784,6 +769,7 @@ void Loader::Impl::downloadLoaderResources(bool useLatestRelease) {
if (!useLatestRelease) { if (!useLatestRelease) {
web::AsyncWebRequest() web::AsyncWebRequest()
.join("loader-tag-exists-check") .join("loader-tag-exists-check")
.userAgent("github_api/1.0")
.fetch(fmt::format( .fetch(fmt::format(
"https://api.github.com/repos/geode-sdk/geode/git/ref/tags/{}", "https://api.github.com/repos/geode-sdk/geode/git/ref/tags/{}",
this->getVersion().toString() this->getVersion().toString()

View file

@ -198,6 +198,10 @@ bool Mod::isUninstalled() const {
return m_impl->isUninstalled(); return m_impl->isUninstalled();
} }
ModRequestedAction Mod::getRequestedAction() const {
return m_impl->getRequestedAction();
}
bool Mod::depends(std::string const& id) const { bool Mod::depends(std::string const& id) const {
return m_impl->depends(id); return m_impl->depends(id);
} }

View file

@ -117,23 +117,15 @@ bool Mod::Impl::isLoaded() const {
} }
bool Mod::Impl::supportsDisabling() 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 { bool Mod::Impl::canDisable() const {
auto deps = m_dependants; return true;
return this->supportsDisabling() &&
(deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) {
return item->canDisable();
}));
} }
bool Mod::Impl::canEnable() const { bool Mod::Impl::canEnable() const {
auto deps = m_metadata.getDependencies(); return true;
return !this->isUninstalled() &&
(deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) {
return item.isResolved();
}));
} }
bool Mod::Impl::needsEarlyLoad() const { bool Mod::Impl::needsEarlyLoad() const {
@ -345,119 +337,74 @@ Result<> Mod::Impl::loadBinary() {
LoaderImpl::get()->releaseNextMod(); 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) { for (auto const& hook : m_hooks) {
if (!hook) { if (!hook) {
log::warn("Hook is null in mod \"{}\"", m_metadata.getName()); log::warn("Hook is null in mod \"{}\"", m_metadata.getName());
continue; continue;
} }
if (hook->getAutoEnable()) { 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) { for (auto const& patch : m_patches) {
if (!patch->apply()) { if (!patch) {
log::warn("Unable to apply patch at {}", patch->getAddress()); log::warn("Patch is null in mod \"{}\"", m_metadata.getName());
continue; continue;
} }
if (patch->getAutoEnable()) {
if (!patch->apply()) {
log::warn("Unable to apply patch at {}", patch->getAddress());
continue;
}
}
} }
m_enabled = true; m_enabled = true;
ModStateEvent(m_self, ModEventType::Loaded).post();
ModStateEvent(m_self, ModEventType::Enabled).post(); ModStateEvent(m_self, ModEventType::Enabled).post();
return Ok(); 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() { Result<> Mod::Impl::disable() {
if (!m_enabled) if (m_requestedAction != ModRequestedAction::None) {
return Ok(); return Err("Mod already has a requested action");
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 (!disabledDependants) m_requestedAction = ModRequestedAction::Disable;
return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled."); Mod::get()->setSavedValue("should-load-" + m_metadata.getID(), false);
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"));
return Ok(); return Ok();
} }
Result<> Mod::Impl::uninstall() { Result<> Mod::Impl::uninstall() {
if (supportsDisabling()) { if (m_requestedAction != ModRequestedAction::None) {
GEODE_UNWRAP(this->disable()); return Err("Mod already has a requested action");
}
else {
for (auto& item : m_dependants) {
if (!item->canDisable())
continue;
GEODE_UNWRAP(item->disable());
}
} }
try { m_requestedAction = ModRequestedAction::Uninstall;
ghc::filesystem::remove(m_metadata.getPath());
} std::error_code ec;
catch (std::exception& e) { ghc::filesystem::remove(m_metadata.getPath(), ec);
if (ec) {
return Err( return Err(
"Unable to delete mod's .geode file! " "Unable to delete mod's .geode file: " + ec.message()
"This might be due to insufficient permissions - "
"try running GD as administrator."
); );
} }
@ -465,7 +412,11 @@ Result<> Mod::Impl::uninstall() {
} }
bool Mod::Impl::isUninstalled() const { 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 // Dependencies
@ -661,7 +612,7 @@ ModJson Mod::Impl::getRuntimeInfo() const {
for (auto patch : m_patches) { for (auto patch : m_patches) {
obj["patches"].as_array().push_back(ModJson(patch->getRuntimeInfo())); obj["patches"].as_array().push_back(ModJson(patch->getRuntimeInfo()));
} }
obj["enabled"] = m_enabled; // obj["enabled"] = m_enabled;
obj["loaded"] = m_binaryLoaded; obj["loaded"] = m_binaryLoaded;
obj["temp-dir"] = this->getTempDir(); obj["temp-dir"] = this->getTempDir();
obj["save-dir"] = this->getSaveDir(); obj["save-dir"] = this->getSaveDir();

View file

@ -62,6 +62,9 @@ namespace geode {
*/ */
bool m_resourcesLoaded = false; bool m_resourcesLoaded = false;
ModRequestedAction m_requestedAction = ModRequestedAction::None;
Impl(Mod* self, ModMetadata const& metadata); Impl(Mod* self, ModMetadata const& metadata);
~Impl(); ~Impl();
@ -122,6 +125,10 @@ namespace geode {
Result<> disable(); Result<> disable();
Result<> uninstall(); Result<> uninstall();
bool isUninstalled() const; bool isUninstalled() const;
// 1.3.0 additions
ModRequestedAction getRequestedAction() const;
bool depends(std::string const& id) const; bool depends(std::string const& id) const;
Result<> updateDependencies(); Result<> updateDependencies();
bool hasUnresolvedDependencies() const; bool hasUnresolvedDependencies() const;

View file

@ -4,13 +4,43 @@
using namespace geode::prelude; using namespace geode::prelude;
bool Patch::apply() { 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() { 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 <> template <>
struct json::Serialize<ByteVector> { struct json::Serialize<ByteVector> {
static json::Value to_json(ByteVector const& bytes) { static json::Value to_json(ByteVector const& bytes) {

View file

@ -251,7 +251,20 @@ static std::string getStacktrace() {
stream >> std::hex >> address >> std::dec; stream >> std::hex >> address >> std::dec;
if (!line.empty()) { 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"; stacktrace << ": " << line << "\n";
} }
else { else {
@ -319,14 +332,15 @@ static void handlerThread() {
s_cv.wait(lock, [] { return s_signal != 0; }); s_cv.wait(lock, [] { return s_signal != 0; });
auto signalAddress = reinterpret_cast<void*>(s_context->uc_mcontext->__ss.__rip); auto signalAddress = reinterpret_cast<void*>(s_context->uc_mcontext->__ss.__rip);
Mod* faultyMod = nullptr; // Mod* faultyMod = nullptr;
for (int i = 1; i < s_backtraceSize; ++i) { // for (int i = 1; i < s_backtraceSize; ++i) {
auto mod = modFromAddress(s_backtrace[i]); // auto mod = modFromAddress(s_backtrace[i]);
if (mod != nullptr) { // if (mod != nullptr) {
faultyMod = mod; // faultyMod = mod;
break; // break;
} // }
} // }
Mod* faultyMod = modFromAddress(signalAddress);
auto text = crashlog::writeCrashlog(faultyMod, getInfo(signalAddress, faultyMod), getStacktrace(), getRegisters()); auto text = crashlog::writeCrashlog(faultyMod, getInfo(signalAddress, faultyMod), getStacktrace(), getRegisters());
@ -334,6 +348,8 @@ static void handlerThread() {
s_signal = 0; s_signal = 0;
s_cv.notify_all(); s_cv.notify_all();
log::debug("Notified");
} }
static bool s_lastLaunchCrashed; static bool s_lastLaunchCrashed;

View file

@ -57,7 +57,7 @@ namespace gd {
return std::string(*this) == std::string(other); 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<int, int>;
template class map<gd::string, gd::string>; template class map<gd::string, gd::string>;
template class map<gd::string, bool>; template class map<gd::string, bool>;

View file

@ -83,7 +83,7 @@ CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
} }
CCNode* geode::createIndexItemLogo(IndexItemHandle item, 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()); CCNode* spr = CCSprite::create(logoPath.string().c_str());
if (!spr) { if (!spr) {
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);

View file

@ -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::LocalModInfoPopup() LocalModInfoPopup::LocalModInfoPopup()
: m_installListener( : m_installListener(
@ -462,8 +551,8 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) {
FLAlertLayer::create( FLAlertLayer::create(
"Update complete", "Update complete",
"Mod successfully updated! :) " "Mod successfully updated! :) "
"(You may need to <cy>restart the game</c> " "(You have to <cy>restart the game</c> "
"for the mod to take full effect)", "for the mod to take effect)",
"OK" "OK"
)->show(); )->show();
@ -493,61 +582,7 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) {
} }
void LocalModInfoPopup::onUpdate(CCObject*) { void LocalModInfoPopup::onUpdate(CCObject*) {
auto list = Index::get()->getInstallList(m_item); this->popupInstallItem(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);
} }
void LocalModInfoPopup::onUninstall(CCObject*) { void LocalModInfoPopup::onUninstall(CCObject*) {
@ -631,12 +666,6 @@ void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
} }
this->onClose(nullptr); this->onClose(nullptr);
} break; } break;
case TAG_CONFIRM_UPDATE: {
if (btn2) {
this->doUpdate();
}
} break;
} }
} }
@ -649,8 +678,8 @@ void LocalModInfoPopup::doUninstall() {
this, this,
"Uninstall complete", "Uninstall complete",
"Mod was successfully uninstalled! :) " "Mod was successfully uninstalled! :) "
"(You may need to <cy>restart the game</c> " "(You have to <cy>restart the game</c> "
"for the mod to take full effect). " "for the mod to take effect). "
"<co>Would you also like to delete the mod's " "<co>Would you also like to delete the mod's "
"save data?</c>", "save data?</c>",
"Keep", "Keep",
@ -687,7 +716,8 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) {
if (!ModInfoPopup::init(item->getMetadata(), list)) return false; 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( m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, "GE_button_01.png"_spr,
@ -719,8 +749,8 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
FLAlertLayer::create( FLAlertLayer::create(
"Install complete", "Install complete",
"Mod successfully installed! :) " "Mod successfully installed! :) "
"(You may need to <cy>restart the game</c> " "(You have to <cy>restart the game</c> "
"for the mod to take full effect)", "for the mod to take effect)",
"OK" "OK"
)->show(); )->show();
@ -750,92 +780,7 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
} }
void IndexItemInfoPopup::onInstall(CCObject*) { void IndexItemInfoPopup::onInstall(CCObject*) {
auto deps = m_item->getMetadata().getDependencies(); this->popupInstallItem(m_item);
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);
} }
CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) { CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {

View file

@ -38,6 +38,7 @@ protected:
MDTextArea* m_detailsArea; MDTextArea* m_detailsArea;
MDTextArea* m_changelogArea = nullptr; MDTextArea* m_changelogArea = nullptr;
Scrollbar* m_scrollbar; Scrollbar* m_scrollbar;
IndexItemHandle m_item;
void onChangelog(CCObject*); void onChangelog(CCObject*);
void onRepository(CCObject*); void onRepository(CCObject*);
@ -51,13 +52,16 @@ protected:
void setInstallStatus(std::optional<UpdateProgress> const& progress); void setInstallStatus(std::optional<UpdateProgress> const& progress);
void popupInstallItem(IndexItemHandle item);
void preInstall();
void onCancelInstall(CCObject*);
virtual CCNode* createLogo(CCSize const& size) = 0; virtual CCNode* createLogo(CCSize const& size) = 0;
virtual ModMetadata getMetadata() const = 0; virtual ModMetadata getMetadata() const = 0;
}; };
class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
protected: protected:
IndexItemHandle m_item;
EventListener<ModInstallFilter> m_installListener; EventListener<ModInstallFilter> m_installListener;
Mod* m_mod; Mod* m_mod;
@ -74,8 +78,6 @@ protected:
void onUpdateProgress(ModInstallEvent* event); void onUpdateProgress(ModInstallEvent* event);
void onUpdate(CCObject*); void onUpdate(CCObject*);
void onCancel(CCObject*);
void doUpdate();
void FLAlert_Clicked(FLAlertLayer*, bool) override; void FLAlert_Clicked(FLAlertLayer*, bool) override;
@ -91,16 +93,12 @@ public:
class IndexItemInfoPopup : public ModInfoPopup { class IndexItemInfoPopup : public ModInfoPopup {
protected: protected:
IndexItemHandle m_item;
EventListener<ModInstallFilter> m_installListener; EventListener<ModInstallFilter> m_installListener;
bool init(IndexItemHandle item, ModListLayer* list); bool init(IndexItemHandle item, ModListLayer* list);
void onInstallProgress(ModInstallEvent* event); void onInstallProgress(ModInstallEvent* event);
void onInstall(CCObject*); void onInstall(CCObject*);
void onCancel(CCObject*);
void preInstall();
CCNode* createLogo(CCSize const& size) override; CCNode* createLogo(CCSize const& size) override;
ModMetadata getMetadata() const override; ModMetadata getMetadata() const override;

View file

@ -8,7 +8,6 @@
#include <Geode/ui/GeodeUI.hpp> #include <Geode/ui/GeodeUI.hpp>
#include <loader/LoaderImpl.hpp> #include <loader/LoaderImpl.hpp>
#include <utility> #include <utility>
#include "../info/TagNode.hpp"
#include "../info/DevProfilePopup.hpp" #include "../info/DevProfilePopup.hpp"
// InstallListCell // InstallListCell
@ -27,8 +26,11 @@ void InstallListCell::setupInfo(
std::variant<VersionInfo, ComparableVersionInfo> version, std::variant<VersionInfo, ComparableVersionInfo> version,
bool inactive bool inactive
) { ) {
m_inactive = inactive;
m_menu = CCMenu::create(); 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); this->addChild(m_menu);
auto logoSize = this->getLogoSize(); auto logoSize = this->getLogoSize();
@ -41,15 +43,15 @@ void InstallListCell::setupInfo(
} }
this->addChild(logoSpr); this->addChild(logoSpr);
auto titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); m_titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f }); m_titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); m_titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
titleLabel->setPositionY(m_height / 2); m_titleLabel->setPositionY(m_height / 2);
titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f); m_titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f);
if (inactive) { if (inactive) {
titleLabel->setColor({ 163, 163, 163 }); m_titleLabel->setColor({ 163, 163, 163 });
} }
this->addChild(titleLabel); this->addChild(m_titleLabel);
m_developerBtn = nullptr; m_developerBtn = nullptr;
if (developer) { if (developer) {
@ -64,43 +66,55 @@ void InstallListCell::setupInfo(
creatorLabel, this, menu_selector(InstallListCell::onViewDev) creatorLabel, this, menu_selector(InstallListCell::onViewDev)
); );
m_developerBtn->setPosition( m_developerBtn->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f + m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f +
creatorLabel->getScaledContentSize().width / 2 - creatorLabel->getScaledContentSize().width / 2,
m_menu->getPositionX(), m_height / 2
-0.5f
); );
m_menu->addChild(m_developerBtn); 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::holds_alternative<VersionInfo>(version) ?
std::get<VersionInfo>(version).toString(false).c_str() : std::get<VersionInfo>(version).toString(false).c_str() :
std::get<ComparableVersionInfo>(version).toString().c_str(), std::get<ComparableVersionInfo>(version).toString().c_str(),
"bigFont.fnt" "bigFont.fnt"
); );
versionLabel->setAnchorPoint({ .0f, .5f }); m_versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.2f); m_versionLabel->setScale(.2f);
versionLabel->setPosition( m_versionLabel->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f + m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f +
(m_developerBtn ? m_developerBtn->getScaledContentSize().width + 3.f : 0.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 }); m_versionLabel->setColor({ 0, 255, 0 });
if (inactive) { if (m_inactive) {
versionLabel->setColor({ 0, 163, 0 }); m_versionLabel->setColor({ 0, 163, 0 });
} }
this->addChild(versionLabel); this->addChild(m_versionLabel);
if (!std::holds_alternative<VersionInfo>(version)) return; if (!std::holds_alternative<VersionInfo>(version)) return;
if (auto tag = std::get<VersionInfo>(version).getTag()) { if (auto tag = std::get<VersionInfo>(version).getTag()) {
auto tagLabel = TagNode::create(tag->toString()); m_tagLabel = TagNode::create(tag->toString());
tagLabel->setAnchorPoint({.0f, .5f}); m_tagLabel->setAnchorPoint({.0f, .5f});
tagLabel->setScale(.2f); m_tagLabel->setScale(.2f);
tagLabel->setPosition( m_tagLabel->setPosition(
versionLabel->getPositionX() + versionLabel->getScaledContentSize().width + 3.f, m_versionLabel->getPositionX() + m_versionLabel->getScaledContentSize().width + 3.f,
versionLabel->getPositionY() 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); this->setupInfo(mod->getMetadata(), true);
auto message = CCLabelBMFont::create("Installed", "bigFont.fnt"); auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
message->setAnchorPoint({ 1.f, .5f }); message->setAnchorPoint({ 1.f, .5f });
message->setPositionX(m_menu->getPositionX()); message->setPositionX(m_width - 10.0f);
message->setPositionY(16.f); message->setPositionY(16.f);
message->setScale(0.4f); message->setScale(0.4f);
message->setColor({ 163, 163, 163 }); message->setColor({ 163, 163, 163 });
@ -174,23 +188,38 @@ bool IndexItemInstallListCell::init(
return false; return false;
m_item = item; m_item = item;
this->setupInfo(item->getMetadata(), item->isInstalled()); this->setupInfo(item->getMetadata(), item->isInstalled());
if (item->isInstalled()) {
auto message = CCLabelBMFont::create("Installed", "bigFont.fnt"); // TODO: show installed label properly
message->setAnchorPoint({ 1.f, .5f }); // if (item->isInstalled()) {
message->setPositionX(m_menu->getPositionX()); // auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
message->setPositionY(16.f); // message->setAnchorPoint({ 1.f, .5f });
message->setScale(0.4f); // message->setPositionX(m_width - 10.0f);
message->setColor({ 163, 163, 163 }); // message->setPositionY(16.f);
this->addChild(message); // message->setScale(0.4f);
return true; // message->setColor({ 163, 163, 163 });
} // this->addChild(message);
// return true;
// }
m_toggle = CCMenuItemToggler::createWithStandardSprites( m_toggle = CCMenuItemToggler::createWithStandardSprites(
m_layer, m_layer,
menu_selector(InstallListPopup::onCellToggle), menu_selector(InstallListPopup::onCellToggle),
.6f .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) { switch (importance) {
case ModMetadata::Dependency::Importance::Required: case ModMetadata::Dependency::Importance::Required:
@ -207,13 +236,18 @@ bool IndexItemInstallListCell::init(
break; break;
} }
if (item->isInstalled()) {
m_toggle->setClickable(false);
m_toggle->toggle(true);
}
if (m_item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) { if (m_item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) {
m_toggle->setClickable(false); m_toggle->setClickable(false);
m_toggle->toggle(false); m_toggle->toggle(false);
auto message = CCLabelBMFont::create("N/A", "bigFont.fnt"); auto message = CCLabelBMFont::create("N/A", "bigFont.fnt");
message->setAnchorPoint({ 1.f, .5f }); 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->setPositionY(16.f);
message->setScale(0.4f); message->setScale(0.4f);
message->setColor({ 240, 31, 31 }); message->setColor({ 240, 31, 31 });
@ -269,6 +303,15 @@ IndexItemHandle IndexItemInstallListCell::getItem() {
return m_item; 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 // UnknownInstallListCell
bool UnknownInstallListCell::init( bool UnknownInstallListCell::init(
@ -317,3 +360,50 @@ std::string UnknownInstallListCell::getID() const {
std::string UnknownInstallListCell::getDeveloper() const { std::string UnknownInstallListCell::getDeveloper() const {
return ""; 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();
}

View file

@ -6,6 +6,8 @@
#include <Geode/loader/ModMetadata.hpp> #include <Geode/loader/ModMetadata.hpp>
#include <Geode/loader/Index.hpp> #include <Geode/loader/Index.hpp>
#include "../info/TagNode.hpp"
using namespace geode::prelude; using namespace geode::prelude;
class InstallListPopup; class InstallListPopup;
@ -17,10 +19,14 @@ class InstallListCell : public CCLayer {
protected: protected:
float m_width; float m_width;
float m_height; float m_height;
InstallListPopup* m_layer; InstallListPopup* m_layer = nullptr;
CCMenu* m_menu; CCMenu* m_menu = nullptr;
CCMenuItemSpriteExtra* m_developerBtn; CCMenuItemSpriteExtra* m_developerBtn = nullptr;
CCLabelBMFont* m_titleLabel = nullptr;
CCLabelBMFont* m_versionLabel = nullptr;
TagNode* m_tagLabel = nullptr;
CCMenuItemToggler* m_toggle = nullptr; CCMenuItemToggler* m_toggle = nullptr;
bool m_inactive = false;
void setupInfo( void setupInfo(
std::string name, std::string name,
@ -29,6 +35,8 @@ protected:
bool inactive bool inactive
); );
void setupVersion(std::variant<VersionInfo, ComparableVersionInfo> version);
bool init(InstallListPopup* list, CCSize const& size); bool init(InstallListPopup* list, CCSize const& size);
void setupInfo(ModMetadata const& metadata, bool inactive); void setupInfo(ModMetadata const& metadata, bool inactive);
void draw() override; void draw() override;
@ -90,6 +98,9 @@ public:
[[nodiscard]] std::string getDeveloper() const override; [[nodiscard]] std::string getDeveloper() const override;
IndexItemHandle getItem(); IndexItemHandle getItem();
void setVersionFromItem(IndexItemHandle item);
void onSelectVersion(CCObject*);
}; };
/** /**
@ -112,3 +123,23 @@ public:
[[nodiscard]] std::string getID() const override; [[nodiscard]] std::string getID() const override;
[[nodiscard]] std::string getDeveloper() 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;
};

View file

@ -112,7 +112,8 @@ CCArray* InstallListPopup::createCells(std::unordered_map<std::string, InstallLi
queued.insert(item.id); queued.insert(item.id);
// installed // 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())); bottom.push_back(ModInstallListCell::create(item.mod, this, this->getCellSize()));
for (auto const& dep : item.mod->getMetadata().getDependencies()) { for (auto const& dep : item.mod->getMetadata().getDependencies()) {
queue.push(dep); queue.push(dep);
@ -204,7 +205,7 @@ void InstallListPopup::onInstall(cocos2d::CCObject* obj) {
CCArray* entries = m_list->m_entries; CCArray* entries = m_list->m_entries;
for (size_t i = entries->count(); i > 0; i--) { for (size_t i = entries->count(); i > 0; i--) {
auto* itemCell = typeinfo_cast<IndexItemInstallListCell*>(entries->objectAtIndex(i - 1)); auto* itemCell = typeinfo_cast<IndexItemInstallListCell*>(entries->objectAtIndex(i - 1));
if (!itemCell || !itemCell->isIncluded()) if (!itemCell || !itemCell->isIncluded() || itemCell->getItem()->isInstalled())
continue; continue;
IndexItemHandle item = itemCell->getItem(); IndexItemHandle item = itemCell->getItem();
list.list.push_back(item); list.list.push_back(item);
@ -227,3 +228,70 @@ InstallListPopup* InstallListPopup::create(
ret->autorelease(); ret->autorelease();
return ret; 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;
}

View file

@ -28,3 +28,24 @@ public:
static InstallListPopup* create(IndexItemHandle item, MiniFunction<void(IndexInstallList const&)> onInstall); 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);
};

View file

@ -227,11 +227,7 @@ void ModCell::onEnable(CCObject* sender) {
else { else {
tryOrAlert(m_mod->disable(), "Error disabling mod"); tryOrAlert(m_mod->disable(), "Error disabling mod");
} }
Loader::get()->queueInMainThread([this]() { m_layer->reloadList();
if (m_layer) {
m_layer->updateAllStates();
}
});
} }
void ModCell::onUnresolvedInfo(CCObject*) { void ModCell::onUnresolvedInfo(CCObject*) {
@ -278,9 +274,9 @@ bool ModCell::init(
return false; return false;
m_mod = mod; 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); auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f);
restartSpr->setScale(.65f); 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"); auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png");
exMark->setScale(.5f); exMark->setScale(.5f);

View file

@ -198,7 +198,7 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer
std::multimap<int, IndexItemHandle> sorted; std::multimap<int, IndexItemHandle> sorted;
auto index = Index::get(); auto index = Index::get();
for (auto const& item : index->getItems()) { for (auto const& item : index->getLatestItems()) {
if (auto match = queryMatch(query, item)) { if (auto match = queryMatch(query, item)) {
sorted.insert({ match.value(), item }); sorted.insert({ match.value(), item });
} }
@ -444,13 +444,18 @@ void ModListLayer::createSearchControl() {
this->addChild(m_searchInput); 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(); auto winSize = CCDirector::sharedDirector()->getWinSize();
if (query) { if (query) {
m_query = query.value(); 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 // set search query
m_query.keywords = m_query.keywords =
m_searchInput && m_searchInput &&
@ -489,6 +494,12 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
m_listLabel->setVisible(false); m_listLabel->setVisible(false);
} }
if (keepScroll) {
list->m_tableView->m_contentLayer->setPosition(
{ 0.0f, scroll }
);
}
// update index if needed // update index if needed
if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) { if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) {
m_listLabel->setVisible(true); m_listLabel->setVisible(true);
@ -658,7 +669,7 @@ void ModListLayer::onTab(CCObject* pSender) {
if (pSender) { if (pSender) {
g_tab = static_cast<ModListType>(pSender->getTag()); g_tab = static_cast<ModListType>(pSender->getTag());
} }
this->reloadList(); this->reloadList(false);
auto toggleTab = [this](CCMenuItemToggler* member) -> void { auto toggleTab = [this](CCMenuItemToggler* member) -> void {
auto isSelected = member->getTag() == static_cast<int>(g_tab); auto isSelected = member->getTag() == static_cast<int>(g_tab);
@ -670,7 +681,7 @@ void ModListLayer::onTab(CCObject* pSender) {
targetMenu->addChild(member); targetMenu->addChild(member);
member->release(); member->release();
} }
if (isSelected) if (isSelected && m_tabsGradientStencil)
m_tabsGradientStencil->setPosition(member->m_onButton->convertToWorldSpace({0.f, -1.f})); 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) { void ModListLayer::textChanged(CCTextInputNode* input) {
this->reloadList(); this->reloadList(false);
} }
// Constructors etc. // Constructors etc.

View file

@ -94,5 +94,5 @@ public:
ModListDisplay getDisplay() const; ModListDisplay getDisplay() const;
ModListQuery& getQuery(); ModListQuery& getQuery();
void reloadList(std::optional<ModListQuery> const& query = std::nullopt); void reloadList(bool keepScroll = true, std::optional<ModListQuery> const& query = std::nullopt);
}; };

View file

@ -175,7 +175,7 @@ void SearchFilterPopup::onPlatformToggle(CCObject* sender) {
void SearchFilterPopup::onClose(CCObject* sender) { void SearchFilterPopup::onClose(CCObject* sender) {
Popup::onClose(sender); Popup::onClose(sender);
m_modLayer->reloadList(); m_modLayer->reloadList(false);
} }
SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) { SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) {

View file

@ -48,7 +48,6 @@ Result<> web::fetchFile(
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
if (prog) { if (prog) {
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, utils::fetch::progress); 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_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBytes); 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); auto res = curl_easy_perform(curl);
if (res != CURLE_OK) { if (res != CURLE_OK) {
curl_easy_cleanup(curl); 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_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeString); 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); auto res = curl_easy_perform(curl);
if (res != CURLE_OK) { if (res != CURLE_OK) {
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
@ -162,9 +159,10 @@ private:
SentAsyncWebRequest* m_self; SentAsyncWebRequest* m_self;
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target = AsyncWebRequestData m_extra;
std::monostate(); std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target;
std::vector<std::string> m_httpHeaders; std::vector<std::string> m_httpHeaders;
template <class T> template <class T>
friend class AsyncWebResult; friend class AsyncWebResult;
@ -187,7 +185,7 @@ static std::unordered_map<std::string, SentAsyncWebRequestHandle> RUNNING_REQUES
static std::mutex RUNNING_REQUESTS_MUTEX; static std::mutex RUNNING_REQUESTS_MUTEX;
SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const& req, std::string const& id) : 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() \ #define AWAIT_RESUME() \
{\ {\
@ -242,8 +240,30 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
// No need to verify SSL, we trust our domains :-) // No need to verify SSL, we trust our domains :-)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
// Github User Agent // User Agent
curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); 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 // Track progress
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
// Follow redirects // Follow redirects
@ -251,10 +271,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
// Fail if response code is 4XX or 5XX // Fail if response code is 4XX or 5XX
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_slist* headers = nullptr; // Headers end
for (auto& header : m_httpHeaders) {
headers = curl_slist_append(headers, header.c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
struct ProgressData { struct ProgressData {
@ -410,11 +427,50 @@ void SentAsyncWebRequest::error(std::string const& error, int code) {
return m_impl->error(error, 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) { AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) {
m_joinID = requestID; m_joinID = requestID;
return *this; 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) { AsyncWebRequest& AsyncWebRequest::header(std::string const& header) {
m_httpHeaders.push_back(header); m_httpHeaders.push_back(header);
return *this; return *this;
@ -448,8 +504,8 @@ AsyncWebRequest& AsyncWebRequest::cancelled(AsyncCancelled cancelledFunc) {
} }
SentAsyncWebRequestHandle AsyncWebRequest::send() { SentAsyncWebRequestHandle AsyncWebRequest::send() {
if (m_sent) return nullptr; if (this->extra().m_sent) return nullptr;
m_sent = true; this->extra().m_sent = true;
std::lock_guard __(RUNNING_REQUESTS_MUTEX); std::lock_guard __(RUNNING_REQUESTS_MUTEX);
@ -486,6 +542,7 @@ SentAsyncWebRequestHandle AsyncWebRequest::send() {
AsyncWebRequest::~AsyncWebRequest() { AsyncWebRequest::~AsyncWebRequest() {
this->send(); this->send();
delete m_extra;
} }
AsyncWebResult<std::monostate> AsyncWebResponse::into(std::ostream& stream) { AsyncWebResult<std::monostate> AsyncWebResponse::into(std::ostream& stream) {