mirror of
https://github.com/geode-sdk/geode.git
synced 2025-04-24 05:14:40 -04:00
Merge remote-tracking branch 'main-repo/main' into altalk
This commit is contained in:
commit
fc394c3cac
64 changed files with 3283 additions and 3323 deletions
CMakeLists.txtVERSION
bindings
loader
CMakeLists.txt
include/Geode
src
hooks
ids
index
internal
loader
platform
ui
internal
GeodeUI.cpp
info
list
ModListCell.cppModListCell.hppModListLayer.cppModListLayer.hppModListView.cppModListView.hppSearchFilterPopup.cpp
settings
nodes
utils
|
@ -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()
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.7.0
|
||||
0.6.1
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,5 +6,6 @@
|
|||
#include "loader/Mod.hpp"
|
||||
#include "loader/Setting.hpp"
|
||||
#include "loader/SettingEvent.hpp"
|
||||
#include "loader/Dirs.hpp"
|
||||
|
||||
#include <Geode/DefaultInclude.hpp>
|
||||
|
|
59
loader/include/Geode/loader/Dirs.hpp
Normal file
59
loader/include/Geode/loader/Dirs.hpp
Normal 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();
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
109
loader/include/Geode/loader/Index.hpp
Normal file
109
loader/include/Geode/loader/Index.hpp
Normal 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);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__; \
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
219
loader/src/ids/EditorUI.cpp
Normal 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;
|
||||
}
|
||||
};
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
77
loader/src/loader/Dirs.cpp
Normal file
77
loader/src/loader/Dirs.cpp
Normal 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();
|
||||
}
|
|
@ -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
502
loader/src/loader/Index.cpp
Normal 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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -10,7 +10,7 @@ bool crashlog::didLastLaunchCrash() {
|
|||
return false;
|
||||
}
|
||||
|
||||
std::string crashlog::getCrashLogDirectory() {
|
||||
ghc::filesystem::path crashlog::getCrashLogDirectory() {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -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()]]];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
53
loader/src/ui/internal/info/DownloadStatusNode.cpp
Normal file
53
loader/src/ui/internal/info/DownloadStatusNode.cpp
Normal 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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
620
loader/src/ui/internal/info/ModInfoPopup.cpp
Normal file
620
loader/src/ui/internal/info/ModInfoPopup.cpp
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
};
|
404
loader/src/ui/internal/list/ModListCell.cpp
Normal file
404
loader/src/ui/internal/list/ModListCell.cpp
Normal 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;
|
||||
}
|
91
loader/src/ui/internal/list/ModListCell.hpp
Normal file
91
loader/src/ui/internal/list/ModListCell.hpp
Normal 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;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>())
|
||||
}
|
||||
)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
22
loader/src/utils/PlatformID.cpp
Normal file
22
loader/src/utils/PlatformID.cpp
Normal 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());
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue