Merge remote-tracking branch 'main-repo/main' into altalk

This commit is contained in:
altalk23 2022-12-10 17:08:45 +03:00
commit fc394c3cac
64 changed files with 3283 additions and 3323 deletions

View file

@ -91,6 +91,10 @@ if (NOT EXISTS ${GEODE_BIN_PATH})
make_directory(${GEODE_BIN_PATH})
endif()
if (NOT EXISTS ${GEODE_BIN_PATH}/${PROJECT_VERSION} AND EXISTS ${GEODE_BIN_PATH}/nightly/)
set(GEODE_LINK_NIGHTLY 1)
endif()
if (${GEODE_LINK_NIGHTLY})
set(GEODE_PLATFORM_BIN_PATH ${GEODE_BIN_PATH}/nightly/${GEODE_PLATFORM_BINARY})
else()

View file

@ -1 +1 @@
0.7.0
0.6.1

View file

@ -5319,7 +5319,7 @@ class TextArea : cocos2d::CCSprite {
virtual void draw() {}
virtual void setOpacity(unsigned char) = mac 0x19f760, win 0x33800;
bool init(gd::string str, char const* font, float width, float height, cocos2d::CCPoint anchor, float scale, bool disableColor) = mac 0x19ec70, win 0x33370, ios 0x92444;
static TextArea* create(gd::string str, char const* font, float width, float height, cocos2d::CCPoint anchor, float scale, bool disableColor) = mac 0x19eb40, win 0x33270;
static TextArea* create(gd::string str, char const* font, float scale, float width, cocos2d::CCPoint anchor, float height, bool disableColor) = mac 0x19eb40, win 0x33270;
void colorAllCharactersTo(cocos2d::ccColor3B color) = win 0x33830;
void setString(gd::string str) = mac 0x19eda0, win 0x33480;

View file

@ -22,7 +22,6 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
src/loader/*.cpp
src/main.cpp
src/utils/*.cpp
src/index/*.cpp
src/ui/nodes/*.cpp
src/ui/internal/*.cpp
src/ui/internal/credits/*.cpp
@ -110,7 +109,6 @@ target_include_directories(${PROJECT_NAME} PRIVATE
src/internal/
src/platform/
src/gui/
src/index/
md4c/src/
hash/
./ # lilac

View file

@ -6,5 +6,6 @@
#include "loader/Mod.hpp"
#include "loader/Setting.hpp"
#include "loader/SettingEvent.hpp"
#include "loader/Dirs.hpp"
#include <Geode/DefaultInclude.hpp>

View file

@ -0,0 +1,59 @@
#pragma once
#include "../external/filesystem/fs/filesystem.hpp"
#include "../DefaultInclude.hpp"
namespace geode::dirs {
/**
* Directory where Geometry Dash is
*/
GEODE_DLL ghc::filesystem::path getGameDir();
/**
* Directory where GD saves its files
*/
GEODE_DLL ghc::filesystem::path getSaveDir();
/**
* Directory where Geode is
*/
GEODE_DLL ghc::filesystem::path getGeodeDir();
/**
* Directory where Geode saves its files
*/
GEODE_DLL ghc::filesystem::path getGeodeSaveDir();
/**
* Directory where Geode's resources are stored
*/
GEODE_DLL ghc::filesystem::path getGeodeResourcesDir();
/**
* Directory where Geode's resources are stored
*/
GEODE_DLL ghc::filesystem::path getGeodeLogDir();
/**
* Directory to store temporary files
*/
GEODE_DLL ghc::filesystem::path getTempDir();
/**
* Directory where mods are stored by default
*/
GEODE_DLL ghc::filesystem::path getModsDir();
/**
* Directory where mods' save data is stored
*/
GEODE_DLL ghc::filesystem::path getModsSaveDir();
/**
* Directory where mods' unzipped packages are stored at runtime
*/
GEODE_DLL ghc::filesystem::path getModRuntimeDir();
/**
* Directory where mods' config files lie
*/
GEODE_DLL ghc::filesystem::path getModConfigDir();
/**
* Directory where Geode stores the cached index
*/
GEODE_DLL ghc::filesystem::path getIndexDir();
/**
* Directory where crashlogs are stored
*/
GEODE_DLL ghc::filesystem::path getCrashlogsDir();
}

View file

@ -71,8 +71,9 @@ namespace geode {
this->enable();
}
EventListener(std::function<Callback> fn, T filter = T()) :
m_callback(fn), m_filter(filter) {
EventListener(std::function<Callback> fn, T filter = T())
: m_callback(fn), m_filter(filter)
{
this->enable();
}
@ -86,6 +87,10 @@ namespace geode {
this->enable();
}
// todo: maybe add these?
EventListener(EventListener const& other) = delete;
EventListener(EventListener&& other) = delete;
void bind(std::function<Callback> fn) {
m_callback = fn;
}
@ -96,24 +101,24 @@ namespace geode {
}
protected:
std::function<Callback> m_callback;
std::function<Callback> m_callback = nullptr;
T m_filter;
};
class GEODE_DLL Event {
private:
static std::unordered_set<EventListenerProtocol*> s_listeners;
Mod* m_sender;
friend EventListenerProtocol;
public:
Mod* sender;
void postFrom(Mod* sender);
inline void post() {
postFrom(Mod::get());
template<class = void>
void post() {
postFrom(getMod());
}
Mod* getSender();
virtual ~Event();
};
}

View file

@ -0,0 +1,109 @@
#pragma once
#include "Types.hpp"
#include "ModInfo.hpp"
#include "Event.hpp"
#include "../utils/Result.hpp"
#include <unordered_set>
namespace geode {
using UpdateFinished = std::monostate;
using UpdateProgress = std::pair<uint8_t, std::string>;
using UpdateError = std::string;
using UpdateStatus = std::variant<UpdateFinished, UpdateProgress, UpdateError>;
struct GEODE_DLL ModInstallEvent : public Event {
const std::string modID;
const UpdateStatus status;
};
class GEODE_DLL ModInstallFilter : public EventFilter<ModInstallEvent> {
protected:
std::string m_id;
public:
using Callback = void(ModInstallEvent*);
ListenerResult handle(std::function<Callback> fn, ModInstallEvent* event);
ModInstallFilter(std::string const& id);
};
struct GEODE_DLL IndexUpdateEvent : public Event {
const UpdateStatus status;
IndexUpdateEvent(const UpdateStatus status);
};
class GEODE_DLL IndexUpdateFilter : public EventFilter<IndexUpdateEvent> {
public:
using Callback = void(IndexUpdateEvent*);
ListenerResult handle(std::function<Callback> fn, IndexUpdateEvent* event);
IndexUpdateFilter();
};
struct IndexSourceImpl;
struct GEODE_DLL IndexSourceImplDeleter {
void operator()(IndexSourceImpl* src);
};
struct SourceUpdateEvent;
using IndexSourcePtr = std::unique_ptr<IndexSourceImpl, IndexSourceImplDeleter>;
struct GEODE_DLL IndexItem {
std::string sourceRepository;
ghc::filesystem::path path;
ModInfo info;
struct {
std::string url;
std::string hash;
std::unordered_set<PlatformID> platforms;
} download;
bool isFeatured;
/**
* Create IndexItem from a directory
*/
static Result<std::shared_ptr<IndexItem>> createFromDir(
std::string const& sourceRepository,
ghc::filesystem::path const& dir
);
};
using IndexItemHandle = std::shared_ptr<IndexItem>;
class GEODE_DLL Index final {
protected:
// 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>;
std::vector<IndexSourcePtr> m_sources;
std::unordered_map<std::string, UpdateStatus> m_sourceStatuses;
std::atomic<bool> m_triedToUpdate = false;
std::unordered_map<std::string, ItemVersions> m_items;
Index();
void onSourceUpdate(SourceUpdateEvent* event);
void checkSourceUpdates(IndexSourceImpl* src);
void downloadSource(IndexSourceImpl* src);
void updateSourceFromLocal(IndexSourceImpl* src);
void cleanupItems();
public:
static Index* get();
void addSource(std::string const& repository);
void removeSource(std::string const& repository);
std::vector<std::string> getSources() const;
std::vector<IndexItemHandle> getItems() const;
bool isKnownItem(std::string const& id, std::optional<size_t> version) const;
IndexItemHandle getItem(std::string const& id, std::optional<size_t> version) const;
IndexItemHandle getItem(ModInfo const& info) const;
IndexItemHandle getItem(Mod* mod) const;
bool updateAvailable(IndexItemHandle item) const;
bool hasTriedToUpdate() const;
bool isUpToDate() const;
void update(bool force = false);
};
}

View file

@ -83,23 +83,5 @@ namespace geode {
static void closePlatfromConsole();
bool didLastLaunchCrash() const;
ghc::filesystem::path getCrashLogDirectory() const;
/**
* Directory where Geometry Dash is
*/
ghc::filesystem::path getGameDirectory() const;
/**
* Directory where GD saves its files
*/
ghc::filesystem::path getSaveDirectory() const;
/**
* Directory where Geode is
*/
ghc::filesystem::path getGeodeDirectory() const;
/**
* Directory where Geode saves its files
*/
ghc::filesystem::path getGeodeSaveDirectory() const;
};
}

View file

@ -40,23 +40,21 @@ namespace geode {
};
}
#define $on_mod(type) \
template <class> \
void GEODE_CONCAT(geodeExecFunction, __LINE__)(ModStateEvent*); \
namespace { \
struct GEODE_CONCAT(ExecFuncUnique, __LINE__) {}; \
} \
static inline auto GEODE_CONCAT(Exec, __LINE__) = \
(geode::Loader::get()->scheduleOnModLoad( \
geode::Mod::get(), \
[]() { \
static auto _ = geode::EventListener( \
&GEODE_CONCAT(geodeExecFunction, __LINE__) < \
GEODE_CONCAT(ExecFuncUnique, __LINE__) >, \
geode::ModStateFilter(geode::Mod::get(), geode::ModEventType::type) \
); \
} \
), \
0); \
template <class> \
void GEODE_CONCAT(geodeExecFunction, __LINE__)(ModStateEvent*)
// clang-format off
#define $on_mod(type) \
template<class> \
void GEODE_CONCAT(geodeExecFunction, __LINE__)(ModStateEvent*); \
namespace { \
struct GEODE_CONCAT(ExecFuncUnique, __LINE__) {}; \
} \
static inline auto GEODE_CONCAT(Exec, __LINE__) = (geode::Loader::get()->scheduleOnModLoad(\
geode::Mod::get(), []() { \
new geode::EventListener( \
&GEODE_CONCAT(geodeExecFunction, __LINE__)<GEODE_CONCAT(ExecFuncUnique, __LINE__)>,\
geode::ModStateFilter(geode::Mod::get(), geode::ModEventType::type)\
); \
} \
), 0); \
template<class> \
void GEODE_CONCAT(geodeExecFunction, __LINE__)(ModStateEvent*)
// clang-format on

View file

@ -129,9 +129,6 @@ namespace geode {
}
};
class Mod;
class Setting;
/**
* Represents if a mod has been loaded &
* its dependencies resolved
@ -151,16 +148,10 @@ namespace geode {
Disabled,
};
static constexpr std::string_view GEODE_DIRECTORY = "geode";
static constexpr std::string_view GEODE_MOD_DIRECTORY = "mods";
static constexpr std::string_view GEODE_LOG_DIRECTORY = "log";
static constexpr std::string_view GEODE_RESOURCE_DIRECTORY = "resources";
static constexpr std::string_view GEODE_CONFIG_DIRECTORY = "config";
static constexpr std::string_view GEODE_TEMP_DIRECTORY = "temp";
static constexpr std::string_view GEODE_MOD_EXTENSION = ".geode";
static constexpr std::string_view GEODE_INDEX_DIRECTORY = "index";
constexpr std::string_view GEODE_MOD_EXTENSION = ".geode";
class Mod;
class Setting;
class Loader;
class Hook;
struct ModInfo;

View file

@ -1,88 +1,9 @@
#pragma once
#include "cplatform.h"
#include <string>
#include <functional>
namespace geode {
class PlatformID {
public:
enum {
Unknown = -1,
Windows,
MacOS,
iOS,
Android,
Linux,
};
using Type = decltype(Unknown);
Type m_value;
PlatformID(Type t) {
m_value = t;
}
PlatformID& operator=(Type t) {
m_value = t;
return *this;
}
bool operator==(int other) const {
return m_value == other;
}
bool operator==(PlatformID const& other) const {
return m_value == other.m_value;
}
bool operator<(PlatformID const& other) const {
return m_value < other.m_value;
}
bool operator>(PlatformID const& other) const {
return m_value > other.m_value;
}
operator int() const {
return m_value;
}
template <class T>
static PlatformID from(T t) {
return static_cast<Type>(t);
}
template <class T>
T to() const {
return static_cast<T>(m_value);
}
static constexpr char const* toString(Type lp) {
switch (lp) {
case Unknown: return "Unknown";
case Windows: return "Windows";
case MacOS: return "MacOS";
case iOS: return "iOS";
case Android: return "Android";
case Linux: return "Linux";
default: break;
}
return "Undefined";
}
};
}
namespace std {
template <>
struct hash<geode::PlatformID> {
inline std::size_t operator()(geode::PlatformID const& id) const {
return std::hash<geode::PlatformID::Type>()(id.m_value);
}
};
}
#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__)
#define GEODE_PRETTY_FUNCTION std::string(__FUNCSIG__)
#else
@ -92,7 +13,6 @@ namespace std {
// Windows
#ifdef GEODE_IS_WINDOWS
#define GEODE_PLATFORM_TARGET PlatformID::Windows
#define GEODE_HIDDEN
#define GEODE_INLINE __forceinline
#define GEODE_VIRTUAL_CONSTEXPR
@ -111,7 +31,6 @@ namespace std {
#elif defined(GEODE_IS_MACOS)
#define GEODE_PLATFORM_TARGET PlatformID::MacOS
#define GEODE_HIDDEN __attribute__((visibility("hidden")))
#define GEODE_INLINE inline __attribute__((always_inline))
#define GEODE_VIRTUAL_CONSTEXPR constexpr
@ -130,7 +49,6 @@ namespace std {
#elif defined(GEODE_IS_IOS)
#define GEODE_PLATFORM_TARGET PlatformID::iOS
#define GEODE_HIDDEN __attribute__((visibility("hidden")))
#define GEODE_INLINE inline __attribute__((always_inline))
#define GEODE_VIRTUAL_CONSTEXPR constexpr
@ -149,7 +67,6 @@ namespace std {
#elif defined(GEODE_IS_ANDROID)
#define GEODE_PLATFORM_TARGET PlatformID::Android
#define GEODE_HIDDEN __attribute__((visibility("hidden")))
#define GEODE_INLINE inline __attribute__((always_inline))
#define GEODE_VIRTUAL_CONSTEXPR constexpr
@ -171,3 +88,105 @@ namespace std {
#error "Unsupported Platform!"
#endif
namespace geode {
class PlatformID {
public:
enum {
Unknown = -1,
Windows,
MacOS,
iOS,
Android,
Linux,
};
using Type = decltype(Unknown);
Type m_value;
constexpr PlatformID(Type t) {
m_value = t;
}
constexpr PlatformID& operator=(Type t) {
m_value = t;
return *this;
}
constexpr bool operator==(int other) const {
return m_value == other;
}
constexpr bool operator==(PlatformID const& other) const {
return m_value == other.m_value;
}
constexpr bool operator<(PlatformID const& other) const {
return m_value < other.m_value;
}
constexpr bool operator>(PlatformID const& other) const {
return m_value > other.m_value;
}
constexpr operator int() const {
return m_value;
}
/**
* Parse string into PlatformID. String should be all-lowercase, for
* example "windows" or "linux"
*/
static GEODE_DLL PlatformID from(const char* str);
static GEODE_DLL PlatformID from(std::string const& str);
static constexpr char const* toString(Type lp) {
switch (lp) {
case Unknown: return "Unknown";
case Windows: return "Windows";
case MacOS: return "MacOS";
case iOS: return "iOS";
case Android: return "Android";
case Linux: return "Linux";
default: break;
}
return "Undefined";
}
template <class T>
requires requires(T t) {
static_cast<Type>(t);
}
constexpr static PlatformID from(T t) {
return static_cast<Type>(t);
}
template <class T>
requires requires(Type t) {
static_cast<T>(t);
}
constexpr T to() const {
return static_cast<T>(m_value);
}
};
}
namespace std {
template <>
struct hash<geode::PlatformID> {
inline std::size_t operator()(geode::PlatformID const& id) const {
return std::hash<geode::PlatformID::Type>()(id.m_value);
}
};
}
#ifdef GEODE_IS_WINDOWS
#define GEODE_PLATFORM_TARGET PlatformID::Windows
#elif defined(GEODE_IS_MACOS)
#define GEODE_PLATFORM_TARGET PlatformID::MacOS
#elif defined(GEODE_IS_IOS)
#define GEODE_PLATFORM_TARGET PlatformID::iOS
#elif defined(GEODE_IS_ANDROID)
#define GEODE_PLATFORM_TARGET PlatformID::Android
#endif

View file

