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
## 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)

View file

@ -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

View file

@ -1 +1 @@
1.2.1
1.3.0

View file

@ -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);
};
}

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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,

View file

@ -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);
});

View file

@ -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,6 +410,7 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) {
return;
}
if (Mod::get()->getSavedValue<bool>("should-load-" + node->getID(), true)) {
log::debug("Load");
auto res = node->m_impl->loadBinary();
if (!res) {
@ -424,22 +424,6 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) {
return;
}
if (Mod::get()->getSavedValue<bool>("should-load-" + node->getID(), true)) {
log::debug("Enable");
res = node->m_impl->enable();
if (!res) {
node->m_impl->m_enabled = true;
(void)node->m_impl->disable();
m_problems.push_back({
LoadProblem::Type::EnableFailed,
node,
res.unwrapErr()
});
log::error("Failed to enable: {}", res.unwrapErr());
log::popNest();
return;
}
for (auto const& dep : node->m_impl->m_dependants) {
this->loadModGraph(dep, early);
}
@ -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()

View file

@ -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);
}

View file

@ -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) {
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::disable() {
if (!m_enabled)
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();
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)
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());
Result<> Mod::Impl::disable() {
if (m_requestedAction != ModRequestedAction::None) {
return Err("Mod already has a requested action");
}
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();

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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>;

View file

@ -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);

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()
: 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) {

View file

@ -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;

View file

@ -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();
}

View file

@ -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;
};

View file

@ -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;
}

View file

@ -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);
};

View file

@ -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->supportsDisabling() && !m_mod->isUninstalled()) {
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);
}
}
auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png");
exMark->setScale(.5f);

View file

@ -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.

View file

@ -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);
};

View file

@ -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) {

View file

@ -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,10 +159,11 @@ 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;
friend class AsyncWebRequest;
@ -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) {