Compare commits

...

14 commits

Author SHA1 Message Date
HJfod
acad3d2a8d Merge branch 'v4' of https://github.com/geode-sdk/geode into v4
Some checks are pending
Build Binaries / Build Windows (push) Waiting to run
Build Binaries / Build macOS (push) Waiting to run
Build Binaries / Build Android (64-bit) (push) Waiting to run
Build Binaries / Build Android (32-bit) (push) Waiting to run
Build Binaries / Publish (push) Blocked by required conditions
2024-11-12 01:03:37 +02:00
HJfod
09fa872781 show outdated mods in the UI + make outdated Geode ver count for that too 2024-11-12 01:03:19 +02:00
Chloe
d415c949d0
add android gameversion to mappings 2024-11-11 15:53:26 -07:00
Chloe
c9e97af18a
disable forward compat mode on android 2024-11-11 15:53:02 -07:00
Chloe
9d6b2954e5
ok there 2024-11-11 15:21:34 -07:00
Chloe
bcb856a302
update android binaries 2024-11-11 15:14:32 -07:00
Cvolton
f5f336532f
fix the stupid misaligned controller button 2024-11-11 21:36:39 +01:00
matcool
324883140a add Task::chain 2024-11-11 16:43:38 -03:00
altalk23
b1ab3eb373 thank you mat 2024-11-11 22:43:04 +03:00
altalk23
6db3084062 priorities production ready (not tested) 2024-11-11 22:40:30 +03:00
altalk23
673317d3cb hook priority changes with pre/post and Priority class for static priority values 2024-11-11 22:38:03 +03:00
altalk23
9c1f48ee64 update tuliphook take 2 2024-11-11 21:37:25 +03:00
altalk23
2940de38bc update tuliphook 2024-11-11 21:33:34 +03:00
altalk23
0c469b98fe update json and result 2024-11-11 21:25:06 +03:00
21 changed files with 394 additions and 84 deletions

View file