@ -11,19 +11,14 @@ namespace geode {
concept InheritsCCNode = std::is_base_of_v<cocos2d::CCNode, T>;
// Base class; exists so event listeners can be placed dynamically at runtime
class GEODE_DLL AEnterLayerEvent : public Event {
protected:
std::string m_layerID;
cocos2d::CCNode* m_layer;
struct GEODE_DLL AEnterLayerEvent : public Event {
const std::string layerID;
cocos2d::CCNode* layer;
public:
AEnterLayerEvent(
std::string const& layerID,
cocos2d::CCNode* layer
);
std::string getID() const;
cocos2d::CCNode* getLayer() const;
};
class GEODE_DLL AEnterLayerFilter : public EventFilter<AEnterLayerEvent> {
@ -50,7 +45,7 @@ namespace geode {
) : AEnterLayerEvent(layerID, layer) {}
T* getLayer() const {
return static_cast<T*>(m_layer);
return static_cast<T*>(this->layer);
}
};

View file

@ -1,6 +1,7 @@
#pragma once
#include "../loader/Mod.hpp"
#include "../loader/Index.hpp"
namespace geode {
/**
@ -11,6 +12,10 @@ namespace geode {
* Open the info popup for a mod
*/
GEODE_DLL void openInfoPopup(Mod* mod);
/**
* Open the issue report popup for a mod
*/
GEODE_DLL void openIssueReportPopup(Mod* mod);
/**
* Open the store page for a mod (if it exists)
*/
@ -19,4 +24,25 @@ namespace geode {
* Open the settings popup for a mod (if it has any settings)
*/
GEODE_DLL void openSettingsPopup(Mod* mod);
/**
* Create a default logo sprite
* @param size Size of the sprite
*/
GEODE_DLL cocos2d::CCNode* createDefaultLogo(
cocos2d::CCSize const& size
);
/**
* Create a logo sprite for a mod
* @param size Size of the sprite
*/
GEODE_DLL cocos2d::CCNode* createModLogo(
Mod* mod, cocos2d::CCSize const& size
);
/**
* Create a logo sprite for an index item
* @param size Size of the sprite
*/
GEODE_DLL cocos2d::CCNode* createIndexItemLogo(
IndexItemHandle item, cocos2d::CCSize const& size
);
}

View file

@ -8,6 +8,7 @@
namespace geode {
constexpr auto NOTIFICATION_DEFAULT_TIME = 1.f;
constexpr auto NOTIFICATION_LONG_TIME = 4.f;
enum class NotificationIcon {
None,
@ -71,6 +72,12 @@ namespace geode {
void setIcon(cocos2d::CCSprite* icon);
void setTime(float time);
/**
* Set the wait time to default, wait the time and hide the notification.
* Equivalent to setTime(NOTIFICATION_DEFAULT_TIME)
*/
void waitAndHide();
/**
* Adds the notification to the current scene if it doesn't have a
* parent yet, and displays the show animation. If the time for the

View file

@ -3,11 +3,11 @@
#include "../external/json/json.hpp"
#include "casts.hpp"
#include "general.hpp"
#include <Geode/DefaultInclude.hpp>
#include "../DefaultInclude.hpp"
#include <cocos2d.h>
#include <functional>
#include <type_traits>
#include "../loader/Event.hpp"
// support converting ccColor3B / ccColor4B to / from json
namespace cocos2d {
@ -184,7 +184,7 @@ namespace geode {
}
}
// Ref
// Ref & Bug
namespace geode {
/**
* A smart pointer to a managed CCObject-deriving class. Retains shared
@ -306,6 +306,53 @@ namespace geode {
return m_obj > other.m_obj;
}
};
template <class Filter>
class EventListenerNode : public cocos2d::CCNode {
protected:
EventListener<Filter> m_listener;
public:
static EventListenerNode* create(EventListener<Filter> listener) {
auto ret = new EventListenerNode();
if (ret && ret->init()) {
ret->m_listener = listener;
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
static EventListenerNode* create(typename Filter::Callback callback) {
auto ret = new EventListenerNode();
if (ret && ret->init()) {
ret->m_listener = EventListener(callback);
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
template <class C>
static EventListenerNode* create(
C* cls, typename EventListener<Filter>::MemberFn<C> callback
) {
// for some reason msvc won't let me just call EventListenerNode::create...
// it claims no return value...
// despite me writing return EventListenerNode::create()......
auto ret = new EventListenerNode();
if (ret && ret->init()) {
ret->m_listener.bind(cls, callback);
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
};
}
// Cocos2d utils
@ -937,133 +984,4 @@ namespace geode::cocos {
return m_dict->allKeys(key)->count();
}
};
// namespace for storing implementation stuff for
// inline member functions
namespace {
// class that holds the lambda (probably should've just used
// std::function but hey, this one's heap-free!)
template <class F, class Ret, class... Args>
struct LambdaHolder {
bool m_assigned = false;
// lambdas don't implement operator= so we
// gotta do this wacky union stuff
union {
F m_lambda;
};
LambdaHolder() {}
~LambdaHolder() {
if (m_assigned) {
m_lambda.~F();
}
}
LambdaHolder(F&& func) {
this->assign(std::forward<F>(func));
}
Ret operator()(Args... args) {
if (m_assigned) {
return m_lambda(std::forward<Args>(args)...);
}
else {
return Ret();
}
}
void assign(F&& func) {
if (m_assigned) {
m_lambda.~F();
}
new (&m_lambda) F(func);
m_assigned = true;
}
};
// Extract parameters and return type from a lambda
template <class Func>
struct ExtractLambda : public ExtractLambda<decltype(&Func::operator())> {};
template <class C, class R, class... Args>
struct ExtractLambda<R (C::*)(Args...) const> {
using Ret = R;
using Params = std::tuple<Args...>;
};
// Class for storing the member function
template <class Base, class Func, class Args>
struct InlineMemberFunction;
template <class Base, class Func, class... Args>
struct InlineMemberFunction<Base, Func, std::tuple<Args...>> : public Base {
using Ret = typename ExtractLambda<Func>::Ret;
using Selector = Ret (Base::*)(Args...);
using Holder = LambdaHolder<Func, Ret, Args...>;
static inline Holder s_selector{};
Ret selector(Args... args) {
return s_selector(std::forward<Args>(args)...);
}
static Selector get(Func&& function) {
s_selector.assign(std::move(function));
return static_cast<Selector>(&InlineMemberFunction::selector);
}
};
}
/**
* Wrap a lambda into a member function pointer. Useful for creating
* callbacks that have to be members of a class without having to deal
* with all of the boilerplate associated with defining a new class
* member function.
*
* Do note that due to implementation problems, captures may have
* unexpected side-effects. In practice, lambda member functions with
* captures do not work properly in loops. If you assign the same
* member lambda to multiple different targets, they will share the
* same captured values.
*/
template <class Base, class Func>
[[deprecated(
"Due to too many implementation problems, "
"makeMemberFunction will be removed in the future."
)]] static auto
makeMemberFunction(Func&& function) {
return InlineMemberFunction<Base, Func, typename ExtractLambda<Func>::Params>::get(
std::move(function)
);
}
/**
* Create a SEL_MenuHandler out of a lambda with optional captures. Useful
* for adding callbacks to CCMenuItemSpriteExtras without needing to add
* the callback as a member to a class. Use the GEODE_MENU_SELECTOR class
* for even more concise code.
*
* Do note that due to implementation problems, captures may have
* unexpected side-effects. In practice, **you should not expect to be able
* to pass any more information than you can pass to a normal menu selector
* through captures**. If you assign the same member lambda to multiple
* different targets, they will share the same captured values.
*/
template <class Func>
[[deprecated(
"Due to too many implementation problems, "
"makeMenuSelector will be removed in the future."
)]] static cocos2d::SEL_MenuHandler
makeMenuSelector(Func&& selector) {
return reinterpret_cast<cocos2d::SEL_MenuHandler>(
makeMemberFunction<cocos2d::CCObject, Func>(std::move(selector))
);
}
#define GEODE_MENU_SELECTOR(senderArg, ...) \
makeMenuSelector([this](senderArg) { \
__VA_ARGS__; \
})
}

View file

@ -3,6 +3,7 @@
#include "Result.hpp"
#include "general.hpp"
#include "../external/json/json.hpp"
#include <Geode/DefaultInclude.hpp>
#include <fs/filesystem.hpp>
#include <string>
@ -10,15 +11,17 @@
namespace geode::utils::file {
GEODE_DLL Result<std::string> readString(ghc::filesystem::path const& path);
GEODE_DLL Result<nlohmann::json> readJson(ghc::filesystem::path const& path);
GEODE_DLL Result<byte_array> readBinary(ghc::filesystem::path const& path);
GEODE_DLL Result<> writeString(ghc::filesystem::path const& path, std::string const& data);
GEODE_DLL Result<> writeBinary(ghc::filesystem::path const& path, byte_array const& data);
GEODE_DLL Result<bool> createDirectory(ghc::filesystem::path const& path);
GEODE_DLL Result<bool> createDirectoryAll(ghc::filesystem::path const& path);
GEODE_DLL Result<std::vector<std::string>> listFiles(std::string const& path);
GEODE_DLL Result<std::vector<std::string>> listFilesRecursively(std::string const& path);
GEODE_DLL Result<> createDirectory(ghc::filesystem::path const& path);
GEODE_DLL Result<> createDirectoryAll(ghc::filesystem::path const& path);
GEODE_DLL Result<std::vector<ghc::filesystem::path>> listFiles(
ghc::filesystem::path const& path, bool recursive = false
);
class UnzipImpl;
@ -71,17 +74,21 @@ namespace geode::utils::file {
* @param dir Directory to unzip the contents to
*/
Result<> extractAllTo(Path const& dir);
/**
* Helper method for quickly unzipping a file
* @param from ZIP file to unzip
* @param to Directory to unzip to
* @param deleteZipAfter Whether to delete the zip after unzipping
* @returns Succesful result on success, errorful result on error
*/
static Result<> intoDir(
Path const& from,
Path const& to,
bool deleteZipAfter = false
);
};
/**
* Unzip file to directory
* @param from File to unzip
* @param to Directory to unzip to
* @returns Ok on success, Error on error
*/
GEODE_DLL Result<> unzipTo(ghc::filesystem::path const& from, ghc::filesystem::path const& to);
GEODE_DLL ghc::filesystem::path geodeRoot();
GEODE_DLL bool openFolder(ghc::filesystem::path const& path);
enum class PickMode {

View file

@ -2,7 +2,7 @@
#include "Result.hpp"
#include <Geode/DefaultInclude.hpp>
#include "../DefaultInclude.hpp"
#include <chrono>
#include <iomanip>
#include <sstream>
@ -41,6 +41,16 @@ namespace geode {
using TypeIdentityType = typename TypeIdentity<T>::type;
namespace utils {
// helper for std::visit
template<class... Ts> struct makeVisitor : Ts... { using Ts::operator()...; };
template<class... Ts> makeVisitor(Ts...) -> makeVisitor<Ts...>;
template<class T, class ... Args>
constexpr T getOr(std::variant<Args...> const& variant, T const& defValue) {
return std::holds_alternative<T>(variant) ?
std::get<T>(variant) : defValue;
}
constexpr unsigned int hash(char const* str, int h = 0) {
return !str[h] ? 5381 : (hash(str, h + 1) * 33) ^ str[h];
}

View file

@ -18,7 +18,6 @@ namespace geode::utils::map {
* beeing looked for, and false if not.
* @returns True if value matching `containFunc` was found,
* false if not.
* @author HJfod
*/
template <typename T, typename R>
bool contains(std::unordered_map<T, R> const& map, std::function<bool(R)> containFunc) {
@ -38,7 +37,6 @@ namespace geode::utils::map {
* @returns The value matching `selectFunc` if one was found,
* otherwise the default value for type R or `nullptr` if R is
* a pointer.
* @author HJfod
*/
template <class T, class R>
R select(std::unordered_map<T, R> const& map, std::function<bool(R)> selectFunc) {
@ -58,7 +56,6 @@ namespace geode::utils::map {
* return true if the item matches what is
* beeing looked for, and false if not.
* @returns Vector of all values that matched.
* @author HJfod
*/
template <class T, class R>
std::vector<R> selectAll(
@ -77,10 +74,9 @@ namespace geode::utils::map {
* Get all values in a map.
* @param map Map to get values from
* @returns Vector of all values.
* @author HJfod
*/
template <class T, class R>
std::vector<R> getValues(std::unordered_map<T, R> const& map) {
std::vector<R> values(std::unordered_map<T, R> const& map) {
std::vector<R> res;
for (auto const& [_, r] : map) {
res.push_back(r);
@ -92,10 +88,9 @@ namespace geode::utils::map {
* Get all keys in a map.
* @param map Map to get keys from
* @returns Vector of all keys.
* @author HJfod
*/
template <class T, class R>
std::vector<T> getKeys(std::unordered_map<T, R> const& map) {
std::vector<T> keys(std::unordered_map<T, R> const& map) {
std::vector<T> res;
for (auto const& [t, _] : map) {
res.push_back(t);

View file

@ -1,104 +0,0 @@
#include "../ui/internal/info/ModInfoLayer.hpp"
#include "../ui/internal/list/ModListLayer.hpp"
#include <Geode/utils/cocos.hpp>
#include <Index.hpp>
#include <InternalLoader.hpp>
#include <InternalMod.hpp>
USE_GEODE_NAMESPACE();
#pragma warning(disable : 4217)
template <class T = CCNode>
requires std::is_base_of_v<CCNode, T>
T* setIDSafe(CCNode* node, int index, char const* id) {
if constexpr (std::is_same_v<CCNode, T>) {
if (auto child = getChild(node, index)) {
child->setID(id);
return child;
}
}
else {
if (auto child = getChildOfType<T>(node, index)) {
child->setID(id);
return child;
}
}
return nullptr;
}
// clang-format off
#include <Geode/modify/LevelSearchLayer.hpp>
struct LevelSearchLayerIDs : Modify<LevelSearchLayerIDs, LevelSearchLayer> {
bool init() {
if (!LevelSearchLayer::init())
return false;
// set the funny ids
this->setID("creator-layer");
setIDSafe(this, 0, "creator-layer-bg");
getChildOfType<CCTextInputNode>(this, 0)->setID("search-bar");
getChildOfType<CCScale9Sprite>(this, 0)->setID("level-search-bg");
getChildOfType<CCScale9Sprite>(this, 1)->setID("level-search-bar-bg");
getChildOfType<CCScale9Sprite>(this, 2)->setID("quick-search-bg");
getChildOfType<CCScale9Sprite>(this, 3)->setID("difficulty-filters-bg");
getChildOfType<CCScale9Sprite>(this, 4)->setID("length-filters-bg");
getChildOfType<CCLabelBMFont>(this, 0)->setID("quick-search-title");
getChildOfType<CCLabelBMFont>(this, 1)->setID("filters-title");
getChildOfType<CCSprite>(this, 1)->setID("left-corner");
getChildOfType<CCSprite>(this, 2)->setID("right-corner");
if (auto filtermenu = getChildOfType<CCMenu>(this, 0)) {
filtermenu->setID("other-filter-menu");
setIDSafe(filtermenu, 0, "clear-filters-button");
setIDSafe(filtermenu, 1, "advanced-filters-button");
}
if (auto searchmenu = getChildOfType<CCMenu>(this, 1)) {
searchmenu->setID("search-button-menu");
setIDSafe(searchmenu, 0, "search-level-button");
setIDSafe(searchmenu, 1, "search-user-button");
}
if (auto quickmenu = getChildOfType<CCMenu>(this, 2)) {
quickmenu->setID("quick-search-menu");
setIDSafe(quickmenu, 0, "most-downloaded-button");
setIDSafe(quickmenu, 1, "most-liked-button");
setIDSafe(quickmenu, 2, "trending-button");
setIDSafe(quickmenu, 3, "recent-button");
setIDSafe(quickmenu, 4, "magic-button");
setIDSafe(quickmenu, 5, "awarded-button");
setIDSafe(quickmenu, 6, "followed-button");
setIDSafe(quickmenu, 7, "friends-button");
}
if (auto filtersmenu = getChildOfType<CCMenu>(this, 3)) {
filtersmenu->setID("difficulty-filter-menu");
setIDSafe(filtersmenu, 0, "na-filter-button");
setIDSafe(filtersmenu, 1, "easy-filter-button");
setIDSafe(filtersmenu, 2, "normal-filter-button");
setIDSafe(filtersmenu, 3, "hard-filter-button");
setIDSafe(filtersmenu, 4, "harder-filter-button");
setIDSafe(filtersmenu, 5, "insane-filter-button");
setIDSafe(filtersmenu, 6, "demon-filter-button");
setIDSafe(filtersmenu, 7, "auto-filter-button");
setIDSafe(filtersmenu, 8, "demon-type-filter-button");
}
if (auto filtersmenu = getChildOfType<CCMenu>(this, 4)) {
filtersmenu->setID("length-filter-menu");
setIDSafe(filtersmenu, 0, "clock-icon");
setIDSafe(filtersmenu, 1, "tiny-filter-button");
setIDSafe(filtersmenu, 2, "short-filter-button");
setIDSafe(filtersmenu, 3, "medium-filter-button");
setIDSafe(filtersmenu, 4, "long-filter-button");
setIDSafe(filtersmenu, 5, "xl-filter-button");
setIDSafe(filtersmenu, 6, "star-filter-button");
}
if (auto backmenu = getChildOfType<CCMenu>(this, 5)) {
backmenu->setID("exit-menu");
setIDSafe(backmenu, 0, "exit-button");
}
return true;
}
};
// clang-format on

View file

@ -1,10 +1,11 @@
#include <InternalLoader.hpp>
#include <array>
USE_GEODE_NAMESPACE();
#include <Geode/modify/LoadingLayer.hpp>
#include <fmt/format.h>
#include <Geode/utils/cocos.hpp>
USE_GEODE_NAMESPACE();
struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
bool m_updatingResources;
@ -27,28 +28,15 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
label->setID("geode-loaded-info");
this->addChild(label);
// for some reason storing the listener as a field caused the
// destructor for the field not to be run
this->addChild(EventListenerNode<ResourceDownloadFilter>::create(
this, &CustomLoadingLayer::updateResourcesProgress
));
// verify loader resources
if (!InternalLoader::get()->verifyLoaderResources(std::bind(
&CustomLoadingLayer::updateResourcesProgress, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3
))) {
// auto bg = CCScale9Sprite::create(
// "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }
// );
// bg->setScale(.6f);
// bg->setColor({ 0, 0, 0 });
// bg->setOpacity(150);
// bg->setPosition(winSize / 2);
// this->addChild(bg);
// m_fields->m_updatingResourcesBG = bg;
// auto label = CCLabelBMFont::create("", "goldFont.fnt");
// label->setScale(1.1f);
// bg->addChild(label);
if (!InternalLoader::get()->verifyLoaderResources()) {
m_fields->m_updatingResources = true;
this->setUpdateText("Downloading Resources");
}
@ -57,41 +45,33 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
void setUpdateText(std::string const& text) {
m_textArea->setString(text.c_str());
// m_fields->m_updatingResources->setString(text.c_str());
// m_fields->m_updatingResourcesBG->setContentSize({
// m_fields->m_updatingResources->getScaledContentSize().width + 30.f,
// 50.f
// });
// m_fields->m_updatingResources->setPosition(
// m_fields->m_updatingResourcesBG->getContentSize() / 2
// );
}
void updateResourcesProgress(UpdateStatus status, std::string const& info, uint8_t progress) {
switch (status) {
case UpdateStatus::Progress: {
this->setUpdateText("Downloading Resources: " + std::to_string(progress) + "%");
} break;
case UpdateStatus::Finished: {
void updateResourcesProgress(ResourceDownloadEvent* event) {
std::visit(makeVisitor {
[&](UpdateProgress const& progress) {
this->setUpdateText(fmt::format(
"Downloading Resources: {}%", progress.first
));
},
[&](UpdateFinished) {
this->setUpdateText("Resources Downloaded");
m_fields->m_updatingResources = false;
this->loadAssets();
} break;
case UpdateStatus::Failed: {
},
[&](UpdateError const& error) {
InternalLoader::platformMessageBox(
"Error updating resources",
"Unable to update Geode resources: " + info +
".\n"
"The game will be loaded as normal, but please be aware "
"that it may very likely crash."
"Unable to update Geode resources: " +
error + ".\n"
"The game will be loaded as normal, but please be aware "
"that it may very likely crash."
);
this->setUpdateText("Resource Download Failed");
m_fields->m_updatingResources = false;
this->loadAssets();
} break;
}
}
}, event->status);
}
void loadAssets() {

View file

@ -1,16 +1,17 @@
#include <Geode/utils/cocos.hpp>
#include "../ui/internal/info/ModInfoLayer.hpp"
#include "../ui/internal/list/ModListLayer.hpp"
#include <Geode/ui/BasedButtonSprite.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/ui/Notification.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/ui/Popup.hpp>
#include <Geode/utils/cocos.hpp>
#include <Index.hpp>
#include <Geode/loader/Index.hpp>
#include <InternalLoader.hpp>
#include "../ids/AddIDs.hpp"
#include <InternalMod.hpp>
#include <Geode/modify/Modify.hpp>
#include <Geode/modify/MenuLayer.hpp>
USE_GEODE_NAMESPACE();
@ -18,42 +19,9 @@ USE_GEODE_NAMESPACE();
class CustomMenuLayer;
static Ref<Notification> g_indexUpdateNotif = nullptr;
static Ref<Notification> INDEX_UPDATE_NOTIF = nullptr;
static Ref<CCSprite> g_geodeButton = nullptr;
static void addUpdateIcon(char const* icon = "updates-available.png"_spr) {
if (g_geodeButton && Index::get()->areUpdatesAvailable()) {
auto updateIcon = CCSprite::createWithSpriteFrameName(icon);
updateIcon->setPosition(g_geodeButton->getContentSize() - CCSize { 10.f, 10.f });
updateIcon->setZOrder(99);
updateIcon->setScale(.5f);
g_geodeButton->addChild(updateIcon);
}
}
static void updateIndexProgress(UpdateStatus status, std::string const& info, uint8_t progress) {
if (status == UpdateStatus::Failed) {
g_indexUpdateNotif->setIcon(NotificationIcon::Error);
g_indexUpdateNotif->setString("Index update failed");
g_indexUpdateNotif->setTime(2.f);
g_indexUpdateNotif = nullptr;
addUpdateIcon("updates-failed.png"_spr);
}
if (status == UpdateStatus::Finished) {
g_indexUpdateNotif->setIcon(NotificationIcon::Success);
if (Index::get()->areUpdatesAvailable()) {
g_indexUpdateNotif->setString("Updates Available");
addUpdateIcon();
} else {
g_indexUpdateNotif->setString("Everything Up-to-Date");
}
g_indexUpdateNotif->setTime(2.f);
g_indexUpdateNotif = nullptr;
}
}
#include <Geode/modify/MenuLayer.hpp>
struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
void destructor() {
g_geodeButton = nullptr;
@ -81,8 +49,6 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
))
.orMake<ButtonSprite>("!!");
addUpdateIcon();
auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu"));
auto btn = CCMenuItemSpriteExtra::create(
@ -122,9 +88,7 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
"No", "Send",
[](auto, bool btn2) {
if (btn2) {
ModInfoLayer::showIssueReportPopup(
InternalMod::get()->getModInfo()
);
geode::openIssueReportPopup(InternalMod::get());
}
},
false
@ -135,18 +99,39 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
}
// update mods index
if (!g_indexUpdateNotif && !Index::get()->isIndexUpdated()) {
g_indexUpdateNotif = Notification::create(
if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) {
this->addChild(EventListenerNode<IndexUpdateFilter>::create(
this, &CustomMenuLayer::onIndexUpdate
));
INDEX_UPDATE_NOTIF = Notification::create(
"Updating Index", NotificationIcon::Loading, 0
);
g_indexUpdateNotif->show();
Index::get()->updateIndex(updateIndexProgress);
INDEX_UPDATE_NOTIF->show();
Index::get()->update();
}
return true;
}
void onIndexUpdate(IndexUpdateEvent* event) {
if (!INDEX_UPDATE_NOTIF) return;
std::visit(makeVisitor {
[](UpdateProgress const& prog) {},
[](UpdateFinished const&) {
INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Success);
INDEX_UPDATE_NOTIF->setString("Index Up-to-Date");
INDEX_UPDATE_NOTIF->waitAndHide();
INDEX_UPDATE_NOTIF = nullptr;
},
[](UpdateError const& info) {
INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Error);
INDEX_UPDATE_NOTIF->setString(info);
INDEX_UPDATE_NOTIF->setTime(NOTIFICATION_LONG_TIME);
INDEX_UPDATE_NOTIF = nullptr;
},
}, event->status);
}
void onGeode(CCObject*) {
ModListLayer::scene();
}

View file

@ -22,21 +22,54 @@ T* setIDSafe(CCNode* node, int index, const char* id) {
return nullptr;
}
static CCMenu* detachIntoOwnMenu(CCNode* parent, CCNode* node, const char* menuID, Layout* layout) {
auto oldMenu = node->getParent();
template <typename ...Args>
void setIDs(CCNode* node, int startIndex, Args... args) {
for (auto i : { args... }) {
setIDSafe(node, startIndex, i);
++startIndex;
}
}
static void switchToMenu(CCNode* node, CCMenu* menu) {
auto worldPos = node->getParent()->convertToWorldSpace(node->getPosition());
node->retain();
node->removeFromParent();
menu->addChild(node);
node->setPosition(menu->convertToNodeSpace(worldPos));
}
static void switchChildToMenu(CCNode* parent, int idx, CCMenu* menu) {
switchToMenu(static_cast<CCNode*>(parent->getChildren()->objectAtIndex(idx)), menu);
}
template <typename ...Args>
static void switchChildrenToMenu(CCNode* parent, CCMenu* menu, Args... args) {
for (auto i : { args... }) {
switchChildToMenu(parent, i, menu);
}
}
template <typename T, typename ...Args>
static CCMenu* detachAndCreateMenu(CCNode* parent, const char* menuID, Layout* layout, T first, Args... args) {
auto oldMenu = first->getParent();
first->retain();
first->removeFromParent();
auto newMenu = CCMenu::create();
newMenu->setPosition(oldMenu->convertToWorldSpace(node->getPosition()));
newMenu->setPosition(parent->convertToNodeSpace(oldMenu->convertToWorldSpace(first->getPosition())));
newMenu->setID(menuID);
node->setPosition(0, 0);
newMenu->addChild(node);
newMenu->setZOrder(oldMenu->getZOrder());
newMenu->setLayout(layout);
parent->addChild(newMenu);
node->release();
first->setPosition(0, 0);
newMenu->addChild(first);
first->release();
(switchToMenu(args, newMenu), ...);
return newMenu;
}
}

View file

@ -25,15 +25,17 @@ $register_ids(CreatorLayer) {
// move vault button to its own menu
if (auto lockBtn = setIDSafe(menu, -2, "vault-button")) {
detachIntoOwnMenu(this, lockBtn, "top-right-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin)
detachAndCreateMenu(this, "top-right-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin),
lockBtn
);
}
// move treasure room button to its own menu
if (auto roomBtn = setIDSafe(menu, -1, "treasure-room-button")) {
detachIntoOwnMenu(this, roomBtn, "bottom-right-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End)
detachAndCreateMenu(this, "bottom-right-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End),
roomBtn
);
}
}

219
loader/src/ids/EditorUI.cpp Normal file
View file

@ -0,0 +1,219 @@
#include <Geode/Modify.hpp>
#include <Geode/Bindings.hpp>
#include <Geode/utils/cocos.hpp>
#include "AddIDs.hpp"
USE_GEODE_NAMESPACE();
$register_ids(EditorUI) {
setIDSafe(this, 0, "position-slider");
setIDSafe(this, this->getChildrenCount() - 2, "layer-index-label");
setIDSafe(this, this->getChildrenCount() - 1, "object-info-label");
if (auto menu = getChildOfType<CCMenu>(this, 0)) {
menu->setID("toolbar-categories-menu");
setIDs(menu, 0,
"build-button",
"edit-button",
"delete-button",
"swipe-button",
"free-move-button",
"snap-button",
"rotate-button",
"undo-button",
"redo-button",
"delete-button",
"music-playback-button",
"playtest-button",
"stop-playtest-button",
"zoom-in-button",
"zoom-out-button",
"link-button",
"unlink-button"
);
detachAndCreateMenu(
this,
"toolbar-toggles-menu",
GridLayout::create(2, GridAlignment::Begin, GridDirection::Column),
menu->getChildByID("swipe-button"),
menu->getChildByID("free-move-button"),
menu->getChildByID("snap-button"),
menu->getChildByID("rotate-button")
);
detachAndCreateMenu(
this,
"top-left-menu",
RowLayout::create(),
menu->getChildByID("undo-button"),
menu->getChildByID("redo-button"),
menu->getChildByID("delete-button")
);
detachAndCreateMenu(
this,
"playback-menu",
RowLayout::create(),
menu->getChildByID("music-playback-button")
);
detachAndCreateMenu(
this,
"playtest-menu",
RowLayout::create(),
menu->getChildByID("playtest-button"),
menu->getChildByID("stop-playtest-button")
);
detachAndCreateMenu(
this,
"zoom-menu",
ColumnLayout::create(),
menu->getChildByID("zoom-in-button"),
menu->getChildByID("zoom-out-button")
);
detachAndCreateMenu(
this,
"link-menu",
ColumnLayout::create(),
menu->getChildByID("link-button"),
menu->getChildByID("unlink-button")
);
}
if (auto menu = getChildOfType<CCMenu>(this, 1)) {
menu->setID("delete-category-menu");
setIDs(menu, 0,
"delete-button",
"delete-startpos-button",
"delete-all-of-button",
"delete-filter-none",
"delete-filter-static",
"delete-filter-detail",
"delete-filter-custom",
"delete-help-icon"
);
detachAndCreateMenu(
menu,
"delete-button-menu",
GridLayout::create(2, GridAlignment::Begin, GridDirection::Column),
menu->getChildByID("delete-button"),
menu->getChildByID("delete-all-of-button"),
menu->getChildByID("delete-startpos-button")
);
detachAndCreateMenu(
menu,
"delete-filter-menu",
GridLayout::create(2, GridAlignment::Begin, GridDirection::Column),
menu->getChildByID("delete-filter-none"),
menu->getChildByID("delete-filter-static"),
menu->getChildByID("delete-filter-detail"),
menu->getChildByID("delete-filter-custom")
);
}
if (auto menu = getChildOfType<CCMenu>(this, 2)) {
menu->setID("build-tabs-menu");
setIDs(menu, 0,
"static-tab-1",
"static-tab-2",
"static-tab-3",
"slope-tab",
"hazard-tab",
"3d-tab",
"portal-tab",
"deco-tab-1",
"deco-tab-2",
"pulse-deco-tab",
"sawblade-tab",
"trigger-tab",
"custom-tab"
);
}
if (auto menu = getChildOfType<CCMenu>(this, 3)) {
setIDs(menu, 0,
"pause-button",
"settings-button",
"copy-paste-button",
"copy-button",
"paste-button",
"hsv-button",
"edit-special-button",
"edit-object-button",
"deselect-button",
"edit-group-button",
"portal-check",
"copy-values-button",
"paste-state-button",
"paste-color-button",
"go-to-layer-button",
"next-layer-button",
"prev-layer-button",
"all-layers-button"
);
detachAndCreateMenu(
this,
"top-right-menu",
RowLayout::create()->setAlignment(Alignment::End),
menu->getChildByID("pause-button"),
menu->getChildByID("settings-button")
);
detachAndCreateMenu(
this,
"editor-buttons-menu",
GridLayout::create(4, GridAlignment::End, GridDirection::Column),
menu->getChildByID("copy-paste-button"),
menu->getChildByID("edit-object-button"),
menu->getChildByID("paste-color-button"),
menu->getChildByID("deselect-button"),
menu->getChildByID("paste-button"),
menu->getChildByID("edit-group-button"),
menu->getChildByID("paste-state-button"),
menu->getChildByID("go-to-layer-button"),
menu->getChildByID("copy-button"),
menu->getChildByID("edit-special-button"),
menu->getChildByID("copy-values-button"),
menu->getChildByID("hsv-button")
);
detachAndCreateMenu(
this,
"layer-menu",
RowLayout::create(),
menu->getChildByID("all-layers-button"),
menu->getChildByID("prev-layer-button"),
this->getChildByID("layer-index-label"),
menu->getChildByID("next-layer-button")
);
}
}
class $modify(EditorUI) {
bool init(LevelEditorLayer* lel) {
if (!EditorUI::init(lel))
return false;
NodeIDs::get()->provide(this);
return true;
}
};

View file

@ -17,8 +17,9 @@ $register_ids(LevelBrowserLayer) {
setIDSafe(menu, 0, "new-level-button");
if (auto myLevelsBtn = setIDSafe(menu, 1, "my-levels-button")) {
detachIntoOwnMenu(this, myLevelsBtn, "my-levels-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End)
detachAndCreateMenu(this, "my-levels-menu",
ColumnLayout::create(5.f, 0.f)->setAlignment(Alignment::End),
myLevelsBtn
);
}

View file

@ -42,9 +42,10 @@ $register_ids(LevelInfoLayer) {
menu->setID("right-side-menu");
if (auto name = setIDSafe(menu, 0, "creator-name")) {
detachIntoOwnMenu(
this, name, "creator-info-menu",
ColumnLayout::create()->setAlignment(Alignment::Begin)
detachAndCreateMenu(
this, "creator-info-menu",
ColumnLayout::create()->setAlignment(Alignment::Begin),
name
);
}

View file

@ -55,8 +55,9 @@ $register_ids(MenuLayer) {
// move daily chest to its own menu
if (auto dailyChest = setIDSafe(menu, -1, "daily-chest-button")) {
detachIntoOwnMenu(this, dailyChest, "right-side-menu",
ColumnLayout::create(0.f, 0.f)
detachAndCreateMenu(this, "right-side-menu",
ColumnLayout::create(0.f, 0.f),
dailyChest
);
}
@ -78,8 +79,9 @@ $register_ids(MenuLayer) {
// move close button to its own menu
if (auto closeBtn = setIDSafe(menu, 1, "close-button")) {
detachIntoOwnMenu(this, closeBtn, "close-menu",
RowLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin)
detachAndCreateMenu(this, "close-menu",
RowLayout::create(5.f, 0.f)->setAlignment(Alignment::Begin),
closeBtn
);
}
}

View file

@ -1,702 +0,0 @@
#include "Index.hpp"
#include <Geode/binding/FLAlertLayer.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/file.hpp>
#include <Geode/utils/general.hpp>
#include <Geode/external/json/json.hpp>
#include <Geode/utils/map.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/string.hpp>
#include <fmt/format.h>
#include <hash.hpp>
#include <thread>
#define GITHUB_DONT_RATE_LIMIT_ME_PLS 0
template <class Json = nlohmann::json>
static Result<Json> readJSON(ghc::filesystem::path const& path) {
GEODE_UNWRAP_INTO(
std::string indexJsonData,
utils::file::readString(path).expect("Unable to read {}", path.string())
);
try {
return Ok(Json::parse(indexJsonData));
}
catch (std::exception& e) {
return Err("Error parsing JSON: " + std::string(e.what()));
}
}
static PlatformID platformFromString(std::string const& str) {
switch (hash(utils::string::trim(utils::string::toLower(str)).c_str())) {
default:
case hash("unknown"): return PlatformID::Unknown;
case hash("windows"): return PlatformID::Windows;
case hash("macos"): return PlatformID::MacOS;
case hash("ios"): return PlatformID::iOS;
case hash("android"): return PlatformID::Android;
case hash("linux"): return PlatformID::Linux;
}
}
Index* Index::get() {
static auto ret = new Index();
return ret;
}
bool Index::isIndexUpdated() const {
return m_upToDate;
}
std::vector<IndexItem> Index::getFeaturedItems() const {
std::vector<IndexItem> items;
items.reserve(m_featured.size());
std::transform(
m_featured.begin(), m_featured.end(), std::back_inserter(items),
[this](auto const& item) {
return this->getKnownItem(item);
}
);
return items;
}
bool Index::isFeaturedItem(std::string const& item) const {
return m_featured.count(item);
}
void Index::updateIndex(IndexUpdateCallback callback, bool force) {
#define RETURN_ERROR(str) \
std::string err__ = (str); \
if (callback) callback(UpdateStatus::Failed, err__, 0); \
log::info("Index update failed: {}", err__); \
return;
// if already updated and no force, let
// delegate know
if (!force && m_upToDate) {
if (callback) {
callback(UpdateStatus::Finished, "Index already updated", 100);
}
return;
}
// create directory for the local clone of
// the index
auto indexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY;
ghc::filesystem::create_directories(indexDir);
#if GITHUB_DONT_RATE_LIMIT_ME_PLS == 1
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err);
}
m_upToDate = true;
m_updating = false;
if (callback) callback(UpdateStatus::Finished, "", 100);
return;
#endif
// read sha of currently installed commit
std::string currentCommitSHA = "";
if (ghc::filesystem::exists(indexDir / "current")) {
auto data = utils::file::readString(indexDir / "current");
if (data) {
currentCommitSHA = data.unwrap();
}
}
web::AsyncWebRequest()
.join("index-update")
.header(fmt::format("If-None-Match: \"{}\"", currentCommitSHA))
.header("Accept: application/vnd.github.sha")
.fetch("https://api.github.com/repos/geode-sdk/mods/commits/main")
.text()
.then([this, force, callback, currentCommitSHA](std::string const& upcomingCommitSHA) {
auto indexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY;
// gee i sure hope no one does 60 commits to the mod index an hour and download every
// single one of them
if (upcomingCommitSHA == "") {
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err.unwrapErr());
}
m_upToDate = true;
m_updating = false;
if (callback) callback(UpdateStatus::Finished, "", 100);
return;
}
// update if forced or latest commit has
// different sha
if (force || currentCommitSHA != upcomingCommitSHA) {
// save new sha in file
(void)utils::file::writeString(indexDir / "current", upcomingCommitSHA);
web::AsyncWebRequest()
.join("index-download")
.fetch("https://github.com/geode-sdk/mods/zipball/main")
.into(indexDir / "index.zip")
.then([this, indexDir, callback](auto) {
// delete old index
try {
if (ghc::filesystem::exists(indexDir / "index")) {
ghc::filesystem::remove_all(indexDir / "index");
}
}
catch (std::exception& e) {
RETURN_ERROR("Unable to delete old index " + std::string(e.what()));
}
// unzip new index
auto unzip = file::unzipTo(indexDir / "index.zip", indexDir);
if (!unzip) {
RETURN_ERROR(unzip.unwrapErr());
}
// update index
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err.unwrapErr());
}
m_upToDate = true;
m_updating = false;
if (callback) callback(UpdateStatus::Finished, "", 100);
})
.expect([callback](std::string const& err) {
RETURN_ERROR(err);
})
.progress([callback](web::SentAsyncWebRequest& req, double now, double total) {
if (callback)
callback(
UpdateStatus::Progress, "Downloading",
static_cast<int>(now / total * 100.0)
);
});
}
else {
auto err = this->updateIndexFromLocalCache();
if (!err) {
RETURN_ERROR(err.unwrapErr());
}
m_upToDate = true;
m_updating = false;
if (callback) callback(UpdateStatus::Finished, "", 100);
}
})
.expect([callback](std::string const& err) {
RETURN_ERROR(err);
})
.progress([callback](web::SentAsyncWebRequest& req, double now, double total) {
if (callback)
callback(
UpdateStatus::Progress, "Downloading", static_cast<int>(now / total * 100.0)
);
});
}
void Index::addIndexItemFromFolder(ghc::filesystem::path const& dir) {
if (ghc::filesystem::exists(dir / "index.json")) {
auto readJson = readJSON(dir / "index.json");
if (!readJson) {
log::warn("Error reading index.json: {}, skipping", readJson.unwrapErr());
return;
}
auto json = readJson.unwrap();
if (!json.is_object()) {
log::warn("[index.json] is not an object, skipping");
return;
}
auto infoRes = ModInfo::createFromFile(dir / "mod.json");
if (!infoRes) {
log::warn("{}: {}, skipping", dir, infoRes.unwrapErr());
return;
}
auto info = infoRes.unwrap();
// make sure only latest version is present in index
auto old = std::find_if(m_items.begin(), m_items.end(), [info](IndexItem const& item) {
return item.m_info.m_id == info.m_id;
});
if (old != m_items.end()) {
// this one is newer
if (old->m_info.m_version < info.m_version) {
m_items.erase(old);
} else {
log::warn(
"Found older version of ({} < {}) of {}, skipping",
info.m_version, old->m_info.m_version, info.m_id
);
return;
}
}
IndexItem item;
item.m_path = dir;
item.m_info = info;
if (!json.contains("download") || !json["download"].is_object()) {
log::warn("[index.json].download is not an object, skipping");
return;
}
#define REQUIRE_DOWNLOAD_KEY(key, type) \
if (!download.contains(key) || !download[key].is_##type()) { \
log::warn("[index.json].download." key " is not a " #type ", skipping"); \
return; \
}
try {
auto download = json["download"];
REQUIRE_DOWNLOAD_KEY("url", string);
REQUIRE_DOWNLOAD_KEY("name", string);
REQUIRE_DOWNLOAD_KEY("hash", string);
REQUIRE_DOWNLOAD_KEY("platforms", array);
item.m_download.m_url = download["url"];
item.m_download.m_filename = download["name"];
item.m_download.m_hash = download["hash"];
for (auto& platform : download["platforms"]) {
item.m_download.m_platforms.insert(platformFromString(platform));
}
if (json.contains("categories")) {
if (!json["categories"].is_array()) {
log::warn("[index.json].categories is not an array, skipping");
return;
}
item.m_categories = json["categories"].template get<std::unordered_set<std::string>>();
m_categories.insert(item.m_categories.begin(), item.m_categories.end());
}
}
catch (std::exception& e) {
log::warn("[index.json] parsing error: {}, skipping", e.what());
return;
}
m_items.push_back(item);
}
else {
log::warn("Index directory {} is missing index.json, skipping", dir);
}
}
Result<> Index::updateIndexFromLocalCache() {
m_items.clear();
auto baseIndexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY;
// load geode.json (index settings)
if (auto baseIndexJson = readJSON(baseIndexDir / "geode.json")) {
auto json = baseIndexJson.unwrap();
auto checker = JsonChecker(json);
checker.root("[index/geode.json]").obj().has("featured").into(m_featured);
}
// load index mods
auto modsDir = baseIndexDir / "index";
if (ghc::filesystem::exists(modsDir)) {
for (auto const& dir : ghc::filesystem::directory_iterator(modsDir)) {
if (ghc::filesystem::is_directory(dir)) {
this->addIndexItemFromFolder(dir);
}
}
log::info("Index updated");
return Ok();
}
else {
return Err(
"Index appears not to have been "
"downloaded, or is fully empty"
);
}
}
std::vector<IndexItem> Index::getItems() const {
return m_items;
}
std::unordered_set<std::string> Index::getCategories() const {
return m_categories;
}
bool Index::isKnownItem(std::string const& id) const {
for (auto& item : m_items) {
if (item.m_info.m_id == id) return true;
}
return false;
}
IndexItem Index::getKnownItem(std::string const& id) const {
for (auto& item : m_items) {
if (item.m_info.m_id == id) {
return item;
}
}
return IndexItem();
}
struct UninstalledDependency {
std::string m_id;
bool m_isInIndex;
};
static void getUninstalledDependenciesRecursive(
ModInfo const& info, std::vector<UninstalledDependency>& deps
) {
for (auto& dep : info.m_dependencies) {
UninstalledDependency d;
d.m_isInIndex = Index::get()->isKnownItem(dep.m_id);
if (!Loader::get()->isModInstalled(dep.m_id)) {
d.m_id = dep.m_id;
deps.push_back(d);
}
if (d.m_isInIndex) {
getUninstalledDependenciesRecursive(Index::get()->getKnownItem(dep.m_id).m_info, deps);
}
}
}
Result<std::vector<std::string>> Index::checkDependenciesForItem(IndexItem const& item) {
// todo: check versions
std::vector<UninstalledDependency> deps;
getUninstalledDependenciesRecursive(item.m_info, deps);
if (deps.size()) {
std::vector<std::string> unknownDeps;
for (auto& dep : deps) {
if (!dep.m_isInIndex) {
unknownDeps.push_back(dep.m_id);
}
}
if (unknownDeps.size()) {
std::string list = "";
for (auto& ud : unknownDeps) {
list += "<cp>" + ud + "</c>, ";
}
list.pop_back();
list.pop_back();
return Err(
"This mod or its dependencies <cb>depends</c> on the "
"following unknown mods: " +
list +
". You will have "
"to manually install these mods before you can install "
"this one."
);
}
std::vector<std::string> list = {};
for (auto& d : deps) {
list.push_back(d.m_id);
}
list.push_back(item.m_info.m_id);
return Ok(list);
}
else {
return Ok<std::vector<std::string>>({ item.m_info.m_id });
}
}
Result<InstallHandle> Index::installItems(std::vector<IndexItem> const& items) {
std::vector<std::string> ids {};
for (auto& item : items) {
if (!item.m_download.m_platforms.count(GEODE_PLATFORM_TARGET)) {
return Err(
"This mod is not available on your "
"current platform \"" GEODE_PLATFORM_NAME "\" - Sorry! :("
);
}
if (!item.m_download.m_url.size()) {
return Err(
"Download URL not set! Report this bug to "
"the Geode developers - this should not happen, ever."
);
}
if (!item.m_download.m_filename.size()) {
return Err(
"Download filename not set! Report this bug to "
"the Geode developers - this should not happen, ever."
);
}
if (!item.m_download.m_hash.size()) {
return Err(
"Checksum not set! Report this bug to "
"the Geode developers - this should not happen, ever."
);
}
GEODE_UNWRAP_INTO(auto list, checkDependenciesForItem(item));
ranges::push(ids, list);
}
auto ret = std::make_shared<InstallItems>(std::unordered_set(ids.begin(), ids.end()));
m_installations.insert(ret);
return Ok(ret);
}
Result<InstallHandle> Index::installItem(IndexItem const& item) {
return this->installItems({ item });
}
bool Index::isUpdateAvailableForItem(std::string const& id) const {
if (!this->isKnownItem(id)) {
return false;
}
return this->isUpdateAvailableForItem(this->getKnownItem(id));
}
bool Index::isUpdateAvailableForItem(IndexItem const& item) const {
if (!Loader::get()->isModInstalled(item.m_info.m_id)) {
return false;
}
// has the mod been updated (but update not yet been applied by restarting game)
if (m_updated.count(item.m_info.m_id)) {
return false;
}
return item.m_info.m_version > Loader::get()->getInstalledMod(item.m_info.m_id)->getVersion();
}
bool Index::areUpdatesAvailable() const {
for (auto& item : m_items) {
if (this->isUpdateAvailableForItem(item.m_info.m_id)) {
return true;
}
}
return false;
}
Result<InstallHandle> Index::installAllUpdates() {
// find items that need updating
std::vector<IndexItem> itemsToUpdate {};
for (auto& item : m_items) {
if (this->isUpdateAvailableForItem(item)) {
itemsToUpdate.push_back(item);
}
}
return this->installItems(itemsToUpdate);
}
std::vector<InstallHandle> Index::getRunningInstallations() const {
return std::vector<InstallHandle>(m_installations.begin(), m_installations.end());
}
InstallHandle Index::isInstallingItem(std::string const& id) {
for (auto& inst : m_installations) {
if (inst->m_toInstall.count(id)) {
return inst;
}
}
return nullptr;
}
std::unordered_set<std::string> InstallItems::toInstall() const {
return m_toInstall;
}
InstallItems::CallbackID InstallItems::join(ItemInstallCallback callback) {
// already finished?
if (m_started && this->finished()) {
callback(shared_from_this(), UpdateStatus::Finished, "", 100);
return 0;
}
// start at one because 0 means invalid callback
static CallbackID COUNTER = 1;
if (callback) {
auto id = COUNTER++;
m_callbacks.insert({ id, callback });
return id;
}
return 0;
}
void InstallItems::leave(InstallItems::CallbackID id) {
m_callbacks.erase(id);
}
void InstallItems::post(UpdateStatus status, std::string const& info, uint8_t progress) {
for (auto& [_, cb] : m_callbacks) {
cb(shared_from_this(), status, info, progress);
}
}
void InstallItems::progress(std::string const& info, uint8_t progress) {
this->post(UpdateStatus::Progress, info, progress);
}
void InstallItems::error(std::string const& info) {
this->post(UpdateStatus::Failed, info, 0);
}
void InstallItems::finish(bool replaceFiles) {
// move files from temp dir to geode directory
auto tempDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY / "temp";
for (auto& file : ghc::filesystem::directory_iterator(tempDir)) {
try {
auto modDir = Loader::get()->getGeodeDirectory() / "mods";
auto targetFile = modDir / file.path().filename();
auto targetName = file.path().stem();
if (!replaceFiles) {
// find valid filename that doesn't exist yet
auto filename = ghc::filesystem::path(targetName).replace_extension("").string();
size_t number = 0;
while (ghc::filesystem::exists(targetFile)) {
targetFile = modDir / (filename + std::to_string(number) + ".geode");
number++;
}
}
// move file
ghc::filesystem::rename(file, targetFile);
}
catch (std::exception& e) {
try {
ghc::filesystem::remove_all(tempDir);
}
catch (...) {
}
return this->error(
"Unable to move downloaded file to mods directory: \"" + std::string(e.what()) +
" \" "
"(This might be due to insufficient permissions to "
"write files under SteamLibrary, try running GD as "
"administrator)"
);
}
}
// load mods
(void)Loader::get()->refreshModsList();
// finished
this->post(UpdateStatus::Finished, "", 100);
// let index know these mods have been updated
for (auto& inst : m_toInstall) {
Index::get()->m_updated.insert(inst);
}
// if no one is listening, show a popup anyway
if (!m_callbacks.size()) {
FLAlertLayer::create(
"Mods installed",
"The following <cy>mods</c> have been installed: " +
ranges::join(m_toInstall, std::string(",")) +
"\n"
"Please <cr>restart the game</c> to apply",
"OK"
)
->show();
}
// no longer need to ensure aliveness
Index::get()->m_installations.erase(shared_from_this());
}
InstallItems::CallbackID InstallItems::start(ItemInstallCallback callback, bool replaceFiles) {
auto id = this->join(callback);
// check if started already, if so, behave like join
if (m_started) return id;
m_started = true;
for (auto& inst : m_toInstall) {
// by virtue of running this function we know item must be valid
auto item = Index::get()->getKnownItem(inst);
auto indexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY;
(void)file::createDirectoryAll(indexDir / "temp");
auto tempFile = indexDir / "temp" / item.m_download.m_filename;
m_downloaded.push_back(tempFile);
auto handle =
web::AsyncWebRequest()
.join("install_mod_" + inst)
.fetch(item.m_download.m_url)
.into(tempFile)
.then([this, replaceFiles, item, inst, indexDir, tempFile](auto) {
// check for 404
auto notFound = utils::file::readString(tempFile);
if (notFound && notFound.unwrap() == "Not Found") {
try {
ghc::filesystem::remove(tempFile);
}
catch (...) {
}
return this->error(
"Binary file download returned \"Not found\". Report "
"this to the Geode development team."
);
}
// verify checksum
this->progress("Verifying", 100);
if (::calculateHash(tempFile) != item.m_download.m_hash) {
try {
ghc::filesystem::remove(tempFile);
}
catch (...) {
}
return this->error(
"Checksum mismatch! (Downloaded file did not match what "
"was expected. Try again, and if the download fails another time, "
"report this to the Geode development team."
);
}
// finished() just checks if the web requests are done
if (this->finished()) {
this->finish(replaceFiles);
}
})
.expect([this, inst](std::string const& error) {
this->error(error);
this->cancel();
})
.cancelled([this, item](auto&) {
this->cancel();
})
.progress([this, inst](web::SentAsyncWebRequest&, double now, double total) {
this->progress("Downloading binary", static_cast<uint8_t>(now / total * 100.0));
})
.send();
m_handles.push_back(handle);
}
// manage installation in the index until it's finished so
// even if no one listens to it it doesn't get freed from
// memory
Index::get()->m_installations.insert(shared_from_this());
return id;
}
bool InstallItems::finished() const {
for (auto& inst : m_handles) {
if (!inst->finished()) {
return false;
}
}
return true;
}
void InstallItems::cancel() {
for (auto& inst : m_handles) {
inst->cancel();
}
}

View file

@ -1,117 +0,0 @@
#pragma once
#include <Geode/utils/web.hpp>
#include <mutex>
#include <optional>
#include <unordered_set>
USE_GEODE_NAMESPACE();
class Index;
struct ModInstallUpdate;
struct InstallItems;
using InstallHandle = std::shared_ptr<InstallItems>;
// todo: make index use events
enum class UpdateStatus {
Progress,
Failed,
Finished,
};
using ItemInstallCallback =
std::function<void(InstallHandle, UpdateStatus, std::string const&, uint8_t)>;
using IndexUpdateCallback = std::function<void(UpdateStatus, std::string const&, uint8_t)>;
struct IndexItem {
struct Download {
std::string m_url;
std::string m_filename;
std::string m_hash;
std::unordered_set<PlatformID> m_platforms;
};
ghc::filesystem::path m_path;
ModInfo m_info;
Download m_download;
std::unordered_set<std::string> m_categories;
};
struct InstallItems final : public std::enable_shared_from_this<InstallItems> {
public:
using CallbackID = size_t;
private:
bool m_started = false;
std::unordered_set<std::string> m_toInstall;
std::vector<web::SentAsyncWebRequestHandle> m_handles;
std::unordered_map<CallbackID, ItemInstallCallback> m_callbacks;
std::vector<ghc::filesystem::path> m_downloaded;
void post(UpdateStatus status, std::string const& info, uint8_t progress);
void progress(std::string const& info, uint8_t progress);
void error(std::string const& info);
void finish(bool replaceFiles);
friend class Index;
public:
std::unordered_set<std::string> toInstall() const;
inline InstallItems(std::unordered_set<std::string> const& toInstall) :
m_toInstall(toInstall) {}
void cancel();
bool finished() const;
CallbackID join(ItemInstallCallback callback);
void leave(CallbackID id);
CallbackID start(ItemInstallCallback callback, bool replaceFiles = true);
};
class Index {
protected:
bool m_upToDate = false;
bool m_updating = false;
mutable std::mutex m_callbacksMutex;
std::vector<IndexItem> m_items;
std::unordered_set<InstallHandle> m_installations;
mutable std::mutex m_ticketsMutex;
std::unordered_set<std::string> m_featured;
std::unordered_set<std::string> m_categories;
std::unordered_set<std::string> m_updated;
void addIndexItemFromFolder(ghc::filesystem::path const& dir);
Result<> updateIndexFromLocalCache();
Result<std::vector<std::string>> checkDependenciesForItem(IndexItem const& item);
friend struct InstallItems;
public:
static Index* get();
std::vector<IndexItem> getItems() const;
bool isKnownItem(std::string const& id) const;
IndexItem getKnownItem(std::string const& id) const;
std::unordered_set<std::string> getCategories() const;
std::vector<IndexItem> getFeaturedItems() const;
bool isFeaturedItem(std::string const& item) const;
Result<InstallHandle> installItems(std::vector<IndexItem> const& item);
Result<InstallHandle> installItem(IndexItem const& item);
std::vector<InstallHandle> getRunningInstallations() const;
InstallHandle isInstallingItem(std::string const& id);
bool isUpdateAvailableForItem(std::string const& id) const;
bool isUpdateAvailableForItem(IndexItem const& item) const;
bool areUpdatesAvailable() const;
Result<InstallHandle> installAllUpdates();
bool isIndexUpdated() const;
void updateIndex(IndexUpdateCallback callback, bool force = false);
};

View file

@ -1,3 +1,14 @@
#include "InternalLoader.hpp"
#include "InternalMod.hpp"
#include "resources.hpp"
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/IPC.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/file.hpp>
#include <fmt/format.h>
#include <hash.hpp>
#include <iostream>
@ -6,15 +17,19 @@
#include <thread>
#include <vector>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/IPC.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/file.hpp>
ResourceDownloadEvent::ResourceDownloadEvent(
UpdateStatus const& status
) : status(status) {}
#include "InternalLoader.hpp"
#include "InternalMod.hpp"
#include "resources.hpp"
ListenerResult ResourceDownloadFilter::handle(
std::function<Callback> fn,
ResourceDownloadEvent* event
) {
fn(event);
return ListenerResult::Propagate;
}
ResourceDownloadFilter::ResourceDownloadFilter() {}
InternalLoader::InternalLoader() : Loader() {}
@ -116,11 +131,10 @@ void InternalLoader::loadInfoAlerts(nlohmann::json& json) {
m_shownInfoAlerts = json["alerts"].get<std::unordered_set<std::string>>();
}
void InternalLoader::downloadLoaderResources(IndexUpdateCallback callback) {
void InternalLoader::downloadLoaderResources() {
auto version = this->getVersion().toString();
auto tempResourcesZip = this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY / "new.zip";
auto resourcesDir =
this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY / InternalMod::get()->getID();
auto tempResourcesZip = dirs::getTempDir() / "new.zip";
auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID();
web::AsyncWebRequest()
.join("update-geode-loader-resources")
@ -128,50 +142,46 @@ void InternalLoader::downloadLoaderResources(IndexUpdateCallback callback) {
"https://github.com/geode-sdk/geode/releases/download/{}/resources.zip", version
))
.into(tempResourcesZip)
.then([tempResourcesZip, resourcesDir, callback](auto) {
.then([tempResourcesZip, resourcesDir](auto) {
// unzip resources zip
auto unzip = file::unzipTo(tempResourcesZip, resourcesDir);
auto unzip = file::Unzip::intoDir(tempResourcesZip, resourcesDir, true);
if (!unzip) {
if (callback)
callback(
UpdateStatus::Failed, "Unable to unzip new resources: " + unzip.unwrapErr(), 0
);
return;
return ResourceDownloadEvent(
UpdateError("Unable to unzip new resources: " + unzip.unwrapErr())
).post();
}
// delete resources zip
try {
ghc::filesystem::remove(tempResourcesZip);
}
catch (...) {
}
if (callback) callback(UpdateStatus::Finished, "Resources updated", 100);
ResourceDownloadEvent(UpdateFinished()).post();
})
.expect([callback](std::string const& info) {
if (callback) callback(UpdateStatus::Failed, info, 0);
.expect([](std::string const& info) {
ResourceDownloadEvent(
UpdateError("Unable to download resources: " + info)
).post();
})
.progress([callback](auto&, double now, double total) {
if (callback)
callback(
UpdateStatus::Progress, "Downloading resources",
static_cast<uint8_t>(now / total * 100.0)
);
.progress([](auto&, double now, double total) {
ResourceDownloadEvent(
UpdateProgress(
static_cast<uint8_t>(now / total * 100.0),
"Downloading resources"
)
).post();
});
}
bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
bool InternalLoader::verifyLoaderResources() {
static std::optional<bool> CACHED = std::nullopt;
if (CACHED.has_value()) {
return CACHED.value();
}
// geode/resources/geode.loader
auto resourcesDir =
this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY / InternalMod::get()->getID();
auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID();
// if the resources dir doesn't exist, then it's probably incorrect
if (!(ghc::filesystem::exists(resourcesDir) && ghc::filesystem::is_directory(resourcesDir))) {
this->downloadLoaderResources(callback);
if (!(
ghc::filesystem::exists(resourcesDir) &&
ghc::filesystem::is_directory(resourcesDir)
)) {
this->downloadLoaderResources();
return false;
}
@ -191,7 +201,7 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
log::debug(
"compare {} {} {}", file.path().string(), hash, LOADER_RESOURCE_HASHES.at(name)
);
this->downloadLoaderResources(callback);
this->downloadLoaderResources();
return false;
}
coverage += 1;
@ -199,7 +209,7 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
// make sure every file was found
if (coverage != LOADER_RESOURCE_HASHES.size()) {
this->downloadLoaderResources(callback);
this->downloadLoaderResources();
return false;
}

View file

@ -1,8 +1,8 @@
#pragma once
#include "../index/Index.hpp"
#include "FileWatcher.hpp"
#include <Geode/loader/Index.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/utils/Result.hpp>
@ -17,6 +17,19 @@
USE_GEODE_NAMESPACE();
struct ResourceDownloadEvent : public Event {
const UpdateStatus status;
ResourceDownloadEvent(UpdateStatus const& status);
};
class GEODE_DLL ResourceDownloadFilter : public EventFilter<ResourceDownloadEvent> {
public:
using Callback = void(ResourceDownloadEvent*);
ListenerResult handle(std::function<Callback> fn, ResourceDownloadEvent* event);
ResourceDownloadFilter();
};
/**
* Internal extension of Loader for private information
* @class InternalLoader
@ -34,7 +47,7 @@ protected:
void saveInfoAlerts(nlohmann::json& json);
void loadInfoAlerts(nlohmann::json& json);
void downloadLoaderResources(IndexUpdateCallback callback);
void downloadLoaderResources();
bool loadHooks();
void setupIPC();
@ -67,7 +80,7 @@ public:
void closePlatformConsole();
static void platformMessageBox(char const* title, std::string const& info);
bool verifyLoaderResources(IndexUpdateCallback callback);
bool verifyLoaderResources();
bool isReadyToHook() const;
void addInternalHook(Hook* hook, Mod* mod);

View file

@ -1,5 +1,5 @@
#include "InternalMod.hpp"
#include <Geode/loader/Dirs.hpp>
#include "InternalLoader.hpp"
#include "about.hpp"
@ -41,7 +41,7 @@ static ModInfo getInternalModInfo() {
}
InternalMod::InternalMod() : Mod(getInternalModInfo()) {
m_saveDirPath = Loader::get()->getGeodeSaveDirectory() / GEODE_MOD_DIRECTORY / m_info.m_id;
m_saveDirPath = dirs::getModsSaveDir() / m_info.m_id;
ghc::filesystem::create_directories(m_saveDirPath);

View file

@ -1,6 +1,7 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include <fs/filesystem.hpp>
#include <string>
/**
@ -22,5 +23,5 @@ namespace crashlog {
* @returns Path to the directory, or an empty string if the platform does
* not support crash logs
*/
std::string GEODE_DLL getCrashLogDirectory();
ghc::filesystem::path GEODE_DLL getCrashLogDirectory();
}

View file

@ -0,0 +1,77 @@
#include <Geode/loader/Dirs.hpp>
#include <cocos2d.h>
#include <crashlog.hpp>
USE_GEODE_NAMESPACE();
ghc::filesystem::path dirs::getGameDir() {
return ghc::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str());
}
ghc::filesystem::path dirs::getSaveDir() {
#ifdef GEODE_IS_MACOS
// not using ~/Library/Caches
return ghc::filesystem::path("/Users/Shared/Geode");
#elif defined(GEODE_IS_WINDOWS)
return ghc::filesystem::path(
ghc::filesystem::weakly_canonical(
CCFileUtils::sharedFileUtils()->getWritablePath().c_str()
).string()
);
#else
return ghc::filesystem::path(
CCFileUtils::sharedFileUtils()->getWritablePath().c_str()
);
#endif
}
ghc::filesystem::path dirs::getGeodeDir() {
#ifdef GEODE_IS_MACOS
char cwd[PATH_MAX];
getcwd(cwd, sizeof(cwd));
return ghc::filesystem::path(cwd) / "geode";
#else
return dirs::getGameDir() / "geode";
#endif
}
ghc::filesystem::path dirs::getGeodeSaveDir() {
return dirs::getSaveDir() / "geode";
}
ghc::filesystem::path dirs::getGeodeResourcesDir() {
return dirs::getGeodeDir() / "resources";
}
ghc::filesystem::path dirs::getGeodeLogDir() {
return dirs::getGeodeDir() / "logs";
}
ghc::filesystem::path dirs::getTempDir() {
return getGeodeDir() / "temp";
}
ghc::filesystem::path dirs::getModsDir() {
return getGeodeDir() / "mods";
}
ghc::filesystem::path dirs::getModsSaveDir() {
return getGeodeSaveDir() / "mods";
}
ghc::filesystem::path dirs::getModRuntimeDir() {
return dirs::getGeodeDir() / "unzipped";
}
ghc::filesystem::path dirs::getModConfigDir() {
return dirs::getGeodeDir() / "config";
}
ghc::filesystem::path dirs::getIndexDir() {
return dirs::getGeodeDir() / "index";
}
ghc::filesystem::path dirs::getCrashlogsDir() {
return crashlog::getCrashLogDirectory();
}

View file

@ -19,7 +19,7 @@ EventListenerProtocol::~EventListenerProtocol() {
Event::~Event() {}
void Event::postFrom(Mod* m) {
if (m) m_sender = m;
if (m) this->sender = m;
for (auto h : Event::s_listeners) {
if (h->passThrough(this) == ListenerResult::Stop) {
@ -27,7 +27,3 @@ void Event::postFrom(Mod* m) {
}
}
}
Mod* Event::getSender() {
return m_sender;
}

502
loader/src/loader/Index.cpp Normal file
View file

@ -0,0 +1,502 @@
#include <Geode/loader/Index.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/string.hpp>
#include <Geode/utils/map.hpp>
USE_GEODE_NAMESPACE();
// The reason sources have private implementation events that are
// turned into the global IndexUpdateEvent is because it makes it much
// simpler to keep track of progress, what errors were received, etc.
// without having to store a ton of members
struct geode::IndexSourceImpl final {
std::string repository;
bool isUpToDate = false;
std::string dirname() const {
return string::replace(this->repository, "/", "_");
}
ghc::filesystem::path path() const {
return dirs::getIndexDir() / this->dirname();
}
};
void IndexSourceImplDeleter::operator()(IndexSourceImpl* src) {
delete src;
}
struct geode::SourceUpdateEvent : public Event {
IndexSourceImpl* source;
const UpdateStatus status;
SourceUpdateEvent(IndexSourceImpl* src, const UpdateStatus status)
: source(src), status(status) {}
};
class SourceUpdateFilter : public EventFilter<SourceUpdateEvent> {
public:
using Callback = void(SourceUpdateEvent*);
ListenerResult handle(std::function<Callback> fn, SourceUpdateEvent* event) {
fn(event);
return ListenerResult::Propagate;
}
SourceUpdateFilter() {}
};
// Save data
struct IndexSourceSaveData {
std::string downloadedCommitSHA;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSourceSaveData, downloadedCommitSHA);
struct IndexSaveData {
std::unordered_map<std::string, IndexSourceSaveData> sources;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSaveData, sources);
// ModInstallEvent
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
if (m_id == event->modID) {
fn(event);
}
return ListenerResult::Propagate;
}
ModInstallFilter::ModInstallFilter(
std::string const& id
) : m_id(id) {}
// IndexUpdateEvent
IndexUpdateEvent::IndexUpdateEvent(
const UpdateStatus status
) : status(status) {}
ListenerResult IndexUpdateFilter::handle(
std::function<Callback> fn,
IndexUpdateEvent* event
) {
fn(event);
return ListenerResult::Propagate;
}
IndexUpdateFilter::IndexUpdateFilter() {}
// IndexItem
Result<IndexItemHandle> IndexItem::createFromDir(
std::string const& sourceRepository,
ghc::filesystem::path const& dir
) {
GEODE_UNWRAP_INTO(
auto entry, file::readJson(dir / "entry.json")
.expect("Unable to read entry.json")
);
GEODE_UNWRAP_INTO(
auto info, ModInfo::createFromFile(dir / "mod.json")
.expect("Unable to read mod.json: {error}")
);
JsonChecker checker(entry);
auto root = checker.root("[entry.json]").obj();
std::unordered_set<PlatformID> platforms;
for (auto& plat : root.has("platforms").iterate()) {
platforms.insert(PlatformID::from(plat.template get<std::string>()));
}
auto item = std::make_shared<IndexItem>(IndexItem {
.sourceRepository = sourceRepository,
.path = dir,
.info = info,
.download = {
.url = root.has("mod").obj().has("download").template get<std::string>(),
.hash = root.has("mod").obj().has("hash").template get<std::string>(),
.platforms = platforms,
},
.isFeatured = root.has("is-featured").template get<bool>(),
});
if (checker.isError()) {
return Err(checker.getError());
}
return Ok(item);
}
// Helpers
static Result<> flattenGithubRepo(ghc::filesystem::path const& dir) {
// github zipballs have a folder at root, but we already have our
// own folder for that so let's just bring everything from that
// folder to ours
GEODE_UNWRAP_INTO(auto files, file::listFiles(dir));
try {
// only flatten if there is only one file and it's a directory
if (files.size() == 1 && ghc::filesystem::is_directory(files[0])) {
for (auto& file : ghc::filesystem::directory_iterator(files[0])) {
ghc::filesystem::rename(
file, dir / ghc::filesystem::relative(file, files[0])
);
}
ghc::filesystem::remove(files[0]);
}
} catch(std::exception& e) {
return Err(e.what());
}
return Ok();
}
// Index
Index::Index() {
new EventListener(
std::bind(&Index::onSourceUpdate, this, std::placeholders::_1),
SourceUpdateFilter()
);
this->addSource("geode-sdk/index-test");
}
Index* Index::get() {
static auto inst = new Index();
return inst;
}
void Index::addSource(std::string const& repository) {
m_sources.emplace_back(new IndexSourceImpl {
.repository = repository
});
}
void Index::removeSource(std::string const& repository) {
ranges::remove(m_sources, [repository](IndexSourcePtr const& src) {
return src->repository == repository;
});
}
std::vector<std::string> Index::getSources() const {
std::vector<std::string> res;
for (auto& src : m_sources) {
res.push_back(src->repository);
}
return res;
}
void Index::onSourceUpdate(SourceUpdateEvent* event) {
// save status for aggregating SourceUpdateEvents to a single global
// IndexUpdateEvent
m_sourceStatuses[event->source->repository] = event->status;
// figure out aggregate event
enum { Finished, Progress, Failed, } whatToPost = Finished;
for (auto& [src, status] : m_sourceStatuses) {
// if some source is still updating, post progress
if (std::holds_alternative<UpdateProgress>(status)) {
whatToPost = Progress;
break;
}
// otherwise, if some source failed, then post failed
else if (std::holds_alternative<UpdateError>(status)) {
if (whatToPost != Progress) {
whatToPost = Failed;
}
}
// otherwise if all are finished, whatToPost is already set to that
}
switch (whatToPost) {
case Finished: {
// clear source statuses to allow updating index again
m_sourceStatuses.clear();
// post finish event
IndexUpdateEvent(UpdateFinished()).post();
} break;
case Progress: {
// get total progress
size_t total = 0;
for (auto& [src, status] : m_sourceStatuses) {
if (std::holds_alternative<UpdateProgress>(status)) {
total += std::get<UpdateProgress>(status).first;
} else {
total += 100;
}
}
IndexUpdateEvent(
UpdateProgress(
static_cast<uint8_t>(total / m_sourceStatuses.size()),
"Downloading"
)
).post();
} break;
case Failed: {
std::string info = "";
for (auto& [src, status] : m_sourceStatuses) {
if (std::holds_alternative<UpdateError>(status)) {
info += src + ": " + std::get<UpdateError>(status) + "\n";
}
}
// clear source statuses to allow updating index again
m_sourceStatuses.clear();
// post finish event
IndexUpdateEvent(UpdateError(info)).post();
} break;
}
}
void Index::checkSourceUpdates(IndexSourceImpl* src) {
if (src->isUpToDate) {
return this->updateSourceFromLocal(src);
}
SourceUpdateEvent(src, UpdateProgress(0, "Checking status")).post();
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
auto oldSHA = data.sources[src->repository].downloadedCommitSHA;
web::AsyncWebRequest()
.join(fmt::format("index-update-{}", src->repository))
.header(fmt::format("If-None-Match: \"{}\"", oldSHA))
.header("Accept: application/vnd.github.sha")
.fetch(fmt::format("https://api.github.com/repos/{}/commits/main", src->repository))
.text()
.then([this, src, oldSHA](std::string const& newSHA) {
// check if should just be updated from local cache
if (
// if no new hash was given (rate limited) or the new hash is the
// same as old
(newSHA.empty() || oldSHA == newSHA) &&
// make sure the downloaded local copy actually exists
ghc::filesystem::exists(src->path()) &&
ghc::filesystem::exists(src->path() / "config.json")
) {
this->updateSourceFromLocal(src);
}
// otherwise save hash and download source
else {
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
data.sources[src->repository].downloadedCommitSHA = newSHA;
this->downloadSource(src);
}
})
.expect([src](std::string const& err) {
SourceUpdateEvent(
src,
UpdateError(fmt::format("Error checking for updates: {}", err))
).post();
});
}
void Index::downloadSource(IndexSourceImpl* src) {
SourceUpdateEvent(src, UpdateProgress(0, "Beginning download")).post();
auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src->dirname());
web::AsyncWebRequest()
.join(fmt::format("index-download-{}", src->repository))
.fetch(fmt::format("https://github.com/{}/zipball/main", src->repository))
.into(targetFile)
.then([this, src, targetFile](auto) {
auto targetDir = src->path();
// delete old unzipped index
try {
if (ghc::filesystem::exists(targetDir)) {
ghc::filesystem::remove_all(targetDir);
}
}
catch(...) {
return SourceUpdateEvent(
src, UpdateError("Unable to clear cached index")
).post();
}
// unzip new index
auto unzip = file::Unzip::intoDir(targetFile, targetDir, true)
.expect("Unable to unzip new index");
if (!unzip) {
return SourceUpdateEvent(
src, UpdateError(unzip.unwrapErr())
).post();
}
// remove the directory github adds to the root of the zip
(void)flattenGithubRepo(targetDir);
// update index
this->updateSourceFromLocal(src);
})
.expect([src](std::string const& err) {
SourceUpdateEvent(
src, UpdateError(fmt::format("Error downloading: {}", err))
).post();
})
.progress([src](auto&, double now, double total) {
SourceUpdateEvent(
src,
UpdateProgress(
static_cast<uint8_t>(now / total * 100.0),
"Downloading"
)
).post();
});
}
void Index::updateSourceFromLocal(IndexSourceImpl* src) {
SourceUpdateEvent(src, UpdateProgress(100, "Updating local cache")).post();
// delete old items from this url if such exist
for (auto& [_, versions] : m_items) {
for (auto it = versions.begin(); it != versions.end(); ) {
if (it->second->sourceRepository == src->repository) {
it = versions.erase(it);
} else {
++it;
}
}
}
this->cleanupItems();
// read directory and add new items
try {
for (auto& dir : ghc::filesystem::directory_iterator(src->path() / "mods")) {
auto addRes = IndexItem::createFromDir(src->repository, dir);
if (!addRes) {
log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr());
continue;
}
auto add = addRes.unwrap();
// check if this major version of this item has already been added
if (m_items[add->info.m_id].count(add->info.m_version.getMajor())) {
log::warn(
"Item {}@{} has already been added, skipping",
add->info.m_id, add->info.m_version
);
continue;
}
// add new major version of this item
m_items[add->info.m_id].insert({
add->info.m_version.getMajor(),
add
});
}
} catch(std::exception& e) {
SourceUpdateEvent(src, fmt::format(
"Unable to read source {}", src->repository
)).post();
return;
}
// mark source as finished
src->isUpToDate = true;
SourceUpdateEvent(src, UpdateFinished()).post();
}
void Index::cleanupItems() {
// delete mods with no versions
for (auto it = m_items.begin(); it != m_items.end(); ) {
if (!it->second.size()) {
it = m_items.erase(it);
} else {
++it;
}
}
}
bool Index::isUpToDate() const {
for (auto& source : m_sources) {
if (!source->isUpToDate) {
return false;
}
}
return true;
}
bool Index::hasTriedToUpdate() const {
return m_triedToUpdate;
}
void Index::update(bool force) {
// create index dir if it doesn't exist
(void)file::createDirectoryAll(dirs::getIndexDir());
m_triedToUpdate = true;
// update all sources in GD thread
Loader::get()->queueInGDThread([force, this]() {
// check if some sources are already being updated
if (m_sourceStatuses.size()) {
return;
}
// update sources
for (auto& src : m_sources) {
if (force) {
this->downloadSource(src.get());
} else {
this->checkSourceUpdates(src.get());
}
}
});
}
std::vector<IndexItemHandle> Index::getItems() const {
std::vector<IndexItemHandle> res;
for (auto& items : map::values(m_items)) {
if (items.size()) {
res.push_back(items.rbegin()->second);
}
}
return res;
}
bool Index::isKnownItem(
std::string const& id,
std::optional<size_t> version
) const {
if (m_items.count(id)) {
if (version) {
return m_items.at(id).count(version.value());
} else {
return true;
}
} else {
return false;
}
}
IndexItemHandle Index::getItem(
std::string const& id,
std::optional<size_t> version
) const {
if (m_items.count(id)) {
auto versions = m_items.at(id);
if (version) {
if (versions.count(version.value())) {
return versions.at(version.value());
}
} else {
if (versions.size()) {
return m_items.at(id).rbegin()->second;
}
}
}
return nullptr;
}
IndexItemHandle Index::getItem(ModInfo const& info) const {
return this->getItem(info.m_id, info.m_version.getMajor());
}
IndexItemHandle Index::getItem(Mod* mod) const {
return this->getItem(mod->getID(), mod->getVersion().getMajor());
}
bool Index::updateAvailable(IndexItemHandle item) const {
auto installed = Loader::get()->getInstalledMod(item->info.m_id);
if (!installed) {
return false;
}
return item->info.m_version > installed->getVersion();
}

View file

@ -1,6 +1,7 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/Dirs.hpp>
#include <InternalLoader.hpp>
#include <InternalMod.hpp>
#include <about.hpp>
@ -20,84 +21,29 @@ Loader::~Loader() {
}
m_mods.clear();
log::Logs::clear();
ghc::filesystem::remove_all(
this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY
);
ghc::filesystem::remove_all(dirs::getModRuntimeDir());
ghc::filesystem::remove_all(dirs::getTempDir());
}
VersionInfo Loader::getVersion() {
return LOADER_VERSION;
}
VersionInfo Loader::minModVersion() {
return VersionInfo { 0, 7, 0 };
}
VersionInfo Loader::maxModVersion() {
return VersionInfo {
Loader::getVersion().getMajor(),
Loader::getVersion().getMinor(),
// todo: dynamic version info (vM.M.*)
99999999,
};
}
bool Loader::isModVersionSupported(VersionInfo const& version) {
return
version >= Loader::minModVersion() &&
version <= Loader::maxModVersion();
}
// Initialization
void Loader::createDirectories() {
auto modDir = this->getGeodeDirectory() / GEODE_MOD_DIRECTORY;
auto logDir = this->getGeodeDirectory() / GEODE_LOG_DIRECTORY;
auto resDir = this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY;
auto tempDir = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
auto confDir = this->getGeodeDirectory() / GEODE_CONFIG_DIRECTORY;
#ifdef GEODE_IS_MACOS
ghc::filesystem::create_directory(this->getSaveDirectory());
ghc::filesystem::create_directory(dirs::getSaveDir());
#endif
ghc::filesystem::create_directories(resDir);
ghc::filesystem::create_directory(confDir);
ghc::filesystem::create_directory(modDir);
ghc::filesystem::create_directory(logDir);
ghc::filesystem::create_directory(tempDir);
ghc::filesystem::create_directories(dirs::getGeodeResourcesDir());
ghc::filesystem::create_directory(dirs::getModConfigDir());
ghc::filesystem::create_directory(dirs::getModsDir());
ghc::filesystem::create_directory(dirs::getGeodeLogDir());
ghc::filesystem::create_directory(dirs::getTempDir());
ghc::filesystem::create_directory(dirs::getModRuntimeDir());
if (!ranges::contains(m_modSearchDirectories, modDir)) {
m_modSearchDirectories.push_back(modDir);
if (!ranges::contains(m_modSearchDirectories, dirs::getModsDir())) {
m_modSearchDirectories.push_back(dirs::getModsDir());
}
}
Result<> Loader::saveData() {
// save mods' data
for (auto& [_, mod] : m_mods) {
auto r = mod->saveData();
if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
// save loader data
GEODE_UNWRAP(InternalMod::get()->saveData());
return Ok();
}
Result<> Loader::loadData() {
auto e = InternalMod::get()->loadData();
if (!e) {
log::warn("Unable to load loader settings: {}", e.unwrapErr());
}
for (auto& [_, mod] : m_mods) {
auto r = mod->loadData();
if (!r) {
log::warn("Unable to load data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
return Ok();
}
Result<> Loader::setup() {
if (m_isSetup) {
return Ok();
@ -130,6 +76,92 @@ Result<> Loader::setup() {
return Ok();
}
void Loader::addSearchPaths() {
CCFileUtils::get()->addPriorityPath(dirs::getGeodeResourcesDir().string().c_str());
CCFileUtils::get()->addPriorityPath(dirs::getModRuntimeDir().string().c_str());
}
void Loader::updateResources() {
log::debug("Adding resources");
// add own spritesheets
this->updateModResources(InternalMod::get());
// add mods' spritesheets
for (auto const& [_, mod] : m_mods) {
this->updateModResources(mod);
}
}
std::vector<Mod*> Loader::getAllMods() {
return map::values(m_mods);
}
Mod* Loader::getInternalMod() {
return InternalMod::get();
}
std::vector<InvalidGeodeFile> Loader::getFailedMods() const {
return m_invalidMods;
}
// Version info
VersionInfo Loader::getVersion() {
return LOADER_VERSION;
}
VersionInfo Loader::minModVersion() {
return VersionInfo { 0, 3, 1 };
}
VersionInfo Loader::maxModVersion() {
return VersionInfo {
Loader::getVersion().getMajor(),
Loader::getVersion().getMinor(),
// todo: dynamic version info (vM.M.*)
99999999,
};
}
bool Loader::isModVersionSupported(VersionInfo const& version) {
return
version >= Loader::minModVersion() &&
version <= Loader::maxModVersion();
}
// Data saving
Result<> Loader::saveData() {
// save mods' data
for (auto& [_, mod] : m_mods) {
auto r = mod->saveData();
if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
// save loader data
GEODE_UNWRAP(InternalMod::get()->saveData());
return Ok();
}
Result<> Loader::loadData() {
auto e = InternalMod::get()->loadData();
if (!e) {
log::warn("Unable to load loader settings: {}", e.unwrapErr());
}
for (auto& [_, mod] : m_mods) {
auto r = mod->loadData();
if (!r) {
log::warn("Unable to load data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
return Ok();
}
// Mod loading
Result<Mod*> Loader::loadModFromInfo(ModInfo const& info) {
if (m_mods.count(info.m_id)) {
return Err(fmt::format("Mod with ID '{}' already loaded", info.m_id));
@ -146,8 +178,7 @@ Result<Mod*> Loader::loadModFromInfo(ModInfo const& info) {
// add mod resources
this->queueInGDThread([this, mod]() {
auto searchPath = this->getGeodeDirectory() /
GEODE_TEMP_DIRECTORY / mod->getID() / "resources";
auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources";
CCFileUtils::get()->addSearchPath(searchPath.string().c_str());
this->updateModResources(mod);
@ -168,6 +199,79 @@ Result<Mod*> Loader::loadModFromFile(ghc::filesystem::path const& file) {
return this->loadModFromInfo(res.unwrap());
}
bool Loader::isModInstalled(std::string const& id) const {
return m_mods.count(id) && !m_mods.at(id)->isUninstalled();
}
Mod* Loader::getInstalledMod(std::string const& id) const {
if (m_mods.count(id) && !m_mods.at(id)->isUninstalled()) {
return m_mods.at(id);
}
return nullptr;
}
bool Loader::isModLoaded(std::string const& id) const {
return m_mods.count(id) && m_mods.at(id)->isLoaded();
}
Mod* Loader::getLoadedMod(std::string const& id) const {
if (m_mods.count(id)) {
auto mod = m_mods.at(id);
if (mod->isLoaded()) {
return mod;
}
}
return nullptr;
}
void Loader::dispatchScheduledFunctions(Mod* mod) {
std::lock_guard _(m_scheduledFunctionsMutex);
for (auto& func : m_scheduledFunctions) {
func();
}
m_scheduledFunctions.clear();
}
void Loader::scheduleOnModLoad(Mod* mod, ScheduledFunction func) {
std::lock_guard _(m_scheduledFunctionsMutex);
if (mod) {
return func();
}
m_scheduledFunctions.push_back(func);
}
void Loader::updateModResources(Mod* mod) {
if (!mod->m_info.m_spritesheets.size()) {
return;
}
auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources";
log::debug("Adding resources for {}", mod->getID());
// add spritesheets
for (auto const& sheet : mod->m_info.m_spritesheets) {
log::debug("Adding sheet {}", sheet);
auto png = sheet + ".png";
auto plist = sheet + ".plist";
auto ccfu = CCFileUtils::get();
if (png == std::string(ccfu->fullPathForFilename(png.c_str(), false)) ||
plist == std::string(ccfu->fullPathForFilename(plist.c_str(), false))) {
log::warn(
"The resource dir of \"{}\" is missing \"{}\" png and/or plist files",
mod->m_info.m_id, sheet
);
}
else {
CCTextureCache::get()->addImage(png.c_str(), false);
CCSpriteFrameCache::get()->addSpriteFramesWithFile(plist.c_str());
}
}
}
// Dependencies and refreshing
Result<> Loader::loadModsFromDirectory(
ghc::filesystem::path const& dir,
bool recursive
@ -255,43 +359,6 @@ Result<> Loader::refreshModsList() {
return Ok();
}
bool Loader::isModInstalled(std::string const& id) const {
return m_mods.count(id) && !m_mods.at(id)->isUninstalled();
}
Mod* Loader::getInstalledMod(std::string const& id) const {
if (m_mods.count(id) && !m_mods.at(id)->isUninstalled()) {
return m_mods.at(id);
}
return nullptr;
}
bool Loader::isModLoaded(std::string const& id) const {
return m_mods.count(id) && m_mods.at(id)->isLoaded();
}
Mod* Loader::getLoadedMod(std::string const& id) const {
if (m_mods.count(id)) {
auto mod = m_mods.at(id);
if (mod->isLoaded()) {
return mod;
}
}
return nullptr;
}
std::vector<Mod*> Loader::getAllMods() {
return map::getValues(m_mods);
}
Mod* Loader::getInternalMod() {
return InternalMod::get();
}
std::vector<InvalidGeodeFile> Loader::getFailedMods() const {
return m_invalidMods;
}
void Loader::updateAllDependencies() {
for (auto const& [_, mod] : m_mods) {
mod->updateDependencyStates();
@ -302,34 +369,16 @@ void Loader::waitForModsToBeLoaded() {
while (!m_earlyLoadFinished) {}
}
void Loader::dispatchScheduledFunctions(Mod* mod) {
std::lock_guard _(m_scheduledFunctionsMutex);
for (auto& func : m_scheduledFunctions) {
func();
}
m_scheduledFunctions.clear();
}
// Misc
void Loader::queueInGDThread(ScheduledFunction func) {
InternalLoader::get()->queueInGDThread(func);
}
void Loader::scheduleOnModLoad(Mod* mod, ScheduledFunction func) {
std::lock_guard _(m_scheduledFunctionsMutex);
if (mod) {
return func();
}
m_scheduledFunctions.push_back(func);
}
bool Loader::didLastLaunchCrash() const {
return crashlog::didLastLaunchCrash();
}
ghc::filesystem::path Loader::getCrashLogDirectory() const {
return crashlog::getCrashLogDirectory();
}
void Loader::openPlatformConsole() {
InternalLoader::get()->openPlatformConsole();
}
@ -337,83 +386,3 @@ void Loader::openPlatformConsole() {
void Loader::closePlatfromConsole() {
InternalLoader::get()->closePlatformConsole();
}
void Loader::updateModResources(Mod* mod) {
if (!mod->m_info.m_spritesheets.size()) {
return;
}
auto searchPath = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY / mod->getID() / "resources";
log::debug("Adding resources for {}", mod->getID());
// add spritesheets
for (auto const& sheet : mod->m_info.m_spritesheets) {
log::debug("Adding sheet {}", sheet);
auto png = sheet + ".png";
auto plist = sheet + ".plist";
auto ccfu = CCFileUtils::get();
if (png == std::string(ccfu->fullPathForFilename(png.c_str(), false)) ||
plist == std::string(ccfu->fullPathForFilename(plist.c_str(), false))) {
log::warn(
"The resource dir of \"{}\" is missing \"{}\" png and/or plist files",
mod->m_info.m_id, sheet
);
}
else {
CCTextureCache::get()->addImage(png.c_str(), false);
CCSpriteFrameCache::get()->addSpriteFramesWithFile(plist.c_str());
}
}
}
void Loader::addSearchPaths() {
CCFileUtils::get()->addPriorityPath(
(this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY).string().c_str()
);
CCFileUtils::get()->addPriorityPath(
(this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY).string().c_str()
);
}
void Loader::updateResources() {
log::debug("Adding resources");
// add own spritesheets
this->updateModResources(InternalMod::get());
// add mods' spritesheets
for (auto const& [_, mod] : m_mods) {
this->updateModResources(mod);
}
}
ghc::filesystem::path Loader::getGameDirectory() const {
return ghc::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str());
}
ghc::filesystem::path Loader::getSaveDirectory() const {
#ifdef GEODE_IS_MACOS
// not using ~/Library/Caches
return ghc::filesystem::path("/Users/Shared/Geode");
#elif defined(GEODE_IS_WINDOWS)
return ghc::filesystem::path(
ghc::filesystem::weakly_canonical(
CCFileUtils::sharedFileUtils()->getWritablePath().c_str()
).string()
);
#else
return ghc::filesystem::path(
CCFileUtils::sharedFileUtils()->getWritablePath().c_str()
);
#endif
}
ghc::filesystem::path Loader::getGeodeDirectory() const {
return geode::utils::file::geodeRoot() / GEODE_DIRECTORY;
}
ghc::filesystem::path Loader::getGeodeSaveDirectory() const {
return this->getSaveDirectory() / GEODE_DIRECTORY;
}

View file

@ -1,4 +1,4 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/casts.hpp>
@ -115,11 +115,7 @@ std::string Log::toString(bool logTime) const {
}
void Logs::setup() {
s_logStream = std::ofstream(
Loader::get()->getGeodeDirectory() /
GEODE_LOG_DIRECTORY /
log::generateLogName()
);
s_logStream = std::ofstream(dirs::getGeodeLogDir() / log::generateLogName());
}
void Logs::push(Log&& log) {

View file

@ -1,4 +1,7 @@
#include <Geode/loader/Hook.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/file.hpp>
#include <InternalLoader.hpp>
@ -11,7 +14,7 @@ USE_GEODE_NAMESPACE();
Mod::Mod(ModInfo const& info) {
m_info = info;
m_saveDirPath = Loader::get()->getGeodeSaveDirectory() / GEODE_MOD_DIRECTORY / info.m_id;
m_saveDirPath = dirs::getModsSaveDir() / info.m_id;
ghc::filesystem::create_directories(m_saveDirPath);
}
@ -190,7 +193,7 @@ bool Mod::hasSetting(std::string const& key) const {
Result<> Mod::loadBinary() {
if (!m_binaryLoaded) {
GEODE_UNWRAP(this->createTempDir().expect("Unable to create temp directory"));
GEODE_UNWRAP(this->createTempDir());
if (this->hasUnresolvedDependencies()) return Err("Mod has unresolved dependencies");
@ -465,41 +468,42 @@ Result<> Mod::unpatch(Patch* patch) {
Result<> Mod::createTempDir() {
// Check if temp dir already exists
if (m_tempDirName.string().empty()) {
// Create geode/temp
auto tempDir = Loader::get()->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
if (!file::createDirectoryAll(tempDir).isOk()) {
return Err("Unable to create Geode temp directory");
}
// Create geode/temp/mod.id
auto tempPath = tempDir / m_info.m_id;
if (!file::createDirectoryAll(tempPath).isOk()) {
return Err("Unable to create mod temp directory");
}
// Unzip .geode file into temp dir
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_info.m_path));
if (!unzip.hasEntry(m_info.m_binaryName)) {
return Err(fmt::format(
"Unable to find platform binary under the name \"{}\"", m_info.m_binaryName
));
}
GEODE_UNWRAP(unzip.extractAllTo(tempPath));
// Mark temp dir creation as succesful
m_tempDirName = tempPath;
if (!m_tempDirName.string().empty()) {
return Ok();
}
// Create geode/temp
auto tempDir = dirs::getModRuntimeDir();
if (!file::createDirectoryAll(tempDir)) {
return Err("Unable to create mods' runtime directory");
}
// Create geode/temp/mod.id
auto tempPath = tempDir / m_info.m_id;
if (!file::createDirectoryAll(tempPath)) {
return Err("Unable to create mod runtime directory");
}
// Unzip .geode file into temp dir
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_info.m_path));
if (!unzip.hasEntry(m_info.m_binaryName)) {
return Err(fmt::format(
"Unable to find platform binary under the name \"{}\"", m_info.m_binaryName
));
}
GEODE_UNWRAP(unzip.extractAllTo(tempPath));
// Mark temp dir creation as succesful
m_tempDirName = tempPath;
return Ok();
}
ghc::filesystem::path Mod::getConfigDir(bool create) const {
auto dir = Loader::get()->getGeodeDirectory() / GEODE_CONFIG_DIRECTORY / m_info.m_id;
if (create && !ghc::filesystem::exists(dir)) {
ghc::filesystem::create_directories(dir);
auto dir = dirs::getModConfigDir() / m_info.m_id;
if (create) {
(void)file::createDirectoryAll(dir);
}
return dir;
}

View file

@ -1,10 +1,12 @@
#include <InternalLoader.hpp>
#include <Geode/loader/Log.hpp>
#include <iostream>
#include <InternalMod.hpp>
#ifdef GEODE_IS_IOS
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Dirs.hpp>
#include <iostream>
#include <InternalMod.hpp>
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h>
@ -16,7 +18,7 @@ void InternalLoader::platformMessageBox(char const* title, std::string const& in
void InternalLoader::openPlatformConsole() {
ghc::filesystem::path(getpwuid(getuid())->pw_dir);
freopen(
ghc::filesystem::path(utils::file::geodeRoot() / "geode_log.txt").string().c_str(), "w",
ghc::filesystem::path(dirs::getGeodeDir() / "geode_log.txt").string().c_str(), "w",
stdout
);
InternalLoader::m_platformConsoleOpen = true;

View file

@ -10,7 +10,7 @@ bool crashlog::didLastLaunchCrash() {
return false;
}
std::string crashlog::getCrashLogDirectory() {
ghc::filesystem::path crashlog::getCrashLogDirectory() {
return "";
}

View file

@ -19,13 +19,6 @@ std::string utils::clipboard::read() {
return std::string([[UIPasteboard generalPasteboard].string UTF8String]);
}
ghc::filesystem::path utils::file::geodeRoot() {
return ghc::filesystem::path([[[NSFileManager defaultManager]
URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject]
.path.UTF8String);
}
void utils::web::openLinkInBrowser(std::string const& url) {
[[UIApplication sharedApplication]
openURL:[NSURL URLWithString:[NSString stringWithUTF8String:url.c_str()]]];

View file

@ -14,7 +14,7 @@ bool crashlog::didLastLaunchCrash() {
return false;
}
std::string crashlog::getCrashLogDirectory() {
ghc::filesystem::path crashlog::getCrashLogDirectory() {
std::array<char, 1024> path;
CFStringGetCString(
(CFStringRef)NSHomeDirectory(), path.data(), path.size(), kCFStringEncodingUTF8

View file

@ -27,13 +27,6 @@ std::string utils::clipboard::read() {
return std::string(clipboard);
}
ghc::filesystem::path utils::file::geodeRoot() {
char cwd[PATH_MAX];
getcwd(cwd, sizeof(cwd));
// utils::clipboard::write(cwd);
return ghc::filesystem::path(cwd);
}
bool utils::file::openFolder(ghc::filesystem::path const& path) {
NSURL* fileURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.string().c_str()]];
NSURL* folderURL = [fileURL URLByDeletingLastPathComponent];

View file

@ -5,7 +5,9 @@
#ifdef GEODE_IS_WINDOWS
#include <crashlog.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <DbgHelp.h>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/file.hpp>
@ -224,7 +226,7 @@ static LONG WINAPI exceptionHandler(LPEXCEPTION_POINTERS info) {
// add a file to let Geode know on next launch that it crashed previously
// this could also be done by saving a loader setting or smth but eh.
(void)utils::file::writeBinary(crashlog::getCrashLogDirectory() + "/last-crashed", {});
(void)utils::file::writeBinary(crashlog::getCrashLogDirectory() / "last-crashed", {});
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
@ -273,7 +275,7 @@ static LONG WINAPI exceptionHandler(LPEXCEPTION_POINTERS info) {
// save actual file
std::ofstream actualFile;
actualFile.open(
crashlog::getCrashLogDirectory() + "/" + getDateString(true) + ".log", std::ios::app
crashlog::getCrashLogDirectory() / (getDateString(true) + ".log"), std::ios::app
);
actualFile << file.rdbuf() << std::flush;
actualFile.close();
@ -283,7 +285,7 @@ static LONG WINAPI exceptionHandler(LPEXCEPTION_POINTERS info) {
bool crashlog::setupPlatformHandler() {
SetUnhandledExceptionFilter(exceptionHandler);
auto lastCrashedFile = crashlog::getCrashLogDirectory() + "/last-crashed";
auto lastCrashedFile = crashlog::getCrashLogDirectory() / "last-crashed";
if (ghc::filesystem::exists(lastCrashedFile)) {
g_lastLaunchCrashed = true;
try {
@ -299,9 +301,8 @@ bool crashlog::didLastLaunchCrash() {
return g_lastLaunchCrashed;
}
std::string crashlog::getCrashLogDirectory() {
static auto dir = (Loader::get()->getGeodeDirectory() / "crashlogs").string();
return dir;
ghc::filesystem::path crashlog::getCrashLogDirectory() {
return dirs::getGeodeDir() / "crashlogs";
}
#endif

View file

@ -70,10 +70,6 @@ std::string utils::clipboard::read() {
return text;
}
ghc::filesystem::path utils::file::geodeRoot() {
return ghc::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str());
}
bool utils::file::openFolder(ghc::filesystem::path const& path) {
ShellExecuteA(NULL, "open", path.string().c_str(), NULL, NULL, SW_SHOWDEFAULT);
return true;

View file

@ -1,22 +1,58 @@
#include "../index/Index.hpp"
#include "info/ModInfoLayer.hpp"
#include <Geode/loader/Index.hpp>
#include <Geode/loader/Dirs.hpp>
#include "info/ModInfoPopup.hpp"
#include "list/ModListLayer.hpp"
#include "settings/ModSettingsPopup.hpp"
#include <Geode/ui/MDPopup.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/utils/web.hpp>
void geode::openModsList() {
ModListLayer::scene();
}
void geode::openIssueReportPopup(Mod* mod) {
if (mod->getModInfo().m_issues) {
MDPopup::create(
"Issue Report",
mod->getModInfo().m_issues.value().m_info +
"\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" +
dirs::getCrashlogsDir().string() + "`",
"OK", (mod->getModInfo().m_issues.value().m_url ? "Open URL" : ""),
[mod](bool btn2) {
if (btn2) {
web::openLinkInBrowser(
mod->getModInfo().m_issues.value().m_url.value()
);
}
}
)->show();
}
else {
MDPopup::create(
"Issue Report",
"Please report your issue on the "
"[#support](https://discord.com/channels/911701438269386882/979352389985390603) "
"channnel in the [Geode Discord Server](https://discord.gg/9e43WMKzhp)\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" + dirs::getCrashlogsDir().string() + "`",
"OK"
)->show();
}
}
void geode::openInfoPopup(Mod* mod) {
ModInfoLayer::create(mod, nullptr)->show();
LocalModInfoPopup::create(mod, nullptr)->show();
}
void geode::openIndexPopup(Mod* mod) {
if (Index::get()->isKnownItem(mod->getID())) {
ModInfoLayer::create(new ModObject(Index::get()->getKnownItem(mod->getID())), nullptr)
->show();
if (auto item = Index::get()->getItem(
mod->getID(), mod->getVersion().getMajor()
)) {
IndexItemInfoPopup::create(item, nullptr)->show();
}
}
@ -25,3 +61,61 @@ void geode::openSettingsPopup(Mod* mod) {
ModSettingsPopup::create(mod)->show();
}
}
CCNode* geode::createDefaultLogo(CCSize const& size) {
CCNode* spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) {
spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
}
limitNodeSize(spr, size, 1.f, .1f);
return spr;
}
CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
CCNode* spr = nullptr;
if (mod == Loader::getInternalMod()) {
spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
}
else {
spr = CCSprite::create(
fmt::format("{}/logo.png", mod->getID()).c_str()
);
}
if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
limitNodeSize(spr, size, 1.f, .1f);
return spr;
}
CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) {
CCNode* spr = nullptr;
auto logoPath = ghc::filesystem::absolute(item->path / "logo.png");
spr = CCSprite::create(logoPath.string().c_str());
if (!spr) {
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
}
if (!spr) {
spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
}
if (item->isFeatured) {
auto glowSize = size + CCSize(4.f, 4.f);
auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr);
logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width);
logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height);
// i dont know why + 1 is needed and its too late for me to figure out why
spr->setPosition(
logoGlow->getContentSize().width / 2, logoGlow->getContentSize().height / 2
);
// scary mathematics
spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX());
spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY());
logoGlow->addChild(spr);
spr = logoGlow;
}
else {
limitNodeSize(spr, size, 1.f, .1f);
}
return spr;
}

View file

@ -0,0 +1,53 @@
#include "ModInfoPopup.hpp"
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/binding/SliderThumb.hpp>
bool DownloadStatusNode::init() {
if (!CCNode::init()) return false;
this->setContentSize({ 150.f, 25.f });
auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setScale(.33f);
bg->setColor({ 0, 0, 0 });
bg->setOpacity(75);
bg->setContentSize(m_obContentSize * 3);
this->addChild(bg);
m_bar = Slider::create(this, nullptr, .6f);
m_bar->setValue(.0f);
m_bar->updateBar();
m_bar->setPosition(0.f, -5.f);
m_bar->m_touchLogic->m_thumb->setVisible(false);
this->addChild(m_bar);
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setAnchorPoint({ .0f, .5f });
m_label->setScale(.45f);
m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f);
this->addChild(m_label);
return true;
}
DownloadStatusNode* DownloadStatusNode::create() {
auto ret = new DownloadStatusNode();
if (ret && ret->init()) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void DownloadStatusNode::setProgress(uint8_t progress) {
m_bar->setValue(progress / 100.f);
m_bar->updateBar();
}
void DownloadStatusNode::setStatus(std::string const& text) {
m_label->setString(text.c_str());
m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f);
}

View file

@ -1,834 +0,0 @@
#include "ModInfoLayer.hpp"
#include "../dev/HookListLayer.hpp"
#include "../list/ModListView.hpp"
#include "../settings/ModSettingsPopup.hpp"
#include "../settings/AdvancedSettingsPopup.hpp"
#include <InternalLoader.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCTextInputNode.hpp>
#include <Geode/binding/GJListLayer.hpp>
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderThumb.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/BasedButton.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/ranges.hpp>
#include <InternalLoader.hpp>
// TODO: die
#undef min
#undef max
static constexpr int const TAG_CONFIRM_UNINSTALL = 5;
static constexpr int const TAG_DELETE_SAVEDATA = 6;
bool DownloadStatusNode::init() {
if (!CCNode::init()) return false;
this->setContentSize({ 150.f, 25.f });
auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setScale(.33f);
bg->setColor({ 0, 0, 0 });
bg->setOpacity(75);
bg->setContentSize(m_obContentSize * 3);
this->addChild(bg);
m_bar = Slider::create(this, nullptr, .6f);
m_bar->setValue(.0f);
m_bar->updateBar();
m_bar->setPosition(0.f, -5.f);
m_bar->m_touchLogic->m_thumb->setVisible(false);
this->addChild(m_bar);
m_label = CCLabelBMFont::create("", "bigFont.fnt");
m_label->setAnchorPoint({ .0f, .5f });
m_label->setScale(.45f);
m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f);
this->addChild(m_label);
return true;
}
DownloadStatusNode* DownloadStatusNode::create() {
auto ret = new DownloadStatusNode();
if (ret && ret->init()) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void DownloadStatusNode::setProgress(uint8_t progress) {
m_bar->setValue(progress / 100.f);
m_bar->updateBar();
}
void DownloadStatusNode::setStatus(std::string const& text) {
m_label->setString(text.c_str());
m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f);
}
void ModInfoLayer::onChangelog(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
auto winSize = CCDirector::get()->getWinSize();
m_detailsArea->setVisible(toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_detailsArea->setPositionX(
toggle->isToggled() ? winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2
: -5000.f
);
m_changelogArea->setVisible(!toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_changelogArea->setPositionX(
!toggle->isToggled() ? winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2
: -5000.f
);
}
bool ModInfoLayer::init(ModObject* obj, ModListView* list) {
m_noElasticity = true;
m_list = list;
m_mod = obj->m_mod;
bool isInstalledMod;
switch (obj->m_type) {
case ModObjectType::Mod:
{
m_info = obj->m_mod->getModInfo();
isInstalledMod = true;
}
break;
case ModObjectType::Index:
{
m_info = obj->m_index.m_info;
isInstalledMod = false;
}
break;
default: return false;
}
auto winSize = CCDirector::sharedDirector()->getWinSize();
CCSize size { 440.f, 290.f };
if (!this->initWithColor({ 0, 0, 0, 105 })) return false;
m_mainLayer = CCLayer::create();
this->addChild(m_mainLayer);
auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setContentSize(size);
bg->setPosition(winSize.width / 2, winSize.height / 2);
bg->setZOrder(-10);
m_mainLayer->addChild(bg);
m_buttonMenu = CCMenu::create();
m_mainLayer->addChild(m_buttonMenu);
constexpr float logoSize = 40.f;
constexpr float logoOffset = 10.f;
auto nameLabel = CCLabelBMFont::create(m_info.m_name.c_str(), "bigFont.fnt");
nameLabel->setAnchorPoint({ .0f, .5f });
nameLabel->limitLabelWidth(200.f, .7f, .1f);
m_mainLayer->addChild(nameLabel, 2);
auto logoSpr = this->createLogoSpr(obj, { logoSize, logoSize });
m_mainLayer->addChild(logoSpr);
auto developerStr = "by " + m_info.m_developer;
auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt");
developerLabel->setScale(.5f);
developerLabel->setAnchorPoint({ .0f, .5f });
m_mainLayer->addChild(developerLabel);
auto logoTitleWidth =
std::max(
nameLabel->getScaledContentSize().width, developerLabel->getScaledContentSize().width
) +
logoSize + logoOffset;
nameLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 125.f
);
logoSpr->setPosition({ winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2,
winSize.height / 2 + 115.f });
developerLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 105.f
);
auto versionLabel = CCLabelBMFont::create(m_info.m_version.toString().c_str(), "bigFont.fnt");
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.4f);
versionLabel->setPosition(
nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f,
winSize.height / 2 + 125.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
this->registerWithTouchDispatcher();
m_detailsArea = MDTextArea::create(
m_info.m_details ? m_info.m_details.value() : "### No description provided.",
{ 350.f, 137.5f }
);
m_detailsArea->setPosition(
winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2,
winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f
);
m_mainLayer->addChild(m_detailsArea);
m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer());
m_scrollbar->setPosition(
winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f,
winSize.height / 2 - 20.f
);
m_mainLayer->addChild(m_scrollbar);
// changelog
if (m_info.m_changelog) {
m_changelogArea = MDTextArea::create(m_info.m_changelog.value(), { 350.f, 137.5f });
m_changelogArea->setPosition(
-5000.f, winSize.height / 2 - m_changelogArea->getScaledContentSize().height / 2 - 20.f
);
m_changelogArea->setVisible(false);
m_mainLayer->addChild(m_changelogArea);
auto changelogBtnOffSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr), 0x20, true, 32.f,
"GJ_button_01.png", 1.f
);
changelogBtnOffSpr->setScale(.65f);
auto changelogBtnOnSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr), 0x20, true, 32.f,
"GJ_button_02.png", 1.f
);
changelogBtnOnSpr->setScale(.65f);
auto changelogBtn = CCMenuItemToggler::create(
changelogBtnOffSpr, changelogBtnOnSpr, this, menu_selector(ModInfoLayer::onChangelog)
);
changelogBtn->setPosition(-size.width / 2 + 21.5f, .0f);
m_buttonMenu->addChild(changelogBtn);
}
// mod info
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
infoSpr->setScale(.85f);
auto infoBtn =
CCMenuItemSpriteExtra::create(infoSpr, this, menu_selector(ModInfoLayer::onInfo));
infoBtn->setPosition(size.width / 2 - 25.f, size.height / 2 - 25.f);
m_buttonMenu->addChild(infoBtn);
// issue report button
if (m_info.m_issues) {
auto issuesBtnSpr =
ButtonSprite::create("Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f);
issuesBtnSpr->setScale(.75f);
auto issuesBtn = CCMenuItemSpriteExtra::create(
issuesBtnSpr, this, menu_selector(ModInfoLayer::onIssues)
);
issuesBtn->setPosition(0.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(issuesBtn);
}
if (isInstalledMod) {
// mod settings
auto settingsSpr = CCSprite::createWithSpriteFrameName(
"GJ_optionsBtn_001.png"
);
settingsSpr->setScale(.65f);
auto settingsBtn = CCMenuItemSpriteExtra::create(
settingsSpr, this, menu_selector(ModInfoLayer::onSettings)
);
settingsBtn->setPosition(-size.width / 2 + 25.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(settingsBtn);
// Check if a config directory for the mod exists
// Mod::getConfigDir auto-creates the directory for user convenience, so
// have to do it manually
if (ghc::filesystem::exists(
Loader::get()->getGeodeDirectory() / GEODE_CONFIG_DIRECTORY / m_mod->getID()
)) {
auto configSpr = CircleButtonSprite::createWithSpriteFrameName(
"pencil.png"_spr, 1.f, CircleBaseColor::Green, CircleBaseSize::Medium2
);
configSpr->setScale(.65f);
auto configBtn = CCMenuItemSpriteExtra::create(
configSpr, this, menu_selector(ModInfoLayer::onOpenConfigDir)
);
configBtn->setPosition(-size.width / 2 + 65.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(configBtn);
}
if (!m_mod->hasSettings()) {
settingsSpr->setColor({ 150, 150, 150 });
settingsBtn->setTarget(this, menu_selector(ModInfoLayer::onNoSettings));
}
if (m_mod->getModInfo().m_repository) {
auto repoBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("github.png"_spr), this,
menu_selector(ModInfoLayer::onRepository)
);
repoBtn->setPosition(size.width / 2 - 25.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(repoBtn);
}
if (m_mod->getModInfo().m_supportInfo) {
auto supportBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("gift.png"_spr), this,
menu_selector(ModInfoLayer::onSupport)
);
supportBtn->setPosition(size.width / 2 - 60.f, -size.height / 2 + 25.f);
m_buttonMenu->addChild(supportBtn);
}
auto enableBtnSpr = ButtonSprite::create("Enable", "bigFont.fnt", "GJ_button_01.png", .6f);
enableBtnSpr->setScale(.6f);
auto disableBtnSpr =
ButtonSprite::create("Disable", "bigFont.fnt", "GJ_button_06.png", .6f);
disableBtnSpr->setScale(.6f);
auto enableBtn = CCMenuItemToggler::create(
disableBtnSpr, enableBtnSpr, this, menu_selector(ModInfoLayer::onEnableMod)
);
enableBtn->setPosition(-155.f, 75.f);
enableBtn->toggle(!obj->m_mod->isEnabled());
m_buttonMenu->addChild(enableBtn);
if (!m_info.m_supportsDisabling) {
enableBtn->setTarget(this, menu_selector(ModInfoLayer::onDisablingNotSupported));
enableBtnSpr->setColor({ 150, 150, 150 });
disableBtnSpr->setColor({ 150, 150, 150 });
}
if (
m_mod != Loader::get()->getInternalMod() &&
m_mod != Mod::get()
) {
// advanced settings
auto advSettSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png");
advSettSpr->setScale(.65f);
auto advSettBtn = CCMenuItemSpriteExtra::create(
advSettSpr, this, menu_selector(ModInfoLayer::onAdvancedSettings)
);
advSettBtn->setPosition(
infoBtn->getPositionX() - 30.f,
infoBtn->getPositionY()
);
m_buttonMenu->addChild(advSettBtn);
auto uninstallBtnSpr = ButtonSprite::create(
"Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f
);
uninstallBtnSpr->setScale(.6f);
auto uninstallBtn = CCMenuItemSpriteExtra::create(
uninstallBtnSpr, this, menu_selector(ModInfoLayer::onUninstall)
);
uninstallBtn->setPosition(-85.f, 75.f);
m_buttonMenu->addChild(uninstallBtn);
// todo: show update button on loader that invokes the installer
if (Index::get()->isUpdateAvailableForItem(m_info.m_id)) {
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr),
"Update", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, menu_selector(ModInfoLayer::onInstallMod)
);
m_installBtn->setPosition(-8.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(winSize.width / 2 + 105.f, winSize.height / 2 + 75.f);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
auto incomingVersion =
Index::get()->getKnownItem(m_info.m_id).m_info.m_version.toString();
m_updateVersionLabel =
CCLabelBMFont::create(("Available: " + incomingVersion).c_str(), "bigFont.fnt");
m_updateVersionLabel->setScale(.35f);
m_updateVersionLabel->setAnchorPoint({ .0f, .5f });
m_updateVersionLabel->setColor({ 94, 219, 255 });
m_updateVersionLabel->setPosition(
winSize.width / 2 + 35.f, winSize.height / 2 + 75.f
);
m_mainLayer->addChild(m_updateVersionLabel);
}
}
}
else {
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr),
"Install", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, menu_selector(ModInfoLayer::onInstallMod)
);
m_installBtn->setPosition(-143.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(winSize.width / 2 - 25.f, winSize.height / 2 + 75.f);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
}
// check if this mod is being installed/updated, and if so, update UI
if (auto handle = Index::get()->isInstallingItem(m_info.m_id)) {
m_installation = handle;
this->install();
}
auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png");
closeSpr->setScale(.8f);
auto closeBtn =
CCMenuItemSpriteExtra::create(closeSpr, this, menu_selector(ModInfoLayer::onClose));
closeBtn->setPosition(-size.width / 2 + 3.f, size.height / 2 - 3.f);
m_buttonMenu->addChild(closeBtn);
this->setKeypadEnabled(true);
this->setTouchEnabled(true);
return true;
}
void ModInfoLayer::onIssues(CCObject*) {
ModInfoLayer::showIssueReportPopup(m_info);
}
void ModInfoLayer::onSupport(CCObject*) {
MDPopup::create("Support " + m_mod->getName(), m_mod->getModInfo().m_supportInfo.value(), "OK")
->show();
}
void ModInfoLayer::onEnableMod(CCObject* pSender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"You may still see some effects of the mod left, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)
->show();
if (m_list) m_list->updateAllStates(nullptr);
return;
}
if (as<CCMenuItemToggler*>(pSender)->isToggled()) {
auto res = m_mod->loadBinary();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Loading Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
else {
auto res = m_mod->disable();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Disabling Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
if (m_list) m_list->updateAllStates(nullptr);
as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled());
}
void ModInfoLayer::onRepository(CCObject*) {
web::openLinkInBrowser(m_mod->getModInfo().m_repository.value());
}
void ModInfoLayer::onInstallMod(CCObject*) {
auto ticketRes = Index::get()->installItem(Index::get()->getKnownItem(m_info.m_id));
if (!ticketRes) {
return FLAlertLayer::create(
"Unable to install", ticketRes.unwrapErr(), "OK"
)->show();
}
m_installation = ticketRes.unwrap();
createQuickPopup(
"Install",
"The following <cb>mods</c> will be installed: " +
ranges::join(m_installation->toInstall(), ",") + ".",
"Cancel", "OK",
[this](FLAlertLayer*, bool btn2) {
if (btn2) {
this->install();
}
else {
this->updateInstallStatus("", 0);
}
}
);
}
void ModInfoLayer::onCancelInstall(CCObject*) {
m_installBtn->setEnabled(false);
m_installBtnSpr->setString("Cancelling");
m_installation->cancel();
m_installation = nullptr;
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(true);
}
}
void ModInfoLayer::onOpenConfigDir(CCObject*) {
file::openFolder(m_mod->getConfigDir());
}
void ModInfoLayer::onUninstall(CCObject*) {
auto layer = FLAlertLayer::create(
this, "Confirm Uninstall",
"Are you sure you want to uninstall <cr>" + m_info.m_name + "</c>?", "Cancel", "OK"
);
layer->setTag(TAG_CONFIRM_UNINSTALL);
layer->show();
}
void ModInfoLayer::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
switch (layer->getTag()) {
case TAG_CONFIRM_UNINSTALL:
{
if (btn2) {
this->uninstall();
}
}
break;
case TAG_DELETE_SAVEDATA:
{
if (btn2) {
if (ghc::filesystem::remove_all(m_mod->getSaveDir())) {
FLAlertLayer::create("Deleted", "The mod's save data was deleted.", "OK")
->show();
}
else {
FLAlertLayer::create(
"Error", "Unable to delete mod's save directory!", "OK"
)
->show();
}
}
if (m_list) m_list->refreshList();
this->onClose(nullptr);
}
break;
}
}
void ModInfoLayer::updateInstallStatus(std::string const& status, uint8_t progress) {
if (status.size()) {
m_installStatus->setVisible(true);
m_installStatus->setStatus(status);
m_installStatus->setProgress(progress);
}
else {
m_installStatus->setVisible(false);
}
}
void ModInfoLayer::modInstallProgress(
InstallHandle, UpdateStatus status, std::string const& info, uint8_t percentage
) {
switch (status) {
case UpdateStatus::Failed:
{
FLAlertLayer::create("Installation failed :(", info, "OK")->show();
this->updateInstallStatus("", 0);
m_installBtn->setEnabled(true);
m_installBtn->setTarget(this, menu_selector(ModInfoLayer::onInstallMod));
m_installBtnSpr->setString("Install");
m_installBtnSpr->setBG("GE_button_01.png"_spr, false);
m_installation = nullptr;
}
break;
case UpdateStatus::Finished:
{
this->updateInstallStatus("", 100);
FLAlertLayer::create(
"Install complete",
"Mod succesfully installed! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect)",
"OK"
)
->show();
m_installation = nullptr;
if (m_list) m_list->refreshList();
this->onClose(nullptr);
}
break;
default:
{
this->updateInstallStatus(info, percentage);
}
break;
}
}
void ModInfoLayer::install() {
if (m_updateVersionLabel) {
m_updateVersionLabel->setVisible(false);
}
this->updateInstallStatus("Starting install", 0);
m_installBtn->setTarget(this, menu_selector(ModInfoLayer::onCancelInstall));
m_installBtnSpr->setString("Cancel");
m_installBtnSpr->setBG("GJ_button_06.png", false);
m_callbackID = m_installation->start(std::bind(
&ModInfoLayer::modInstallProgress, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4
));
}
void ModInfoLayer::uninstall() {
auto res = m_mod->uninstall();
if (!res) {
return FLAlertLayer::create(
"Uninstall failed :(", res.unwrapErr(), "OK"
)->show();
}
auto layer = FLAlertLayer::create(
this, "Uninstall complete",
"Mod was succesfully uninstalled! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect). "
"<co>Would you also like to delete the mod's "
"save data?</c>",
"Cancel", "Delete", 350.f
);
layer->setTag(TAG_DELETE_SAVEDATA);
layer->show();
}
void ModInfoLayer::onDisablingNotSupported(CCObject* pSender) {
FLAlertLayer::create("Unsupported", "<cr>Disabling</c> is not supported for this mod.", "OK")
->show();
as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled());
}
void ModInfoLayer::onHooks(CCObject*) {
auto layer = HookListLayer::create(this->m_mod);
this->addChild(layer);
layer->showLayer(false);
}
void ModInfoLayer::onSettings(CCObject*) {
ModSettingsPopup::create(m_mod)->show();
}
void ModInfoLayer::onNoSettings(CCObject*) {
FLAlertLayer::create("No Settings Found", "This mod has no customizable settings.", "OK")
->show();
}
void ModInfoLayer::onAdvancedSettings(CCObject*) {
AdvancedSettingsPopup::create(m_mod)->show();
}
void ModInfoLayer::onInfo(CCObject*) {
FLAlertLayer::create(
nullptr, ("About " + m_info.m_name).c_str(),
"<cr>ID: " + m_info.m_id +
"</c>\n"
"<cg>Version: " +
m_info.m_version.toString() +
"</c>\n"
"<cp>Developer: " +
m_info.m_developer +
"</c>\n"
"<cb>Path: " +
m_info.m_path.string() + "</c>\n",
"OK", nullptr, 400.f
)
->show();
}
void ModInfoLayer::keyDown(enumKeyCodes key) {
if (key == KEY_Escape) return this->onClose(nullptr);
if (key == KEY_Space) return;
return FLAlertLayer::keyDown(key);
}
void ModInfoLayer::onClose(CCObject* pSender) {
this->setKeyboardEnabled(false);
this->removeFromParentAndCleanup(true);
if (m_installation) {
m_installation->leave(m_callbackID);
}
};
ModInfoLayer* ModInfoLayer::create(Mod* mod, ModListView* list) {
auto ret = new ModInfoLayer;
if (ret && ret->init(new ModObject(mod), list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
ModInfoLayer* ModInfoLayer::create(ModObject* obj, ModListView* list) {
auto ret = new ModInfoLayer;
if (ret && ret->init(obj, list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
CCNode* ModInfoLayer::createLogoSpr(ModObject* modObj, CCSize const& size) {
switch (modObj->m_type) {
case ModObjectType::Mod:
{
return ModInfoLayer::createLogoSpr(modObj->m_mod, size);
}
break;
case ModObjectType::Index:
{
return ModInfoLayer::createLogoSpr(modObj->m_index, size);
}
break;
default:
{
auto spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
spr->setScaleX(size.width / spr->getContentSize().width);
spr->setScaleY(size.height / spr->getContentSize().height);
if (!spr) {
return CCLabelBMFont::create("OwO", "goldFont.fnt");
}
return spr;
}
break;
}
}
CCNode* ModInfoLayer::createLogoSpr(Mod* mod, CCSize const& size) {
CCNode* spr = nullptr;
if (mod == Loader::getInternalMod()) {
spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
}
else {
spr = CCSprite::create(
fmt::format("{}/logo.png", mod->getID()).c_str()
);
}
if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
spr->setScaleX(size.width / spr->getContentSize().width);
spr->setScaleY(size.height / spr->getContentSize().height);
return spr;
}
CCNode* ModInfoLayer::createLogoSpr(IndexItem const& item, CCSize const& size) {
CCNode* spr = nullptr;
auto logoPath = ghc::filesystem::absolute(item.m_path / "logo.png");
spr = CCSprite::create(logoPath.string().c_str());
if (!spr) {
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
}
if (!spr) {
spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
}
if (Index::get()->isFeaturedItem(item.m_info.m_id)) {
auto glowSize = size + CCSize(4.f, 4.f);
auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr);
logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width);
logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height);
// i dont know why + 1 is needed and its too late for me to figure out why
spr->setPosition(
logoGlow->getContentSize().width / 2, logoGlow->getContentSize().height / 2
);
// scary mathematics
spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX());
spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY());
logoGlow->addChild(spr);
spr = logoGlow;
}
else {
spr->setScaleX(size.width / spr->getContentSize().width);
spr->setScaleY(size.height / spr->getContentSize().height);
}
return spr;
}
void ModInfoLayer::showIssueReportPopup(ModInfo const& info) {
if (info.m_issues) {
MDPopup::create(
"Issue Report",
info.m_issues.value().m_info +
"\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" +
Loader::get()->getCrashLogDirectory().string() + "`",
"OK", (info.m_issues.value().m_url ? "Open URL" : ""),
[info](bool btn2) {
if (btn2) {
web::openLinkInBrowser(info.m_issues.value().m_url.value());
}
}
)->show();
}
else {
MDPopup::create(
"Issue Report",
"Please report your issue on the "
"[#support](https://discord.com/channels/911701438269386882/979352389985390603) "
"channnel in the [Geode Discord Server](https://discord.gg/9e43WMKzhp)\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" +
Loader::get()->getCrashLogDirectory().string() + "`",
"OK"
)
->show();
}
}

View file

@ -0,0 +1,620 @@
#include "ModInfoPopup.hpp"
#include "../dev/HookListLayer.hpp"
#include "../list/ModListView.hpp"
#include "../settings/ModSettingsPopup.hpp"
#include "../settings/AdvancedSettingsPopup.hpp"
#include <InternalLoader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCTextInputNode.hpp>
#include <Geode/binding/GJListLayer.hpp>
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderThumb.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/BasedButton.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/web.hpp>
#include <InternalLoader.hpp>
// TODO: die
#undef min
#undef max
static constexpr int const TAG_CONFIRM_UNINSTALL = 5;
static constexpr int const TAG_DELETE_SAVEDATA = 6;
static const CCSize LAYER_SIZE = { 440.f, 290.f };
bool ModInfoPopup::init(ModInfo const& info, ModListView* list) {
m_noElasticity = true;
m_list = list;
auto winSize = CCDirector::sharedDirector()->getWinSize();
if (!this->initWithColor({ 0, 0, 0, 105 })) return false;
m_mainLayer = CCLayer::create();
this->addChild(m_mainLayer);
auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f });
bg->setContentSize(LAYER_SIZE);
bg->setPosition(winSize.width / 2, winSize.height / 2);
bg->setZOrder(-10);
m_mainLayer->addChild(bg);
m_buttonMenu = CCMenu::create();
m_mainLayer->addChild(m_buttonMenu);
constexpr float logoSize = 40.f;
constexpr float logoOffset = 10.f;
auto nameLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt");
nameLabel->setAnchorPoint({ .0f, .5f });
nameLabel->limitLabelWidth(200.f, .7f, .1f);
m_mainLayer->addChild(nameLabel, 2);
auto logoSpr = this->createLogo({ logoSize, logoSize });
m_mainLayer->addChild(logoSpr);
auto developerStr = "by " + info.m_developer;
auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt");
developerLabel->setScale(.5f);
developerLabel->setAnchorPoint({ .0f, .5f });
m_mainLayer->addChild(developerLabel);
auto logoTitleWidth =
std::max(
nameLabel->getScaledContentSize().width,
developerLabel->getScaledContentSize().width
) +
logoSize + logoOffset;
nameLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset,
winSize.height / 2 + 125.f
);
logoSpr->setPosition({
winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2,
winSize.height / 2 + 115.f
});
developerLabel->setPosition(
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset,
winSize.height / 2 + 105.f
);
auto versionLabel = CCLabelBMFont::create(
info.m_version.toString().c_str(),
"bigFont.fnt"
);
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.4f);
versionLabel->setPosition(
nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f,
winSize.height / 2 + 125.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
this->registerWithTouchDispatcher();
m_detailsArea = MDTextArea::create(
(info.m_details ? info.m_details.value() : "### No description provided."),
{ 350.f, 137.5f }
);
m_detailsArea->setPosition(
winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2,
winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f
);
m_mainLayer->addChild(m_detailsArea);
m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer());
m_scrollbar->setPosition(
winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f,
winSize.height / 2 - 20.f
);
m_mainLayer->addChild(m_scrollbar);
// changelog
if (info.m_changelog) {
m_changelogArea = MDTextArea::create(info.m_changelog.value(), { 350.f, 137.5f });
m_changelogArea->setPosition(
-5000.f, winSize.height / 2 -
m_changelogArea->getScaledContentSize().height / 2 - 20.f
);
m_changelogArea->setVisible(false);
m_mainLayer->addChild(m_changelogArea);
auto changelogBtnOffSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr),
0x20, true, 32.f, "GJ_button_01.png", 1.f
);
changelogBtnOffSpr->setScale(.65f);
auto changelogBtnOnSpr = ButtonSprite::create(
CCSprite::createWithSpriteFrameName("changelog.png"_spr),
0x20, true, 32.f, "GJ_button_02.png", 1.f
);
changelogBtnOnSpr->setScale(.65f);
auto changelogBtn = CCMenuItemToggler::create(
changelogBtnOffSpr, changelogBtnOnSpr,
this, menu_selector(ModInfoPopup::onChangelog)
);
changelogBtn->setPosition(-LAYER_SIZE.width / 2 + 21.5f, .0f);
m_buttonMenu->addChild(changelogBtn);
}
// mod info
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
infoSpr->setScale(.85f);
m_infoBtn = CCMenuItemSpriteExtra::create(
infoSpr, this, menu_selector(ModInfoPopup::onInfo)
);
m_infoBtn->setPosition(
LAYER_SIZE.width / 2 - 25.f,
LAYER_SIZE.height / 2 - 25.f
);
m_buttonMenu->addChild(m_infoBtn);
// repo button
if (info.m_repository) {
auto repoBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("github.png"_spr), this,
menu_selector(ModInfoPopup::onRepository)
);
repoBtn->setPosition(
LAYER_SIZE.width / 2 - 25.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(repoBtn);
}
// support button
if (info.m_supportInfo) {
auto supportBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("gift.png"_spr), this,
menu_selector(ModInfoPopup::onSupport)
);
supportBtn->setPosition(
LAYER_SIZE.width / 2 - 60.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(supportBtn);
}
auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png");
closeSpr->setScale(.8f);
auto closeBtn = CCMenuItemSpriteExtra::create(
closeSpr, this, menu_selector(ModInfoPopup::onClose)
);
closeBtn->setPosition(
-LAYER_SIZE.width / 2 + 3.f,
LAYER_SIZE.height / 2 - 3.f
);
m_buttonMenu->addChild(closeBtn);
this->setKeypadEnabled(true);
this->setTouchEnabled(true);
return true;
}
void ModInfoPopup::onSupport(CCObject*) {
MDPopup::create(
"Support " + this->getModInfo().m_name,
this->getModInfo().m_supportInfo.value(),
"OK"
)->show();
}
void ModInfoPopup::onRepository(CCObject*) {
web::openLinkInBrowser(this->getModInfo().m_repository.value());
}
void ModInfoPopup::onInfo(CCObject*) {
auto info = this->getModInfo();
FLAlertLayer::create(
nullptr,
("About " + info.m_name).c_str(),
fmt::format(
"<cr>ID: {}</c>\n"
"<cg>Version: {}</c>\n"
"<cp>Developer: {}</c>\n"
"<cb>Path: {}</c>\n",
info.m_id,
info.m_version.toString(),
info.m_developer,
info.m_path.string()
),
"OK", nullptr, 400.f
)->show();
}
void ModInfoPopup::onChangelog(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
auto winSize = CCDirector::get()->getWinSize();
m_detailsArea->setVisible(toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_detailsArea->setPositionX(
toggle->isToggled() ?
winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2 :
-5000.f
);
m_changelogArea->setVisible(!toggle->isToggled());
// as it turns out, cocos2d is stupid and still passes touch
// events to invisible nodes
m_changelogArea->setPositionX(
!toggle->isToggled() ?
winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2 :
-5000.f
);
}
void ModInfoPopup::keyDown(enumKeyCodes key) {
if (key == KEY_Escape) return this->onClose(nullptr);
if (key == KEY_Space) return;
return FLAlertLayer::keyDown(key);
}
void ModInfoPopup::onClose(CCObject* pSender) {
this->setKeyboardEnabled(false);
this->removeFromParentAndCleanup(true);
};
// LocalModInfoPopup
bool LocalModInfoPopup::init(Mod* mod, ModListView* list) {
m_mod = mod;
if (!ModInfoPopup::init(mod->getModInfo(), list))
return false;
auto winSize = CCDirector::sharedDirector()->getWinSize();
// mod settings
auto settingsSpr = CCSprite::createWithSpriteFrameName(
"GJ_optionsBtn_001.png"
);
settingsSpr->setScale(.65f);
auto settingsBtn = CCMenuItemSpriteExtra::create(
settingsSpr, this, menu_selector(LocalModInfoPopup::onSettings)
);
settingsBtn->setPosition(
-LAYER_SIZE.width / 2 + 25.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(settingsBtn);
// Check if a config directory for the mod exists
if (ghc::filesystem::exists(mod->getConfigDir(false))) {
auto configSpr = CircleButtonSprite::createWithSpriteFrameName(
"pencil.png"_spr, 1.f, CircleBaseColor::Green, CircleBaseSize::Medium2
);
configSpr->setScale(.65f);
auto configBtn = CCMenuItemSpriteExtra::create(
configSpr, this, menu_selector(LocalModInfoPopup::onOpenConfigDir)
);
configBtn->setPosition(
-LAYER_SIZE.width / 2 + 65.f,
-LAYER_SIZE.height / 2 + 25.f
);
m_buttonMenu->addChild(configBtn);
}
if (!mod->hasSettings()) {
settingsSpr->setColor({ 150, 150, 150 });
settingsBtn->setTarget(this, menu_selector(LocalModInfoPopup::onNoSettings));
}
auto enableBtnSpr = ButtonSprite::create(
"Enable", "bigFont.fnt", "GJ_button_01.png", .6f
);
enableBtnSpr->setScale(.6f);
auto disableBtnSpr = ButtonSprite::create(
"Disable", "bigFont.fnt", "GJ_button_06.png", .6f
);
disableBtnSpr->setScale(.6f);
auto enableBtn = CCMenuItemToggler::create(
disableBtnSpr, enableBtnSpr,
this, menu_selector(LocalModInfoPopup::onEnableMod)
);
enableBtn->setPosition(-155.f, 75.f);
enableBtn->toggle(!mod->isEnabled());
m_buttonMenu->addChild(enableBtn);
if (!mod->supportsDisabling()) {
enableBtn->setTarget(this, menu_selector(LocalModInfoPopup::onDisablingNotSupported));
enableBtnSpr->setColor({ 150, 150, 150 });
disableBtnSpr->setColor({ 150, 150, 150 });
}
if (mod != Loader::get()->getInternalMod()) {
// advanced settings
auto advSettSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png");
advSettSpr->setScale(.65f);
auto advSettBtn = CCMenuItemSpriteExtra::create(
advSettSpr, this, menu_selector(LocalModInfoPopup::onAdvancedSettings)
);
advSettBtn->setPosition(
m_infoBtn->getPositionX() - 30.f,
m_infoBtn->getPositionY()
);
m_buttonMenu->addChild(advSettBtn);
auto uninstallBtnSpr = ButtonSprite::create(
"Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f
);
uninstallBtnSpr->setScale(.6f);
auto uninstallBtn = CCMenuItemSpriteExtra::create(
uninstallBtnSpr, this, menu_selector(LocalModInfoPopup::onUninstall)
);
uninstallBtn->setPosition(-85.f, 75.f);
m_buttonMenu->addChild(uninstallBtn);
auto indexItem = Index::get()->getItem(mod->getModInfo());
// todo: show update button on loader that invokes the installer
if (indexItem && Index::get()->updateAvailable(indexItem)) {
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr,
CCSprite::createWithSpriteFrameName("install.png"_spr),
"Update", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, nullptr
);
m_installBtn->setPosition(-8.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(
winSize.width / 2 + 105.f,
winSize.height / 2 + 75.f
);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
m_updateVersionLabel = CCLabelBMFont::create(
("Available: " + indexItem->info.m_version.toString()).c_str(),
"bigFont.fnt"
);
m_updateVersionLabel->setScale(.35f);
m_updateVersionLabel->setAnchorPoint({ .0f, .5f });
m_updateVersionLabel->setColor({ 94, 219, 255 });
m_updateVersionLabel->setPosition(
winSize.width / 2 + 35.f, winSize.height / 2 + 75.f
);
m_mainLayer->addChild(m_updateVersionLabel);
}
}
// issue report button
if (mod->getModInfo().m_issues) {
auto issuesBtnSpr = ButtonSprite::create(
"Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f
);
issuesBtnSpr->setScale(.75f);
auto issuesBtn = CCMenuItemSpriteExtra::create(
issuesBtnSpr, this, menu_selector(LocalModInfoPopup::onIssues)
);
issuesBtn->setPosition(0.f, -LAYER_SIZE.height / 2 + 25.f);
m_buttonMenu->addChild(issuesBtn);
}
return true;
}
CCNode* LocalModInfoPopup::createLogo(CCSize const& size) {
return geode::createModLogo(m_mod, size);
}
ModInfo LocalModInfoPopup::getModInfo() const {
return m_mod->getModInfo();
}
void LocalModInfoPopup::onIssues(CCObject*) {
geode::openIssueReportPopup(m_mod);
}
void LocalModInfoPopup::onUninstall(CCObject*) {
auto layer = FLAlertLayer::create(
this, "Confirm Uninstall",
fmt::format(
"Are you sure you want to uninstall <cr>{}</c>?",
m_mod->getName()
),
"Cancel", "OK"
);
layer->setTag(TAG_CONFIRM_UNINSTALL);
layer->show();
}
void LocalModInfoPopup::onEnableMod(CCObject* sender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"You may still see some effects of the mod left, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)
->show();
if (m_list) m_list->updateAllStates(nullptr);
return;
}
if (as<CCMenuItemToggler*>(sender)->isToggled()) {
auto res = m_mod->loadBinary();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Loading Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
else {
auto res = m_mod->disable();
if (!res) {
FLAlertLayer::create(
nullptr, "Error Disabling Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
if (m_list) m_list->updateAllStates(nullptr);
as<CCMenuItemToggler*>(sender)->toggle(m_mod->isEnabled());
}
void LocalModInfoPopup::onOpenConfigDir(CCObject*) {
file::openFolder(m_mod->getConfigDir());
}
void LocalModInfoPopup::onDisablingNotSupported(CCObject* pSender) {
FLAlertLayer::create(
"Unsupported",
"<cr>Disabling</c> is not supported for this mod.",
"OK"
)->show();
as<CCMenuItemToggler*>(pSender)->toggle(m_mod->isEnabled());
}
void LocalModInfoPopup::onSettings(CCObject*) {
ModSettingsPopup::create(m_mod)->show();
}
void LocalModInfoPopup::onNoSettings(CCObject*) {
FLAlertLayer::create("No Settings Found", "This mod has no customizable settings.", "OK")
->show();
}
void LocalModInfoPopup::onAdvancedSettings(CCObject*) {
AdvancedSettingsPopup::create(m_mod)->show();
}
void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) {
switch (layer->getTag()) {
case TAG_CONFIRM_UNINSTALL: {
if (btn2) {
this->uninstall();
}
} break;
case TAG_DELETE_SAVEDATA: {
if (btn2) {
if (ghc::filesystem::remove_all(m_mod->getSaveDir())) {
FLAlertLayer::create(
"Deleted", "The mod's save data was deleted.", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Error", "Unable to delete mod's save directory!", "OK"
)->show();
}
}
if (m_list) m_list->refreshList();
this->onClose(nullptr);
} break;
}
}
void LocalModInfoPopup::uninstall() {
auto res = m_mod->uninstall();
if (!res) {
return FLAlertLayer::create(
"Uninstall failed :(", res.unwrapErr(), "OK"
)->show();
}
auto layer = FLAlertLayer::create(
this, "Uninstall complete",
"Mod was succesfully uninstalled! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect). "
"<co>Would you also like to delete the mod's "
"save data?</c>",
"Cancel", "Delete", 350.f
);
layer->setTag(TAG_DELETE_SAVEDATA);
layer->show();
}
LocalModInfoPopup* LocalModInfoPopup::create(Mod* mod, ModListView* list) {
auto ret = new LocalModInfoPopup;
if (ret && ret->init(mod, list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
// IndexItemInfoPopup
bool IndexItemInfoPopup::init(IndexItemHandle item, ModListView* list) {
m_item = item;
auto winSize = CCDirector::sharedDirector()->getWinSize();
if (!ModInfoPopup::init(item->info, list))
return false;
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr),
"Install", "bigFont.fnt"
);
m_installBtnSpr->setScale(.6f);
m_installBtn = CCMenuItemSpriteExtra::create(
m_installBtnSpr, this, nullptr
);
m_installBtn->setPosition(-143.0f, 75.f);
m_buttonMenu->addChild(m_installBtn);
m_installStatus = DownloadStatusNode::create();
m_installStatus->setPosition(
winSize.width / 2 - 25.f,
winSize.height / 2 + 75.f
);
m_installStatus->setVisible(false);
m_mainLayer->addChild(m_installStatus);
return true;
}
CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {
return geode::createIndexItemLogo(m_item, size);
}
ModInfo IndexItemInfoPopup::getModInfo() const {
return m_item->info;
}
IndexItemInfoPopup* IndexItemInfoPopup::create(
IndexItemHandle item, ModListView* list
) {
auto ret = new IndexItemInfoPopup;
if (ret && ret->init(item, list)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}

View file

@ -4,7 +4,8 @@
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/ui/MDTextArea.hpp>
#include <Geode/ui/Scrollbar.hpp>
#include <Index.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/loader/Index.hpp>
USE_GEODE_NAMESPACE();
@ -25,58 +26,66 @@ public:
void setStatus(std::string const& text);
};
class ModInfoLayer : public FLAlertLayer, public FLAlertLayerProtocol {
class ModInfoPopup : public FLAlertLayer {
protected:
Mod* m_mod = nullptr;
ModInfo m_info;
bool m_isIndexMod = false;
ModListView* m_list = nullptr;
DownloadStatusNode* m_installStatus = nullptr;
IconButtonSprite* m_installBtnSpr;
CCMenuItemSpriteExtra* m_installBtn;
CCMenuItemSpriteExtra* m_infoBtn;
CCLabelBMFont* m_updateVersionLabel = nullptr;
InstallHandle m_installation;
InstallItems::CallbackID m_callbackID;
MDTextArea* m_detailsArea;
MDTextArea* m_changelogArea;
Scrollbar* m_scrollbar;
void onHooks(CCObject*);
void onSettings(CCObject*);
void onNoSettings(CCObject*);
void onInfo(CCObject*);
void onEnableMod(CCObject*);
void onInstallMod(CCObject*);
void onCancelInstall(CCObject*);
void onUninstall(CCObject*);
void onDisablingNotSupported(CCObject*);
void onChangelog(CCObject*);
void onIssues(CCObject*);
void onRepository(CCObject*);
void onSupport(CCObject*);
void onOpenConfigDir(CCObject*);
void onAdvancedSettings(CCObject*);
void install();
void uninstall();
void updateInstallStatus(std::string const& status, uint8_t progress);
void onInfo(CCObject*);
void modInstallProgress(
InstallHandle handle, UpdateStatus status, std::string const& info, uint8_t percentage
);
void FLAlert_Clicked(FLAlertLayer*, bool) override;
bool init(ModObject* obj, ModListView* list);
bool init(ModInfo const& info, ModListView* list);
void keyDown(cocos2d::enumKeyCodes) override;
void onClose(cocos2d::CCObject*);
public:
static ModInfoLayer* create(Mod* mod, ModListView* list);
static ModInfoLayer* create(ModObject* obj, ModListView* list);
static void showIssueReportPopup(ModInfo const& info);
static CCNode* createLogoSpr(ModObject* modObj, CCSize const& size);
static CCNode* createLogoSpr(Mod* mod, CCSize const& size);
static CCNode* createLogoSpr(IndexItem const& item, CCSize const& size);
virtual CCNode* createLogo(CCSize const& size) = 0;
virtual ModInfo getModInfo() const = 0;
};
class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
protected:
Mod* m_mod;
bool init(Mod* mod, ModListView* list);
void onIssues(CCObject*);
void onSettings(CCObject*);
void onNoSettings(CCObject*);
void onDisablingNotSupported(CCObject*);
void onEnableMod(CCObject*);
void onUninstall(CCObject*);
void onOpenConfigDir(CCObject*);
void onAdvancedSettings(CCObject*);
void uninstall();
void FLAlert_Clicked(FLAlertLayer*, bool) override;
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
public:
static LocalModInfoPopup* create(Mod* mod, ModListView* list);
};
class IndexItemInfoPopup : public ModInfoPopup {
protected:
IndexItemHandle m_item;
bool init(IndexItemHandle item, ModListView* list);
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
public:
static IndexItemInfoPopup* create(IndexItemHandle item, ModListView* list);
};

View file

@ -0,0 +1,404 @@
#include "ModListCell.hpp"
#include "ModListView.hpp"
#include "../info/ModInfoPopup.hpp"
#include <Geode/binding/StatsCell.hpp>
#include <Geode/binding/FLAlertLayer.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <InternalLoader.hpp>
template <class T>
static bool tryOrAlert(Result<T> const& res, char const* title) {
if (!res) {
FLAlertLayer::create(title, res.unwrapErr(), "OK")->show();
}
return res.isOk();
}
ModListCell::ModListCell(char const* name, CCSize const& size)
: TableViewCell(name, size.width, size.height) {}
void ModListCell::draw() {
reinterpret_cast<StatsCell*>(this)->StatsCell::draw();
}
void ModListCell::setupInfo(ModInfo const& info, bool spaceForCategories) {
m_mainLayer->setVisible(true);
m_backgroundLayer->setOpacity(255);
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
m_mainLayer->addChild(m_menu);
auto logoSize = m_height / 1.5f;
auto logoSpr = this->createLogo({ logoSize, logoSize });
logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 });
m_mainLayer->addChild(logoSpr);
bool hasDesc =
m_display == ModListDisplay::Expanded &&
info.m_description.has_value();
auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && spaceForCategories) {
titleLabel->setPositionY(m_height / 2 + 20.f);
}
else if (hasDesc || spaceForCategories) {
titleLabel->setPositionY(m_height / 2 + 15.f);
}
else {
titleLabel->setPositionY(m_height / 2 + 7.f);
}
titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f);
m_mainLayer->addChild(titleLabel);
auto versionLabel = CCLabelBMFont::create(info.m_version.toString().c_str(), "bigFont.fnt");
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.3f);
versionLabel->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f,
titleLabel->getPositionY() - 1.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
auto creatorStr = "by " + info.m_developer;
auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt");
creatorLabel->setAnchorPoint({ .0f, .5f });
creatorLabel->setScale(.43f);
creatorLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && spaceForCategories) {
creatorLabel->setPositionY(m_height / 2 + 7.5f);
}
else if (hasDesc || spaceForCategories) {
creatorLabel->setPositionY(m_height / 2);
}
else {
creatorLabel->setPositionY(m_height / 2 - 7.f);
}
m_mainLayer->addChild(creatorLabel);
if (hasDesc) {
auto descBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
descBG->setColor({ 0, 0, 0 });
descBG->setOpacity(90);
descBG->setContentSize({ m_width * 2, 60.f });
descBG->setAnchorPoint({ .0f, .5f });
descBG->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (spaceForCategories) {
descBG->setPositionY(m_height / 2 - 7.5f);
}
else {
descBG->setPositionY(m_height / 2 - 17.f);
}
descBG->setScale(.25f);
m_mainLayer->addChild(descBG);
auto descText = CCLabelBMFont::create(info.m_description.value().c_str(), "chatFont.fnt");
descText->setAnchorPoint({ .0f, .5f });
descText->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY());
descText->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f);
m_mainLayer->addChild(descText);
}
}
void ModListCell::updateBGColor(int index) {
if (index % 2) {
m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e));
}
else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c));
m_backgroundLayer->setOpacity(0xff);
}
bool ModListCell::init(ModListView* list, ModListDisplay display) {
m_list = list;
m_display = display;
return true;
}
// ModCell
ModCell::ModCell(const char* name, CCSize const& size)
: ModListCell(name, size) {}
ModCell* ModCell::create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
) {
auto ret = new ModCell(key, size);
if (ret && ret->init(list, display)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void ModCell::onEnable(CCObject* sender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"<cb>Disabling</c> a <cy>mod</c> removes its hooks & patches and "
"calls its user-defined disable function if one exists. You may "
"still see some effects of the mod left however, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)->show();
m_list->updateAllStates(this);
return;
}
if (!as<CCMenuItemToggler*>(sender)->isToggled()) {
tryOrAlert(m_mod->enable(), "Error enabling mod");
}
else {
tryOrAlert(m_mod->disable(), "Error disabling mod");
}
m_list->updateAllStates(this);
}
void ModCell::onUnresolvedInfo(CCObject*) {
std::string info =
"This mod has the following "
"<cr>unresolved dependencies</c>: ";
for (auto const& dep : m_mod->getUnresolvedDependencies()) {
info += fmt::format(
"<cg>{}</c> (<cy>{}</c>), ",
dep.m_id, dep.m_version.toString()
);
}
info.pop_back();
info.pop_back();
FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show();
}
void ModCell::onInfo(CCObject*) {
LocalModInfoPopup::create(m_mod, m_list)->show();
}
void ModCell::updateState() {
bool unresolved = m_mod->hasUnresolvedDependencies();
if (m_enableToggle) {
m_enableToggle->toggle(m_mod->isEnabled());
m_enableToggle->setEnabled(!unresolved);
m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255));
m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255));
}
m_unresolvedExMark->setVisible(unresolved);
}
void ModCell::loadFromMod(Mod* mod) {
m_mod = mod;
this->setupInfo(mod->getModInfo(), false);
auto viewSpr = ButtonSprite::create(
"View", "bigFont.fnt", "GJ_button_01.png", .8f
);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
viewSpr, this, menu_selector(ModCell::onInfo)
);
m_menu->addChild(viewBtn);
if (m_mod->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) {
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);
m_unresolvedExMark = CCMenuItemSpriteExtra::create(
exMark, this, menu_selector(ModCell::onUnresolvedInfo)
);
m_unresolvedExMark->setPosition(-80.f, 0.f);
m_unresolvedExMark->setVisible(false);
m_menu->addChild(m_unresolvedExMark);
// if (m_mod->wasSuccesfullyLoaded()) {
// if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) {
// viewSpr->updateBGImage("GE_button_01.png"_spr);
// auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
// updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f });
// updateIcon->setZOrder(99);
// updateIcon->setScale(.5f);
// viewSpr->addChild(updateIcon);
// }
// }
this->updateState();
}
CCNode* ModCell::createLogo(CCSize const& size) {
return geode::createModLogo(m_mod, size);
}
// IndexItemCell
IndexItemCell::IndexItemCell(char const* name, CCSize const& size)
: ModListCell(name, size) {}
void IndexItemCell::onInfo(CCObject*) {
IndexItemInfoPopup::create(m_item, m_list)->show();
}
IndexItemCell* IndexItemCell::create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
) {
auto ret = new IndexItemCell(key, size);
if (ret && ret->init(list, display)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void IndexItemCell::loadFromItem(IndexItemHandle item) {
m_item = item;
this->setupInfo(item->info, true);
auto viewSpr = ButtonSprite::create(
"View", "bigFont.fnt", "GJ_button_01.png", .8f
);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
viewSpr, this, menu_selector(IndexItemCell::onInfo)
);
m_menu->addChild(viewBtn);
// if (hasCategories) {
// float x = m_height / 2 + logoSize / 2 + 13.f;
// for (auto& category : modobj->m_index.m_categories) {
// auto node = CategoryNode::create(category);
// node->setAnchorPoint({ .0f, .5f });
// node->setPositionX(x);
// node->setScale(.3f);
// if (hasDesc) {
// node->setPositionY(m_height / 2 - 23.f);
// }
// else {
// node->setPositionY(m_height / 2 - 17.f);
// }
// m_mainLayer->addChild(node);
// x += node->getScaledContentSize().width + 5.f;
// }
// }
this->updateState();
}
void IndexItemCell::updateState() {}
CCNode* IndexItemCell::createLogo(CCSize const& size) {
return geode::createIndexItemLogo(m_item, size);
}
// InvalidGeodeFileCell
InvalidGeodeFileCell::InvalidGeodeFileCell(const char* name, CCSize const& size)
: ModListCell(name, size) {}
void InvalidGeodeFileCell::onInfo(CCObject*) {
FLAlertLayer::create(
this, "Error Info",
m_info.m_reason,
"OK", "Remove file", 360.f
)->show();
}
void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
if (btn2) {
try {
if (ghc::filesystem::remove(m_info.m_path)) {
FLAlertLayer::create(
"File removed", "Removed <cy>" + m_info.m_path.string() + "</c>", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_info.m_path.string() + "</c>", "OK"
)->show();
}
}
catch (std::exception& e) {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_info.m_path.string() + "</c>: <cr>" +
std::string(e.what()) + "</c>",
"OK"
)->show();
}
(void)Loader::get()->refreshModsList();
m_list->refreshList();
}
}
InvalidGeodeFileCell* InvalidGeodeFileCell::create(
ModListView* list, ModListDisplay display,
char const* key, CCSize const& size
) {
auto ret = new InvalidGeodeFileCell(key, size);
if (ret && ret->init(list, display)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void InvalidGeodeFileCell::loadFromInfo(InvalidGeodeFile const& info) {
m_info = info;
m_mainLayer->setVisible(true);
auto menu = CCMenu::create();
menu->setPosition(m_width - m_height, m_height / 2);
m_mainLayer->addChild(menu);
auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setScale(.5f);
titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f);
m_mainLayer->addChild(titleLabel);
auto pathLabel = CCLabelBMFont::create(
m_info.m_path.string().c_str(),
"chatFont.fnt"
);
pathLabel->setAnchorPoint({ .0f, .5f });
pathLabel->setScale(.43f);
pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f);
pathLabel->setColor({ 255, 255, 0 });
m_mainLayer->addChild(pathLabel);
auto whySpr = ButtonSprite::create(
"Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f
);
whySpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
whySpr, this, menu_selector(InvalidGeodeFileCell::onInfo)
);
menu->addChild(viewBtn);
}
void InvalidGeodeFileCell::updateState() {}
CCNode* InvalidGeodeFileCell::createLogo(CCSize const& size) {
return nullptr;
}

View file

@ -0,0 +1,91 @@
#pragma once
#include <Geode/binding/TableViewCell.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModInfo.hpp>
#include <Geode/loader/Index.hpp>
USE_GEODE_NAMESPACE();
class ModListView;
enum class ModListDisplay;
class ModListCell : public TableViewCell {
protected:
ModListView* m_list;
CCMenu* m_menu;
CCMenuItemToggler* m_enableToggle = nullptr;
CCMenuItemSpriteExtra* m_unresolvedExMark;
ModListDisplay m_display;
ModListCell(char const* name, CCSize const& size);
bool init(ModListView* list, ModListDisplay display);
void setupInfo(ModInfo const& info, bool spaceForCategories);
void draw() override;
public:
void updateBGColor(int index);
virtual void updateState() = 0;
virtual CCNode* createLogo(CCSize const& size) = 0;
};
class ModCell : public ModListCell {
protected:
Mod* m_mod;
ModCell(char const* name, CCSize const& size);
void onInfo(CCObject*);
void onEnable(CCObject*);
void onUnresolvedInfo(CCObject*);
public:
static ModCell* create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
);
void loadFromMod(Mod* mod);
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
};
class IndexItemCell : public ModListCell {
protected:
IndexItemHandle m_item;
IndexItemCell(char const* name, CCSize const& size);
void onInfo(CCObject*);
public:
static IndexItemCell* create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
);
void loadFromItem(IndexItemHandle item);
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
};
class InvalidGeodeFileCell : public ModListCell, public FLAlertLayerProtocol {
protected:
InvalidGeodeFile m_info;
InvalidGeodeFileCell(char const* name, CCSize const& size);
void onInfo(CCObject*);
void FLAlert_Clicked(FLAlertLayer*, bool btn2) override;
public:
static InvalidGeodeFileCell* create(
ModListView* list, ModListDisplay display,
const char* key, CCSize const& size
);
void loadFromInfo(InvalidGeodeFile const& file);
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
};

View file

@ -13,6 +13,7 @@
#include <Geode/ui/BasedButton.hpp>
#include <Geode/ui/Notification.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/loader/Dirs.hpp>
#include <optional>
static ModListType g_tab = ModListType::Installed;
@ -21,6 +22,8 @@ static ModListLayer* g_instance = nullptr;
bool ModListLayer::init() {
if (!CCLayer::init()) return false;
m_indexListener.bind(this, &ModListLayer::onIndexUpdate);
auto winSize = CCDirector::sharedDirector()->getWinSize();
// create background
@ -204,40 +207,6 @@ void ModListLayer::createSearchControl() {
this->addChild(m_searchInput);
}
void ModListLayer::indexUpdateProgress(
UpdateStatus status, std::string const& info, uint8_t percentage
) {
// if we have a check for updates button
// visible, disable it from being clicked
// again
if (m_checkForUpdatesBtn) {
m_checkForUpdatesBtn->setEnabled(false);
as<ButtonSprite*>(m_checkForUpdatesBtn->getNormalImage())->setString("Updating Index");
}
// if finished, refresh list
if (status == UpdateStatus::Finished) {
m_indexUpdateLabel->setVisible(false);
this->reloadList();
// make sure to release global instance
// and set it back to null
CC_SAFE_RELEASE_NULL(g_instance);
}
else {
m_indexUpdateLabel->setVisible(true);
m_indexUpdateLabel->setString(info.c_str());
}
if (status == UpdateStatus::Failed) {
FLAlertLayer::create("Error Updating Index", info, "OK")->show();
// make sure to release global instance
// and set it back to null
CC_SAFE_RELEASE(g_instance);
}
}
void ModListLayer::reloadList() {
auto winSize = CCDirector::sharedDirector()->getWinSize();
@ -247,26 +216,23 @@ void ModListLayer::reloadList() {
m_list->removeFromParent();
}
auto items = ModListView::modsForType(g_tab);
// create new list
m_query.m_searchFilter =
m_searchInput && m_searchInput->getString() && strlen(m_searchInput->getString())
? std::optional<std::string>(m_searchInput->getString())
: std::nullopt;
auto list = ModListView::create(g_tab, m_expandedList, 358.f, 190.f, m_query);
auto list = ModListView::create(items, m_display);
list->setLayer(this);
// set list status
auto status = list->getStatusAsString();
if (status.size()) {
if (!items->count()) {
m_listLabel->setVisible(true);
m_listLabel->setString(status.c_str());
}
else {
m_listLabel->setString("No mods found");
} else {
m_listLabel->setVisible(false);
}
// update index if needed
if (g_tab == ModListType::Download && !Index::get()->isIndexUpdated()) {
if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) {
m_listLabel->setVisible(true);
m_listLabel->setString("Updating index...");
if (!m_loadingCircle) {
m_loadingCircle = LoadingCircle::create();
@ -310,9 +276,9 @@ void ModListLayer::reloadList() {
// check if the user has searched something,
// and show visual indicator if so
auto hasQuery = m_query.m_searchFilter.has_value();
m_searchBtn->setVisible(!hasQuery);
m_searchClearBtn->setVisible(hasQuery);
// auto hasQuery = m_query.m_searchFilter.has_value();
// m_searchBtn->setVisible(!hasQuery);
// m_searchClearBtn->setVisible(hasQuery);
// add/remove "Check for Updates" button
if (
@ -320,7 +286,7 @@ void ModListLayer::reloadList() {
g_tab == ModListType::Installed &&
// check if index is updated, and if not
// add button if it doesn't exist yet
!Index::get()->isIndexUpdated()
!Index::get()->isUpToDate()
) {
if (!m_checkForUpdatesBtn) {
auto checkSpr = ButtonSprite::create("Check for Updates");
@ -350,11 +316,19 @@ void ModListLayer::onCheckForUpdates(CCObject*) {
g_instance->retain();
// update index
Index::get()->updateIndex(
[](UpdateStatus status, std::string const& info, uint8_t progress) -> void {
g_instance->indexUpdateProgress(status, info, progress);
Index::get()->update();
}
void ModListLayer::onIndexUpdate(IndexUpdateEvent* event) {
std::visit(makeVisitor {
[&](UpdateProgress const& prog) {},
[&](UpdateFinished const&) {
this->reloadList();
},
[&](UpdateError const& error) {
this->reloadList();
}
);
}, event->status);
}
void ModListLayer::textChanged(CCTextInputNode* input) {
@ -373,7 +347,9 @@ void ModListLayer::onReload(CCObject*) {
}
void ModListLayer::onExpand(CCObject* sender) {
m_expandedList = !static_cast<CCMenuItemToggler*>(sender)->isToggled();
m_display = static_cast<CCMenuItemToggler*>(sender)->isToggled() ?
ModListDisplay::Concise :
ModListDisplay::Expanded;
this->reloadList();
}
@ -382,7 +358,7 @@ void ModListLayer::onFilters(CCObject*) {
}
void ModListLayer::onOpenFolder(CCObject*) {
file::openFolder(ghc::filesystem::canonical(Loader::get()->getGeodeDirectory() / "mods"));
file::openFolder(ghc::filesystem::canonical(dirs::getModsDir()));
}
void ModListLayer::onResetSearch(CCObject*) {
@ -442,5 +418,5 @@ ModListLayer* ModListLayer::scene() {
}
ModListLayer::~ModListLayer() {
removeAllChildrenWithCleanup(true);
this->removeAllChildrenWithCleanup(true);
}

View file

@ -3,7 +3,7 @@
#include "ModListView.hpp"
#include <Geode/binding/TextInputDelegate.hpp>
#include <Index.hpp>
#include <Geode/loader/Index.hpp>
USE_GEODE_NAMESPACE();
@ -25,9 +25,9 @@ protected:
CCNode* m_searchBG = nullptr;
CCTextInputNode* m_searchInput = nullptr;
LoadingCircle* m_loadingCircle = nullptr;
ModListQuery m_query;
CCMenuItemSpriteExtra* m_filterBtn;
bool m_expandedList = false;
ModListDisplay m_display = ModListDisplay::Concise;
EventListener<IndexUpdateFilter> m_indexListener;
virtual ~ModListLayer();
@ -43,8 +43,8 @@ protected:
void onFilters(CCObject*);
void keyDown(enumKeyCodes) override;
void textChanged(CCTextInputNode*) override;
void indexUpdateProgress(UpdateStatus status, std::string const& info, uint8_t percentage);
void createSearchControl();
void onIndexUpdate(IndexUpdateEvent* event);
friend class SearchFilterPopup;

View file

@ -1,369 +1,31 @@
#include "ModListView.hpp"
#include "../info/CategoryNode.hpp"
#include "../info/ModInfoLayer.hpp"
#include "ModListLayer.hpp"
#include "ModListCell.hpp"
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/StatsCell.hpp>
#include <Geode/binding/TableView.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/binding/CCContentLayer.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/utils/string.hpp>
#include <Index.hpp>
#include <Geode/loader/Index.hpp>
#include <InternalLoader.hpp>
template <class T>
static bool tryOrAlert(Result<T> const& res, char const* title) {
if (!res) {
FLAlertLayer::create(title, res.unwrapErr(), "OK")->show();
}
return res.isOk();
}
ModCell::ModCell(char const* name, CCSize size) : TableViewCell(name, size.width, size.height) {}
void ModCell::draw() {
reinterpret_cast<StatsCell*>(this)->StatsCell::draw();
}
void ModCell::onFailedInfo(CCObject*) {
FLAlertLayer::create(
this, "Error Info",
m_obj->m_info.m_reason.size() ?
m_obj->m_info.m_reason :
"Unable to load mod",
"OK", "Remove file", 360.f
)->show();
}
void ModCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
if (btn2) {
try {
if (ghc::filesystem::remove(m_obj->m_info.m_path)) {
FLAlertLayer::create(
"File removed", "Removed <cy>" + m_obj->m_info.m_path.string() + "</c>", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_obj->m_info.m_path.string() + "</c>", "OK"
)->show();
}
void ModListView::updateAllStates(ModListCell* toggled) {
for (auto cell : CCArrayExt<ModListCell>(m_tableView->m_cellArray)) {
if (toggled != cell) {
cell->updateState();
}
catch (std::exception& e) {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_obj->m_info.m_path.string() + "</c>: <cr>" +
std::string(e.what()) + "</c>",
"OK"
)->show();
}
(void)Loader::get()->refreshModsList();
m_list->refreshList();
}
}
void ModCell::setupUnloaded() {
m_mainLayer->setVisible(true);
auto menu = CCMenu::create();
menu->setPosition(m_width - m_height, m_height / 2);
m_mainLayer->addChild(menu);
auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setScale(.5f);
titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f);
m_mainLayer->addChild(titleLabel);
auto pathLabel = CCLabelBMFont::create(
m_obj->m_info.m_path.string().c_str(),
"chatFont.fnt"
);
pathLabel->setAnchorPoint({ .0f, .5f });
pathLabel->setScale(.43f);
pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f);
pathLabel->setColor({ 255, 255, 0 });
m_mainLayer->addChild(pathLabel);
auto whySpr = ButtonSprite::create("Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f);
whySpr->setScale(.65f);
auto viewBtn =
CCMenuItemSpriteExtra::create(whySpr, this, menu_selector(ModCell::onFailedInfo));
menu->addChild(viewBtn);
}
void ModCell::setupLoadedButtons() {
auto viewSpr = m_obj->m_mod->wasSuccesfullyLoaded()
? ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f)
: ButtonSprite::create("Why", "bigFont.fnt", "GJ_button_06.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(
viewSpr, this,
m_obj->m_mod->wasSuccesfullyLoaded() ? menu_selector(ModCell::onInfo)
: menu_selector(ModCell::onFailedInfo)
);
m_menu->addChild(viewBtn);
if (m_obj->m_mod->wasSuccesfullyLoaded() && m_obj->m_mod->supportsDisabling()) {
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);
m_unresolvedExMark =
CCMenuItemSpriteExtra::create(exMark, this, menu_selector(ModCell::onUnresolvedInfo));
m_unresolvedExMark->setPosition(-80.f, 0.f);
m_unresolvedExMark->setVisible(false);
m_menu->addChild(m_unresolvedExMark);
if (m_obj->m_mod->wasSuccesfullyLoaded()) {
if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) {
viewSpr->updateBGImage("GE_button_01.png"_spr);
auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f });
updateIcon->setZOrder(99);
updateIcon->setScale(.5f);
viewSpr->addChild(updateIcon);
}
}
}
void ModCell::setupIndexButtons() {
auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo));
m_menu->addChild(viewBtn);
}
void ModCell::loadFromObject(ModObject* modobj) {
m_obj = modobj;
if (modobj->m_type == ModObjectType::Unloaded) {
return this->setupUnloaded();
}
m_mainLayer->setVisible(true);
m_backgroundLayer->setOpacity(255);
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
m_mainLayer->addChild(m_menu);
auto logoSize = m_height / 1.5f;
auto logoSpr = ModInfoLayer::createLogoSpr(modobj, { logoSize, logoSize });
logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 });
m_mainLayer->addChild(logoSpr);
bool hasCategories = false;
ModInfo info;
switch (modobj->m_type) {
case ModObjectType::Mod: info = modobj->m_mod->getModInfo(); break;
case ModObjectType::Index:
info = modobj->m_index.m_info;
hasCategories = m_expanded && modobj->m_index.m_categories.size();
break;
default: return;
}
bool hasDesc = m_expanded && info.m_description.has_value();
auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && hasCategories) {
titleLabel->setPositionY(m_height / 2 + 20.f);
}
else if (hasDesc || hasCategories) {
titleLabel->setPositionY(m_height / 2 + 15.f);
}
else {
titleLabel->setPositionY(m_height / 2 + 7.f);
}
titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f);
m_mainLayer->addChild(titleLabel);
auto versionLabel = CCLabelBMFont::create(info.m_version.toString().c_str(), "bigFont.fnt");
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.3f);
versionLabel->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f,
titleLabel->getPositionY() - 1.f
);
versionLabel->setColor({ 0, 255, 0 });
m_mainLayer->addChild(versionLabel);
auto creatorStr = "by " + info.m_developer;
auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt");
creatorLabel->setAnchorPoint({ .0f, .5f });
creatorLabel->setScale(.43f);
creatorLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && hasCategories) {
creatorLabel->setPositionY(m_height / 2 + 7.5f);
}
else if (hasDesc || hasCategories) {
creatorLabel->setPositionY(m_height / 2);
}
else {
creatorLabel->setPositionY(m_height / 2 - 7.f);
}
m_mainLayer->addChild(creatorLabel);
if (hasDesc) {
auto descBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
descBG->setColor({ 0, 0, 0 });
descBG->setOpacity(90);
descBG->setContentSize({ m_width * 2, 60.f });
descBG->setAnchorPoint({ .0f, .5f });
descBG->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasCategories) {
descBG->setPositionY(m_height / 2 - 7.5f);
}
else {
descBG->setPositionY(m_height / 2 - 17.f);
}
descBG->setScale(.25f);
m_mainLayer->addChild(descBG);
auto descText = CCLabelBMFont::create(info.m_description.value().c_str(), "chatFont.fnt");
descText->setAnchorPoint({ .0f, .5f });
descText->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY());
descText->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f);
m_mainLayer->addChild(descText);
}
if (hasCategories) {
float x = m_height / 2 + logoSize / 2 + 13.f;
for (auto& category : modobj->m_index.m_categories) {
auto node = CategoryNode::create(category);
node->setAnchorPoint({ .0f, .5f });
node->setPositionX(x);
node->setScale(.3f);
if (hasDesc) {
node->setPositionY(m_height / 2 - 23.f);
}
else {
node->setPositionY(m_height / 2 - 17.f);
}
m_mainLayer->addChild(node);
x += node->getScaledContentSize().width + 5.f;
}
}
switch (modobj->m_type) {
case ModObjectType::Mod: this->setupLoadedButtons(); break;
case ModObjectType::Index: this->setupIndexButtons(); break;
default: break;
}
this->updateState();
}
void ModCell::onInfo(CCObject*) {
ModInfoLayer::create(m_obj, m_list)->show();
}
void ModCell::updateBGColor(int index) {
if (index & 1) m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e));
else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c));
m_backgroundLayer->setOpacity(0xff);
}
void ModCell::onEnable(CCObject* pSender) {
if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) {
FLAlertLayer::create(
"Notice",
"<cb>Disabling</c> a <cy>mod</c> removes its hooks & patches and "
"calls its user-defined disable function if one exists. You may "
"still see some effects of the mod left however, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)->show();
m_list->updateAllStates(this);
return;
}
if (!as<CCMenuItemToggler*>(pSender)->isToggled()) {
tryOrAlert(m_obj->m_mod->enable(), "Error enabling mod");
}
else {
tryOrAlert(m_obj->m_mod->disable(), "Error disabling mod");
}
m_list->updateAllStates(this);
}
void ModCell::onUnresolvedInfo(CCObject* pSender) {
std::string info =
"This mod has the following "
"<cr>unresolved dependencies</c>: ";
for (auto const& dep : m_obj->m_mod->getUnresolvedDependencies()) {
info += "<cg>" + dep.m_id +
"</c> "
"(<cy>" +
dep.m_version.toString() + "</c>), ";
}
info.pop_back();
info.pop_back();
FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show();
}
bool ModCell::init(ModListView* list, bool expanded) {
m_list = list;
m_expanded = expanded;
return true;
}
void ModCell::updateState(bool invert) {
if (m_obj->m_type == ModObjectType::Mod) {
bool unresolved = m_obj->m_mod->hasUnresolvedDependencies();
if (m_enableToggle) {
m_enableToggle->toggle(m_obj->m_mod->isEnabled() ^ invert);
m_enableToggle->setEnabled(!unresolved);
m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255));
m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255));
}
m_unresolvedExMark->setVisible(unresolved);
}
}
ModCell* ModCell::create(ModListView* list, bool expanded, char const* key, CCSize size) {
auto pRet = new ModCell(key, size);
if (pRet && pRet->init(list, expanded)) {
return pRet;
}
CC_SAFE_DELETE(pRet);
return nullptr;
}
void ModListView::updateAllStates(ModCell* toggled) {
for (auto cell : CCArrayExt<ModCell>(m_tableView->m_cellArray)) {
cell->updateState(toggled == cell);
}
}
void ModListView::setupList() {
m_itemSeparation = m_expandedList ? 60.f : 40.0f;
m_itemSeparation = m_display == ModListDisplay::Expanded ? 60.f : 40.0f;
if (!m_entries->count()) return;
@ -372,8 +34,10 @@ void ModListView::setupList() {
// fix content layer content size so the
// list is properly aligned to the top
auto coverage = calculateChildCoverage(m_tableView->m_contentLayer);
m_tableView->m_contentLayer->setContentSize({ -coverage.origin.x + coverage.size.width,
-coverage.origin.y + coverage.size.height });
m_tableView->m_contentLayer->setContentSize({
-coverage.origin.x + coverage.size.width,
-coverage.origin.y + coverage.size.height
});
if (m_entries->count() == 1) {
m_tableView->moveToTopWithOffset(m_itemSeparation * 2);
@ -387,72 +51,21 @@ void ModListView::setupList() {
}
TableViewCell* ModListView::getListCell(char const* key) {
return ModCell::create(this, m_expandedList, key, { m_width, m_itemSeparation });
return ModCell::create(this, m_display, key, { m_width, m_itemSeparation });
}
void ModListView::loadCell(TableViewCell* cell, unsigned int index) {
auto obj = as<ModObject*>(m_entries->objectAtIndex(index));
as<ModCell*>(cell)->loadFromObject(obj);
if (obj->m_type == ModObjectType::Mod) {
if (obj->m_mod->wasSuccesfullyLoaded()) {
as<ModCell*>(cell)->updateBGColor(index);
}
else {
cell->m_backgroundLayer->setOpacity(255);
cell->m_backgroundLayer->setColor({ 153, 0, 0 });
}
if (obj->m_mod->isUninstalled()) {
cell->m_backgroundLayer->setColor({ 50, 50, 50 });
}
auto obj = m_entries->objectAtIndex(index);
if (auto mod = typeinfo_cast<ModObject*>(obj)) {
as<ModCell*>(cell)->loadFromMod(mod->mod);
}
else {
as<ModCell*>(cell)->updateBGColor(index);
if (auto mod = typeinfo_cast<IndexItemObject*>(obj)) {
// as<IndexItemCell*>(cell)->loadFromItem(mod->item);
}
}
bool ModListView::filter(ModInfo const& info, ModListQuery const& query) {
// the UI for this functionality has been removed, however
// the code has been kept in case we want to add it back at
// some point.
if (!query.m_searchFilter) return true;
auto check = [query](SearchFlags flag, std::string const& name) -> bool {
if (!(query.m_searchFlags & flag)) return false;
return utils::string::contains(
utils::string::toLower(name), utils::string::toLower(query.m_searchFilter.value())
);
};
if (check(SearchFlag::Name, info.m_name)) return true;
if (check(SearchFlag::ID, info.m_id)) return true;
if (check(SearchFlag::Developer, info.m_developer)) return true;
if (check(SearchFlag::Description, info.m_description.value_or(""))) return true;
if (check(SearchFlag::Details, info.m_details.value_or(""))) return true;
return false;
}
bool ModListView::filter(IndexItem const& item, ModListQuery const& query) {
if (!query.m_showInstalled) {
if (Loader::get()->isModInstalled(item.m_info.m_id)) {
return false;
}
if (auto failed = typeinfo_cast<InvalidGeodeFileObject*>(obj)) {
as<InvalidGeodeFileCell*>(cell)->loadFromInfo(failed->info);
}
if (query.m_categories.size()) {
bool found = false;
for (auto& cat : query.m_categories) {
if (item.m_categories.count(cat)) {
found = true;
}
}
if (!found) {
return false;
}
}
for (auto& plat : query.m_platforms) {
if (item.m_download.m_platforms.count(plat)) {
return filter(item.m_info, query);
}
}
return false;
as<ModListCell*>(cell)->updateBGColor(index);
}
static void sortInstalledMods(std::vector<Mod*>& mods) {
@ -462,16 +75,18 @@ static void sortInstalledMods(std::vector<Mod*>& mods) {
auto front = mods.front();
for (auto mod = mods.begin(); mod != mods.end(); mod++) {
// move mods with updates to front
if (Index::get()->isUpdateAvailableForItem((*mod)->getID())) {
// swap first object and updatable mod
// if the updatable mod is the first object,
// nothing changes
std::rotate(mods.begin(), mod, mod + 1);
if (auto item = Index::get()->getItem(*mod)) {
if (Index::get()->updateAvailable(item)) {
// swap first object and updatable mod
// if the updatable mod is the first object,
// nothing changes
std::rotate(mods.begin(), mod, mod + 1);
// get next object at front for next mod
// to sort
frontIndex++;
front = mods[frontIndex];
// get next object at front for next mod
// to sort
frontIndex++;
front = mods[frontIndex];
}
}
}
}
@ -482,81 +97,50 @@ static std::vector<Mod*> sortedInstalledMods() {
return std::move(mods);
}
bool ModListView::init(
CCArray* mods, ModListType type, bool expanded, float width, float height, ModListQuery query
) {
m_expandedList = expanded;
if (!mods) {
switch (type) {
case ModListType::Installed:
{
mods = CCArray::create();
// failed mods first
for (auto const& mod : Loader::get()->getFailedMods()) {
mods->addObject(new ModObject(mod));
}
// internal geode representation always at the top
auto imod = Loader::getInternalMod();
if (this->filter(imod->getModInfo(), query)) {
mods->addObject(new ModObject(imod));
}
// then other mods
for (auto const& mod : sortedInstalledMods()) {
// if the mod is no longer installed nor
// loaded, it's as good as not existing
// (because it doesn't)
if (mod->isUninstalled() && !mod->isLoaded()) continue;
if (this->filter(mod->getModInfo(), query)) {
mods->addObject(new ModObject(mod));
}
}
if (!mods->count()) {
m_status = Status::SearchEmpty;
}
}
break;
case ModListType::Download:
{
mods = CCArray::create();
for (auto const& item : Index::get()->getItems()) {
if (this->filter(item, query)) {
mods->addObject(new ModObject(item));
}
}
if (!mods->count()) {
m_status = Status::NoModsFound;
}
}
break;
case ModListType::Featured:
{
mods = CCArray::create();
for (auto const& item : Index::get()->getFeaturedItems()) {
if (this->filter(item, query)) {
mods->addObject(new ModObject(item));
}
}
if (!mods->count()) {
m_status = Status::NoModsFound;
}
}
break;
default: return false;
}
}
return CustomListView::init(mods, BoomListType::Default, width, height);
bool ModListView::init(CCArray* mods, ModListDisplay display) {
m_display = display;
return CustomListView::init(mods, BoomListType::Default, 358.f, 190.f);
}
ModListView* ModListView::create(
CCArray* mods, ModListType type, bool expanded, float width, float height,
ModListQuery const& query
) {
CCArray* ModListView::modsForType(ModListType type) {
auto mods = CCArray::create();
switch (type) {
default:
case ModListType::Installed: {
// failed mods first
for (auto const& mod : Loader::get()->getFailedMods()) {
mods->addObject(new InvalidGeodeFileObject(mod));
}
// internal geode representation always at the top
auto imod = Loader::getInternalMod();
mods->addObject(new ModObject(imod));
// then other mods
for (auto const& mod : sortedInstalledMods()) {
// if the mod is no longer installed nor
// loaded, it's as good as not existing
// (because it doesn't)
if (mod->isUninstalled() && !mod->isLoaded()) continue;
mods->addObject(new ModObject(mod));
}
} break;
case ModListType::Download: {
for (auto const& item : Index::get()->getItems()) {
mods->addObject(new IndexItemObject(item));
}
} break;
case ModListType::Featured: {
} break;
}
return mods;
}
ModListView* ModListView::create(CCArray* mods, ModListDisplay display) {
auto pRet = new ModListView;
if (pRet) {
if (pRet->init(mods, type, expanded, width, height, query)) {
if (pRet->init(mods, display)) {
pRet->autorelease();
return pRet;
}
@ -565,24 +149,8 @@ ModListView* ModListView::create(
return nullptr;
}
ModListView* ModListView::create(
ModListType type, bool expanded, float width, float height, ModListQuery const& query
) {
return ModListView::create(nullptr, type, expanded, width, height, query);
}
ModListView::Status ModListView::getStatus() const {
return m_status;
}
std::string ModListView::getStatusAsString() const {
switch (m_status) {
case Status::OK: return "";
case Status::Unknown: return "Unknown Issue";
case Status::NoModsFound: return "No Mods Found";
case Status::SearchEmpty: return "No Mods Match Search Query";
}
return "Unrecorded Status";
ModListView* ModListView::create(ModListType type, ModListDisplay display) {
return ModListView::create(modsForType(type), display);
}
void ModListView::setLayer(ModListLayer* layer) {

View file

@ -3,145 +3,65 @@
#include <Geode/binding/CustomListView.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/binding/TableViewCell.hpp>
#include <Index.hpp>
#include <Geode/loader/Index.hpp>
#include <Geode/loader/Loader.hpp>
#include <optional>
USE_GEODE_NAMESPACE();
struct ModListQuery;
enum class ModListType {
Installed,
Download,
Featured,
};
enum class ModObjectType {
Mod,
Unloaded,
Index,
enum class ModListDisplay {
Concise,
Expanded,
};
class ModListLayer;
class ModListCell;
// for passing invalid files as CCObject
struct InvalidGeodeFileObject : public CCObject {
InvalidGeodeFile info;
inline InvalidGeodeFileObject(InvalidGeodeFile const& info) : info(info) {
this->autorelease();
}
};
// Wrapper so you can pass Mods in a CCArray
struct ModObject : public CCObject {
ModObjectType m_type;
Mod* m_mod;
InvalidGeodeFile m_info;
IndexItem m_index;
inline ModObject(Mod* mod) : m_mod(mod), m_type(ModObjectType::Mod) {
Mod* mod;
inline ModObject(Mod* mod) : mod(mod) {
this->autorelease();
};
inline ModObject(InvalidGeodeFile const& info) :
m_info(info), m_type(ModObjectType::Unloaded) {
this->autorelease();
};
inline ModObject(IndexItem const& index) : m_index(index), m_type(ModObjectType::Index) {
this->autorelease();
};
}
};
class ModListView;
class ModCell : public TableViewCell, public FLAlertLayerProtocol {
protected:
ModListView* m_list;
ModObject* m_obj;
CCMenu* m_menu;
CCMenuItemToggler* m_enableToggle = nullptr;
CCMenuItemSpriteExtra* m_unresolvedExMark;
bool m_expanded;
ModCell(char const* name, CCSize size);
void draw() override;
void onInfo(CCObject*);
void onFailedInfo(CCObject*);
void onEnable(CCObject*);
void onUnresolvedInfo(CCObject*);
void setupUnloaded();
void setupLoadedButtons();
void setupIndexButtons();
void FLAlert_Clicked(FLAlertLayer*, bool btn2) override;
bool init(ModListView* list, bool expanded);
public:
void updateBGColor(int index);
void loadFromObject(ModObject*);
void updateState(bool invert = false);
static ModCell* create(ModListView* list, bool expanded, char const* key, CCSize size);
};
struct SearchFlag {
enum : int {
Name = 0b1,
ID = 0b10,
Developer = 0b100,
Credits = 0b1000,
Description = 0b10000,
Details = 0b100000,
};
};
using SearchFlags = int;
static constexpr SearchFlags ALL_FLAGS = SearchFlag::Name | SearchFlag::ID | SearchFlag::Developer |
SearchFlag::Credits | SearchFlag::Description | SearchFlag::Details;
struct ModListQuery {
std::optional<std::string> m_searchFilter = std::nullopt;
int m_searchFlags = ALL_FLAGS;
bool m_showInstalled = false;
std::unordered_set<PlatformID> m_platforms { GEODE_PLATFORM_TARGET };
std::unordered_set<std::string> m_categories {};
struct IndexItemObject : public CCObject {
IndexItemHandle item;
inline IndexItemObject(IndexItemHandle item) : item(item) {
this->autorelease();
}
};
class ModListView : public CustomListView {
protected:
enum class Status {
OK,
Unknown,
NoModsFound,
SearchEmpty,
};
Status m_status = Status::OK;
ModListLayer* m_layer = nullptr;
bool m_expandedList;
ModListDisplay m_display;
void setupList() override;
TableViewCell* getListCell(char const* key) override;
void loadCell(TableViewCell* cell, unsigned int index) override;
bool init(
CCArray* mods, ModListType type, bool expanded, float width, float height,
ModListQuery query
);
bool filter(ModInfo const& info, ModListQuery const& query);
bool filter(IndexItem const& item, ModListQuery const& query);
bool init(CCArray* mods, ModListDisplay display);
public:
static ModListView* create(
CCArray* mods, ModListType type = ModListType::Installed, bool expanded = false,
float width = 358.f, float height = 220.f, ModListQuery const& query = ModListQuery()
);
static ModListView* create(
ModListType type, bool expanded = false, float width = 358.f, float height = 220.f,
ModListQuery const& query = ModListQuery()
);
static ModListView* create(CCArray* mods, ModListDisplay display);
static ModListView* create(ModListType type, ModListDisplay display);
static CCArray* modsForType(ModListType type);
void updateAllStates(ModCell* toggled = nullptr);
void updateAllStates(ModListCell* except = nullptr);
void setLayer(ModListLayer* layer);
void refreshList();
Status getStatus() const;
std::string getStatusAsString() const;
};

View file

@ -5,6 +5,7 @@
#include "ModListView.hpp"
#include <Geode/binding/GameToolbox.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/ui/SelectList.hpp>
bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
@ -53,10 +54,10 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f };
this->addToggle(
"Show Installed", menu_selector(SearchFilterPopup::onShowInstalled),
layer->m_query.m_showInstalled, 0, pos
);
// this->addToggle(
// "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled),
// layer->m_query.m_showInstalled, 0, pos
// );
// categories
@ -75,23 +76,23 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f };
for (auto& category : Index::get()->getCategories()) {
auto toggle = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(SearchFilterPopup::onCategory), .5f
);
toggle->toggle(m_modLayer->m_query.m_categories.count(category));
toggle->setPosition(pos - winSize / 2);
toggle->setUserObject(CCString::create(category));
m_buttonMenu->addChild(toggle);
// for (auto& category : Index::get()->getCategories()) {
// auto toggle = CCMenuItemToggler::createWithStandardSprites(
// this, menu_selector(SearchFilterPopup::onCategory), .5f
// );
// toggle->toggle(m_modLayer->m_query.m_categories.count(category));
// toggle->setPosition(pos - winSize / 2);
// toggle->setUserObject(CCString::create(category));
// m_buttonMenu->addChild(toggle);
auto label = CategoryNode::create(category, CategoryNodeStyle::Dot);
label->setScale(.4f);
label->setAnchorPoint({ .0f, .5f });
label->setPosition(pos.x + 10.f, pos.y);
m_mainLayer->addChild(label);
// auto label = CategoryNode::create(category, CategoryNodeStyle::Dot);
// label->setScale(.4f);
// label->setAnchorPoint({ .0f, .5f });
// label->setPosition(pos.x + 10.f, pos.y);
// m_mainLayer->addChild(label);
pos.y -= 22.5f;
}
// pos.y -= 22.5f;
// }
return true;
}
@ -99,13 +100,13 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
void SearchFilterPopup::onCategory(CCObject* sender) {
try {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
auto category = static_cast<CCString*>(toggle->getUserObject())->getCString();
if (!toggle->isToggled()) {
m_modLayer->m_query.m_categories.insert(category);
}
else {
m_modLayer->m_query.m_categories.erase(category);
}
// auto category = static_cast<CCString*>(toggle->getUserObject())->getCString();
// if (!toggle->isToggled()) {
// m_modLayer->m_query.m_categories.insert(category);
// }
// else {
// m_modLayer->m_query.m_categories.erase(category);
// }
}
catch (...) {
}
@ -113,7 +114,7 @@ void SearchFilterPopup::onCategory(CCObject* sender) {
void SearchFilterPopup::onShowInstalled(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
m_modLayer->m_query.m_showInstalled = !toggle->isToggled();
// m_modLayer->m_query.m_showInstalled = !toggle->isToggled();
}
void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) {
@ -137,37 +138,39 @@ CCMenuItemToggler* SearchFilterPopup::addToggle(
}
CCMenuItemToggler* SearchFilterPopup::addSearchMatch(char const* title, int flag, CCPoint& pos) {
return this->addToggle(
title, menu_selector(SearchFilterPopup::onSearchToggle),
m_modLayer->m_query.m_searchFlags & flag, flag, pos
);
// return this->addToggle(
// title, menu_selector(SearchFilterPopup::onSearchToggle),
// m_modLayer->m_query.m_searchFlags & flag, flag, pos
// );
return nullptr;
}
CCMenuItemToggler* SearchFilterPopup::addPlatformToggle(
char const* title, PlatformID id, CCPoint& pos
) {
return this->addToggle(
title, menu_selector(SearchFilterPopup::onPlatformToggle),
m_modLayer->m_query.m_platforms.count(id), id.to<int>(), pos
);
// return this->addToggle(
// title, menu_selector(SearchFilterPopup::onPlatformToggle),
// m_modLayer->m_query.m_platforms.count(id), id.to<int>(), pos
// );
return nullptr;
}
void SearchFilterPopup::onSearchToggle(CCObject* sender) {
if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) {
m_modLayer->m_query.m_searchFlags &= ~sender->getTag();
}
else {
m_modLayer->m_query.m_searchFlags |= sender->getTag();
}
// if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) {
// m_modLayer->m_query.m_searchFlags &= ~sender->getTag();
// }
// else {
// m_modLayer->m_query.m_searchFlags |= sender->getTag();
// }
}
void SearchFilterPopup::onPlatformToggle(CCObject* sender) {
if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) {
m_modLayer->m_query.m_platforms.erase(PlatformID::from(sender->getTag()));
}
else {
m_modLayer->m_query.m_platforms.insert(PlatformID::from(sender->getTag()));
}
// if (static_cast<CCMenuItemToggler*>(sender)->isToggled()) {
// m_modLayer->m_query.m_platforms.erase(PlatformID::from(sender->getTag()));
// }
// else {
// m_modLayer->m_query.m_platforms.insert(PlatformID::from(sender->getTag()));
// }
}
void SearchFilterPopup::onClose(CCObject* sender) {

View file

@ -4,6 +4,9 @@
#include <Geode/binding/CCTextInputNode.hpp>
#include <Geode/binding/ColorChannelSprite.hpp>
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Dirs.hpp>
// BoolSettingNode
@ -120,7 +123,7 @@ void FileSettingNode::onPickFile(CCObject*) {
if (auto path = file::pickFile(
file::PickMode::OpenFile,
{
file::geodeRoot(),
dirs::getGameDir(),
setting->getFileFilters().value_or(std::vector<file::FilePickOptions::Filter>())
}
)) {

View file

@ -5,19 +5,11 @@ USE_GEODE_NAMESPACE();
AEnterLayerEvent::AEnterLayerEvent(
std::string const& layerID,
cocos2d::CCNode* layer
) : m_layerID(layerID),
m_layer(layer) {}
std::string AEnterLayerEvent::getID() const {
return m_layerID;
}
cocos2d::CCNode* AEnterLayerEvent::getLayer() const {
return m_layer;
}
) : layerID(layerID),
layer(layer) {}
ListenerResult AEnterLayerFilter::handle(Callback fn, AEnterLayerEvent* event) {
if (m_targetID == event->getID()) {
if (m_targetID == event->layerID) {
fn(event);
}
return ListenerResult::Propagate;

View file

@ -119,8 +119,7 @@ void Notification::setIcon(cocos2d::CCSprite* icon) {
if (m_icon) {
m_icon->removeFromParent();
}
m_icon = icon;
if (icon) {
if ((m_icon = icon)) {
m_bg->addChild(icon);
}
this->updateLayout();
@ -146,6 +145,10 @@ void Notification::animateOut() {
m_bg->runAction(CCFadeTo::create(NOTIFICATION_FADEOUT, 0));
}
void Notification::waitAndHide() {
this->setTime(NOTIFICATION_DEFAULT_TIME);
}
void Notification::show() {
if (!m_showing) {
if (!s_queue->containsObject(this)) {

View file

@ -0,0 +1,22 @@
#include <Geode/platform/platform.hpp>
#include <Geode/utils/general.hpp>
USE_GEODE_NAMESPACE();
PlatformID PlatformID::from(const char* str) {
switch (hash(str)) {
default:
case hash("unknown"): return PlatformID::Unknown;
case hash("windows"): return PlatformID::Windows;
case hash("macos"): return PlatformID::MacOS;
case hash("ios"): return PlatformID::iOS;
case hash("android"): return PlatformID::Android;
case hash("linux"): return PlatformID::Linux;
}
}
PlatformID PlatformID::from(std::string const& str) {
return PlatformID::from(str.c_str());
}

View file

@ -28,6 +28,22 @@ Result<std::string> utils::file::readString(ghc::filesystem::path const& path) {
return Err("Unable to open file");
}
Result<nlohmann::json> utils::file::readJson(ghc::filesystem::path const& path) {
#if _WIN32
std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);
#else
std::ifstream in(path.string(), std::ios::in | std::ios::binary);
#endif
if (in) {
try {
return Ok(nlohmann::json::parse(in));
} catch(std::exception const& e) {
return Err("Unable to parse JSON: " + std::string(e.what()));
}
}
return Err("Unable to open file");
}
Result<byte_array> utils::file::readBinary(ghc::filesystem::path const& path) {
#if _WIN32
std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);
@ -74,55 +90,55 @@ Result<> utils::file::writeBinary(ghc::filesystem::path const& path, byte_array
return Err("Unable to open file");
}
Result<bool> utils::file::createDirectory(ghc::filesystem::path const& path) {
Result<> utils::file::createDirectory(ghc::filesystem::path const& path) {
try {
return Ok(ghc::filesystem::create_directory(path));
ghc::filesystem::create_directory(path);
return Ok();
}
catch (...) {
return Err("Unable to create directory");
}
}
Result<bool> utils::file::createDirectoryAll(ghc::filesystem::path const& path) {
Result<> utils::file::createDirectoryAll(ghc::filesystem::path const& path) {
try {
return Ok(ghc::filesystem::create_directories(path));
ghc::filesystem::create_directories(path);
return Ok();
}
catch (...) {
return Err("Unable to create directories");
}
}
Result<std::vector<std::string>> utils::file::listFiles(std::string const& path) {
if (!ghc::filesystem::exists(path)) return Err("Directory does not exist");
std::vector<std::string> res;
for (auto const& file : ghc::filesystem::directory_iterator(path)) {
res.push_back(file.path().string());
Result<std::vector<ghc::filesystem::path>> utils::file::listFiles(
ghc::filesystem::path const& path, bool recursive
) {
if (!ghc::filesystem::exists(path)) {
return Err("Directory does not exist");
}
if (!ghc::filesystem::is_directory(path)) {
return Err("Path is not a directory");
}
std::vector<ghc::filesystem::path> res;
if (recursive) {
for (auto const& file : ghc::filesystem::recursive_directory_iterator(path)) {
res.push_back(file.path());
}
} else {
for (auto const& file : ghc::filesystem::directory_iterator(path)) {
res.push_back(file.path());
}
}
return Ok(res);
}
Result<std::vector<std::string>> utils::file::listFilesRecursively(std::string const& path) {
if (!ghc::filesystem::exists(path)) return Err("Directory does not exist");
std::vector<std::string> res;
for (auto const& file : ghc::filesystem::recursive_directory_iterator(path)) {
res.push_back(file.path().string());
}
return Ok(res);
}
Result<> utils::file::unzipTo(ghc::filesystem::path const& from, ghc::filesystem::path const& to) {
GEODE_UNWRAP_INTO(auto unzip, Unzip::create(from));
return unzip.extractAllTo(to);
}
static constexpr auto MAX_ENTRY_PATH_LEN = 256;
struct ZipEntry {
unz_file_pos m_pos;
ZPOS64_T m_compressedSize;
ZPOS64_T m_uncompressedSize;
bool isDirectory;
unz_file_pos pos;
ZPOS64_T compressedSize;
ZPOS64_T uncompressedSize;
};
class file::UnzipImpl final {
@ -151,12 +167,17 @@ public:
// Read file and add to entries
unz_file_pos pos;
if (unzGetFilePos(m_zip, &pos) == UNZ_OK) {
auto len = strlen(fileName);
m_entries.insert({
fileName,
ZipEntry {
.m_pos = pos,
.m_compressedSize = fileInfo.compressed_size,
.m_uncompressedSize = fileInfo.uncompressed_size,
.isDirectory =
fileInfo.uncompressed_size == 0 &&
len > 0 &&
(fileName[len - 1] == '/' || fileName[len - 1] == '\\'),
.pos = pos,
.compressedSize = fileInfo.compressed_size,
.uncompressedSize = fileInfo.uncompressed_size,
},
});
}
@ -175,16 +196,20 @@ public:
auto entry = m_entries.at(name);
if (unzGoToFilePos(m_zip, &entry.m_pos) != UNZ_OK) {
if (entry.isDirectory) {
return Err("Entry is directory");
}
if (unzGoToFilePos(m_zip, &entry.pos) != UNZ_OK) {
return Err("Unable to navigate to entry");
}
if (unzOpenCurrentFile(m_zip) != UNZ_OK) {
return Err("Unable to open entry");
}
byte_array res;
res.resize(entry.m_uncompressedSize);
auto size = unzReadCurrentFile(m_zip, res.data(), entry.m_uncompressedSize);
if (size < 0 || size != entry.m_uncompressedSize) {
res.resize(entry.uncompressedSize);
auto size = unzReadCurrentFile(m_zip, res.data(), entry.uncompressedSize);
if (size < 0 || size != entry.uncompressedSize) {
return Err("Unable to extract entry");
}
unzCloseCurrentFile(m_zip);
@ -238,7 +263,7 @@ ghc::filesystem::path Unzip::getPath() const {
}
std::vector<ghc::filesystem::path> Unzip::getEntries() const {
return map::getKeys(m_impl->entries());
return map::keys(m_impl->entries());
}
bool Unzip::hasEntry(Path const& name) {
@ -251,14 +276,48 @@ Result<byte_array> Unzip::extract(Path const& name) {
Result<> Unzip::extractTo(Path const& name, Path const& path) {
GEODE_UNWRAP_INTO(auto bytes, m_impl->extract(name));
GEODE_UNWRAP(file::writeBinary(path, bytes));
// create containing directories for target path
if (path.has_parent_path()) {
GEODE_UNWRAP(file::createDirectoryAll(path.parent_path()));
}
GEODE_UNWRAP(file::writeBinary(path, bytes).expect("Unable to write file {}: {error}", path.string()));
return Ok();
}
Result<> Unzip::extractAllTo(Path const& dir) {
GEODE_UNWRAP(file::createDirectoryAll(dir));
for (auto& [entry, _] : m_impl->entries()) {
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
for (auto& [entry, info] : m_impl->entries()) {
// make sure zip files like root/../../file.txt don't get extracted to
// avoid zip attacks
if (!ghc::filesystem::relative(dir / entry, dir).empty()) {
if (info.isDirectory) {
GEODE_UNWRAP(file::createDirectoryAll(dir / entry));
} else {
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
}
} else {
log::error(
"Zip entry '{}' is not contained within zip bounds",
dir / entry
);
}
}
return Ok();
}
Result<> Unzip::intoDir(
Path const& from,
Path const& to,
bool deleteZipAfter
) {
// scope to ensure the zip is closed after extracting so the zip can be
// removed
{
GEODE_UNWRAP_INTO(auto unzip, Unzip::create(from));
GEODE_UNWRAP(unzip.extractAllTo(to));
}
if (deleteZipAfter) {
try { ghc::filesystem::remove(from); } catch(...) {}
}
return Ok();
}