@ -189,13 +189,10 @@ include(cmake/Platform.cmake)
include(cmake/GeodeFile.cmake)
if (NOT DEFINED GEODE_GD_VERSION)
if (GEODE_TARGET_PLATFORM STREQUAL "Win64" OR GEODE_TARGET_PLATFORM STREQUAL "MacOS")
set(GEODE_GD_VERSION 2.2073)
set(GEODE_COMP_GD_VERSION 22073)
else()
set(GEODE_GD_VERSION 2.206)
set(GEODE_COMP_GD_VERSION 22060)
endif()
# this is incorrect!
# Android and iOS are on 2.2074 so please fix when 2.2074 releases for desktop :(
set(GEODE_GD_VERSION 2.2073)
set(GEODE_COMP_GD_VERSION 22073)
endif()
target_compile_definitions(
@ -241,8 +238,8 @@ if (ANDROID)
endif()
set(MAT_JSON_AS_INTERFACE ON)
CPMAddPackage("gh:geode-sdk/result@1.1.1")
CPMAddPackage("gh:geode-sdk/json@3.0.1")
CPMAddPackage("gh:geode-sdk/result@1.2.0")
CPMAddPackage("gh:geode-sdk/json@3.0.2")
CPMAddPackage("gh:fmtlib/fmt#11.0.2")
target_compile_definitions(${PROJECT_NAME} INTERFACE MAT_JSON_DYNAMIC=1)
@ -272,7 +269,7 @@ if (DEFINED GEODE_TULIPHOOK_REPO_PATH)
message(STATUS "Using ${GEODE_TULIPHOOK_REPO_PATH} for TulipHook")
add_subdirectory(${GEODE_TULIPHOOK_REPO_PATH} ${GEODE_TULIPHOOK_REPO_PATH}/build)
else()
CPMAddPackage("gh:geode-sdk/TulipHook#4e5d607")
CPMAddPackage("gh:geode-sdk/TulipHook@2.4.0")
endif()
set(CMAKE_WARN_DEPRECATED ON CACHE BOOL "" FORCE)

View file

@ -46,6 +46,21 @@ namespace geode {
Type type;
std::variant<std::filesystem::path, ModMetadata, Mod*> cause;
std::string message;
bool isSuggestion() const {
return
type == LoadProblem::Type::Recommendation ||
type == LoadProblem::Type::Suggestion;
}
bool isOutdated() const {
return
type == LoadProblem::Type::UnsupportedVersion ||
type == LoadProblem::Type::NeedsNewerGeodeVersion ||
type == LoadProblem::Type::UnsupportedGeodeVersion;
}
bool isProblem() const {
return !isSuggestion() && !isOutdated();
}
};
class LoaderImpl;

View file

@ -446,7 +446,16 @@ namespace geode {
bool isLoggingEnabled() const;
void setLoggingEnabled(bool enabled);
bool targetsOutdatedGDVersion() const;
/**
* If this mod is built for an outdated GD or Geode version, returns the
* `LoadProblem` describing the situation. Otherwise `nullopt` if the
* mod is made for the correct version of the game and Geode
*/
std::optional<LoadProblem> targetsOutdatedVersion() const;
/**
* @note Make sure to also call `targetsOutdatedVersion` if you want to
* make sure the mod is actually loadable
*/
bool hasLoadProblems() const;
std::vector<LoadProblem> getAllProblems() const;
std::vector<LoadProblem> getProblems() const;

View file

@ -104,8 +104,36 @@
} \
} while (0);
namespace geode::modifier {
namespace geode {
class Priority {
public:
static inline constexpr int32_t First = -3000;
static inline constexpr int32_t VeryEarly = -2000;
static inline constexpr int32_t Early = -1000;
static inline constexpr int32_t Normal = 0;
static inline constexpr int32_t Late = 1000;
static inline constexpr int32_t VeryLate = 2000;
static inline constexpr int32_t Last = 3000;
static inline constexpr int32_t FirstPre = First;
static inline constexpr int32_t VeryEarlyPre = VeryEarly;
static inline constexpr int32_t EarlyPre = Early;
static inline constexpr int32_t NormalPre = Normal;
static inline constexpr int32_t LatePre = Late;
static inline constexpr int32_t VeryLatePre = VeryLate;
static inline constexpr int32_t LastPre = Last;
static inline constexpr int32_t FirstPost = Last;
static inline constexpr int32_t VeryEarlyPost = VeryLate;
static inline constexpr int32_t EarlyPost = Late;
static inline constexpr int32_t NormalPost = Normal;
static inline constexpr int32_t LatePost = Early;
static inline constexpr int32_t VeryLatePost = VeryEarly;
static inline constexpr int32_t LastPost = First;
};
}
namespace geode::modifier {
template <class Derived, class Base>
class ModifyDerive;
@ -114,6 +142,9 @@ namespace geode::modifier {
public:
std::map<std::string, std::shared_ptr<Hook>> m_hooks;
/// @brief Get a hook by name
/// @param name The name of the hook to get
/// @returns Ok if the hook was found, Err if the hook was not found
Result<Hook*> getHook(std::string const& name) {
if (m_hooks.find(name) == m_hooks.end()) {
return Err("Hook not in this modify");
@ -121,15 +152,128 @@ namespace geode::modifier {
return Ok(m_hooks[name].get());
}
Result<> setHookPriority(std::string const& name, int32_t priority) {
auto res = this->getHook(name);
if (!res) {
return Err(res.unwrapErr());
}
res.unwrap()->setPriority(priority);
/// @brief Set the priority of a hook
/// @param name The name of the hook to set the priority of
/// @param priority The priority to set the hook to
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
Result<> setHookPriority(std::string const& name, int32_t priority = Priority::Normal) {
GEODE_UNWRAP_INTO(auto hook, this->getHook(name));
hook->setPriority(priority);
return Ok();
}
/// @brief Set the priority of a hook
/// @param name The name of the hook to set the priority of
/// @param priority The priority to set the hook to
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
Result<> setHookPriorityPre(std::string const& name, int32_t priority = Priority::Normal) {
return this->setHookPriority(name, priority);
}
/// @brief Set the priority of a hook
/// @param name The name of the hook to set the priority of
/// @param priority The priority to set the hook to
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
Result<> setHookPriorityPost(std::string const& name, int32_t priority = Priority::Normal) {
return this->setHookPriority(name, -priority);
}
/// @brief Set the priority of a hook to be after another hook in different mods
/// @param name The name of the hook to set the priority of
/// @param after The mod ids of the mods to set the priority after
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
template<class... C> requires (std::is_convertible_v<C, std::string> && ...)
Result<> setHookPriorityAfter(std::string const& name, C&&... after) {
GEODE_UNWRAP_INTO(auto hook, this->getHook(name));
([&](){
auto mod = Loader::get()->getInstalledMod(after);
if (!mod) return;
auto hooks = mod->getHooks();
auto func = [=](){
for (auto modHook : hooks) {
if (modHook->getAddress() != hook->getAddress()) continue;
auto priority = hook->getPriority();
if (priority < modHook->getPriority()) {
hook->setPriority(modHook->getPriority() + 1);
}
}
};
if (Loader::get()->isModLoaded(mod)) {
func();
}
else {
new EventListener(func, ModStateFilter(mod, ModEventType::Loaded));
}
} (), ...);
return Ok();
}
/// @brief Set the priority of a hook to be before another hook in different mods
/// @param name The name of the hook to set the priority of
/// @param before The mod ids of the mods to set the priority before
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
template<class... C> requires (std::is_convertible_v<C, std::string> && ...)
Result<> setHookPriorityBefore(std::string const& name, C&&... before) {
GEODE_UNWRAP_INTO(auto hook, this->getHook(name));
([&](){
auto mod = Loader::get()->getInstalledMod(before);
if (!mod) return;
auto hooks = mod->getHooks();
auto func = [=](){
for (auto modHook : hooks) {
if (modHook->getAddress() != hook->getAddress()) continue;
auto priority = hook->getPriority();
if (priority > modHook->getPriority()) {
hook->setPriority(modHook->getPriority() - 1);
}
}
};
if (Loader::get()->isModLoaded(mod)) {
func();
}
else {
new EventListener(func, ModStateFilter(mod, ModEventType::Loaded));
}
} (), ...);
return Ok();
}
/// @brief Set the priority of a hook to be after another hook in different mods
/// @param name The name of the hook to set the priority of
/// @param after The mod ids of the mods to set the priority after
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
template<class... C> requires (std::is_convertible_v<C, std::string> && ...)
Result<> setHookPriorityAfterPre(std::string const& name, C&&... after) {
return this->setHookPriorityAfter(name, Priority::NormalPre, std::forward<C>(after)...);
}
/// @brief Set the priority of a hook to be before another hook in different mods
/// @param name The name of the hook to set the priority of
/// @param before The mod ids of the mods to set the priority before
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
template<class... C> requires (std::is_convertible_v<C, std::string> && ...)
Result<> setHookPriorityBeforePre(std::string const& name, C&&... before) {
return this->setHookPriorityBefore(name, Priority::NormalPre, std::forward<C>(before)...);
}
/// @brief Set the priority of a hook to be after another hook in different mods
/// @param name The name of the hook to set the priority of
/// @param after The mod ids of the mods to set the priority after
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
template<class... C> requires (std::is_convertible_v<C, std::string> && ...)
Result<> setHookPriorityAfterPost(std::string const& name, C&&... after) {
return this->setHookPriorityBefore(name, Priority::NormalPost, std::forward<C>(after)...);
}
/// @brief Set the priority of a hook to be before another hook in different mods
/// @param name The name of the hook to set the priority of
/// @param before The mod ids of the mods to set the priority before
/// @returns Ok if the hook was found and the priority was set, Err if the hook was not found
template<class... C> requires (std::is_convertible_v<C, std::string> && ...)
Result<> setHookPriorityBeforePost(std::string const& name, C&&... before) {
return this->setHookPriorityAfter(name, Priority::NormalPost, std::forward<C>(before)...);
}
// unordered_map<handles> idea
ModifyBase() {
struct EboCheck : ModifyDerived::Base {

View file

@ -1,9 +1,11 @@
#pragma once
#include <Geode/modify/Modify.hpp>
#include "cocos.hpp"
namespace geode::node_ids {
using namespace cocos2d;
static constexpr int32_t GEODE_ID_PRIORITY = 0x100000;
static constexpr int32_t GEODE_ID_PRIORITY = Priority::VeryEarlyPost;
template <class T = CCNode>
requires std::is_base_of_v<CCNode, T>

View file

@ -751,6 +751,93 @@ namespace geode {
this->listen(std::move(onResult), [](auto const&) {}, [] {});
}
/**
* Create a new Task that listens to this Task and maps the values using
* the provided function. The new Task will only start when this Task finishes.
* @param mapper Function that makes a new task given the finished value of this task.
* The function signature should be `Task<NewType, NewProgress>(T*)`, and it will be executed
* on the main thread.
* @param name The name of the Task; used for debugging.
* @return The new Task that will run when this Task finishes.
* @note Progress from this task is not sent through, only progress from the new task is.
*/
template <std::invocable<T*> Mapper>
auto chain(Mapper mapper, std::string_view name = "<Chained Task>") const -> decltype(mapper(std::declval<T*>())) {
using NewTask = decltype(mapper(std::declval<T*>()));
using NewType = typename NewTask::Value;
using NewProgress = typename NewTask::Progress;
std::unique_lock<std::recursive_mutex> lock(m_handle->m_mutex);
if (m_handle->m_status == Status::Cancelled) {
// if the current task has been cancelled already, make an immediate cancelled task
return NewTask::cancelled();
}
else if (m_handle->m_status == Status::Finished) {
// if the current task is already done, we can just call the mapper directly
return mapper(&*m_handle->m_resultValue);
}
// otherwise, make a wrapper task that waits for the current task to finish,
// and then runs the mapper on the result. this new task will also wait for the task
// created by the mapper to finish, and will just forward the values through.
// do this because we cant really change the handle of the task we already returned
NewTask task = NewTask::Handle::create(fmt::format("{} <- {}", name, m_handle->m_name));
task.m_handle->m_extraData = std::make_unique<typename NewTask::Handle::ExtraData>(
// make the first event listener that waits for the current task
static_cast<void*>(new EventListener<Task>(
[handle = std::weak_ptr(task.m_handle), mapper = std::move(mapper)](Event* event) mutable {
if (auto v = event->getValue()) {
auto newInnerTask = mapper(v);
// this is scary.. but it doesn't seem to crash lol
handle.lock()->m_extraData = std::make_unique<typename NewTask::Handle::ExtraData>(
// make the second event listener that waits for the mapper's task
// and just forwards everything through
static_cast<void*>(new EventListener<NewTask>(
[handle](Event* event) mutable {
if (auto v = event->getValue()) {
NewTask::finish(handle.lock(), std::move(*v));
}
else if (auto p = event->getProgress()) {
NewTask::progress(handle.lock(), std::move(*p));
}
else if (event->isCancelled()) {
NewTask::cancel(handle.lock());
}
},
std::move(newInnerTask)
)),
+[](void* ptr) {
delete static_cast<EventListener<NewTask>*>(ptr);
},
+[](void* ptr) {
static_cast<EventListener<NewTask>*>(ptr)->getFilter().cancel();
}
);
}
else if (auto p = event->getProgress()) {
// no guarantee P and NewProgress are compatible
// nor does it seem like the intended behavior?
// TODO: maybe add a mapper for progress?
}
else if (event->isCancelled()) {
NewTask::cancel(handle.lock());
}
},
*this
)),
+[](void* ptr) {
delete static_cast<EventListener<Task>*>(ptr);
},
+[](void* ptr) {
static_cast<EventListener<Task>*>(ptr)->getFilter().cancel();
}
);
return task;
}
ListenerResult handle(std::function<Callback> fn, Event* e) {
if (e->m_handle == m_handle && (!e->m_for || e->m_for == m_listener)) {
fn(e);

View file

@ -75,6 +75,7 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
this->fixSocialMenu();
//this code doesnt run have fun figuring out why idc enough
if (auto node = this->getChildByID("settings-gamepad-icon")) {
node->setPositionX(
bottomMenu->getChildByID("settings-button")->getPositionX() + winSize.width / 2

View file

@ -46,6 +46,11 @@ $register_ids(MenuLayer) {
}
setIDSafe<CCLabelBMFont>(this, labelOffset++, "player-username");
if(auto node = this->getChildByID("settings-gamepad-icon")) {
// hide it until someone figures out how to bind the positioning to the actual button
node->setVisible(false);
}
// main menu
if (auto menu = this->getChildByType<CCMenu>(0)) {

View file

@ -46,7 +46,7 @@ void crashlog::printMods(std::stringstream& stream) {
mod->isCurrentlyLoading() ? "o"sv :
mod->isEnabled() ? "x"sv :
mod->hasLoadProblems() ? "!"sv : // thank you for this bug report
mod->targetsOutdatedGDVersion() ? "*"sv : // thank you very much for this bug report
mod->targetsOutdatedVersion() ? "*"sv : // thank you very much for this bug report
mod->shouldLoad() ? "~"sv :
" "sv,
mod->getVersion().toVString(), mod->getID()

View file

@ -71,11 +71,7 @@ std::vector<LoadProblem> Loader::getAllProblems() const {
std::vector<LoadProblem> Loader::getLoadProblems() const {
std::vector<LoadProblem> result;
for (auto problem : this->getAllProblems()) {
if (
problem.type != LoadProblem::Type::Recommendation &&
problem.type != LoadProblem::Type::Suggestion &&
problem.type != LoadProblem::Type::UnsupportedVersion
) {
if (problem.isProblem()) {
result.push_back(problem);
}
}
@ -84,7 +80,7 @@ std::vector<LoadProblem> Loader::getLoadProblems() const {
std::vector<LoadProblem> Loader::getOutdated() const {
std::vector<LoadProblem> result;
for (auto problem : this->getAllProblems()) {
if (problem.type == LoadProblem::Type::UnsupportedVersion) {
if (problem.isOutdated()) {
result.push_back(problem);
}
}
@ -93,10 +89,7 @@ std::vector<LoadProblem> Loader::getOutdated() const {
std::vector<LoadProblem> Loader::getRecommendations() const {
std::vector<LoadProblem> result;
for (auto problem : this->getAllProblems()) {
if (
problem.type == LoadProblem::Type::Recommendation ||
problem.type == LoadProblem::Type::Suggestion
) {
if (problem.isSuggestion()) {
result.push_back(problem);
}
}

View file

@ -42,6 +42,11 @@ Loader::Impl::~Impl() = default;
// Initialization
bool Loader::Impl::isForwardCompatMode() {
#ifdef GEODE_IS_ANDROID
// forward compat mode doesn't really make sense on android
return false;
#endif
if (!m_forwardCompatMode.has_value()) {
m_forwardCompatMode = !this->getGameVersion().empty() &&
this->getGameVersion() != GEODE_STR(GEODE_GD_VERSION);
@ -384,6 +389,37 @@ void Loader::Impl::buildModGraph() {
}
void Loader::Impl::loadModGraph(Mod* node, bool early) {
// Check version first, as it's not worth trying to load a mod with an
// invalid target version
// Also this makes it so that when GD updates, outdated mods get shown as
// "Outdated" in the UI instead of "Missing Dependencies"
auto res = node->getMetadata().checkGameVersion();
if (!res) {
this->addProblem({
LoadProblem::Type::UnsupportedVersion,
node,
res.unwrapErr()
});
log::error("{}", res.unwrapErr());
log::popNest();
return;
}
if (!this->isModVersionSupported(node->getMetadata().getGeodeVersion())) {
this->addProblem({
node->getMetadata().getGeodeVersion() > this->getVersion() ? LoadProblem::Type::NeedsNewerGeodeVersion : LoadProblem::Type::UnsupportedGeodeVersion,
node,
fmt::format(
"Geode version {}\nis required to run this mod\n(installed: {})",
node->getMetadata().getGeodeVersion().toVString(),
this->getVersion().toVString()
)
});
log::error("Unsupported Geode version: {}", node->getMetadata().getGeodeVersion());
log::popNest();
return;
}
if (node->hasUnresolvedDependencies()) {
log::debug("{} {} has unresolved dependencies", node->getID(), node->getVersion());
return;
@ -444,35 +480,6 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) {
log::popNest();
return;
}
auto res = node->getMetadata().checkGameVersion();
if (!res) {
this->addProblem({
LoadProblem::Type::UnsupportedVersion,
node,
res.unwrapErr()
});
log::error("{}", res.unwrapErr());
m_refreshingModCount -= 1;
log::popNest();
return;
}
if (!this->isModVersionSupported(node->getMetadata().getGeodeVersion())) {
this->addProblem({
node->getMetadata().getGeodeVersion() > this->getVersion() ? LoadProblem::Type::NeedsNewerGeodeVersion : LoadProblem::Type::UnsupportedGeodeVersion,
node,
fmt::format(
"Geode version {}\nis required to run this mod\n(installed: {})",
node->getMetadata().getGeodeVersion().toVString(),
this->getVersion().toVString()
)
});
log::error("Unsupported Geode version: {}", node->getMetadata().getGeodeVersion());
m_refreshingModCount -= 1;
log::popNest();
return;
}
}
if (early) {
@ -524,6 +531,10 @@ void Loader::Impl::findProblems() {
log::debug("{} is not enabled", id);
continue;
}
if (mod->targetsOutdatedVersion()) {
log::debug("{} is outdated", id);
continue;
}
log::debug("{}", id);
log::pushNest();

View file

@ -255,13 +255,13 @@ bool Mod::hasSavedValue(std::string_view key) {
bool Mod::hasLoadProblems() const {
return m_impl->hasLoadProblems();
}
bool Mod::targetsOutdatedGDVersion() const {
std::optional<LoadProblem> Mod::targetsOutdatedVersion() const {
for (auto problem : this->getAllProblems()) {
if (problem.type == LoadProblem::Type::UnsupportedVersion) {
return true;
if (problem.isOutdated()) {
return problem;
}
}
return false;
return std::nullopt;
}
std::vector<LoadProblem> Mod::getAllProblems() const {
return m_impl->getProblems();
@ -269,11 +269,7 @@ std::vector<LoadProblem> Mod::getAllProblems() const {
std::vector<LoadProblem> Mod::getProblems() const {
std::vector<LoadProblem> result;
for (auto problem : this->getAllProblems()) {
if (
problem.type != LoadProblem::Type::Recommendation &&
problem.type != LoadProblem::Type::Suggestion &&
problem.type != LoadProblem::Type::UnsupportedVersion
) {
if (problem.isProblem()) {
result.push_back(problem);
}
}
@ -282,10 +278,7 @@ std::vector<LoadProblem> Mod::getProblems() const {
std::vector<LoadProblem> Mod::getRecommendations() const {
std::vector<LoadProblem> result;
for (auto problem : this->getAllProblems()) {
if (
problem.type == LoadProblem::Type::Recommendation ||
problem.type == LoadProblem::Type::Suggestion
) {
if (problem.isSuggestion()) {
result.push_back(problem);
}
}

View file

@ -706,11 +706,7 @@ bool Mod::Impl::isCurrentlyLoading() const {
bool Mod::Impl::hasLoadProblems() const {
for (auto const& problem : m_problems) {
if (
problem.type != LoadProblem::Type::Recommendation &&
problem.type != LoadProblem::Type::Suggestion &&
problem.type != LoadProblem::Type::UnsupportedVersion
) {
if (problem.isProblem()) {
return true;
}
}

View file

@ -21,6 +21,7 @@ std::string Loader::Impl::getGameVersion() {
case 37: m_gdVersion = "2.200"; break;
case 38: m_gdVersion = "2.205"; break;
case 39: m_gdVersion = "2.206"; break;
case 40: m_gdVersion = "2.2074"; break;
default: m_gdVersion = std::to_string(version_code);
}
}

View file

@ -12,6 +12,8 @@ $on_mod(Loaded) {
ColorProvider::get()->define("mod-list-version-label-updates-available"_spr, ccc3(88, 202, 255));
ColorProvider::get()->define("mod-list-restart-required-label"_spr, ccc3(153, 245, 245));
ColorProvider::get()->define("mod-list-restart-required-label-bg"_spr, ccc3(123, 156, 163));
ColorProvider::get()->define("mod-list-outdated-label"_spr, ccc3(245, 153, 245));
ColorProvider::get()->define("mod-list-outdated-label-bg"_spr, ccc3(156, 123, 163));
ColorProvider::get()->define("mod-list-search-bg"_spr, { 83, 65, 109, 255 });
ColorProvider::get()->define("mod-list-updates-available-bg"_spr, { 139, 89, 173, 255 });
ColorProvider::get()->define("mod-list-updates-available-bg-2"_spr, { 45, 110, 222, 255 });

View file

@ -95,6 +95,17 @@ bool ModItem::init(ModSource&& source) {
m_restartRequiredLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .75f));
m_infoContainer->addChild(m_restartRequiredLabel);
m_outdatedLabel = createTagLabel(
fmt::format("Outdated (GD {})", m_source.getMetadata().getGameVersion().value_or("*")),
{
to3B(ColorProvider::get()->color("mod-list-outdated-label"_spr)),
to3B(ColorProvider::get()->color("mod-list-outdated-label-bg"_spr))
}
);
m_outdatedLabel->setID("outdated-label");
m_outdatedLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .75f));
m_infoContainer->addChild(m_outdatedLabel);
m_downloadBarContainer = CCNode::create();
m_downloadBarContainer->setID("download-bar-container");
m_downloadBarContainer->setContentSize({ 320, 30 });
@ -185,7 +196,7 @@ bool ModItem::init(ModSource&& source) {
m_viewMenu->addChild(m_enableToggle);
m_viewMenu->updateLayout();
}
if (mod->hasLoadProblems() || mod->targetsOutdatedGDVersion()) {
if (mod->hasLoadProblems() || mod->targetsOutdatedVersion()) {
auto viewErrorSpr = createGeodeCircleButton(
CCSprite::createWithSpriteFrameName("exclamation.png"_spr), 1.f,
CircleBaseSize::Small
@ -342,7 +353,6 @@ void ModItem::updateState() {
m_downloadBarContainer->setVisible(false);
m_downloadWaiting->setVisible(false);
}
m_infoContainer->updateLayout();
// Set default colors based on source to start off with
// (possibly overriding later based on state)
@ -410,16 +420,22 @@ void ModItem::updateState() {
m_titleContainer->updateLayout();
// If there were problems, tint the BG red
m_outdatedLabel->setVisible(false);
if (m_source.asMod()) {
if (m_source.asMod()->hasLoadProblems()) {
m_bg->setColor("mod-list-errors-found"_cc3b);
m_bg->setOpacity(isGeodeTheme() ? 25 : 90);
}
if (m_source.asMod()->targetsOutdatedGDVersion()) {
m_bg->setOpacity(isGeodeTheme() ? 0 : 0);
if (m_source.asMod()->targetsOutdatedVersion()) {
m_bg->setColor("mod-list-outdated-label"_cc3b);
m_bg->setOpacity(isGeodeTheme() ? 25 : 90);
m_outdatedLabel->setVisible(true);
m_developers->setVisible(false);
}
}
m_infoContainer->updateLayout();
// Highlight item via BG if it wants to restart for extra UI attention
if (wantsRestart) {
m_bg->setColor("mod-list-restart-required-label"_cc3b);
@ -541,7 +557,38 @@ void ModItem::onView(CCObject*) {
}
void ModItem::onViewError(CCObject*) {
if (auto mod = m_source.asMod()) {
ModErrorPopup::create(mod)->show();
if (auto problem = mod->targetsOutdatedVersion()) {
std::string issue;
std::string howToFix;
switch (problem->type) {
default:
case LoadProblem::Type::UnsupportedVersion: {
issue = fmt::format("<cy>{}</c>", problem->message);
howToFix = "wait for the developer to <cj>release an update to "
"the mod</c> that supports the newer version.";
} break;
case LoadProblem::Type::UnsupportedGeodeVersion: {
issue = "This mod is made for a <cp>newer version of Geode</c>.";
howToFix = "<cp>update Geode</c> by enabling <co>Automatic Updates</c> "
"or redownloading it from the Geode website.";
} break;
case LoadProblem::Type::NeedsNewerGeodeVersion: {
issue = "This mod is made for an <cy>older version of Geode</c>.";
howToFix = "wait for the developer to <cj>release an update to "
"the mod</c> that supports the newer version.";
} break;
}
FLAlertLayer::create(
"Outdated",
fmt::format("{} Please {}", issue, howToFix),
"OK"
)->show();
}
else {
ModErrorPopup::create(mod)->show();
}
}
}
void ModItem::onEnable(CCObject*) {

View file

@ -25,6 +25,7 @@ protected:
CCNode* m_recommendedBy;
CCLabelBMFont* m_developerLabel;
ButtonSprite* m_restartRequiredLabel;
ButtonSprite* m_outdatedLabel;
CCNode* m_downloadWaiting;
CCNode* m_downloadBarContainer;
Slider* m_downloadBar;

View file

@ -13,7 +13,7 @@ bool InstalledModsQuery::preCheck(ModSource const& src) const {
}
// If only errors requested, only show mods with errors (duh)
if (type == InstalledModListType::OnlyOutdated) {
return src.asMod()->targetsOutdatedGDVersion();
return src.asMod()->targetsOutdatedVersion().has_value();
}
if (type == InstalledModListType::OnlyErrors) {
return src.asMod()->hasLoadProblems();

View file

@ -230,7 +230,13 @@ void filterModsWithLocalQuery(ModListSource::ProvidedMods& mods, Query const& qu
if (a.second != b.second) {
return a.second > b.second;
}
// Sort secondarily alphabetically
// Make sure outdated mods are always last by default
auto aIsOutdated = a.first.getMetadata().checkGameVersion().isErr();
auto bIsOutdated = b.first.getMetadata().checkGameVersion().isErr();
if (aIsOutdated != bIsOutdated) {
return !aIsOutdated;
}
// Fallback sort alphabetically
return utils::string::caseInsensitiveCompare(
a.first.getMetadata().getName(),
b.first.getMetadata().getName()