mirror of
https://github.com/geode-sdk/geode.git
synced 2025-04-24 05:14:40 -04:00
Merge branch 'main' into tulip-hook
This commit is contained in:
commit
bcfe6a6914
56 changed files with 2567 additions and 1971 deletions
CMakeLists.txtVERSIONentry.cpp
loader
CMakeLists.txt
include/Geode
cocos/support/zip_support
loader
utils
src
hooks
internal
loader
Event.cppHook.cppLoader.cppLoaderImpl.cppLoaderImpl.hppMod.cppModImpl.cppModImpl.hppModInfo.cppSetting.cppSettingNode.cpp
main.cppplatform
ui/internal
utils
test
|
@ -1,9 +1,22 @@
|
|||
cmake_minimum_required(VERSION 3.21 FATAL_ERROR)
|
||||
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build libraries static" FORCE)
|
||||
|
||||
# Read version
|
||||
file(READ VERSION GEODE_VERSION)
|
||||
string(STRIP "${GEODE_VERSION}" GEODE_VERSION)
|
||||
|
||||
# Check if version has a tag like v1.0.0-alpha
|
||||
string(FIND ${GEODE_VERSION} "-" GEODE_VERSION_HAS_TAG)
|
||||
if (GEODE_VERSION_HAS_TAG)
|
||||
string(REGEX MATCH "[a-z]+[0-9]?$" GEODE_VERSION_TAG ${GEODE_VERSION})
|
||||
string(SUBSTRING "${GEODE_VERSION}" 0 ${GEODE_VERSION_HAS_TAG} GEODE_VERSION)
|
||||
else()
|
||||
set(GEODE_VERSION_TAG "")
|
||||
endif()
|
||||
|
||||
message(STATUS "Version: ${GEODE_VERSION}, tag: ${GEODE_VERSION_TAG}")
|
||||
|
||||
project(geode-sdk VERSION ${GEODE_VERSION} LANGUAGES CXX C)
|
||||
|
||||
add_library(${PROJECT_NAME} INTERFACE)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.6.1
|
||||
1.0.0-alpha
|
11
entry.cpp
11
entry.cpp
|
@ -1,8 +1,7 @@
|
|||
// included by default in every geode project
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
|
||||
GEODE_API void GEODE_CALL geode_implicit_load(geode::Mod* m) {
|
||||
// geode::Mod::setSharedMod(m);
|
||||
// geode::Loader::get()->dispatchScheduledFunctions(m);
|
||||
}
|
||||
namespace {
|
||||
// to make sure the instance is set into the sharedMod<> in load time
|
||||
static auto mod = geode::getMod();
|
||||
}
|
|
@ -130,6 +130,9 @@ target_compile_definitions(${PROJECT_NAME} PUBLIC GEODE_EXPORTING)
|
|||
# Markdown support
|
||||
CPMAddPackage("gh:mity/md4c#e9ff661")
|
||||
|
||||
# Regex support
|
||||
CPMAddPackage("gh:google/re2#954656f")
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${md4c_SOURCE_DIR}/src)
|
||||
|
||||
# Tulip hook (hooking)
|
||||
|
|
|
@ -25,6 +25,7 @@ THE SOFTWARE.
|
|||
#define __SUPPORT_ZIPUTILS_H__
|
||||
|
||||
#include <string>
|
||||
#include <ghc/filesystem.hpp>
|
||||
#include "../../platform/CCPlatformDefine.h"
|
||||
#include "../../platform/CCPlatformConfig.h"
|
||||
#include "../../include/ccMacros.h"
|
||||
|
|
|
@ -89,10 +89,6 @@ 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;
|
||||
}
|
||||
|
|
|
@ -111,8 +111,8 @@ namespace geode {
|
|||
protected:
|
||||
Mod* m_owner;
|
||||
void* m_address;
|
||||
byte_array m_original;
|
||||
byte_array m_patch;
|
||||
ByteVector m_original;
|
||||
ByteVector m_patch;
|
||||
bool m_applied;
|
||||
|
||||
// Only allow friend classes to create
|
||||
|
|
|
@ -59,7 +59,7 @@ namespace geode {
|
|||
bool isModLoaded(std::string const& id) const;
|
||||
Mod* getLoadedMod(std::string const& id) const;
|
||||
std::vector<Mod*> getAllMods();
|
||||
Mod* getInternalMod();
|
||||
Mod* getModImpl();
|
||||
void updateAllDependencies();
|
||||
std::vector<InvalidGeodeFile> getFailedMods() const;
|
||||
|
||||
|
|
|
@ -32,79 +32,19 @@ namespace geode {
|
|||
~HandleToSaved();
|
||||
};
|
||||
|
||||
inline GEODE_HIDDEN Mod* takeNextLoaderMod();
|
||||
GEODE_HIDDEN Mod* takeNextLoaderMod();
|
||||
|
||||
class ModImpl;
|
||||
|
||||
/**
|
||||
* @class Mod
|
||||
* Represents a Mod ingame.
|
||||
* @abstract
|
||||
*/
|
||||
class GEODE_DLL Mod {
|
||||
protected:
|
||||
/**
|
||||
* Mod info
|
||||
*/
|
||||
ModInfo m_info;
|
||||
/**
|
||||
* Platform-specific info
|
||||
*/
|
||||
PlatformInfo* m_platformInfo = nullptr;
|
||||
/**
|
||||
* Hooks owned by this mod
|
||||
*/
|
||||
std::vector<Hook*> m_hooks;
|
||||
/**
|
||||
* Patches owned by this mod
|
||||
*/
|
||||
std::vector<Patch*> m_patches;
|
||||
/**
|
||||
* Whether the mod is enabled or not
|
||||
*/
|
||||
bool m_enabled = false;
|
||||
/**
|
||||
* Whether the mod binary is loaded or not
|
||||
*/
|
||||
bool m_binaryLoaded = false;
|
||||
/**
|
||||
* Mod temp directory name
|
||||
*/
|
||||
ghc::filesystem::path m_tempDirName;
|
||||
/**
|
||||
* Mod save directory name
|
||||
*/
|
||||
ghc::filesystem::path m_saveDirPath;
|
||||
/**
|
||||
* Pointers to mods that depend on
|
||||
* this Mod. Makes it possible to
|
||||
* enable / disable them automatically,
|
||||
* when their dependency is disabled.
|
||||
*/
|
||||
std::vector<Mod*> m_parentDependencies;
|
||||
decltype(geode_implicit_load)* m_implicitLoadFunc;
|
||||
/**
|
||||
* Saved values
|
||||
*/
|
||||
nlohmann::json m_saved;
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
|
||||
/**
|
||||
* Load the platform binary
|
||||
*/
|
||||
Result<> loadPlatformBinary();
|
||||
Result<> unloadPlatformBinary();
|
||||
Result<> createTempDir();
|
||||
|
||||
// no copying
|
||||
Mod(Mod const&) = delete;
|
||||
Mod operator=(Mod const&) = delete;
|
||||
|
||||
/**
|
||||
* Protected constructor/destructor
|
||||
*/
|
||||
Mod() = delete;
|
||||
Mod(ModInfo const& info);
|
||||
virtual ~Mod();
|
||||
|
||||
friend class ::InternalMod;
|
||||
friend class Loader;
|
||||
friend struct ModInfo;
|
||||
|
||||
|
@ -120,6 +60,16 @@ namespace geode {
|
|||
friend void GEODE_CALL ::geode_implicit_load(Mod*);
|
||||
|
||||
public:
|
||||
// no copying
|
||||
Mod(Mod const&) = delete;
|
||||
Mod operator=(Mod const&) = delete;
|
||||
|
||||
// Protected constructor/destructor
|
||||
Mod() = delete;
|
||||
Mod(ModInfo const& info);
|
||||
~Mod();
|
||||
|
||||
|
||||
std::string getID() const;
|
||||
std::string getName() const;
|
||||
std::string getDeveloper() const;
|
||||
|
@ -149,33 +99,42 @@ namespace geode {
|
|||
ghc::filesystem::path getConfigDir(bool create = true) const;
|
||||
|
||||
bool hasSettings() const;
|
||||
decltype(ModInfo::settings) getSettings() const;
|
||||
std::vector<std::string> getSettingKeys() const;
|
||||
bool hasSetting(std::string const& key) const;
|
||||
std::shared_ptr<Setting> getSetting(std::string const& key) const;
|
||||
std::optional<Setting> getSettingDefinition(std::string const& key) const;
|
||||
SettingValue* getSetting(std::string const& key) const;
|
||||
void registerCustomSetting(
|
||||
std::string const& key,
|
||||
std::unique_ptr<SettingValue> value
|
||||
);
|
||||
|
||||
nlohmann::json& getSaveContainer();
|
||||
|
||||
template <class T>
|
||||
T getSettingValue(std::string const& key) const {
|
||||
if (this->hasSetting(key)) {
|
||||
return geode::getBuiltInSettingValue<T>(this->getSetting(key));
|
||||
if (auto sett = this->getSetting(key)) {
|
||||
return SettingValueSetter<T>::get(sett);
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool setSettingValue(std::string const& key, T const& value) {
|
||||
if (this->hasSetting(key)) {
|
||||
geode::setBuiltInSettingValue<T>(this->getSetting(key), value);
|
||||
return true;
|
||||
T setSettingValue(std::string const& key, T const& value) {
|
||||
if (auto sett = this->getSetting(key)) {
|
||||
auto old = this->getSettingValue<T>(sett);
|
||||
SettingValueSetter<T>::set(sett, value);
|
||||
return old;
|
||||
}
|
||||
return false;
|
||||
return T();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T getSavedValue(std::string const& key) {
|
||||
if (m_saved.count(key)) {
|
||||
auto& saved = this->getSaveContainer();
|
||||
if (saved.count(key)) {
|
||||
try {
|
||||
// json -> T may fail
|
||||
return m_saved.at(key);
|
||||
return saved.at(key);
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
|
@ -185,28 +144,19 @@ namespace geode {
|
|||
|
||||
template <class T>
|
||||
T getSavedValue(std::string const& key, T const& defaultValue) {
|
||||
if (m_saved.count(key)) {
|
||||
auto& saved = this->getSaveContainer();
|
||||
if (saved.count(key)) {
|
||||
try {
|
||||
// json -> T may fail
|
||||
return m_saved.at(key);
|
||||
return saved.at(key);
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
}
|
||||
m_saved[key] = defaultValue;
|
||||
saved[key] = defaultValue;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
HandleToSaved<T> getSavedMutable(std::string const& key) {
|
||||
return HandleToSaved(key, this, this->getSavedValue<T>(key));
|
||||
}
|
||||
|
||||
template <class T>
|
||||
HandleToSaved<T> getSavedMutable(std::string const& key, T const& defaultValue) {
|
||||
return HandleToSaved(key, this, this->getSavedValue<T>(key, defaultValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of an automatically saved variable. When the game is
|
||||
* closed, the value is automatically saved under the key
|
||||
|
@ -216,8 +166,9 @@ namespace geode {
|
|||
*/
|
||||
template<class T>
|
||||
T setSavedValue(std::string const& key, T const& value) {
|
||||
auto& saved = this->getSaveContainer();
|
||||
auto old = this->getSavedValue<T>(key);
|
||||
m_saved[key] = value;
|
||||
saved[key] = value;
|
||||
return old;
|
||||
}
|
||||
|
||||
|
@ -294,7 +245,7 @@ namespace geode {
|
|||
* @returns Successful result on success,
|
||||
* errorful result with info on error
|
||||
*/
|
||||
Result<Patch*> patch(void* address, byte_array data);
|
||||
Result<Patch*> patch(void* address, ByteVector const& data);
|
||||
|
||||
/**
|
||||
* Remove a patch owned by this Mod
|
||||
|
@ -382,12 +333,9 @@ namespace geode {
|
|||
* @note For IPC
|
||||
*/
|
||||
ModJson getRuntimeInfo() const;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
HandleToSaved<T>::~HandleToSaved() {
|
||||
m_mod->setSavedValue(m_key, static_cast<T>(*this));
|
||||
}
|
||||
friend class ModImpl;
|
||||
};
|
||||
|
||||
/**
|
||||
* To bypass the need for cyclic dependencies,
|
||||
|
|
|
@ -10,8 +10,6 @@ namespace geode {
|
|||
class Unzip;
|
||||
}
|
||||
|
||||
using ModJson = nlohmann::ordered_json;
|
||||
|
||||
struct GEODE_DLL Dependency {
|
||||
std::string id;
|
||||
ComparableVersionInfo version;
|
||||
|
@ -111,8 +109,9 @@ namespace geode {
|
|||
std::vector<std::string> spritesheets;
|
||||
/**
|
||||
* Mod settings
|
||||
* @note Not a map because insertion order must be preserved
|
||||
*/
|
||||
std::vector<std::pair<std::string, std::shared_ptr<Setting>>> settings;
|
||||
std::vector<std::pair<std::string, Setting>> settings;
|
||||
/**
|
||||
* Whether the mod can be disabled or not
|
||||
*/
|
||||
|
|
|
@ -1,502 +1,236 @@
|
|||
#pragma once
|
||||
|
||||
#include "Types.hpp"
|
||||
#include "../DefaultInclude.hpp"
|
||||
#include "../utils/JsonValidation.hpp"
|
||||
#include "../utils/Result.hpp"
|
||||
#include "../external/json/json.hpp"
|
||||
#include "../utils/file.hpp"
|
||||
#include "../utils/ranges.hpp"
|
||||
#include "../utils/cocos.hpp"
|
||||
#include "ModInfo.hpp"
|
||||
#include "../external/json/json.hpp"
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <unordered_set>
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4275)
|
||||
|
||||
namespace geode {
|
||||
class Setting;
|
||||
class SettingNode;
|
||||
class BoolSetting;
|
||||
class IntSetting;
|
||||
class FloatSetting;
|
||||
class StringSetting;
|
||||
class SettingValue;
|
||||
|
||||
struct ModInfo;
|
||||
struct GEODE_DLL BoolSetting final {
|
||||
using ValueType = bool;
|
||||
|
||||
enum class SettingType {
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
String,
|
||||
File,
|
||||
Color,
|
||||
ColorAlpha,
|
||||
User,
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
bool defaultValue;
|
||||
|
||||
static Result<BoolSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for all settings in Geode mods. Note that for most purposes
|
||||
* you should use the built-in setting types. If you need a custom setting
|
||||
* type however, inherit from this class. Do note that you are responsible
|
||||
* for things like storing the default value, broadcasting value change
|
||||
* events, making the setting node etc.
|
||||
*/
|
||||
class Setting : public std::enable_shared_from_this<Setting> {
|
||||
struct GEODE_DLL IntSetting final {
|
||||
using ValueType = int64_t;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
ValueType defaultValue;
|
||||
std::optional<ValueType> min;
|
||||
std::optional<ValueType> max;
|
||||
struct {
|
||||
bool arrows = true;
|
||||
bool bigArrows = false;
|
||||
size_t arrowStep = 1;
|
||||
size_t bigArrowStep = 5;
|
||||
bool slider = true;
|
||||
std::optional<ValueType> sliderStep = std::nullopt;
|
||||
bool input = true;
|
||||
} controls;
|
||||
|
||||
static Result<IntSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
struct GEODE_DLL FloatSetting final {
|
||||
using ValueType = double;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
ValueType defaultValue;
|
||||
std::optional<ValueType> min;
|
||||
std::optional<ValueType> max;
|
||||
struct {
|
||||
bool arrows = true;
|
||||
bool bigArrows = false;
|
||||
size_t arrowStep = 1;
|
||||
size_t bigArrowStep = 5;
|
||||
bool slider = true;
|
||||
std::optional<ValueType> sliderStep = std::nullopt;
|
||||
bool input = true;
|
||||
} controls;
|
||||
|
||||
static Result<FloatSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
struct GEODE_DLL StringSetting final {
|
||||
using ValueType = std::string;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
ValueType defaultValue;
|
||||
std::optional<std::string> match;
|
||||
|
||||
static Result<StringSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
struct GEODE_DLL FileSetting final {
|
||||
using ValueType = ghc::filesystem::path;
|
||||
using Filter = utils::file::FilePickOptions::Filter;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
ValueType defaultValue;
|
||||
struct {
|
||||
std::vector<Filter> filters;
|
||||
} controls;
|
||||
|
||||
static Result<FileSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
struct GEODE_DLL ColorSetting final {
|
||||
using ValueType = cocos2d::ccColor3B;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
ValueType defaultValue;
|
||||
|
||||
static Result<ColorSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
struct GEODE_DLL ColorAlphaSetting final {
|
||||
using ValueType = cocos2d::ccColor4B;
|
||||
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::string> description;
|
||||
ValueType defaultValue;
|
||||
|
||||
static Result<ColorAlphaSetting> parse(JsonMaybeObject<ModJson>& obj);
|
||||
};
|
||||
|
||||
struct GEODE_DLL CustomSetting final {
|
||||
ModJson json;
|
||||
};
|
||||
|
||||
using SettingKind = std::variant<
|
||||
BoolSetting,
|
||||
IntSetting,
|
||||
FloatSetting,
|
||||
StringSetting,
|
||||
FileSetting,
|
||||
ColorSetting,
|
||||
ColorAlphaSetting,
|
||||
CustomSetting
|
||||
>;
|
||||
|
||||
struct GEODE_DLL Setting final {
|
||||
private:
|
||||
std::string m_key;
|
||||
SettingKind m_kind;
|
||||
|
||||
Setting() = default;
|
||||
|
||||
public:
|
||||
static Result<Setting> parse(
|
||||
std::string const& key,
|
||||
JsonMaybeValue<ModJson>& obj
|
||||
);
|
||||
Setting(std::string const& key, SettingKind const& kind);
|
||||
|
||||
template<class T>
|
||||
std::optional<T> get() {
|
||||
if (std::holds_alternative<T>(m_kind)) {
|
||||
return std::get<T>(m_kind);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::unique_ptr<SettingValue> createDefaultValue() const;
|
||||
bool isCustom() const;
|
||||
std::string getDisplayName() const;
|
||||
std::optional<std::string> getDescription() const;
|
||||
};
|
||||
|
||||
class GEODE_DLL SettingValue {
|
||||
protected:
|
||||
std::string m_key;
|
||||
std::string m_modID;
|
||||
|
||||
friend struct ModInfo;
|
||||
|
||||
static Result<std::shared_ptr<Setting>> parse(
|
||||
std::string const& type, std::string const& key, JsonMaybeObject<ModJson>& obj
|
||||
);
|
||||
|
||||
SettingValue(std::string const& key);
|
||||
|
||||
public:
|
||||
GEODE_DLL virtual ~Setting() = default;
|
||||
|
||||
// Load from mod.json
|
||||
GEODE_DLL static Result<std::shared_ptr<Setting>> parse(
|
||||
std::string const& key, ModJson const& json
|
||||
);
|
||||
// Load value from saved settings
|
||||
virtual ~SettingValue() = default;
|
||||
virtual bool load(nlohmann::json const& json) = 0;
|
||||
// Save setting value
|
||||
virtual bool save(nlohmann::json& json) const = 0;
|
||||
|
||||
virtual SettingNode* createNode(float width) = 0;
|
||||
|
||||
GEODE_DLL void valueChanged();
|
||||
|
||||
GEODE_DLL std::string getKey() const;
|
||||
virtual SettingType getType() const = 0;
|
||||
std::string getKey() const;
|
||||
};
|
||||
|
||||
// built-in settings' implementation details
|
||||
namespace {
|
||||
#define GEODE_INT_PARSE_SETTING_IMPL(obj, func, ...) \
|
||||
if constexpr (std::is_base_of_v<__VA_ARGS__, Class>) { \
|
||||
auto r = std::static_pointer_cast<Class>(res)->func(obj); \
|
||||
if (!r) return Err(r.unwrapErr()); \
|
||||
}
|
||||
template<class T>
|
||||
class GeodeSettingValue : public SettingValue {
|
||||
public:
|
||||
using ValueType = typename T::ValueType;
|
||||
|
||||
#define GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(func, ...) \
|
||||
if constexpr (std::is_base_of_v<__VA_ARGS__, Class>) { \
|
||||
auto res = static_cast<Class*>(this)->func(value); \
|
||||
if (!res) { \
|
||||
return res; \
|
||||
} \
|
||||
}
|
||||
protected:
|
||||
ValueType m_value;
|
||||
T m_definition;
|
||||
|
||||
template <class ValueType>
|
||||
class IMinMax;
|
||||
template <class Class, class ValueType>
|
||||
class IOneOf;
|
||||
template <class Class, class ValueType>
|
||||
class IMatch;
|
||||
using Valid = std::pair<ValueType, std::optional<std::string>>;
|
||||
|
||||
class ICArrows;
|
||||
template <class ValueType>
|
||||
class ICSlider;
|
||||
class ICInput;
|
||||
class ICFileFilters;
|
||||
GEODE_DLL Valid toValid(ValueType const& value) const;
|
||||
|
||||
template <class Class, class ValueType, SettingType Type>
|
||||
class GeodeSetting : public Setting {
|
||||
protected:
|
||||
ValueType m_default;
|
||||
ValueType m_value;
|
||||
std::optional<std::string> m_name;
|
||||
std::optional<std::string> m_description;
|
||||
bool m_canResetToDefault = true;
|
||||
public:
|
||||
GeodeSettingValue(std::string const& key, T const& definition)
|
||||
: SettingValue(key),
|
||||
m_definition(definition),
|
||||
m_value(definition.defaultValue) {}
|
||||
|
||||
friend class Setting;
|
||||
|
||||
static Result<std::shared_ptr<Class>> parse(
|
||||
std::string const& key, JsonMaybeObject<ModJson>& obj
|
||||
) {
|
||||
auto res = std::make_shared<Class>();
|
||||
|
||||
res->m_key = key;
|
||||
obj.needs("default").into(res->m_default);
|
||||
obj.has("name").intoAs<std::string>(res->m_name);
|
||||
obj.has("description").intoAs<std::string>(res->m_description);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(obj, parseMinMax, IMinMax<ValueType>);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(obj, parseOneOf, IOneOf<Class, ValueType>);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(obj, parseMatch, IMatch<Class, ValueType>);
|
||||
res->setValue(res->m_default);
|
||||
|
||||
if (auto controls = obj.has("control").obj()) {
|
||||
// every built-in setting type has a reset button
|
||||
// by default
|
||||
controls.has("can-reset").into(res->m_canResetToDefault);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, parseArrows, ICArrows);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, parseSlider, ICSlider<ValueType>);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, parseInput, ICInput);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, parseFileFilters, ICFileFilters);
|
||||
}
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
public:
|
||||
using value_t = ValueType;
|
||||
|
||||
std::optional<std::string> getName() const {
|
||||
return m_name;
|
||||
}
|
||||
|
||||
std::string getDisplayName() const {
|
||||
return m_name.value_or(m_key);
|
||||
}
|
||||
|
||||
std::optional<std::string> getDescription() const {
|
||||
return m_description;
|
||||
}
|
||||
|
||||
ValueType getDefault() const {
|
||||
return m_default;
|
||||
}
|
||||
|
||||
ValueType getValue() const {
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void setValue(ValueType const& value) {
|
||||
m_value = value;
|
||||
if constexpr (std::is_base_of_v<IMinMax<ValueType>, Class>) {
|
||||
(void)static_cast<Class*>(this)->constrainMinMax(m_value);
|
||||
}
|
||||
if constexpr (std::is_base_of_v<IOneOf<Class, ValueType>, Class>) {
|
||||
(void)static_cast<Class*>(this)->constrainOneOf(m_value);
|
||||
}
|
||||
if constexpr (std::is_base_of_v<IMatch<Class, ValueType>, Class>) {
|
||||
(void)static_cast<Class*>(this)->constrainMatch(m_value);
|
||||
}
|
||||
this->valueChanged();
|
||||
}
|
||||
|
||||
Result<> isValidValue(ValueType value) {
|
||||
GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainMinMax, IMinMax<ValueType>);
|
||||
GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainOneOf, IOneOf<Class, ValueType>);
|
||||
GEODE_INT_CONSTRAIN_SETTING_CAN_IMPL(constrainMatch, IMatch<Class, ValueType>);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool load(nlohmann::json const& json) override {
|
||||
auto rawJson = json;
|
||||
JsonChecker(rawJson).root("[setting value]").into(m_value);
|
||||
bool load(nlohmann::json const& json) override {
|
||||
try {
|
||||
m_value = json.get<ValueType>();
|
||||
return true;
|
||||
} catch(...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool save(nlohmann::json& json) const override {
|
||||
json = m_value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool canResetToDefault() const {
|
||||
return m_canResetToDefault;
|
||||
}
|
||||
|
||||
SettingType getType() const override {
|
||||
return Type;
|
||||
}
|
||||
};
|
||||
|
||||
template <class ValueType>
|
||||
class IMinMax {
|
||||
protected:
|
||||
std::optional<ValueType> m_min = std::nullopt;
|
||||
std::optional<ValueType> m_max = std::nullopt;
|
||||
|
||||
public:
|
||||
Result<> constrainMinMax(ValueType& value) {
|
||||
if (m_min && value < m_min.value()) {
|
||||
value = m_min.value();
|
||||
return Err(
|
||||
"Value must be between " + std::to_string(m_min.value()) + " and " +
|
||||
std::to_string(m_max.value())
|
||||
);
|
||||
}
|
||||
if (m_max && value > m_max.value()) {
|
||||
value = m_max.value();
|
||||
return Err(
|
||||
"Value must be between " + std::to_string(m_min.value()) + " and " +
|
||||
std::to_string(m_max.value())
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> parseMinMax(JsonMaybeObject<ModJson>& obj) {
|
||||
obj.has("min").intoAs<ValueType>(m_min);
|
||||
obj.has("max").intoAs<ValueType>(m_max);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
std::optional<ValueType> getMin() const {
|
||||
return m_min;
|
||||
}
|
||||
|
||||
std::optional<ValueType> getMax() const {
|
||||
return m_max;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Class, class ValueType>
|
||||
class IOneOf {
|
||||
protected:
|
||||
std::optional<std::unordered_set<ValueType>> m_oneOf = std::nullopt;
|
||||
|
||||
public:
|
||||
Result<> constrainOneOf(ValueType& value) {
|
||||
if (m_oneOf && !m_oneOf.value().count(value)) {
|
||||
value = static_cast<Class*>(this)->getDefault();
|
||||
return Err(
|
||||
"Value must be one of " + utils::ranges::join(m_oneOf.value(), ", ")
|
||||
);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> parseOneOf(JsonMaybeObject<ModJson>& obj) {
|
||||
std::unordered_set<ValueType> oneOf {};
|
||||
for (auto& item : obj.has("one-of").iterate()) {
|
||||
oneOf.insert(item.get<ValueType>());
|
||||
}
|
||||
if (oneOf.size()) {
|
||||
m_oneOf = oneOf;
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
auto getOneOf() const {
|
||||
return m_oneOf;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Class, class ValueType>
|
||||
class IMatch {
|
||||
protected:
|
||||
std::optional<ValueType> m_matchRegex = std::nullopt;
|
||||
|
||||
public:
|
||||
Result<> constrainMatch(ValueType& value) {
|
||||
if (m_matchRegex) {
|
||||
auto regex = std::regex(m_matchRegex.value());
|
||||
if (!std::regex_match(value, regex)) {
|
||||
value = static_cast<Class*>(this)->getDefault();
|
||||
return Err("Value must match regex " + m_matchRegex.value());
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> parseMatch(JsonMaybeObject<ModJson>& obj) {
|
||||
obj.has("match").intoAs<ValueType>(m_matchRegex);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
std::optional<ValueType> getMatch() const {
|
||||
return m_matchRegex;
|
||||
}
|
||||
};
|
||||
|
||||
#define GEODE_INT_DECL_SETTING_CONTROL(Name, name, default, json) \
|
||||
class IC##Name { \
|
||||
protected: \
|
||||
bool m_##name = default; \
|
||||
\
|
||||
public: \
|
||||
Result<> parse##Name(JsonMaybeObject<ModJson>& obj) { \
|
||||
obj.has(json).into(m_##name); \
|
||||
return Ok(); \
|
||||
} \
|
||||
bool has##Name() const { return m_##name; } \
|
||||
}
|
||||
|
||||
class ICArrows {
|
||||
protected:
|
||||
bool m_hasArrows = true;
|
||||
bool m_hasBigArrows = false;
|
||||
size_t m_arrowStep = 1;
|
||||
size_t m_bigArrowStep = 5;
|
||||
|
||||
public:
|
||||
Result<> parseArrows(JsonMaybeObject<ModJson>& obj) {
|
||||
obj.has("arrows").into(m_hasArrows);
|
||||
obj.has("arrow-step").into(m_arrowStep);
|
||||
obj.has("big-arrows").into(m_hasBigArrows);
|
||||
obj.has("big-arrow-step").into(m_bigArrowStep);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool hasArrows() const {
|
||||
return m_hasArrows;
|
||||
}
|
||||
|
||||
bool hasBigArrows() const {
|
||||
return m_hasBigArrows;
|
||||
}
|
||||
|
||||
size_t getArrowStepSize() const {
|
||||
return m_arrowStep;
|
||||
}
|
||||
|
||||
size_t getBigArrowStepSize() const {
|
||||
return m_bigArrowStep;
|
||||
}
|
||||
};
|
||||
|
||||
template <class ValueType>
|
||||
class ICSlider {
|
||||
protected:
|
||||
bool m_hasSlider = true;
|
||||
std::optional<ValueType> m_sliderStep = std::nullopt;
|
||||
|
||||
public:
|
||||
Result<> parseSlider(JsonMaybeObject<ModJson>& obj) {
|
||||
obj.has("slider").into(m_hasSlider);
|
||||
obj.has("slider-step").intoAs<ValueType>(m_sliderStep);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool hasSlider() const {
|
||||
return m_hasSlider;
|
||||
}
|
||||
|
||||
std::optional<ValueType> getSliderStepSize() const {
|
||||
return m_sliderStep;
|
||||
}
|
||||
};
|
||||
|
||||
class ICFileFilters {
|
||||
protected:
|
||||
using Filter = utils::file::FilePickOptions::Filter;
|
||||
|
||||
std::optional<std::vector<Filter>> m_filters = std::nullopt;
|
||||
|
||||
public:
|
||||
Result<> parseFileFilters(JsonMaybeObject<ModJson>& obj) {
|
||||
std::vector<Filter> filters {};
|
||||
for (auto& item : obj.has("filters").iterate()) {
|
||||
if (auto iobj = item.obj()) {
|
||||
Filter filter;
|
||||
iobj.has("description").into(filter.description);
|
||||
iobj.has("files").into(filter.files);
|
||||
filters.push_back(filter);
|
||||
}
|
||||
}
|
||||
if (filters.size()) {
|
||||
m_filters = filters;
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
auto getFileFilters() const {
|
||||
return m_filters;
|
||||
}
|
||||
};
|
||||
|
||||
GEODE_INT_DECL_SETTING_CONTROL(Input, hasInput, true, "input");
|
||||
}
|
||||
|
||||
class BoolSetting : public GeodeSetting<BoolSetting, bool, SettingType::Bool> {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
class IntSetting :
|
||||
public GeodeSetting<IntSetting, int64_t, SettingType::Int>,
|
||||
public IOneOf<IntSetting, int64_t>,
|
||||
public IMinMax<int64_t>,
|
||||
public ICArrows,
|
||||
public ICSlider<int64_t>,
|
||||
public ICInput {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
class FloatSetting :
|
||||
public GeodeSetting<FloatSetting, double, SettingType::Float>,
|
||||
public IOneOf<FloatSetting, double>,
|
||||
public IMinMax<double>,
|
||||
public ICArrows,
|
||||
public ICSlider<double>,
|
||||
public ICInput {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
class StringSetting :
|
||||
public GeodeSetting<StringSetting, std::string, SettingType::String>,
|
||||
public IOneOf<StringSetting, std::string>,
|
||||
public IMatch<StringSetting, std::string> {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
class FileSetting :
|
||||
public GeodeSetting<FileSetting, ghc::filesystem::path, SettingType::File>,
|
||||
public ICFileFilters {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
class ColorSetting : public GeodeSetting<ColorSetting, cocos2d::ccColor3B, SettingType::Color> {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
class ColorAlphaSetting :
|
||||
public GeodeSetting<ColorAlphaSetting, cocos2d::ccColor4B, SettingType::ColorAlpha> {
|
||||
public:
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
// these can't be member functions because C++ is single-pass >:(
|
||||
|
||||
#define GEODE_INT_BUILTIN_SETTING_IF(type, action, ...) \
|
||||
if constexpr (__VA_ARGS__) { \
|
||||
if (setting->getType() == SettingType::type) { \
|
||||
return std::static_pointer_cast<type##Setting>(setting)->action; \
|
||||
} \
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
|
||||
template <class T>
|
||||
T getBuiltInSettingValue(const std::shared_ptr<Setting> setting) {
|
||||
GEODE_INT_BUILTIN_SETTING_IF(Bool, getValue(), std::is_same_v<T, bool>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Float, getValue(), std::is_floating_point_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Int, getValue(), std::is_integral_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(String, getValue(), std::is_same_v<T, std::string>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(File, getValue(), std::is_same_v<T, ghc::filesystem::path>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Color, getValue(), std::is_same_v<T, cocos2d::ccColor3B>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(ColorAlpha, getValue(), std::is_same_v<T, cocos2d::ccColor4B>)
|
||||
else {
|
||||
static_assert(!std::is_same_v<T, T>, "Unsupported type for getting setting value!");
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void setBuiltInSettingValue(const std::shared_ptr<Setting> setting, T const& value) {
|
||||
GEODE_INT_BUILTIN_SETTING_IF(Bool, setValue(value), std::is_same_v<T, bool>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Float, setValue(value), std::is_floating_point_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Int, setValue(value), std::is_integral_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(String, setValue(value), std::is_same_v<T, std::string>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(File, setValue(value), std::is_same_v<T, ghc::filesystem::path>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Color, setValue(value), std::is_same_v<T, cocos2d::ccColor3B>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(ColorAlpha, setValue(value), std::is_same_v<T, cocos2d::ccColor4B>)
|
||||
else {
|
||||
static_assert(!std::is_same_v<T, T>, "Unsupported type for getting setting value!");
|
||||
bool save(nlohmann::json& json) const {
|
||||
json = m_value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// clang-format on
|
||||
GEODE_DLL SettingNode* createNode(float width) override;
|
||||
T castDefinition() const {
|
||||
return m_definition;
|
||||
}
|
||||
Setting getDefinition() const {
|
||||
return Setting(m_key, m_definition);
|
||||
}
|
||||
|
||||
ValueType getValue() const {
|
||||
return m_value;
|
||||
}
|
||||
GEODE_DLL void setValue(ValueType const& value);
|
||||
GEODE_DLL Result<> validate(ValueType const& value) const;
|
||||
};
|
||||
|
||||
using BoolSettingValue = GeodeSettingValue<BoolSetting>;
|
||||
using IntSettingValue = GeodeSettingValue<IntSetting>;
|
||||
using FloatSettingValue = GeodeSettingValue<FloatSetting>;
|
||||
using StringSettingValue = GeodeSettingValue<StringSetting>;
|
||||
using FileSettingValue = GeodeSettingValue<FileSetting>;
|
||||
using ColorSettingValue = GeodeSettingValue<ColorSetting>;
|
||||
using ColorAlphaSettingValue = GeodeSettingValue<ColorAlphaSetting>;
|
||||
|
||||
template<class T>
|
||||
struct SettingValueSetter {
|
||||
static GEODE_DLL T get(SettingValue* setting);
|
||||
static GEODE_DLL void set(SettingValue* setting, T const& value);
|
||||
};
|
||||
}
|
||||
|
||||
#pragma warning(pop)
|
||||
|
|
|
@ -7,64 +7,70 @@
|
|||
#include <optional>
|
||||
|
||||
namespace geode {
|
||||
class GEODE_DLL SettingChangedEvent : public Event {
|
||||
protected:
|
||||
std::string m_modID;
|
||||
Setting* m_setting;
|
||||
struct GEODE_DLL SettingChangedEvent : public Event {
|
||||
Mod* mod;
|
||||
SettingValue* value;
|
||||
|
||||
public:
|
||||
SettingChangedEvent(std::string const& modID, Setting* setting);
|
||||
std::string getModID() const;
|
||||
Setting* getSetting() const;
|
||||
SettingChangedEvent(Mod* mod, SettingValue* value);
|
||||
};
|
||||
|
||||
template <typename T = Setting, typename = std::enable_if_t<std::is_base_of_v<Setting, T>>>
|
||||
class SettingChangedFilter : public EventFilter<SettingChangedEvent> {
|
||||
class GEODE_DLL SettingChangedFilter : public EventFilter<SettingChangedEvent> {
|
||||
protected:
|
||||
std::string m_modID;
|
||||
std::optional<std::string> m_targetKey;
|
||||
|
||||
public:
|
||||
using Callback = void(T*);
|
||||
using Event = SettingChangedEvent;
|
||||
using Callback = void(SettingValue*);
|
||||
|
||||
ListenerResult handle(std::function<Callback> fn, SettingChangedEvent* event);
|
||||
/**
|
||||
* Listen to changes on a setting, or all settings
|
||||
* @param modID Mod whose settings to listen to
|
||||
* @param settingID Setting to listen to, or all settings if nullopt
|
||||
*/
|
||||
SettingChangedFilter(
|
||||
std::string const& modID,
|
||||
std::optional<std::string> const& settingKey
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for built-in setting changes
|
||||
*/
|
||||
template<class T>
|
||||
class GeodeSettingChangedFilter : public SettingChangedFilter {
|
||||
public:
|
||||
using Callback = void(T);
|
||||
|
||||
ListenerResult handle(std::function<Callback> fn, SettingChangedEvent* event) {
|
||||
if (m_modID == event->getModID() &&
|
||||
(!m_targetKey || m_targetKey.value() == event->getSetting()->getKey())) {
|
||||
fn(static_cast<T*>(event->getSetting()));
|
||||
if (
|
||||
m_modID == event->mod->getID() &&
|
||||
(!m_targetKey || m_targetKey.value() == event->value->getKey())
|
||||
) {
|
||||
fn(SettingValueSetter<T>::get(event->value));
|
||||
}
|
||||
return ListenerResult::Propagate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to changes on a specific setting
|
||||
*/
|
||||
SettingChangedFilter(
|
||||
std::string const& modID, std::string const& settingID
|
||||
) :
|
||||
m_modID(modID),
|
||||
m_targetKey(settingID) {}
|
||||
|
||||
/**
|
||||
* Listen to changes on all of a mods' settings
|
||||
*/
|
||||
SettingChangedFilter(std::string const& modID) :
|
||||
m_modID(modID), m_targetKey(std::nullopt) {}
|
||||
protected:
|
||||
std::string m_modID;
|
||||
std::optional<std::string> m_targetKey;
|
||||
GeodeSettingChangedFilter(
|
||||
std::string const& modID,
|
||||
std::string const& settingID
|
||||
) : SettingChangedFilter(modID, settingID) {}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
requires std::is_base_of_v<Setting, T>
|
||||
std::monostate listenForSettingChanges(
|
||||
std::string const& settingID, void (*callback)(T*)
|
||||
std::string const& settingKey, void (*callback)(T)
|
||||
) {
|
||||
(void)new EventListener(
|
||||
callback, SettingChangedFilter<T>(getMod()->getID(), settingID)
|
||||
callback, GeodeSettingChangedFilter<T>(getMod()->getID(), settingKey)
|
||||
);
|
||||
return std::monostate();
|
||||
}
|
||||
|
||||
static std::monostate listenForAllSettingChanges(void (*callback)(Setting*)) {
|
||||
static std::monostate listenForAllSettingChanges(void (*callback)(SettingValue*)) {
|
||||
(void)new EventListener(
|
||||
callback, SettingChangedFilter(getMod()->getID())
|
||||
callback, SettingChangedFilter(getMod()->getID(), std::nullopt)
|
||||
);
|
||||
return std::monostate();
|
||||
}
|
||||
|
|
|
@ -7,17 +7,19 @@
|
|||
namespace geode {
|
||||
class SettingNode;
|
||||
|
||||
struct GEODE_DLL SettingNodeDelegate {
|
||||
virtual void settingValueChanged(SettingNode* node);
|
||||
virtual void settingValueCommitted(SettingNode* node);
|
||||
struct SettingNodeDelegate {
|
||||
virtual void settingValueChanged(SettingNode* node) {}
|
||||
virtual void settingValueCommitted(SettingNode* node) {}
|
||||
};
|
||||
|
||||
class GEODE_DLL SettingNode : public cocos2d::CCNode {
|
||||
protected:
|
||||
std::shared_ptr<Setting> m_setting;
|
||||
SettingValue* m_value;
|
||||
SettingNodeDelegate* m_delegate = nullptr;
|
||||
|
||||
bool init(std::shared_ptr<Setting> setting);
|
||||
bool init(SettingValue* value);
|
||||
void dispatchChanged();
|
||||
void dispatchCommitted();
|
||||
|
||||
public:
|
||||
void setDelegate(SettingNodeDelegate* delegate);
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
#include "../DefaultInclude.hpp"
|
||||
#include "../platform/cplatform.h"
|
||||
#include "../external/json/json.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
class InternalMod;
|
||||
|
||||
namespace geode {
|
||||
/**
|
||||
* Describes the severity of the log
|
||||
|
@ -147,6 +146,7 @@ namespace geode {
|
|||
class FieldIntermediate;
|
||||
}
|
||||
|
||||
using ModJson = nlohmann::ordered_json;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -105,6 +105,7 @@ namespace geode {
|
|||
);
|
||||
|
||||
GEODE_DLL bool isError() const;
|
||||
GEODE_DLL std::string getError() const;
|
||||
|
||||
GEODE_DLL operator bool() const;
|
||||
};
|
||||
|
|
|
@ -311,12 +311,14 @@ namespace geode {
|
|||
class EventListenerNode : public cocos2d::CCNode {
|
||||
protected:
|
||||
EventListener<Filter> m_listener;
|
||||
|
||||
EventListenerNode(EventListener<Filter>&& listener)
|
||||
: m_listener(std::move(listener)) {}
|
||||
|
||||
public:
|
||||
static EventListenerNode* create(EventListener<Filter> listener) {
|
||||
auto ret = new EventListenerNode();
|
||||
auto ret = new EventListenerNode(std::move(listener));
|
||||
if (ret && ret->init()) {
|
||||
ret->m_listener = listener;
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
|
@ -325,9 +327,8 @@ namespace geode {
|
|||
}
|
||||
|
||||
static EventListenerNode* create(typename Filter::Callback callback) {
|
||||
auto ret = new EventListenerNode();
|
||||
auto ret = new EventListenerNode(EventListener<Filter>(callback));
|
||||
if (ret && ret->init()) {
|
||||
ret->m_listener = EventListener(callback);
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
|
@ -343,9 +344,8 @@ namespace geode {
|
|||
// 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();
|
||||
auto ret = new EventListenerNode(EventListener<Filter>(cls, callback));
|
||||
if (ret && ret->init()) {
|
||||
ret->m_listener.bind(cls, callback);
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -9,13 +9,19 @@
|
|||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
// allow converting ghc filesystem to json and back
|
||||
namespace ghc::filesystem {
|
||||
void GEODE_DLL to_json(nlohmann::json& json, path const& path);
|
||||
void GEODE_DLL from_json(nlohmann::json const& json, path& path);
|
||||
}
|
||||
|
||||
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<ByteVector> 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<> writeBinary(ghc::filesystem::path const& path, ByteVector const& data);
|
||||
|
||||
GEODE_DLL Result<> createDirectory(ghc::filesystem::path const& path);
|
||||
GEODE_DLL Result<> createDirectoryAll(ghc::filesystem::path const& path);
|
||||
|
@ -56,7 +62,7 @@ namespace geode::utils::file {
|
|||
/**
|
||||
* Add an entry to the zip with data
|
||||
*/
|
||||
Result<> add(Path const& entry, byte_array const& data);
|
||||
Result<> add(Path const& entry, ByteVector const& data);
|
||||
/**
|
||||
* Add an entry to the zip with string data
|
||||
*/
|
||||
|
@ -122,7 +128,7 @@ namespace geode::utils::file {
|
|||
* Extract entry to memory
|
||||
* @param name Entry path in zip
|
||||
*/
|
||||
Result<byte_array> extract(Path const& name);
|
||||
Result<ByteVector> extract(Path const& name);
|
||||
/**
|
||||
* Extract entry to file
|
||||
* @param name Entry path in zip
|
||||
|
|
|
@ -20,11 +20,11 @@ struct std::hash<ghc::filesystem::path> {
|
|||
};
|
||||
|
||||
namespace geode {
|
||||
using byte_array = std::vector<uint8_t>;
|
||||
using ByteVector = std::vector<uint8_t>;
|
||||
|
||||
template <typename T>
|
||||
byte_array to_byte_array(T const& a) {
|
||||
byte_array out;
|
||||
ByteVector to_byte_array(T const& a) {
|
||||
ByteVector out;
|
||||
out.resize(sizeof(T));
|
||||
std::memcpy(out.data(), &a, sizeof(T));
|
||||
return out;
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace geode::utils::web {
|
|||
* @param url URL to fetch
|
||||
* @returns Returned data as bytes, or error on error
|
||||
*/
|
||||
GEODE_DLL Result<byte_array> fetchBytes(std::string const& url);
|
||||
GEODE_DLL Result<ByteVector> fetchBytes(std::string const& url);
|
||||
|
||||
/**
|
||||
* Synchronously fetch data from the internet
|
||||
|
@ -66,7 +66,7 @@ namespace geode::utils::web {
|
|||
|
||||
using AsyncProgress = std::function<void(SentAsyncWebRequest&, double, double)>;
|
||||
using AsyncExpect = std::function<void(std::string const&)>;
|
||||
using AsyncThen = std::function<void(SentAsyncWebRequest&, byte_array const&)>;
|
||||
using AsyncThen = std::function<void(SentAsyncWebRequest&, ByteVector const&)>;
|
||||
using AsyncCancelled = std::function<void(SentAsyncWebRequest&)>;
|
||||
|
||||
/**
|
||||
|
@ -110,7 +110,7 @@ namespace geode::utils::web {
|
|||
using SentAsyncWebRequestHandle = std::shared_ptr<SentAsyncWebRequest>;
|
||||
|
||||
template <class T>
|
||||
using DataConverter = Result<T> (*)(byte_array const&);
|
||||
using DataConverter = Result<T> (*)(ByteVector const&);
|
||||
|
||||
/**
|
||||
* An asynchronous, thread-safe web request. Downloads data from the
|
||||
|
@ -272,7 +272,7 @@ namespace geode::utils::web {
|
|||
* @returns AsyncWebResult, where you can specify the `then` action for
|
||||
* after the download is finished
|
||||
*/
|
||||
AsyncWebResult<byte_array> bytes();
|
||||
AsyncWebResult<ByteVector> bytes();
|
||||
/**
|
||||
* Download into memory as JSON
|
||||
* @returns AsyncWebResult, where you can specify the `then` action for
|
||||
|
@ -298,7 +298,7 @@ namespace geode::utils::web {
|
|||
template <class T>
|
||||
AsyncWebRequest& AsyncWebResult<T>::then(std::function<void(T)> handle) {
|
||||
m_request.m_then = [converter = m_converter,
|
||||
handle](SentAsyncWebRequest& req, byte_array const& arr) {
|
||||
handle](SentAsyncWebRequest& req, ByteVector const& arr) {
|
||||
auto conv = converter(arr);
|
||||
if (conv) {
|
||||
handle(conv.unwrap());
|
||||
|
@ -313,7 +313,7 @@ namespace geode::utils::web {
|
|||
template <class T>
|
||||
AsyncWebRequest& AsyncWebResult<T>::then(std::function<void(SentAsyncWebRequest&, T)> handle) {
|
||||
m_request.m_then = [converter = m_converter,
|
||||
handle](SentAsyncWebRequest& req, byte_array const& arr) {
|
||||
handle](SentAsyncWebRequest& req, ByteVector const& arr) {
|
||||
auto conv = converter(arr);
|
||||
if (conv) {
|
||||
handle(req, conv.value());
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
#include <Geode/ui/Notification.hpp>
|
||||
#include <Geode/ui/Popup.hpp>
|
||||
#include <Geode/utils/cocos.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
|
@ -102,7 +102,7 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
|
|||
"Send",
|
||||
[](auto, bool btn2) {
|
||||
if (btn2) {
|
||||
geode::openIssueReportPopup(InternalMod::get());
|
||||
geode::openIssueReportPopup(Mod::get());
|
||||
}
|
||||
},
|
||||
false
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#include "../loader/LoaderImpl.hpp"
|
||||
#include <loader/LoaderImpl.hpp>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
#include "InternalMod.hpp"
|
||||
|
||||
#include "about.hpp"
|
||||
|
||||
#include <Geode/loader/Dirs.hpp>
|
||||
#include <LoaderImpl.hpp>
|
||||
|
||||
static constexpr char const* SUPPORT_INFO = R"MD(
|
||||
**Geode** is funded through your gracious <cy>**donations**</c>!
|
||||
You can support our work by sending <cp>**catgirl pictures**</c> to [HJfod](user:104257) :))
|
||||
)MD";
|
||||
|
||||
static ModInfo getInternalModInfo() {
|
||||
try {
|
||||
auto json = ModJson::parse(LOADER_MOD_JSON);
|
||||
auto infoRes = ModInfo::create(json);
|
||||
if (infoRes.isErr()) {
|
||||
LoaderImpl::get()->platformMessageBox(
|
||||
"Fatal Internal Error",
|
||||
"Unable to parse loader mod.json: \"" + infoRes.unwrapErr() +
|
||||
"\"\n"
|
||||
"This is a fatal internal error in the loader, please "
|
||||
"contact Geode developers immediately!"
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
auto info = infoRes.unwrap();
|
||||
info.details = LOADER_ABOUT_MD;
|
||||
info.supportInfo = SUPPORT_INFO;
|
||||
info.supportsDisabling = false;
|
||||
return info;
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
LoaderImpl::get()->platformMessageBox(
|
||||
"Fatal Internal Error",
|
||||
"Unable to parse loader mod.json: \"" + std::string(e.what()) +
|
||||
"\"\n"
|
||||
"This is a fatal internal error in the loader, please "
|
||||
"contact Geode developers immediately!"
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
InternalMod::InternalMod() : Mod(ModInfo()) {}
|
||||
|
||||
void InternalMod::setModInfo() {
|
||||
m_info = getInternalModInfo();
|
||||
m_saveDirPath = dirs::getModsSaveDir() / m_info.id;
|
||||
ghc::filesystem::create_directories(m_saveDirPath);
|
||||
}
|
||||
|
||||
InternalMod::~InternalMod() {}
|
||||
|
||||
InternalMod* InternalMod::get() {
|
||||
static auto g_mod = new InternalMod;
|
||||
return g_mod;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
class InternalMod;
|
||||
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
class InternalMod : public Mod {
|
||||
protected:
|
||||
InternalMod();
|
||||
virtual ~InternalMod();
|
||||
|
||||
public:
|
||||
static InternalMod* get();
|
||||
|
||||
void setModInfo();
|
||||
};
|
|
@ -29,4 +29,4 @@ void Event::postFrom(Mod* m) {
|
|||
std::unordered_set<EventListenerProtocol*>& Event::listeners() {
|
||||
static std::unordered_set<EventListenerProtocol*> listeners;
|
||||
return listeners;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#include <Geode/utils/ranges.hpp>
|
||||
#include <vector>
|
||||
// #include <hook/hook.hpp>
|
||||
#include "InternalMod.hpp"
|
||||
#include "ModImpl.hpp"
|
||||
|
||||
#include <Geode/hook-core/Hook.hpp>
|
||||
#include "HookImpl.hpp"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include "LoaderImpl.hpp"
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
Loader::Loader() : m_impl(new Impl) {}
|
||||
|
||||
Loader::~Loader() {}
|
||||
|
@ -81,8 +83,8 @@ std::vector<Mod*> Loader::getAllMods() {
|
|||
return m_impl->getAllMods();
|
||||
}
|
||||
|
||||
Mod* Loader::getInternalMod() {
|
||||
return m_impl->getInternalMod();
|
||||
Mod* Loader::getModImpl() {
|
||||
return m_impl->getModImpl();
|
||||
}
|
||||
|
||||
void Loader::updateAllDependencies() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
#include "LoaderImpl.hpp"
|
||||
|
||||
#include <cocos2d.h>
|
||||
#include <Geode/loader/Dirs.hpp>
|
||||
#include <Geode/loader/IPC.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
|
@ -10,7 +10,7 @@
|
|||
#include <Geode/utils/map.hpp>
|
||||
#include <Geode/utils/ranges.hpp>
|
||||
#include <Geode/utils/web.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include "ModImpl.hpp"
|
||||
#include <about.hpp>
|
||||
#include <crashlog.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
@ -105,7 +105,7 @@ void Loader::Impl::updateResources() {
|
|||
log::debug("Adding resources");
|
||||
|
||||
// add own spritesheets
|
||||
this->updateModResources(InternalMod::get());
|
||||
this->updateModResources(Mod::get());
|
||||
|
||||
// add mods' spritesheets
|
||||
for (auto const& [_, mod] : m_mods) {
|
||||
|
@ -117,8 +117,8 @@ std::vector<Mod*> Loader::Impl::getAllMods() {
|
|||
return map::values(m_mods);
|
||||
}
|
||||
|
||||
Mod* Loader::Impl::getInternalMod() {
|
||||
return InternalMod::get();
|
||||
Mod* Loader::Impl::getModImpl() {
|
||||
return Mod::get();
|
||||
}
|
||||
|
||||
std::vector<InvalidGeodeFile> Loader::Impl::getFailedMods() const {
|
||||
|
@ -154,20 +154,21 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) {
|
|||
|
||||
Result<> Loader::Impl::saveData() {
|
||||
// save mods' data
|
||||
for (auto& [_, mod] : m_mods) {
|
||||
for (auto& [id, mod] : m_mods) {
|
||||
Mod::get()->setSavedValue("should-load-" + id, mod->isEnabled());
|
||||
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());
|
||||
GEODE_UNWRAP(Mod::get()->saveData());
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Loader::Impl::loadData() {
|
||||
auto e = InternalMod::get()->loadData();
|
||||
auto e = Mod::get()->loadData();
|
||||
if (!e) {
|
||||
log::warn("Unable to load loader settings: {}", e.unwrapErr());
|
||||
}
|
||||
|
@ -189,8 +190,16 @@ Result<Mod*> Loader::Impl::loadModFromInfo(ModInfo const& info) {
|
|||
|
||||
// create Mod instance
|
||||
auto mod = new Mod(info);
|
||||
auto setupRes = mod->m_impl->setup();
|
||||
if (!setupRes) {
|
||||
return Err(fmt::format(
|
||||
"Unable to setup mod '{}': {}",
|
||||
info.id, setupRes.unwrapErr()
|
||||
));
|
||||
}
|
||||
|
||||
m_mods.insert({ info.id, mod });
|
||||
mod->m_enabled = InternalMod::get()->getSavedValue<bool>(
|
||||
mod->m_impl->m_enabled = Mod::get()->getSavedValue<bool>(
|
||||
"should-load-" + info.id, true
|
||||
);
|
||||
|
||||
|
@ -246,7 +255,7 @@ Mod* Loader::Impl::getLoadedMod(std::string const& id) const {
|
|||
}
|
||||
|
||||
void Loader::Impl::updateModResources(Mod* mod) {
|
||||
if (!mod->m_info.spritesheets.size()) {
|
||||
if (!mod->m_impl->m_info.spritesheets.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -255,7 +264,7 @@ void Loader::Impl::updateModResources(Mod* mod) {
|
|||
log::debug("Adding resources for {}", mod->getID());
|
||||
|
||||
// add spritesheets
|
||||
for (auto const& sheet : mod->m_info.spritesheets) {
|
||||
for (auto const& sheet : mod->m_impl->m_info.spritesheets) {
|
||||
log::debug("Adding sheet {}", sheet);
|
||||
auto png = sheet + ".png";
|
||||
auto plist = sheet + ".plist";
|
||||
|
@ -265,7 +274,7 @@ void Loader::Impl::updateModResources(Mod* mod) {
|
|||
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.id, sheet
|
||||
mod->m_impl->m_info.id, sheet
|
||||
);
|
||||
}
|
||||
else {
|
||||
|
@ -300,7 +309,7 @@ void Loader::Impl::loadModsFromDirectory(
|
|||
}
|
||||
// skip this entry if it's already loaded
|
||||
if (map::contains<std::string, Mod*>(m_mods, [entry](Mod* p) -> bool {
|
||||
return p->m_info.path == entry.path();
|
||||
return p->m_impl->m_info.path == entry.path();
|
||||
})) {
|
||||
continue;
|
||||
}
|
||||
|
@ -389,9 +398,6 @@ bool Loader::Impl::didLastLaunchCrash() const {
|
|||
return crashlog::didLastLaunchCrash();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void Loader::Impl::reset() {
|
||||
this->closePlatformConsole();
|
||||
|
||||
|
@ -403,6 +409,7 @@ void Loader::Impl::reset() {
|
|||
ghc::filesystem::remove_all(dirs::getModRuntimeDir());
|
||||
ghc::filesystem::remove_all(dirs::getTempDir());
|
||||
}
|
||||
|
||||
bool Loader::Impl::isReadyToHook() const {
|
||||
return m_readyToHook;
|
||||
}
|
||||
|
@ -459,7 +466,7 @@ bool Loader::Impl::platformConsoleOpen() const {
|
|||
void Loader::Impl::downloadLoaderResources() {
|
||||
auto version = this->getVersion().toString();
|
||||
auto tempResourcesZip = dirs::getTempDir() / "new.zip";
|
||||
auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID();
|
||||
auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID();
|
||||
|
||||
web::AsyncWebRequest()
|
||||
.join("update-geode-loader-resources")
|
||||
|
@ -499,7 +506,7 @@ bool Loader::Impl::verifyLoaderResources() {
|
|||
}
|
||||
|
||||
// geode/resources/geode.loader
|
||||
auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID();
|
||||
auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID();
|
||||
|
||||
// if the resources dir doesn't exist, then it's probably incorrect
|
||||
if (!(
|
||||
|
@ -585,24 +592,18 @@ void Loader::Impl::provideNextMod(Mod* mod) {
|
|||
m_nextModLock.lock();
|
||||
m_nextMod = mod;
|
||||
}
|
||||
|
||||
Mod* Loader::Impl::takeNextMod() {
|
||||
if (!m_nextMod) {
|
||||
// this means we're hopefully loading the internal mod
|
||||
// TODO: make this less hacky
|
||||
auto res = this->setupInternalMod();
|
||||
if (!res) {
|
||||
log::error("{}", res.unwrapErr());
|
||||
return nullptr;
|
||||
}
|
||||
return m_nextMod;
|
||||
this->setupInternalMod();
|
||||
m_nextMod = Mod::sharedMod<>;
|
||||
}
|
||||
auto ret = m_nextMod;
|
||||
m_nextModCV.notify_all();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Loader::Impl::releaseNextMod() {
|
||||
auto lock = std::unique_lock<std::mutex>(m_nextModAccessMutex);
|
||||
m_nextModCV.wait(lock);
|
||||
m_nextMod = nullptr;
|
||||
|
||||
m_nextModLock.unlock();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#include <Geode/utils/Result.hpp>
|
||||
#include <Geode/utils/map.hpp>
|
||||
#include <Geode/utils/ranges.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include "ModImpl.hpp"
|
||||
#include <about.hpp>
|
||||
#include <crashlog.hpp>
|
||||
#include <mutex>
|
||||
|
@ -20,127 +20,125 @@
|
|||
#include <vector>
|
||||
#include <tulip/TulipHook.hpp>
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
// TODO: Find a file convention for impl headers
|
||||
namespace geode {
|
||||
class LoaderImpl;
|
||||
}
|
||||
struct ResourceDownloadEvent : public Event {
|
||||
const UpdateStatus status;
|
||||
ResourceDownloadEvent(UpdateStatus const& status);
|
||||
};
|
||||
|
||||
class Loader::Impl {
|
||||
public:
|
||||
mutable std::mutex m_mutex;
|
||||
class GEODE_DLL ResourceDownloadFilter : public EventFilter<ResourceDownloadEvent> {
|
||||
public:
|
||||
using Callback = void(ResourceDownloadEvent*);
|
||||
|
||||
std::vector<ghc::filesystem::path> m_modSearchDirectories;
|
||||
std::vector<ModInfo> m_modsToLoad;
|
||||
std::vector<InvalidGeodeFile> m_invalidMods;
|
||||
std::unordered_map<std::string, Mod*> m_mods;
|
||||
std::vector<ghc::filesystem::path> m_texturePaths;
|
||||
bool m_isSetup = false;
|
||||
ListenerResult handle(std::function<Callback> fn, ResourceDownloadEvent* event);
|
||||
ResourceDownloadFilter();
|
||||
};
|
||||
|
||||
std::condition_variable m_earlyLoadFinishedCV;
|
||||
std::mutex m_earlyLoadFinishedMutex;
|
||||
std::atomic_bool m_earlyLoadFinished = false;
|
||||
std::vector<std::function<void(void)>> m_gdThreadQueue;
|
||||
mutable std::mutex m_gdThreadMutex;
|
||||
bool m_platformConsoleOpen = false;
|
||||
std::vector<std::pair<Hook*, Mod*>> m_internalHooks;
|
||||
bool m_readyToHook = false;
|
||||
class Loader::Impl {
|
||||
public:
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
std::mutex m_nextModMutex;
|
||||
std::unique_lock<std::mutex> m_nextModLock = std::unique_lock<std::mutex>(m_nextModMutex, std::defer_lock);
|
||||
std::condition_variable m_nextModCV;
|
||||
std::mutex m_nextModAccessMutex;
|
||||
Mod* m_nextMod = nullptr;
|
||||
std::vector<ghc::filesystem::path> m_modSearchDirectories;
|
||||
std::vector<ModInfo> m_modsToLoad;
|
||||
std::vector<InvalidGeodeFile> m_invalidMods;
|
||||
std::unordered_map<std::string, Mod*> m_mods;
|
||||
std::vector<ghc::filesystem::path> m_texturePaths;
|
||||
bool m_isSetup = false;
|
||||
|
||||
Result<> setupInternalMod();
|
||||
std::condition_variable m_earlyLoadFinishedCV;
|
||||
std::mutex m_earlyLoadFinishedMutex;
|
||||
std::atomic_bool m_earlyLoadFinished = false;
|
||||
std::vector<std::function<void(void)>> m_gdThreadQueue;
|
||||
mutable std::mutex m_gdThreadMutex;
|
||||
bool m_platformConsoleOpen = false;
|
||||
std::vector<std::pair<Hook*, Mod*>> m_internalHooks;
|
||||
bool m_readyToHook = false;
|
||||
|
||||
void provideNextMod(Mod* mod);
|
||||
Mod* takeNextMod();
|
||||
void releaseNextMod();
|
||||
std::mutex m_nextModMutex;
|
||||
std::unique_lock<std::mutex> m_nextModLock = std::unique_lock<std::mutex>(m_nextModMutex, std::defer_lock);
|
||||
std::condition_variable m_nextModCV;
|
||||
std::mutex m_nextModAccessMutex;
|
||||
Mod* m_nextMod = nullptr;
|
||||
|
||||
std::unordered_map<void*, tulip::hook::HandlerHandle> m_handlerHandles;
|
||||
Result<> setupInternalMod();
|
||||
|
||||
Result<> createHandler(void* address, tulip::hook::HandlerMetadata const& metadata);
|
||||
bool hasHandler(void* address);
|
||||
Result<tulip::hook::HandlerHandle> getHandler(void* address);
|
||||
Result<> removeHandler(void* address);
|
||||
void provideNextMod(Mod* mod);
|
||||
Mod* takeNextMod();
|
||||
void releaseNextMod();
|
||||
|
||||
void downloadLoaderResources();
|
||||
std::unordered_map<void*, tulip::hook::HandlerHandle> m_handlerHandles;
|
||||
|
||||
bool loadHooks();
|
||||
void setupIPC();
|
||||
Result<> createHandler(void* address, tulip::hook::HandlerMetadata const& metadata);
|
||||
bool hasHandler(void* address);
|
||||
Result<tulip::hook::HandlerHandle> getHandler(void* address);
|
||||
Result<> removeHandler(void* address);
|
||||
|
||||
Impl();
|
||||
~Impl();
|
||||
void downloadLoaderResources();
|
||||
|
||||
void createDirectories();
|
||||
bool loadHooks();
|
||||
void setupIPC();
|
||||
|
||||
void updateModResources(Mod* mod);
|
||||
void addSearchPaths();
|
||||
Impl();
|
||||
~Impl();
|
||||
|
||||
friend void GEODE_CALL ::geode_implicit_load(Mod*);
|
||||
void createDirectories();
|
||||
|
||||
Result<Mod*> loadModFromInfo(ModInfo const& info);
|
||||
void updateModResources(Mod* mod);
|
||||
void addSearchPaths();
|
||||
|
||||
Result<> setup();
|
||||
void reset();
|
||||
friend void GEODE_CALL ::geode_implicit_load(Mod*);
|
||||
|
||||
Result<> saveData();
|
||||
Result<> loadData();
|
||||
Result<Mod*> loadModFromInfo(ModInfo const& info);
|
||||
|
||||
VersionInfo getVersion();
|
||||
VersionInfo minModVersion();
|
||||
VersionInfo maxModVersion();
|
||||
bool isModVersionSupported(VersionInfo const& version);
|
||||
Result<> setup();
|
||||
void reset();
|
||||
|
||||
Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
|
||||
void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true);
|
||||
void refreshModsList();
|
||||
bool isModInstalled(std::string const& id) const;
|
||||
Mod* getInstalledMod(std::string const& id) const;
|
||||
bool isModLoaded(std::string const& id) const;
|
||||
Mod* getLoadedMod(std::string const& id) const;
|
||||
std::vector<Mod*> getAllMods();
|
||||
Mod* getInternalMod();
|
||||
void updateAllDependencies();
|
||||
std::vector<InvalidGeodeFile> getFailedMods() const;
|
||||
Result<> saveData();
|
||||
Result<> loadData();
|
||||
|
||||
void updateResources();
|
||||
VersionInfo getVersion();
|
||||
VersionInfo minModVersion();
|
||||
VersionInfo maxModVersion();
|
||||
bool isModVersionSupported(VersionInfo const& version);
|
||||
|
||||
void waitForModsToBeLoaded();
|
||||
Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
|
||||
void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true);
|
||||
void refreshModsList();
|
||||
bool isModInstalled(std::string const& id) const;
|
||||
Mod* getInstalledMod(std::string const& id) const;
|
||||
bool isModLoaded(std::string const& id) const;
|
||||
Mod* getLoadedMod(std::string const& id) const;
|
||||
std::vector<Mod*> getAllMods();
|
||||
Mod* getModImpl();
|
||||
void updateAllDependencies();
|
||||
std::vector<InvalidGeodeFile> getFailedMods() const;
|
||||
|
||||
bool didLastLaunchCrash() const;
|
||||
void updateResources();
|
||||
|
||||
nlohmann::json processRawIPC(void* rawHandle, std::string const& buffer);
|
||||
void waitForModsToBeLoaded();
|
||||
|
||||
void queueInGDThread(ScheduledFunction func);
|
||||
void executeGDThreadQueue();
|
||||
bool didLastLaunchCrash() const;
|
||||
|
||||
void logConsoleMessage(std::string const& msg);
|
||||
bool platformConsoleOpen() const;
|
||||
void openPlatformConsole();
|
||||
void closePlatformConsole();
|
||||
void platformMessageBox(char const* title, std::string const& info);
|
||||
nlohmann::json processRawIPC(void* rawHandle, std::string const& buffer);
|
||||
|
||||
bool verifyLoaderResources();
|
||||
void queueInGDThread(ScheduledFunction func);
|
||||
void executeGDThreadQueue();
|
||||
|
||||
bool isReadyToHook() const;
|
||||
void addInternalHook(Hook* hook, Mod* mod);
|
||||
};
|
||||
void logConsoleMessage(std::string const& msg);
|
||||
bool platformConsoleOpen() const;
|
||||
void openPlatformConsole();
|
||||
void closePlatformConsole();
|
||||
void platformMessageBox(char const* title, std::string const& info);
|
||||
|
||||
bool verifyLoaderResources();
|
||||
|
||||
bool isReadyToHook() const;
|
||||
void addInternalHook(Hook* hook, Mod* mod);
|
||||
|
||||
void setupInternalMod();
|
||||
};
|
||||
|
||||
namespace geode {
|
||||
class LoaderImpl {
|
||||
public:
|
||||
static Loader::Impl* get();
|
||||
|
|
|
@ -1,561 +1,188 @@
|
|||
#include "LoaderImpl.hpp"
|
||||
|
||||
#include <Geode/loader/Dirs.hpp>
|
||||
#include <Geode/loader/Hook.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/loader/Log.hpp>
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <Geode/utils/file.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "ModImpl.hpp"
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
Mod::Mod(ModInfo const& info) {
|
||||
m_info = info;
|
||||
m_saveDirPath = dirs::getModsSaveDir() / info.id;
|
||||
ghc::filesystem::create_directories(m_saveDirPath);
|
||||
}
|
||||
Mod::Mod(ModInfo const& info) : m_impl(std::make_unique<Impl>(this, info)) {}
|
||||
|
||||
Mod::~Mod() {
|
||||
(void)this->unloadBinary();
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
ghc::filesystem::path Mod::getSaveDir() const {
|
||||
return m_saveDirPath;
|
||||
}
|
||||
Mod::~Mod() {}
|
||||
|
||||
std::string Mod::getID() const {
|
||||
return m_info.id;
|
||||
return m_impl->getID();
|
||||
}
|
||||
|
||||
std::string Mod::getName() const {
|
||||
return m_info.name;
|
||||
return m_impl->getName();
|
||||
}
|
||||
|
||||
std::string Mod::getDeveloper() const {
|
||||
return m_info.developer;
|
||||
return m_impl->getDeveloper();
|
||||
}
|
||||
|
||||
std::optional<std::string> Mod::getDescription() const {
|
||||
return m_info.description;
|
||||
return m_impl->getDescription();
|
||||
}
|
||||
|
||||
std::optional<std::string> Mod::getDetails() const {
|
||||
return m_info.details;
|
||||
}
|
||||
|
||||
ModInfo Mod::getModInfo() const {
|
||||
return m_info;
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::getTempDir() const {
|
||||
return m_tempDirName;
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::getBinaryPath() const {
|
||||
return m_tempDirName / m_info.binaryName;
|
||||
return m_impl->getDetails();
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::getPackagePath() const {
|
||||
return m_info.path;
|
||||
return m_impl->getPackagePath();
|
||||
}
|
||||
|
||||
VersionInfo Mod::getVersion() const {
|
||||
return m_info.version;
|
||||
return m_impl->getVersion();
|
||||
}
|
||||
|
||||
nlohmann::json& Mod::getSaveContainer() {
|
||||
return m_impl->getSaveContainer();
|
||||
}
|
||||
|
||||
bool Mod::isEnabled() const {
|
||||
return m_enabled;
|
||||
return m_impl->isEnabled();
|
||||
}
|
||||
|
||||
bool Mod::isLoaded() const {
|
||||
return m_binaryLoaded;
|
||||
return m_impl->isLoaded();
|
||||
}
|
||||
|
||||
bool Mod::supportsDisabling() const {
|
||||
return m_info.supportsDisabling;
|
||||
return m_impl->supportsDisabling();
|
||||
}
|
||||
|
||||
bool Mod::supportsUnloading() const {
|
||||
return m_info.supportsUnloading;
|
||||
return m_impl->supportsUnloading();
|
||||
}
|
||||
|
||||
bool Mod::wasSuccesfullyLoaded() const {
|
||||
return !this->isEnabled() || this->isLoaded();
|
||||
return m_impl->wasSuccesfullyLoaded();
|
||||
}
|
||||
|
||||
std::vector<Hook*> Mod::getHooks() const {
|
||||
return m_hooks;
|
||||
ModInfo Mod::getModInfo() const {
|
||||
return m_impl->getModInfo();
|
||||
}
|
||||
|
||||
bool Mod::hasSettings() const {
|
||||
return m_info.settings.size();
|
||||
ghc::filesystem::path Mod::getTempDir() const {
|
||||
return m_impl->getTempDir();
|
||||
}
|
||||
|
||||
decltype(ModInfo::settings) Mod::getSettings() const {
|
||||
return m_info.settings;
|
||||
}
|
||||
|
||||
// Settings and saved values
|
||||
|
||||
Result<> Mod::loadData() {
|
||||
ModStateEvent(this, ModEventType::DataLoaded).post();
|
||||
|
||||
// Settings
|
||||
// Check if settings exist
|
||||
auto settingPath = m_saveDirPath / "settings.json";
|
||||
if (ghc::filesystem::exists(settingPath)) {
|
||||
GEODE_UNWRAP_INTO(auto settingData, utils::file::readString(settingPath));
|
||||
try {
|
||||
// parse settings.json
|
||||
auto json = nlohmann::json::parse(settingData);
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("[settings.json]");
|
||||
|
||||
for (auto& [key, value] : root.items()) {
|
||||
// check if this is a known setting
|
||||
if (auto setting = this->getSetting(key)) {
|
||||
// load its value
|
||||
if (!setting->load(value.json()))
|
||||
return Err("Unable to load value for setting \"" + key + "\"");
|
||||
}
|
||||
else {
|
||||
log::internalLog(
|
||||
Severity::Warning,
|
||||
this,
|
||||
"Encountered unknown setting \"{}\" while loading "
|
||||
"settings",
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
return Err(std::string("Unable to parse settings: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// Saved values
|
||||
auto savedPath = m_saveDirPath / "saved.json";
|
||||
if (ghc::filesystem::exists(savedPath)) {
|
||||
GEODE_UNWRAP_INTO(auto data, utils::file::readString(savedPath));
|
||||
try {
|
||||
m_saved = nlohmann::json::parse(data);
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
return Err(std::string("Unable to parse saved values: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
ghc::filesystem::path Mod::getBinaryPath() const {
|
||||
return m_impl->getBinaryPath();
|
||||
}
|
||||
|
||||
Result<> Mod::saveData() {
|
||||
ModStateEvent(this, ModEventType::DataSaved).post();
|
||||
|
||||
// Settings
|
||||
auto json = nlohmann::json::object();
|
||||
for (auto& [key, value] : m_info.settings) {
|
||||
if (!value->save(json[key])) return Err("Unable to save setting \"" + key + "\"");
|
||||
}
|
||||
|
||||
GEODE_UNWRAP(utils::file::writeString(m_saveDirPath / "settings.json", json.dump(4)));
|
||||
|
||||
// Saved values
|
||||
GEODE_UNWRAP(utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump(4)));
|
||||
|
||||
return Ok();
|
||||
return m_impl->saveData();
|
||||
}
|
||||
|
||||
std::shared_ptr<Setting> Mod::getSetting(std::string const& key) const {
|
||||
for (auto& setting : m_info.settings) {
|
||||
if (setting.first == key) {
|
||||
return setting.second;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
Result<> Mod::loadData() {
|
||||
return m_impl->loadData();
|
||||
}
|
||||
|
||||
bool Mod::hasSetting(std::string const& key) const {
|
||||
for (auto& setting : m_info.settings) {
|
||||
if (setting.first == key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loading, Toggling, Installing
|
||||
|
||||
Result<> Mod::loadBinary() {
|
||||
if (m_binaryLoaded) {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
GEODE_UNWRAP(this->createTempDir());
|
||||
|
||||
if (this->hasUnresolvedDependencies()) {
|
||||
return Err("Mod has unresolved dependencies");
|
||||
}
|
||||
|
||||
LoaderImpl::get()->provideNextMod(this);
|
||||
|
||||
GEODE_UNWRAP(this->loadPlatformBinary());
|
||||
m_binaryLoaded = true;
|
||||
|
||||
// Call implicit entry point to place hooks etc.
|
||||
m_implicitLoadFunc(this);
|
||||
|
||||
ModStateEvent(this, ModEventType::Loaded).post();
|
||||
|
||||
auto loadRes = this->loadData();
|
||||
if (!loadRes) {
|
||||
log::warn("Unable to load data for \"{}\": {}", m_info.id, loadRes.unwrapErr());
|
||||
}
|
||||
|
||||
Loader::get()->updateAllDependencies();
|
||||
|
||||
GEODE_UNWRAP(this->enable());
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::unloadBinary() {
|
||||
if (!m_binaryLoaded) {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
if (!m_info.supportsUnloading) {
|
||||
return Err("Mod does not support unloading");
|
||||
}
|
||||
|
||||
GEODE_UNWRAP(this->saveData());
|
||||
|
||||
GEODE_UNWRAP(this->disable());
|
||||
ModStateEvent(this, ModEventType::Unloaded).post();
|
||||
|
||||
// Disabling unhooks and unpatches already
|
||||
for (auto const& hook : m_hooks) {
|
||||
delete hook;
|
||||
}
|
||||
m_hooks.clear();
|
||||
|
||||
for (auto const& patch : m_patches) {
|
||||
delete patch;
|
||||
}
|
||||
m_patches.clear();
|
||||
|
||||
GEODE_UNWRAP(this->unloadPlatformBinary());
|
||||
m_binaryLoaded = false;
|
||||
|
||||
Loader::get()->updateAllDependencies();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::enable() {
|
||||
if (!m_binaryLoaded) {
|
||||
return this->loadBinary();
|
||||
}
|
||||
|
||||
for (auto const& hook : m_hooks) {
|
||||
GEODE_UNWRAP(this->enableHook(hook));
|
||||
}
|
||||
|
||||
for (auto const& patch : m_patches) {
|
||||
if (!patch->apply()) {
|
||||
return Err("Unable to apply patch at " + std::to_string(patch->getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
ModStateEvent(this, ModEventType::Enabled).post();
|
||||
m_enabled = true;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::disable() {
|
||||
if (!m_enabled) {
|
||||
return Ok();
|
||||
}
|
||||
if (!m_info.supportsDisabling) {
|
||||
return Err("Mod does not support disabling");
|
||||
}
|
||||
|
||||
ModStateEvent(this, ModEventType::Disabled).post();
|
||||
|
||||
for (auto const& hook : m_hooks) {
|
||||
GEODE_UNWRAP(this->disableHook(hook));
|
||||
}
|
||||
for (auto const& patch : m_patches) {
|
||||
if (!patch->restore()) {
|
||||
return Err("Unable to restore patch at " + std::to_string(patch->getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
m_enabled = false;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::uninstall() {
|
||||
if (m_info.supportsDisabling) {
|
||||
GEODE_UNWRAP(this->disable());
|
||||
if (m_info.supportsUnloading) {
|
||||
GEODE_UNWRAP(this->unloadBinary());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ghc::filesystem::remove(m_info.path);
|
||||
} catch(std::exception& e) {
|
||||
return Err(
|
||||
"Unable to delete mod's .geode file! "
|
||||
"This might be due to insufficient permissions - "
|
||||
"try running GD as administrator."
|
||||
);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool Mod::isUninstalled() const {
|
||||
return this != InternalMod::get() && !ghc::filesystem::exists(m_info.path);
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
|
||||
Result<> Mod::updateDependencies() {
|
||||
bool hasUnresolved = false;
|
||||
for (auto& dep : m_info.dependencies) {
|
||||
// set the dependency's loaded mod if such exists
|
||||
if (!dep.mod) {
|
||||
dep.mod = Loader::get()->getLoadedMod(dep.id);
|
||||
// verify loaded dependency version
|
||||
if (dep.mod && !dep.version.compare(dep.mod->getVersion())) {
|
||||
dep.mod = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the dependency is loaded
|
||||
if (dep.mod) {
|
||||
// update the dependency recursively
|
||||
GEODE_UNWRAP(dep.mod->updateDependencies());
|
||||
|
||||
// enable mod if it's resolved & enabled
|
||||
if (!dep.mod->hasUnresolvedDependencies()) {
|
||||
if (dep.mod->isEnabled()) {
|
||||
GEODE_UNWRAP(dep.mod->loadBinary()
|
||||
.expect("Unable to load dependency: {error}")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if the dependency is resolved now
|
||||
if (!dep.isResolved()) {
|
||||
GEODE_UNWRAP(this->unloadBinary()
|
||||
.expect("Unable to unload mod: {error}")
|
||||
);
|
||||
hasUnresolved = true;
|
||||
}
|
||||
}
|
||||
// load if there weren't any unresolved dependencies
|
||||
if (!hasUnresolved) {
|
||||
log::debug("All dependencies for {} found", m_info.id);
|
||||
if (m_enabled) {
|
||||
log::debug("Resolved & loading {}", m_info.id);
|
||||
GEODE_UNWRAP(this->loadBinary());
|
||||
}
|
||||
else {
|
||||
log::debug("Resolved {}, however not loading it as it is disabled", m_info.id);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool Mod::hasUnresolvedDependencies() const {
|
||||
for (auto const& dep : m_info.dependencies) {
|
||||
if (!dep.isResolved()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Dependency> Mod::getUnresolvedDependencies() {
|
||||
std::vector<Dependency> unresolved;
|
||||
for (auto const& dep : m_info.dependencies) {
|
||||
if (!dep.isResolved()) {
|
||||
unresolved.push_back(dep);
|
||||
}
|
||||
}
|
||||
return unresolved;
|
||||
}
|
||||
|
||||
bool Mod::depends(std::string const& id) const {
|
||||
return utils::ranges::contains(
|
||||
m_info.dependencies,
|
||||
[id](Dependency const& t) {
|
||||
return t.id == id;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Hooks
|
||||
|
||||
Result<> Mod::enableHook(Hook* hook) {
|
||||
log::debug("Enabling hook {} for mod {} at address {}", (void*)hook, m_info.id, (void*)hook->getAddress());
|
||||
auto res = hook->enable();
|
||||
if (res) m_hooks.push_back(hook);
|
||||
else {
|
||||
log::error("Can't enable hook {} for mod {}: {}", (void*)hook, m_info.id, res.unwrapErr());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Result<> Mod::disableHook(Hook* hook) {
|
||||
return hook->disable();
|
||||
}
|
||||
|
||||
Result<Hook*> Mod::addHook(Hook* hook) {
|
||||
if (LoaderImpl::get()->isReadyToHook()) {
|
||||
if (hook->getAutoEnable()) {
|
||||
auto res = this->enableHook(hook);
|
||||
if (!res) {
|
||||
delete hook;
|
||||
return Err("Can't create hook: "+ res.unwrapErr());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
LoaderImpl::get()->addInternalHook(hook, this);
|
||||
}
|
||||
|
||||
return Ok(hook);
|
||||
}
|
||||
|
||||
Result<> Mod::removeHook(Hook* hook) {
|
||||
auto res = this->disableHook(hook);
|
||||
if (res) {
|
||||
ranges::remove(m_hooks, hook);
|
||||
delete hook;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Patches
|
||||
|
||||
byte_array readMemory(void* address, size_t amount) {
|
||||
byte_array ret;
|
||||
for (size_t i = 0; i < amount; i++) {
|
||||
ret.push_back(*reinterpret_cast<uint8_t*>(reinterpret_cast<uintptr_t>(address) + i));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Result<Patch*> Mod::patch(void* address, byte_array data) {
|
||||
auto p = new Patch;
|
||||
p->m_address = address;
|
||||
p->m_original = readMemory(address, data.size());
|
||||
p->m_owner = this;
|
||||
p->m_patch = data;
|
||||
if (!p->apply()) {
|
||||
delete p;
|
||||
return Err("Unable to enable patch at " + std::to_string(p->getAddress()));
|
||||
}
|
||||
m_patches.push_back(p);
|
||||
return Ok(p);
|
||||
}
|
||||
|
||||
Result<> Mod::unpatch(Patch* patch) {
|
||||
if (patch->restore()) {
|
||||
ranges::remove(m_patches, patch);
|
||||
delete patch;
|
||||
return Ok();
|
||||
}
|
||||
return Err("Unable to restore patch!");
|
||||
}
|
||||
|
||||
// Misc.
|
||||
|
||||
Result<> Mod::createTempDir() {
|
||||
// Check if temp dir already exists
|
||||
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.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.path));
|
||||
if (!unzip.hasEntry(m_info.binaryName)) {
|
||||
return Err(fmt::format(
|
||||
"Unable to find platform binary under the name \"{}\"", m_info.binaryName
|
||||
));
|
||||
}
|
||||
GEODE_UNWRAP(unzip.extractAllTo(tempPath));
|
||||
|
||||
// Mark temp dir creation as succesful
|
||||
m_tempDirName = tempPath;
|
||||
|
||||
return Ok();
|
||||
ghc::filesystem::path Mod::getSaveDir() const {
|
||||
return m_impl->getSaveDir();
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::getConfigDir(bool create) const {
|
||||
auto dir = dirs::getModConfigDir() / m_info.id;
|
||||
if (create) {
|
||||
(void)file::createDirectoryAll(dir);
|
||||
}
|
||||
return dir;
|
||||
return m_impl->getConfigDir(create);
|
||||
}
|
||||
|
||||
const char* Mod::expandSpriteName(const char* name) {
|
||||
static std::unordered_map<std::string, const char*> expanded = {};
|
||||
if (expanded.count(name)) return expanded[name];
|
||||
bool Mod::hasSettings() const {
|
||||
return m_impl->hasSettings();
|
||||
}
|
||||
|
||||
auto exp = new char[strlen(name) + 2 + m_info.id.size()];
|
||||
auto exps = m_info.id + "/" + name;
|
||||
memcpy(exp, exps.c_str(), exps.size() + 1);
|
||||
std::vector<std::string> Mod::getSettingKeys() const {
|
||||
return m_impl->getSettingKeys();
|
||||
}
|
||||
|
||||
expanded[name] = exp;
|
||||
bool Mod::hasSetting(std::string const& key) const {
|
||||
return m_impl->hasSetting(key);
|
||||
}
|
||||
|
||||
return exp;
|
||||
std::optional<Setting> Mod::getSettingDefinition(std::string const& key) const {
|
||||
return m_impl->getSettingDefinition(key);
|
||||
}
|
||||
|
||||
SettingValue* Mod::getSetting(std::string const& key) const {
|
||||
return m_impl->getSetting(key);
|
||||
}
|
||||
|
||||
void Mod::registerCustomSetting(std::string const& key, std::unique_ptr<SettingValue> value) {
|
||||
return m_impl->registerCustomSetting(key, std::move(value));
|
||||
}
|
||||
|
||||
std::vector<Hook*> Mod::getHooks() const {
|
||||
return m_impl->getHooks();
|
||||
}
|
||||
|
||||
Result<Hook*> Mod::addHook(Hook* hook) {
|
||||
return m_impl->addHook(hook);
|
||||
}
|
||||
|
||||
Result<> Mod::enableHook(Hook* hook) {
|
||||
return m_impl->enableHook(hook);
|
||||
}
|
||||
|
||||
Result<> Mod::disableHook(Hook* hook) {
|
||||
return m_impl->disableHook(hook);
|
||||
}
|
||||
|
||||
Result<> Mod::removeHook(Hook* hook) {
|
||||
return m_impl->removeHook(hook);
|
||||
}
|
||||
|
||||
Result<Patch*> Mod::patch(void* address, ByteVector const& data) {
|
||||
return m_impl->patch(address, data);
|
||||
}
|
||||
|
||||
Result<> Mod::unpatch(Patch* patch) {
|
||||
return m_impl->unpatch(patch);
|
||||
}
|
||||
|
||||
Result<> Mod::loadBinary() {
|
||||
return m_impl->loadBinary();
|
||||
}
|
||||
|
||||
Result<> Mod::unloadBinary() {
|
||||
return m_impl->unloadBinary();
|
||||
}
|
||||
|
||||
Result<> Mod::enable() {
|
||||
return m_impl->enable();
|
||||
}
|
||||
|
||||
Result<> Mod::disable() {
|
||||
return m_impl->disable();
|
||||
}
|
||||
|
||||
Result<> Mod::uninstall() {
|
||||
return m_impl->uninstall();
|
||||
}
|
||||
|
||||
bool Mod::isUninstalled() const {
|
||||
return m_impl->isUninstalled();
|
||||
}
|
||||
|
||||
bool Mod::depends(std::string const& id) const {
|
||||
return m_impl->depends(id);
|
||||
}
|
||||
|
||||
bool Mod::hasUnresolvedDependencies() const {
|
||||
return m_impl->hasUnresolvedDependencies();
|
||||
}
|
||||
|
||||
Result<> Mod::updateDependencies() {
|
||||
return m_impl->updateDependencies();
|
||||
}
|
||||
|
||||
std::vector<Dependency> Mod::getUnresolvedDependencies() {
|
||||
return m_impl->getUnresolvedDependencies();
|
||||
}
|
||||
|
||||
char const* Mod::expandSpriteName(char const* name) {
|
||||
return m_impl->expandSpriteName(name);
|
||||
}
|
||||
|
||||
ModJson Mod::getRuntimeInfo() const {
|
||||
auto json = m_info.toJSON();
|
||||
|
||||
auto obj = ModJson::object();
|
||||
obj["hooks"] = ModJson::array();
|
||||
for (auto hook : m_hooks) {
|
||||
obj["hooks"].push_back(ModJson(hook->getRuntimeInfo()));
|
||||
}
|
||||
obj["patches"] = ModJson::array();
|
||||
for (auto patch : m_patches) {
|
||||
obj["patches"].push_back(ModJson(patch->getRuntimeInfo()));
|
||||
}
|
||||
obj["enabled"] = m_enabled;
|
||||
obj["loaded"] = m_binaryLoaded;
|
||||
obj["temp-dir"] = this->getTempDir();
|
||||
obj["save-dir"] = this->getSaveDir();
|
||||
obj["config-dir"] = this->getConfigDir(false);
|
||||
json["runtime"] = obj;
|
||||
|
||||
return json;
|
||||
}
|
||||
return m_impl->getRuntimeInfo();
|
||||
}
|
674
loader/src/loader/ModImpl.cpp
Normal file
674
loader/src/loader/ModImpl.cpp
Normal file
|
@ -0,0 +1,674 @@
|
|||
#include "ModImpl.hpp"
|
||||
#include "LoaderImpl.hpp"
|
||||
#include "about.hpp"
|
||||
|
||||
#include <Geode/loader/Dirs.hpp>
|
||||
#include <Geode/loader/Hook.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/loader/Log.hpp>
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <Geode/utils/file.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
Mod::Impl* ModImpl::getImpl(Mod* mod) {
|
||||
return mod->m_impl.get();
|
||||
}
|
||||
|
||||
Mod::Impl::Impl(Mod* self, ModInfo const& info) : m_self(self), m_info(info) {
|
||||
m_saveDirPath = dirs::getModsSaveDir() / info.id;
|
||||
ghc::filesystem::create_directories(m_saveDirPath);
|
||||
}
|
||||
|
||||
Mod::Impl::~Impl() {
|
||||
(void)this->unloadBinary();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::setup() {
|
||||
this->setupSettings();
|
||||
auto loadRes = this->loadData();
|
||||
if (!loadRes) {
|
||||
log::warn("Unable to load data for \"{}\": {}", m_info.id, loadRes.unwrapErr());
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
ghc::filesystem::path Mod::Impl::getSaveDir() const {
|
||||
return m_saveDirPath;
|
||||
}
|
||||
|
||||
std::string Mod::Impl::getID() const {
|
||||
return m_info.id;
|
||||
}
|
||||
|
||||
std::string Mod::Impl::getName() const {
|
||||
return m_info.name;
|
||||
}
|
||||
|
||||
std::string Mod::Impl::getDeveloper() const {
|
||||
return m_info.developer;
|
||||
}
|
||||
|
||||
std::optional<std::string> Mod::Impl::getDescription() const {
|
||||
return m_info.description;
|
||||
}
|
||||
|
||||
std::optional<std::string> Mod::Impl::getDetails() const {
|
||||
return m_info.details;
|
||||
}
|
||||
|
||||
ModInfo Mod::Impl::getModInfo() const {
|
||||
return m_info;
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::Impl::getTempDir() const {
|
||||
return m_tempDirName;
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::Impl::getBinaryPath() const {
|
||||
return m_tempDirName / m_info.binaryName;
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::Impl::getPackagePath() const {
|
||||
return m_info.path;
|
||||
}
|
||||
|
||||
VersionInfo Mod::Impl::getVersion() const {
|
||||
return m_info.version;
|
||||
}
|
||||
|
||||
nlohmann::json& Mod::Impl::getSaveContainer() {
|
||||
return m_saved;
|
||||
}
|
||||
|
||||
bool Mod::Impl::isEnabled() const {
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
bool Mod::Impl::isLoaded() const {
|
||||
return m_binaryLoaded;
|
||||
}
|
||||
|
||||
bool Mod::Impl::supportsDisabling() const {
|
||||
return m_info.supportsDisabling;
|
||||
}
|
||||
|
||||
bool Mod::Impl::supportsUnloading() const {
|
||||
return m_info.supportsUnloading;
|
||||
}
|
||||
|
||||
bool Mod::Impl::wasSuccesfullyLoaded() const {
|
||||
return !this->isEnabled() || this->isLoaded();
|
||||
}
|
||||
|
||||
std::vector<Hook*> Mod::Impl::getHooks() const {
|
||||
return m_hooks;
|
||||
}
|
||||
|
||||
// Settings and saved values
|
||||
|
||||
Result<> Mod::Impl::loadData() {
|
||||
ModStateEvent(this->m_self, ModEventType::DataLoaded).post();
|
||||
|
||||
// Settings
|
||||
// Check if settings exist
|
||||
auto settingPath = m_saveDirPath / "settings.json";
|
||||
if (ghc::filesystem::exists(settingPath)) {
|
||||
GEODE_UNWRAP_INTO(auto settingData, utils::file::readString(settingPath));
|
||||
try {
|
||||
// parse settings.json
|
||||
auto json = nlohmann::json::parse(settingData);
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("[settings.json]");
|
||||
|
||||
m_savedSettingsData = json;
|
||||
|
||||
for (auto& [key, value] : root.items()) {
|
||||
// check if this is a known setting
|
||||
if (auto setting = this->getSetting(key)) {
|
||||
// load its value
|
||||
if (!setting->load(value.json())) {
|
||||
log::internalLog(
|
||||
Severity::Error,
|
||||
this->m_self,
|
||||
"{}: Unable to load value for setting \"{}\"",
|
||||
m_info.id,
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
log::internalLog(
|
||||
Severity::Warning,
|
||||
this->m_self,
|
||||
"Encountered unknown setting \"{}\" while loading "
|
||||
"settings",
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
return Err(std::string("Unable to parse settings: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// Saved values
|
||||
auto savedPath = m_saveDirPath / "saved.json";
|
||||
if (ghc::filesystem::exists(savedPath)) {
|
||||
GEODE_UNWRAP_INTO(auto data, utils::file::readString(savedPath));
|
||||
try {
|
||||
m_saved = nlohmann::json::parse(data);
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
return Err(std::string("Unable to parse saved values: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::saveData() {
|
||||
ModStateEvent(this->m_self, ModEventType::DataSaved).post();
|
||||
|
||||
// Data saving should be fully fail-safe
|
||||
|
||||
std::unordered_set<std::string> coveredSettings;
|
||||
|
||||
// Settings
|
||||
auto json = nlohmann::json::object();
|
||||
for (auto& [key, value] : m_settings) {
|
||||
coveredSettings.insert(key);
|
||||
if (!value->save(json[key])) {
|
||||
log::error("Unable to save setting \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
// if some settings weren't provided a custom settings handler (for example,
|
||||
// the mod was not loaded) then make sure to save their previous state in
|
||||
// order to not lose data
|
||||
try {
|
||||
log::debug("Check covered");
|
||||
for (auto& [key, value] : m_savedSettingsData.items()) {
|
||||
log::debug("Check if {} is saved", key);
|
||||
if (!coveredSettings.count(key)) {
|
||||
json[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
|
||||
auto res = utils::file::writeString(m_saveDirPath / "settings.json", json.dump(4));
|
||||
if (!res) {
|
||||
log::error("Unable to save settings: {}", res.unwrapErr());
|
||||
}
|
||||
|
||||
auto res2 = utils::file::writeString(m_saveDirPath / "saved.json", m_saved.dump(4));
|
||||
if (!res2) {
|
||||
log::error("Unable to save values: {}", res2.unwrapErr());
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
void Mod::Impl::setupSettings() {
|
||||
for (auto& [key, sett] : m_info.settings) {
|
||||
if (auto value = sett.createDefaultValue()) {
|
||||
m_settings.emplace(key, std::move(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Mod::Impl::registerCustomSetting(std::string const& key, std::unique_ptr<SettingValue> value) {
|
||||
if (!m_settings.count(key)) {
|
||||
// load data
|
||||
if (m_savedSettingsData.count(key)) {
|
||||
value->load(m_savedSettingsData.at(key));
|
||||
}
|
||||
m_settings.emplace(key, std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
bool Mod::Impl::hasSettings() const {
|
||||
return m_info.settings.size();
|
||||
}
|
||||
|
||||
std::vector<std::string> Mod::Impl::getSettingKeys() const {
|
||||
std::vector<std::string> keys;
|
||||
for (auto& [key, _] : m_info.settings) {
|
||||
keys.push_back(key);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
std::optional<Setting> Mod::Impl::getSettingDefinition(std::string const& key) const {
|
||||
for (auto& setting : m_info.settings) {
|
||||
if (setting.first == key) {
|
||||
return setting.second;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SettingValue* Mod::Impl::getSetting(std::string const& key) const {
|
||||
if (m_settings.count(key)) {
|
||||
return m_settings.at(key).get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Mod::Impl::hasSetting(std::string const& key) const {
|
||||
for (auto& setting : m_info.settings) {
|
||||
if (setting.first == key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loading, Toggling, Installing
|
||||
|
||||
Result<> Mod::Impl::loadBinary() {
|
||||
if (m_binaryLoaded) {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
GEODE_UNWRAP(this->createTempDir());
|
||||
|
||||
if (this->hasUnresolvedDependencies()) {
|
||||
return Err("Mod has unresolved dependencies");
|
||||
}
|
||||
|
||||
LoaderImpl::get()->provideNextMod(this->m_self);
|
||||
|
||||
GEODE_UNWRAP(this->loadPlatformBinary());
|
||||
m_binaryLoaded = true;
|
||||
|
||||
LoaderImpl::get()->releaseNextMod();
|
||||
|
||||
ModStateEvent(this->m_self, ModEventType::Loaded).post();
|
||||
|
||||
Loader::get()->updateAllDependencies();
|
||||
|
||||
GEODE_UNWRAP(this->enable());
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::unloadBinary() {
|
||||
if (!m_binaryLoaded) {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
if (!m_info.supportsUnloading) {
|
||||
return Err("Mod does not support unloading");
|
||||
}
|
||||
|
||||
GEODE_UNWRAP(this->saveData());
|
||||
|
||||
GEODE_UNWRAP(this->disable());
|
||||
ModStateEvent(this->m_self, ModEventType::Unloaded).post();
|
||||
|
||||
// Disabling unhooks and unpatches already
|
||||
for (auto const& hook : m_hooks) {
|
||||
delete hook;
|
||||
}
|
||||
m_hooks.clear();
|
||||
|
||||
for (auto const& patch : m_patches) {
|
||||
delete patch;
|
||||
}
|
||||
m_patches.clear();
|
||||
|
||||
GEODE_UNWRAP(this->unloadPlatformBinary());
|
||||
m_binaryLoaded = false;
|
||||
|
||||
Loader::get()->updateAllDependencies();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::enable() {
|
||||
if (!m_binaryLoaded) {
|
||||
return this->loadBinary();
|
||||
}
|
||||
|
||||
for (auto const& hook : m_hooks) {
|
||||
GEODE_UNWRAP(this->enableHook(hook));
|
||||
}
|
||||
|
||||
for (auto const& patch : m_patches) {
|
||||
if (!patch->apply()) {
|
||||
return Err("Unable to apply patch at " + std::to_string(patch->getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
ModStateEvent(this->m_self, ModEventType::Enabled).post();
|
||||
m_enabled = true;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::disable() {
|
||||
if (!m_enabled) {
|
||||
return Ok();
|
||||
}
|
||||
if (!m_info.supportsDisabling) {
|
||||
return Err("Mod does not support disabling");
|
||||
}
|
||||
|
||||
ModStateEvent(this->m_self, ModEventType::Disabled).post();
|
||||
|
||||
for (auto const& hook : m_hooks) {
|
||||
GEODE_UNWRAP(this->disableHook(hook));
|
||||
}
|
||||
for (auto const& patch : m_patches) {
|
||||
if (!patch->restore()) {
|
||||
return Err("Unable to restore patch at " + std::to_string(patch->getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
m_enabled = false;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::uninstall() {
|
||||
if (m_info.supportsDisabling) {
|
||||
GEODE_UNWRAP(this->disable());
|
||||
if (m_info.supportsUnloading) {
|
||||
GEODE_UNWRAP(this->unloadBinary());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ghc::filesystem::remove(m_info.path);
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
return Err(
|
||||
"Unable to delete mod's .geode file! "
|
||||
"This might be due to insufficient permissions - "
|
||||
"try running GD as administrator."
|
||||
);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool Mod::Impl::isUninstalled() const {
|
||||
return this->m_self != Mod::get() && !ghc::filesystem::exists(m_info.path);
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
|
||||
Result<> Mod::Impl::updateDependencies() {
|
||||
bool hasUnresolved = false;
|
||||
for (auto& dep : m_info.dependencies) {
|
||||
// set the dependency's loaded mod if such exists
|
||||
if (!dep.mod) {
|
||||
dep.mod = Loader::get()->getLoadedMod(dep.id);
|
||||
// verify loaded dependency version
|
||||
if (dep.mod && !dep.version.compare(dep.mod->getVersion())) {
|
||||
dep.mod = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the dependency is loaded
|
||||
if (dep.mod) {
|
||||
// update the dependency recursively
|
||||
GEODE_UNWRAP(dep.mod->updateDependencies());
|
||||
|
||||
// enable mod if it's resolved & enabled
|
||||
if (!dep.mod->hasUnresolvedDependencies()) {
|
||||
if (dep.mod->isEnabled()) {
|
||||
GEODE_UNWRAP(dep.mod->loadBinary().expect("Unable to load dependency: {error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if the dependency is resolved now
|
||||
if (!dep.isResolved()) {
|
||||
GEODE_UNWRAP(this->unloadBinary().expect("Unable to unload mod: {error}"));
|
||||
hasUnresolved = true;
|
||||
}
|
||||
}
|
||||
// load if there weren't any unresolved dependencies
|
||||
if (!hasUnresolved) {
|
||||
log::debug("All dependencies for {} found", m_info.id);
|
||||
if (m_enabled) {
|
||||
log::debug("Resolved & loading {}", m_info.id);
|
||||
GEODE_UNWRAP(this->loadBinary());
|
||||
}
|
||||
else {
|
||||
log::debug("Resolved {}, however not loading it as it is disabled", m_info.id);
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
bool Mod::Impl::hasUnresolvedDependencies() const {
|
||||
for (auto const& dep : m_info.dependencies) {
|
||||
if (!dep.isResolved()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Dependency> Mod::Impl::getUnresolvedDependencies() {
|
||||
std::vector<Dependency> unresolved;
|
||||
for (auto const& dep : m_info.dependencies) {
|
||||
if (!dep.isResolved()) {
|
||||
unresolved.push_back(dep);
|
||||
}
|
||||
}
|
||||
return unresolved;
|
||||
}
|
||||
|
||||
bool Mod::Impl::depends(std::string const& id) const {
|
||||
return utils::ranges::contains(m_info.dependencies, [id](Dependency const& t) {
|
||||
return t.id == id;
|
||||
});
|
||||
}
|
||||
|
||||
// Hooks
|
||||
|
||||
Result<> Mod::Impl::enableHook(Hook* hook) {
|
||||
auto res = hook->enable();
|
||||
if (res) m_hooks.push_back(hook);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::disableHook(Hook* hook) {
|
||||
return hook->disable();
|
||||
}
|
||||
|
||||
Result<Hook*> Mod::Impl::addHook(Hook* hook) {
|
||||
if (LoaderImpl::get()->isReadyToHook()) {
|
||||
auto res = this->enableHook(hook);
|
||||
if (!res) {
|
||||
delete hook;
|
||||
return Err("Can't create hook");
|
||||
}
|
||||
}
|
||||
else {
|
||||
LoaderImpl::get()->addInternalHook(hook, this->m_self);
|
||||
}
|
||||
|
||||
return Ok(hook);
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::removeHook(Hook* hook) {
|
||||
auto res = this->disableHook(hook);
|
||||
if (res) {
|
||||
ranges::remove(m_hooks, hook);
|
||||
delete hook;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Patches
|
||||
|
||||
// TODO: replace this with a safe one
|
||||
static ByteVector readMemory(void* address, size_t amount) {
|
||||
ByteVector ret;
|
||||
for (size_t i = 0; i < amount; i++) {
|
||||
ret.push_back(*reinterpret_cast<uint8_t*>(reinterpret_cast<uintptr_t>(address) + i));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Result<Patch*> Mod::Impl::patch(void* address, ByteVector const& data) {
|
||||
auto p = new Patch;
|
||||
p->m_address = address;
|
||||
p->m_original = readMemory(address, data.size());
|
||||
p->m_owner = this->m_self;
|
||||
p->m_patch = data;
|
||||
if (!p->apply()) {
|
||||
delete p;
|
||||
return Err("Unable to enable patch at " + std::to_string(p->getAddress()));
|
||||
}
|
||||
m_patches.push_back(p);
|
||||
return Ok(p);
|
||||
}
|
||||
|
||||
Result<> Mod::Impl::unpatch(Patch* patch) {
|
||||
if (patch->restore()) {
|
||||
ranges::remove(m_patches, patch);
|
||||
delete patch;
|
||||
return Ok();
|
||||
}
|
||||
return Err("Unable to restore patch!");
|
||||
}
|
||||
|
||||
// Misc.
|
||||
|
||||
Result<> Mod::Impl::createTempDir() {
|
||||
// Check if temp dir already exists
|
||||
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.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.path));
|
||||
if (!unzip.hasEntry(m_info.binaryName)) {
|
||||
return Err(
|
||||
fmt::format("Unable to find platform binary under the name \"{}\"", m_info.binaryName)
|
||||
);
|
||||
}
|
||||
GEODE_UNWRAP(unzip.extractAllTo(tempPath));
|
||||
|
||||
// Mark temp dir creation as succesful
|
||||
m_tempDirName = tempPath;
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
ghc::filesystem::path Mod::Impl::getConfigDir(bool create) const {
|
||||
auto dir = dirs::getModConfigDir() / m_info.id;
|
||||
if (create) {
|
||||
(void)file::createDirectoryAll(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
char const* Mod::Impl::expandSpriteName(char const* name) {
|
||||
static std::unordered_map<std::string, char const*> expanded = {};
|
||||
if (expanded.count(name)) return expanded[name];
|
||||
|
||||
auto exp = new char[strlen(name) + 2 + m_info.id.size()];
|
||||
auto exps = m_info.id + "/" + name;
|
||||
memcpy(exp, exps.c_str(), exps.size() + 1);
|
||||
|
||||
expanded[name] = exp;
|
||||
|
||||
return exp;
|
||||
}
|
||||
|
||||
ModJson Mod::Impl::getRuntimeInfo() const {
|
||||
auto json = m_info.toJSON();
|
||||
|
||||
auto obj = ModJson::object();
|
||||
obj["hooks"] = ModJson::array();
|
||||
for (auto hook : m_hooks) {
|
||||
obj["hooks"].push_back(ModJson(hook->getRuntimeInfo()));
|
||||
}
|
||||
obj["patches"] = ModJson::array();
|
||||
for (auto patch : m_patches) {
|
||||
obj["patches"].push_back(ModJson(patch->getRuntimeInfo()));
|
||||
}
|
||||
obj["enabled"] = m_enabled;
|
||||
obj["loaded"] = m_binaryLoaded;
|
||||
obj["temp-dir"] = this->getTempDir();
|
||||
obj["save-dir"] = this->getSaveDir();
|
||||
obj["config-dir"] = this->getConfigDir(false);
|
||||
json["runtime"] = obj;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
static constexpr char const* SUPPORT_INFO = R"MD(
|
||||
**Geode** is funded through your gracious <cy>**donations**</c>!
|
||||
You can support our work by sending <cp>**catgirl pictures**</c> to [HJfod](https://youtu.be/LOHSF9MmBDw) :))
|
||||
)MD";
|
||||
|
||||
static ModInfo getModImplInfo() {
|
||||
try {
|
||||
auto json = ModJson::parse(LOADER_MOD_JSON);
|
||||
auto infoRes = ModInfo::create(json);
|
||||
if (infoRes.isErr()) {
|
||||
LoaderImpl::get()->platformMessageBox(
|
||||
"Fatal Internal Error",
|
||||
"Unable to parse loader mod.json: \"" + infoRes.unwrapErr() +
|
||||
"\"\n"
|
||||
"This is a fatal internal error in the loader, please "
|
||||
"contact Geode developers immediately!"
|
||||
);
|
||||
return ModInfo();
|
||||
}
|
||||
auto info = infoRes.unwrap();
|
||||
info.details = LOADER_ABOUT_MD;
|
||||
info.supportInfo = SUPPORT_INFO;
|
||||
info.supportsDisabling = false;
|
||||
return info;
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
LoaderImpl::get()->platformMessageBox(
|
||||
"Fatal Internal Error",
|
||||
"Unable to parse loader mod.json: \"" + std::string(e.what()) +
|
||||
"\"\n"
|
||||
"This is a fatal internal error in the loader, please "
|
||||
"contact Geode developers immediately!"
|
||||
);
|
||||
return ModInfo();
|
||||
}
|
||||
}
|
||||
|
||||
void Loader::Impl::setupInternalMod() {
|
||||
auto& mod = Mod::sharedMod<>;
|
||||
if (mod) return;
|
||||
mod = new Mod(getModImplInfo());
|
||||
|
||||
auto setupRes = mod->m_impl->setup();
|
||||
if (!setupRes) {
|
||||
log::error("Failed to setup internal mod! ({})", setupRes.unwrapErr());
|
||||
}
|
||||
}
|
129
loader/src/loader/ModImpl.hpp
Normal file
129
loader/src/loader/ModImpl.hpp
Normal file
|
@ -0,0 +1,129 @@
|
|||
#pragma once
|
||||
|
||||
namespace geode {
|
||||
class Mod::Impl {
|
||||
public:
|
||||
Mod* m_self;
|
||||
/**
|
||||
* Mod info
|
||||
*/
|
||||
ModInfo m_info;
|
||||
/**
|
||||
* Platform-specific info
|
||||
*/
|
||||
PlatformInfo* m_platformInfo = nullptr;
|
||||
/**
|
||||
* Hooks owned by this mod
|
||||
*/
|
||||
std::vector<Hook*> m_hooks;
|
||||
/**
|
||||
* Patches owned by this mod
|
||||
*/
|
||||
std::vector<Patch*> m_patches;
|
||||
/**
|
||||
* Whether the mod is enabled or not
|
||||
*/
|
||||
bool m_enabled = false;
|
||||
/**
|
||||
* Whether the mod binary is loaded or not
|
||||
*/
|
||||
bool m_binaryLoaded = false;
|
||||
/**
|
||||
* Mod temp directory name
|
||||
*/
|
||||
ghc::filesystem::path m_tempDirName;
|
||||
/**
|
||||
* Mod save directory name
|
||||
*/
|
||||
ghc::filesystem::path m_saveDirPath;
|
||||
/**
|
||||
* Pointers to mods that depend on
|
||||
* this Mod. Makes it possible to
|
||||
* enable / disable them automatically,
|
||||
* when their dependency is disabled.
|
||||
*/
|
||||
std::vector<Mod*> m_parentDependencies;
|
||||
/**
|
||||
* Saved values
|
||||
*/
|
||||
nlohmann::json m_saved;
|
||||
/**
|
||||
* Setting values
|
||||
*/
|
||||
std::unordered_map<std::string, std::unique_ptr<SettingValue>> m_settings;
|
||||
/**
|
||||
* Settings save data. Stored for efficient loading of custom settings
|
||||
*/
|
||||
nlohmann::json m_savedSettingsData;
|
||||
|
||||
Impl(Mod* self, ModInfo const& info);
|
||||
~Impl();
|
||||
|
||||
Result<> setup();
|
||||
|
||||
Result<> loadPlatformBinary();
|
||||
Result<> unloadPlatformBinary();
|
||||
Result<> createTempDir();
|
||||
|
||||
void setupSettings();
|
||||
|
||||
std::string getID() const;
|
||||
std::string getName() const;
|
||||
std::string getDeveloper() const;
|
||||
std::optional<std::string> getDescription() const;
|
||||
std::optional<std::string> getDetails() const;
|
||||
ghc::filesystem::path getPackagePath() const;
|
||||
VersionInfo getVersion() const;
|
||||
bool isEnabled() const;
|
||||
bool isLoaded() const;
|
||||
bool supportsDisabling() const;
|
||||
bool supportsUnloading() const;
|
||||
bool wasSuccesfullyLoaded() const;
|
||||
ModInfo getModInfo() const;
|
||||
ghc::filesystem::path getTempDir() const;
|
||||
ghc::filesystem::path getBinaryPath() const;
|
||||
|
||||
nlohmann::json& getSaveContainer();
|
||||
|
||||
Result<> saveData();
|
||||
Result<> loadData();
|
||||
|
||||
ghc::filesystem::path getSaveDir() const;
|
||||
ghc::filesystem::path getConfigDir(bool create = true) const;
|
||||
|
||||
bool hasSettings() const;
|
||||
std::vector<std::string> getSettingKeys() const;
|
||||
bool hasSetting(std::string const& key) const;
|
||||
std::optional<Setting> getSettingDefinition(std::string const& key) const;
|
||||
SettingValue* getSetting(std::string const& key) const;
|
||||
void registerCustomSetting(std::string const& key, std::unique_ptr<SettingValue> value);
|
||||
|
||||
std::vector<Hook*> getHooks() const;
|
||||
Result<Hook*> addHook(Hook* hook);
|
||||
Result<> enableHook(Hook* hook);
|
||||
Result<> disableHook(Hook* hook);
|
||||
Result<> removeHook(Hook* hook);
|
||||
Result<Patch*> patch(void* address, ByteVector const& data);
|
||||
Result<> unpatch(Patch* patch);
|
||||
Result<> loadBinary();
|
||||
Result<> unloadBinary();
|
||||
Result<> enable();
|
||||
Result<> disable();
|
||||
Result<> uninstall();
|
||||
bool isUninstalled() const;
|
||||
bool depends(std::string const& id) const;
|
||||
bool hasUnresolvedDependencies() const;
|
||||
Result<> updateDependencies();
|
||||
std::vector<Dependency> getUnresolvedDependencies();
|
||||
|
||||
char const* expandSpriteName(char const* name);
|
||||
ModJson getRuntimeInfo() const;
|
||||
};
|
||||
|
||||
class ModImpl : public Mod {
|
||||
public:
|
||||
static Mod* get();
|
||||
|
||||
static Mod::Impl* getImpl(Mod* mod);
|
||||
};
|
||||
}
|
|
@ -70,8 +70,7 @@ Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) {
|
|||
}
|
||||
|
||||
for (auto& [key, value] : root.has("settings").items()) {
|
||||
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, value.json()));
|
||||
sett->m_modID = info.id;
|
||||
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, value));
|
||||
info.settings.push_back({ key, sett });
|
||||
}
|
||||
|
||||
|
|
|
@ -4,106 +4,382 @@
|
|||
#include <Geode/loader/SettingEvent.hpp>
|
||||
#include <Geode/loader/SettingNode.hpp>
|
||||
#include <Geode/utils/general.hpp>
|
||||
#include <re2/re2.h>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
#define PROPAGATE(err) \
|
||||
{ \
|
||||
auto err__ = err; \
|
||||
if (!err__) return Err(err__.error()); \
|
||||
}
|
||||
template<class T>
|
||||
static void parseCommon(T& sett, JsonMaybeObject<ModJson>& obj) {
|
||||
obj.has("name").into(sett.name);
|
||||
obj.has("description").into(sett.description);
|
||||
obj.has("default").into(sett.defaultValue);
|
||||
}
|
||||
|
||||
std::string Setting::getKey() const {
|
||||
Result<BoolSetting> BoolSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
BoolSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<IntSetting> IntSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
IntSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
obj.has("min").into(sett.min);
|
||||
obj.has("max").into(sett.max);
|
||||
if (auto controls = obj.has("control").obj()) {
|
||||
controls.has("arrows").into(sett.controls.arrows);
|
||||
controls.has("big-arrows").into(sett.controls.bigArrows);
|
||||
controls.has("arrow-step").into(sett.controls.arrowStep);
|
||||
controls.has("big-arrow-step").into(sett.controls.bigArrowStep);
|
||||
controls.has("slider").into(sett.controls.slider);
|
||||
controls.has("slider-step").into(sett.controls.sliderStep);
|
||||
controls.has("input").into(sett.controls.input);
|
||||
}
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<FloatSetting> FloatSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
FloatSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
obj.has("min").into(sett.min);
|
||||
obj.has("max").into(sett.max);
|
||||
if (auto controls = obj.has("control").obj()) {
|
||||
controls.has("arrows").into(sett.controls.arrows);
|
||||
controls.has("big-arrows").into(sett.controls.bigArrows);
|
||||
controls.has("arrow-step").into(sett.controls.arrowStep);
|
||||
controls.has("big-arrow-step").into(sett.controls.bigArrowStep);
|
||||
controls.has("slider").into(sett.controls.slider);
|
||||
controls.has("slider-step").into(sett.controls.sliderStep);
|
||||
controls.has("input").into(sett.controls.input);
|
||||
}
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<StringSetting> StringSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
StringSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
obj.has("match").into(sett.match);
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<FileSetting> FileSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
FileSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
if (auto controls = obj.has("control").obj()) {
|
||||
for (auto& item : controls.has("filters").iterate()) {
|
||||
if (auto iobj = item.obj()) {
|
||||
Filter filter;
|
||||
iobj.has("description").into(filter.description);
|
||||
iobj.has("files").into(filter.files);
|
||||
sett.controls.filters.push_back(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<ColorSetting> ColorSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
ColorSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<ColorAlphaSetting> ColorAlphaSetting::parse(JsonMaybeObject<ModJson>& obj) {
|
||||
ColorAlphaSetting sett;
|
||||
parseCommon(sett, obj);
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Result<Setting> Setting::parse(
|
||||
std::string const& key,
|
||||
JsonMaybeValue<ModJson>& value
|
||||
) {
|
||||
auto sett = Setting();
|
||||
sett.m_key = key;
|
||||
if (auto obj = value.obj()) {
|
||||
std::string type;
|
||||
obj.needs("type").into(type);
|
||||
if (type.size()) {
|
||||
switch (hash(type.c_str())) {
|
||||
case hash("bool"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, BoolSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("int"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, IntSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("float"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, FloatSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("string"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, StringSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("rgb"): case hash("color"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, ColorSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("rgba"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, ColorAlphaSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("path"): case hash("file"): {
|
||||
GEODE_UNWRAP_INTO(sett.m_kind, FileSetting::parse(obj));
|
||||
} break;
|
||||
|
||||
case hash("custom"): {
|
||||
sett.m_kind = CustomSetting {
|
||||
.json = obj.json()
|
||||
};
|
||||
// skip checking unknown keys
|
||||
return Ok(sett);
|
||||
} break;
|
||||
|
||||
default: return Err("Unknown setting type \"" + type + "\"");
|
||||
}
|
||||
}
|
||||
obj.checkUnknownKeys();
|
||||
}
|
||||
// if the type wasn't an object or a string, the JsonChecker that gave the
|
||||
// JsonMaybeValue will fail eventually so we can continue on
|
||||
return Ok(sett);
|
||||
}
|
||||
|
||||
Setting::Setting(std::string const& key, SettingKind const& kind)
|
||||
: m_key(key), m_kind(kind) {}
|
||||
|
||||
std::unique_ptr<SettingValue> Setting::createDefaultValue() const {
|
||||
return std::visit(makeVisitor {
|
||||
[&](BoolSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<BoolSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](IntSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<IntSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](FloatSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<FloatSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](StringSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<StringSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](FileSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<FileSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](ColorSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<ColorSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](ColorAlphaSetting const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return std::make_unique<ColorAlphaSettingValue>(m_key, sett);
|
||||
},
|
||||
[&](auto const& sett) -> std::unique_ptr<SettingValue> {
|
||||
return nullptr;
|
||||
},
|
||||
}, m_kind);
|
||||
}
|
||||
|
||||
bool Setting::isCustom() const {
|
||||
return std::holds_alternative<CustomSetting>(m_kind);
|
||||
}
|
||||
|
||||
std::string Setting::getDisplayName() const {
|
||||
return std::visit(makeVisitor {
|
||||
[&](CustomSetting const& sett) {
|
||||
return std::string();
|
||||
},
|
||||
[&](auto const& sett) {
|
||||
return sett.name.value_or(m_key);
|
||||
},
|
||||
}, m_kind);
|
||||
}
|
||||
|
||||
std::optional<std::string> Setting::getDescription() const {
|
||||
return std::visit(makeVisitor {
|
||||
[&](CustomSetting const& sett) -> std::optional<std::string> {
|
||||
return std::nullopt;
|
||||
},
|
||||
[&](auto const& sett) {
|
||||
return sett.description;
|
||||
},
|
||||
}, m_kind);
|
||||
}
|
||||
|
||||
// SettingValue
|
||||
|
||||
SettingValue::SettingValue(std::string const& key) : m_key(key) {}
|
||||
|
||||
std::string SettingValue::getKey() const {
|
||||
return m_key;
|
||||
}
|
||||
|
||||
Result<std::shared_ptr<Setting>> Setting::parse(
|
||||
std::string const& type, std::string const& key, JsonMaybeObject<ModJson>& obj
|
||||
) {
|
||||
switch (hash(type.c_str())) {
|
||||
case hash("bool"): return BoolSetting::parse(key, obj);
|
||||
case hash("int"): return IntSetting::parse(key, obj);
|
||||
case hash("float"): return FloatSetting::parse(key, obj);
|
||||
case hash("string"): return StringSetting::parse(key, obj);
|
||||
case hash("rgb"):
|
||||
case hash("color"): return ColorSetting::parse(key, obj);
|
||||
case hash("rgba"): return ColorAlphaSetting::parse(key, obj);
|
||||
case hash("path"):
|
||||
case hash("file"): return FileSetting::parse(key, obj);
|
||||
default: return Err("Setting \"" + key + "\" has unknown type \"" + type + "\"");
|
||||
// GeodeSettingValue & SettingValueSetter specializations
|
||||
|
||||
#define IMPL_NODE_AND_SETTERS(type_) \
|
||||
template<> \
|
||||
SettingNode* GeodeSettingValue< \
|
||||
type_##Setting \
|
||||
>::createNode(float width) { \
|
||||
return type_##SettingNode::create(this, width); \
|
||||
} \
|
||||
template<> \
|
||||
void GeodeSettingValue< \
|
||||
type_##Setting \
|
||||
>::setValue(ValueType const& value) { \
|
||||
m_value = this->toValid(value).first; \
|
||||
} \
|
||||
template<> \
|
||||
Result<> GeodeSettingValue< \
|
||||
type_##Setting \
|
||||
>::validate(ValueType const& value) const { \
|
||||
auto reason = this->toValid(value).second; \
|
||||
if (reason.has_value()) { \
|
||||
return Err(static_cast<std::string>(reason.value())); \
|
||||
} \
|
||||
return Ok(); \
|
||||
} \
|
||||
template<> \
|
||||
typename type_##Setting::ValueType SettingValueSetter< \
|
||||
typename type_##Setting::ValueType \
|
||||
>::get(SettingValue* setting) { \
|
||||
if (auto b = typeinfo_cast<type_##SettingValue*>(setting)) { \
|
||||
return b->getValue(); \
|
||||
} \
|
||||
return typename type_##Setting::ValueType(); \
|
||||
} \
|
||||
template<> \
|
||||
void SettingValueSetter< \
|
||||
typename type_##Setting::ValueType \
|
||||
>::set( \
|
||||
SettingValue* setting, \
|
||||
typename type_##Setting::ValueType const& value \
|
||||
) { \
|
||||
if (auto b = typeinfo_cast<type_##SettingValue*>(setting)) { \
|
||||
b->setValue(value); \
|
||||
} \
|
||||
}
|
||||
|
||||
#define IMPL_TO_VALID(type_) \
|
||||
template<> \
|
||||
typename GeodeSettingValue<type_##Setting>::Valid \
|
||||
GeodeSettingValue<type_##Setting>::toValid( \
|
||||
typename type_##Setting::ValueType const& value \
|
||||
) const
|
||||
|
||||
// instantiate value setters
|
||||
|
||||
namespace geode {
|
||||
template struct SettingValueSetter<typename BoolSetting::ValueType>;
|
||||
template struct SettingValueSetter<typename IntSetting::ValueType>;
|
||||
template struct SettingValueSetter<typename FloatSetting::ValueType>;
|
||||
template struct SettingValueSetter<typename StringSetting::ValueType>;
|
||||
template struct SettingValueSetter<typename FileSetting::ValueType>;
|
||||
template struct SettingValueSetter<typename ColorSetting::ValueType>;
|
||||
template struct SettingValueSetter<typename ColorAlphaSetting::ValueType>;
|
||||
}
|
||||
|
||||
Result<std::shared_ptr<Setting>> Setting::parse(std::string const& key, ModJson const& rawJson) {
|
||||
if (rawJson.is_object()) {
|
||||
auto json = rawJson;
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("[setting \"" + key + "\"]").obj();
|
||||
// instantiate values
|
||||
|
||||
auto res = Setting::parse(root.needs("type").get<std::string>(), key, root);
|
||||
root.checkUnknownKeys();
|
||||
if (checker.isError()) {
|
||||
return Err(checker.getError());
|
||||
namespace geode {
|
||||
template class GeodeSettingValue<BoolSetting>;
|
||||
template class GeodeSettingValue<IntSetting>;
|
||||
template class GeodeSettingValue<FloatSetting>;
|
||||
template class GeodeSettingValue<StringSetting>;
|
||||
template class GeodeSettingValue<FileSetting>;
|
||||
template class GeodeSettingValue<ColorSetting>;
|
||||
template class GeodeSettingValue<ColorAlphaSetting>;
|
||||
}
|
||||
|
||||
IMPL_TO_VALID(Bool) {
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
IMPL_TO_VALID(Int) {
|
||||
if (m_definition.min && value < m_definition.min) {
|
||||
return { m_definition.min.value(), fmt::format(
|
||||
"Value must be more than or equal to {}",
|
||||
m_definition.min.value()
|
||||
) };
|
||||
}
|
||||
if (m_definition.max && value > m_definition.max) {
|
||||
return { m_definition.max.value(), fmt::format(
|
||||
"Value must be less than or equal to {}",
|
||||
m_definition.max.value()
|
||||
) };
|
||||
}
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
IMPL_TO_VALID(Float) {
|
||||
if (m_definition.min && value < m_definition.min) {
|
||||
return { m_definition.min.value(), fmt::format(
|
||||
"Value must be more than or equal to {}",
|
||||
m_definition.min.value()
|
||||
) };
|
||||
}
|
||||
if (m_definition.max && value > m_definition.max) {
|
||||
return { m_definition.max.value(), fmt::format(
|
||||
"Value must be less than or equal to {}",
|
||||
m_definition.max.value()
|
||||
) };
|
||||
}
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
IMPL_TO_VALID(String) {
|
||||
if (m_definition.match) {
|
||||
if (!re2::RE2::FullMatch(value, m_definition.match.value())) {
|
||||
return {
|
||||
m_definition.defaultValue,
|
||||
fmt::format(
|
||||
"Value must match regex {}",
|
||||
m_definition.match.value()
|
||||
)
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return Err("Setting value is not an object");
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
void Setting::valueChanged() {
|
||||
SettingChangedEvent(m_modID, this).post();
|
||||
IMPL_TO_VALID(File) {
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
SettingNode* BoolSetting::createNode(float width) {
|
||||
return BoolSettingNode::create(
|
||||
std::static_pointer_cast<BoolSetting>(shared_from_this()), width
|
||||
);
|
||||
IMPL_TO_VALID(Color) {
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
SettingNode* IntSetting::createNode(float width) {
|
||||
return IntSettingNode::create(std::static_pointer_cast<IntSetting>(shared_from_this()), width);
|
||||
IMPL_TO_VALID(ColorAlpha) {
|
||||
return { value, std::nullopt };
|
||||
}
|
||||
|
||||
SettingNode* FloatSetting::createNode(float width) {
|
||||
return FloatSettingNode::create(
|
||||
std::static_pointer_cast<FloatSetting>(shared_from_this()), width
|
||||
);
|
||||
IMPL_NODE_AND_SETTERS(Bool);
|
||||
IMPL_NODE_AND_SETTERS(Int);
|
||||
IMPL_NODE_AND_SETTERS(Float);
|
||||
IMPL_NODE_AND_SETTERS(String);
|
||||
IMPL_NODE_AND_SETTERS(File);
|
||||
IMPL_NODE_AND_SETTERS(Color);
|
||||
IMPL_NODE_AND_SETTERS(ColorAlpha);
|
||||
|
||||
// SettingChangedEvent
|
||||
|
||||
SettingChangedEvent::SettingChangedEvent(Mod* mod, SettingValue* value)
|
||||
: mod(mod), value(value) {}
|
||||
|
||||
// SettingChangedFilter
|
||||
|
||||
ListenerResult SettingChangedFilter::handle(
|
||||
std::function<Callback> fn, SettingChangedEvent* event
|
||||
) {
|
||||
if (m_modID == event->mod->getID() &&
|
||||
(!m_targetKey || m_targetKey.value() == event->value->getKey())
|
||||
) {
|
||||
fn(event->value);
|
||||
}
|
||||
return ListenerResult::Propagate;
|
||||
}
|
||||
|
||||
SettingNode* StringSetting::createNode(float width) {
|
||||
return StringSettingNode::create(
|
||||
std::static_pointer_cast<StringSetting>(shared_from_this()), width
|
||||
);
|
||||
}
|
||||
|
||||
SettingNode* FileSetting::createNode(float width) {
|
||||
return FileSettingNode::create(
|
||||
std::static_pointer_cast<FileSetting>(shared_from_this()), width
|
||||
);
|
||||
}
|
||||
|
||||
SettingNode* ColorSetting::createNode(float width) {
|
||||
return ColorSettingNode::create(
|
||||
std::static_pointer_cast<ColorSetting>(shared_from_this()), width
|
||||
);
|
||||
}
|
||||
|
||||
SettingNode* ColorAlphaSetting::createNode(float width) {
|
||||
return ColorAlphaSettingNode::create(
|
||||
std::static_pointer_cast<ColorAlphaSetting>(shared_from_this()), width
|
||||
);
|
||||
}
|
||||
|
||||
SettingChangedEvent::SettingChangedEvent(
|
||||
std::string const& modID, Setting* setting
|
||||
) :
|
||||
m_modID(modID),
|
||||
m_setting(setting) {}
|
||||
|
||||
std::string SettingChangedEvent::getModID() const {
|
||||
return m_modID;
|
||||
}
|
||||
|
||||
Setting* SettingChangedEvent::getSetting() const {
|
||||
return m_setting;
|
||||
}
|
||||
SettingChangedFilter::SettingChangedFilter(
|
||||
std::string const& modID,
|
||||
std::optional<std::string> const& settingKey
|
||||
) : m_modID(modID), m_targetKey(settingKey) {}
|
||||
|
|
|
@ -3,12 +3,20 @@
|
|||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
void SettingNodeDelegate::settingValueChanged(SettingNode* node) {}
|
||||
void SettingNode::dispatchChanged() {
|
||||
if (m_delegate) {
|
||||
m_delegate->settingValueChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingNodeDelegate::settingValueCommitted(SettingNode* node) {}
|
||||
void SettingNode::dispatchCommitted() {
|
||||
if (m_delegate) {
|
||||
m_delegate->settingValueCommitted(this);
|
||||
}
|
||||
}
|
||||
|
||||
bool SettingNode::init(std::shared_ptr<Setting> setting) {
|
||||
m_setting = setting;
|
||||
bool SettingNode::init(SettingValue* value) {
|
||||
m_value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
#include <Geode/loader/Mod.hpp>
|
||||
#include <Geode/loader/Setting.hpp>
|
||||
#include <Geode/loader/SettingEvent.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
#include <array>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
@ -106,56 +106,52 @@ BOOL WINAPI DllMain(HINSTANCE lib, DWORD reason, LPVOID) {
|
|||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
#define $_ GEODE_CONCAT(unnamedVar_, __LINE__)
|
||||
|
||||
static auto $_ =
|
||||
listenForSettingChanges<BoolSetting>("show-platform-console", [](BoolSetting* setting) {
|
||||
if (setting->getValue()) {
|
||||
$execute {
|
||||
listenForSettingChanges("show-platform-console", +[](bool value) {
|
||||
if (value) {
|
||||
Loader::get()->openPlatformConsole();
|
||||
}
|
||||
else {
|
||||
Loader::get()->closePlatformConsole();
|
||||
}
|
||||
});
|
||||
|
||||
listenForIPC("ipc-test", [](IPCEvent* event) -> nlohmann::json {
|
||||
return "Hello from Geode!";
|
||||
});
|
||||
|
||||
static auto $_ = listenForIPC("ipc-test", [](IPCEvent* event) -> nlohmann::json {
|
||||
return "Hello from Geode!";
|
||||
});
|
||||
listenForIPC("loader-info", [](IPCEvent* event) -> nlohmann::json {
|
||||
return Loader::get()->getModImpl()->getModInfo();
|
||||
});
|
||||
|
||||
static auto $_ = listenForIPC("loader-info", [](IPCEvent* event) -> nlohmann::json {
|
||||
return Loader::get()->getInternalMod()->getModInfo();
|
||||
});
|
||||
listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json {
|
||||
std::vector<nlohmann::json> res;
|
||||
|
||||
static auto $_ = listenForIPC("list-mods", [](IPCEvent* event) -> nlohmann::json {
|
||||
std::vector<nlohmann::json> res;
|
||||
auto args = event->messageData;
|
||||
JsonChecker checker(args);
|
||||
auto root = checker.root("").obj();
|
||||
|
||||
auto args = event->messageData;
|
||||
JsonChecker checker(args);
|
||||
auto root = checker.root("").obj();
|
||||
auto includeRunTimeInfo = root.has("include-runtime-info").template get<bool>();
|
||||
auto dontIncludeLoader = root.has("dont-include-loader").template get<bool>();
|
||||
|
||||
auto includeRunTimeInfo = root.has("include-runtime-info").template get<bool>();
|
||||
auto dontIncludeLoader = root.has("dont-include-loader").template get<bool>();
|
||||
if (!dontIncludeLoader) {
|
||||
res.push_back(
|
||||
includeRunTimeInfo ? Loader::get()->getModImpl()->getRuntimeInfo() :
|
||||
Loader::get()->getModImpl()->getModInfo().toJSON()
|
||||
);
|
||||
}
|
||||
|
||||
if (!dontIncludeLoader) {
|
||||
res.push_back(
|
||||
includeRunTimeInfo ? Loader::get()->getInternalMod()->getRuntimeInfo() :
|
||||
Loader::get()->getInternalMod()->getModInfo().toJSON()
|
||||
);
|
||||
}
|
||||
for (auto& mod : Loader::get()->getAllMods()) {
|
||||
res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getModInfo().toJSON());
|
||||
}
|
||||
|
||||
for (auto& mod : Loader::get()->getAllMods()) {
|
||||
res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getModInfo().toJSON());
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
int geodeEntry(void* platformData) {
|
||||
// setup internals
|
||||
|
||||
Loader::get()->openPlatformConsole();
|
||||
|
||||
if (!geode::core::hook::initialize()) {
|
||||
LoaderImpl::get()->platformMessageBox(
|
||||
"Unable to load Geode!",
|
||||
|
@ -180,9 +176,9 @@ int geodeEntry(void* platformData) {
|
|||
|
||||
log::debug("Set up loader");
|
||||
|
||||
// if (InternalMod::get()->getSettingValue<bool>("show-platform-console")) {
|
||||
// Loader::get()->openPlatformConsole();
|
||||
// }
|
||||
if (Mod::get()->getSettingValue<bool>("show-platform-console")) {
|
||||
Loader::get()->openPlatformConsole();
|
||||
}
|
||||
|
||||
log::debug("Entry done.");
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#include <Geode/loader/Dirs.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/loader/Log.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
#include <iostream>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
#include <Geode/DefaultInclude.hpp>
|
||||
|
||||
#ifdef GEODE_IS_IOS
|
||||
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
#include <dlfcn.h>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
@ -15,16 +17,10 @@ T findSymbolOrMangled(void* dylib, char const* name, char const* mangled) {
|
|||
return res;
|
||||
}
|
||||
|
||||
Result<> Mod::loadPlatformBinary() {
|
||||
Result<> Mod::Impl::loadPlatformBinary() {
|
||||
auto dylib =
|
||||
dlopen((this->m_tempDirName / this->m_info.binaryName).string().c_str(), RTLD_LAZY);
|
||||
if (dylib) {
|
||||
this->m_implicitLoadFunc =
|
||||
findSymbolOrMangled<geode_load>(dylib, "geode_implicit_load", "_geode_implicit_load");
|
||||
|
||||
if (!this->m_implicitLoadFunc) {
|
||||
return Err("Unable to find mod entry point");
|
||||
}
|
||||
if (this->m_platformInfo) {
|
||||
delete this->m_platformInfo;
|
||||
}
|
||||
|
@ -36,12 +32,11 @@ Result<> Mod::loadPlatformBinary() {
|
|||
return Err("Unable to load the DYLIB: dlerror returned (" + err + ")");
|
||||
}
|
||||
|
||||
Result<> Mod::unloadPlatformBinary() {
|
||||
Result<> Mod::Impl::unloadPlatformBinary() {
|
||||
auto dylib = this->m_platformInfo->m_dylib;
|
||||
delete this->m_platformInfo;
|
||||
this->m_platformInfo = nullptr;
|
||||
if (dlclose(dylib) == 0) {
|
||||
this->m_implicitLoadFunc = nullptr;
|
||||
return Ok();
|
||||
}
|
||||
else {
|
|
@ -1,13 +1,15 @@
|
|||
#include <Geode/loader/IPC.hpp>
|
||||
#include <Geode/loader/Log.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include <iostream>
|
||||
#include <loader/LoaderImpl.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
|
||||
#ifdef GEODE_IS_MACOS
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
void Loader::Impl::platformMessageBox(char const* title, std::string const& info) {
|
||||
CFStringRef cfTitle = CFStringCreateWithCString(NULL, title, kCFStringEncodingUTF8);
|
||||
CFStringRef cfMessage = CFStringCreateWithCString(NULL, info.c_str(), kCFStringEncodingUTF8);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#ifdef GEODE_IS_MACOS
|
||||
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
#include <dlfcn.h>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
@ -16,17 +17,10 @@ T findSymbolOrMangled(void* dylib, char const* name, char const* mangled) {
|
|||
return res;
|
||||
}
|
||||
|
||||
Result<> Mod::loadPlatformBinary() {
|
||||
Result<> Mod::Impl::loadPlatformBinary() {
|
||||
auto dylib =
|
||||
dlopen((this->m_tempDirName / this->m_info.binaryName).string().c_str(), RTLD_LAZY);
|
||||
if (dylib) {
|
||||
this->m_implicitLoadFunc =
|
||||
findSymbolOrMangled<decltype(geode_implicit_load)*>(dylib, "geode_implicit_load", "_geode_implicit_load");
|
||||
|
||||
if (!this->m_implicitLoadFunc) {
|
||||
return Err("Unable to find mod entry point");
|
||||
}
|
||||
|
||||
if (this->m_platformInfo) {
|
||||
delete this->m_platformInfo;
|
||||
}
|
||||
|
@ -38,12 +32,11 @@ Result<> Mod::loadPlatformBinary() {
|
|||
return Err("Unable to load the DYLIB: dlerror returned (" + err + ")");
|
||||
}
|
||||
|
||||
Result<> Mod::unloadPlatformBinary() {
|
||||
Result<> Mod::Impl::unloadPlatformBinary() {
|
||||
auto dylib = this->m_platformInfo->m_dylib;
|
||||
delete this->m_platformInfo;
|
||||
this->m_platformInfo = nullptr;
|
||||
if (dlclose(dylib) == 0) {
|
||||
this->m_implicitLoadFunc = nullptr;
|
||||
return Ok();
|
||||
}
|
||||
else {
|
|
@ -1,6 +1,6 @@
|
|||
#include <Geode/loader/IPC.hpp>
|
||||
#include <Geode/loader/Log.hpp>
|
||||
#include <InternalMod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
#include <iostream>
|
||||
#include <loader/LoaderImpl.hpp>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#ifdef GEODE_IS_WINDOWS
|
||||
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <loader/ModImpl.hpp>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
|
@ -71,14 +72,9 @@ std::string getLastWinError() {
|
|||
return msg + " (" + std::to_string(err) + ")";
|
||||
}
|
||||
|
||||
Result<> Mod::loadPlatformBinary() {
|
||||
Result<> Mod::Impl::loadPlatformBinary() {
|
||||
auto load = LoadLibraryW((m_tempDirName / m_info.binaryName).wstring().c_str());
|
||||
if (load) {
|
||||
if (!(m_implicitLoadFunc = findSymbolOrMangled<decltype(geode_implicit_load)*>(
|
||||
load, "geode_implicit_load", "_geode_implicit_load@4"
|
||||
))) {
|
||||
return Err("Unable to find mod entry point");
|
||||
}
|
||||
if (m_platformInfo) {
|
||||
delete m_platformInfo;
|
||||
}
|
||||
|
@ -88,11 +84,10 @@ Result<> Mod::loadPlatformBinary() {
|
|||
return Err("Unable to load the DLL: " + getLastWinError());
|
||||
}
|
||||
|
||||
Result<> Mod::unloadPlatformBinary() {
|
||||
Result<> Mod::Impl::unloadPlatformBinary() {
|
||||
auto hmod = m_platformInfo->m_hmod;
|
||||
delete m_platformInfo;
|
||||
if (FreeLibrary(hmod)) {
|
||||
m_implicitLoadFunc = nullptr;
|
||||
return Ok();
|
||||
}
|
||||
else {
|
|
@ -74,7 +74,7 @@ CCNode* geode::createDefaultLogo(CCSize const& size) {
|
|||
|
||||
CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
|
||||
CCNode* spr = nullptr;
|
||||
if (mod == Loader::get()->getInternalMod()) {
|
||||
if (mod == Loader::get()->getModImpl()) {
|
||||
spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
#include <Geode/utils/string.hpp>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
std::string expandKnownLink(std::string const& link) {
|
||||
switch (hash(utils::string::toLower(link).c_str())) {
|
||||
case hash("github"):
|
||||
if (!utils::string::contains(link, "/")) {
|
||||
return "https://github.com/" + link;
|
||||
}
|
||||
break;
|
||||
|
||||
case hash("youtube"):
|
||||
if (!utils::string::contains(link, "/")) {
|
||||
return "https://youtube.com/channel/" + link;
|
||||
}
|
||||
break;
|
||||
|
||||
case hash("twitter"):
|
||||
if (!utils::string::contains(link, "/")) {
|
||||
return "https://twitter.com/" + link;
|
||||
}
|
||||
break;
|
||||
|
||||
case hash("newgrounds"):
|
||||
if (!utils::string::contains(link, "/")) {
|
||||
return "https://" + link + ".newgrounds.com";
|
||||
}
|
||||
break;
|
||||
}
|
||||
return link;
|
||||
}
|
|
@ -323,7 +323,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
|
|||
disableBtnSpr->setColor({150, 150, 150});
|
||||
}
|
||||
|
||||
if (mod != Loader::get()->getInternalMod()) {
|
||||
if (mod != Loader::get()->getModImpl()) {
|
||||
auto uninstallBtnSpr = ButtonSprite::create(
|
||||
"Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#include <Geode/binding/FLAlertLayer.hpp>
|
||||
#include <Geode/binding/StatsCell.hpp>
|
||||
#include <Geode/ui/GeodeUI.hpp>
|
||||
#include "../../../loader/LoaderImpl.hpp" // how should i include this src/loader/LoaderImpl.hpp
|
||||
#include <loader/LoaderImpl.hpp>
|
||||
#include "../info/TagNode.hpp"
|
||||
#include "../info/DevProfilePopup.hpp"
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/ui/ListView.hpp>
|
||||
#include <Geode/utils/string.hpp>
|
||||
#include <Geode/utils/ranges.hpp>
|
||||
|
||||
#define FTS_FUZZY_MATCH_IMPLEMENTATION
|
||||
#include <Geode/external/fts/fts_fuzzy_match.h>
|
||||
|
|
|
@ -8,6 +8,107 @@
|
|||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/loader/Dirs.hpp>
|
||||
|
||||
// Helpers
|
||||
|
||||
template <class Num>
|
||||
Num parseNumForInput(std::string const& str) {
|
||||
try {
|
||||
if constexpr (std::is_same_v<Num, int64_t>) {
|
||||
return std::stoll(str);
|
||||
}
|
||||
else if constexpr (std::is_same_v<Num, double>) {
|
||||
return std::stod(str);
|
||||
}
|
||||
else {
|
||||
static_assert(!std::is_same_v<Num, Num>, "Impl Num for parseNumForInput");
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static float valueToSlider(T const& setting, typename T::ValueType value) {
|
||||
auto min = setting.min ? setting.min.value() : -100;
|
||||
auto max = setting.max ? setting.max.value() : +100;
|
||||
auto range = max - min;
|
||||
return static_cast<float>(clamp(static_cast<double>(value - min) / range, 0.0, 1.0));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static typename T::ValueType valueFromSlider(T const& setting, float num) {
|
||||
auto min = setting.min ? setting.min.value() : -100;
|
||||
auto max = setting.max ? setting.max.value() : +100;
|
||||
auto range = max - min;
|
||||
auto value = static_cast<typename T::ValueType>(num * range + min);
|
||||
if (auto step = setting.controls.sliderStep) {
|
||||
value = static_cast<typename T::ValueType>(
|
||||
round(value / step.value()) * step.value()
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
template<class C, class T>
|
||||
InputNode* createInput(C* node, GeodeSettingValue<T>* setting, float width) {
|
||||
auto input = InputNode::create(width / 2 - 70.f, "Text", "chatFont.fnt");
|
||||
input->setPosition({
|
||||
-(width / 2 - 70.f) / 2,
|
||||
setting->castDefinition().controls.slider ? 5.f : 0.f
|
||||
});
|
||||
input->setScale(.65f);
|
||||
input->getInput()->setDelegate(node);
|
||||
return input;
|
||||
}
|
||||
|
||||
template<class C, class T>
|
||||
Slider* createSlider(C* node, GeodeSettingValue<T>* setting, float width) {
|
||||
auto slider = Slider::create(
|
||||
node, menu_selector(C::onSlider), .5f
|
||||
);
|
||||
slider->setPosition(-50.f, -15.f);
|
||||
node->updateSlider();
|
||||
return slider;
|
||||
}
|
||||
|
||||
template<class C, class T>
|
||||
std::pair<
|
||||
CCMenuItemSpriteExtra*, CCMenuItemSpriteExtra*
|
||||
> createArrows(C* node, GeodeSettingValue<T>* setting, float width, bool big) {
|
||||
auto yPos = setting->castDefinition().controls.slider ? 5.f : 0.f;
|
||||
auto decArrowSpr = CCSprite::createWithSpriteFrameName(
|
||||
big ? "double-nav.png"_spr : "navArrowBtn_001.png"
|
||||
);
|
||||
decArrowSpr->setFlipX(true);
|
||||
decArrowSpr->setScale(.3f);
|
||||
|
||||
auto decArrow = CCMenuItemSpriteExtra::create(
|
||||
decArrowSpr, node, menu_selector(C::onArrow)
|
||||
);
|
||||
decArrow->setPosition(-width / 2 + (big ? 65.f : 80.f), yPos);
|
||||
decArrow->setTag(big ?
|
||||
-setting->castDefinition().controls.bigArrowStep :
|
||||
-setting->castDefinition().controls.arrowStep
|
||||
);
|
||||
|
||||
auto incArrowSpr = CCSprite::createWithSpriteFrameName(
|
||||
big ? "double-nav.png"_spr : "navArrowBtn_001.png"
|
||||
);
|
||||
incArrowSpr->setScale(.3f);
|
||||
|
||||
auto incArrow = CCMenuItemSpriteExtra::create(
|
||||
incArrowSpr, node, menu_selector(C::onArrow)
|
||||
);
|
||||
incArrow->setTag(big ?
|
||||
setting->castDefinition().controls.bigArrowStep :
|
||||
setting->castDefinition().controls.arrowStep
|
||||
);
|
||||
incArrow->setPosition(big ? 5.f : -10.f, yPos);
|
||||
|
||||
return { decArrow, incArrow };
|
||||
}
|
||||
|
||||
// BoolSettingNode
|
||||
|
||||
void BoolSettingNode::valueChanged(bool updateText) {
|
||||
|
@ -21,7 +122,7 @@ void BoolSettingNode::onToggle(CCObject*) {
|
|||
m_toggle->toggle(!m_uncommittedValue);
|
||||
}
|
||||
|
||||
bool BoolSettingNode::setup(std::shared_ptr<BoolSetting> setting, float width) {
|
||||
bool BoolSettingNode::setup(BoolSettingValue* setting, float width) {
|
||||
m_toggle = CCMenuItemToggler::createWithStandardSprites(
|
||||
this, menu_selector(BoolSettingNode::onToggle), .6f
|
||||
);
|
||||
|
@ -34,42 +135,172 @@ bool BoolSettingNode::setup(std::shared_ptr<BoolSetting> setting, float width) {
|
|||
|
||||
// IntSettingNode
|
||||
|
||||
float IntSettingNode::setupHeight(std::shared_ptr<IntSetting> setting) const {
|
||||
return setting->hasSlider() ? 55.f : 40.f;
|
||||
float IntSettingNode::setupHeight(IntSettingValue* setting) const {
|
||||
return setting->castDefinition().controls.slider ? 55.f : 40.f;
|
||||
}
|
||||
|
||||
bool IntSettingNode::setup(std::shared_ptr<IntSetting> setting, float width) {
|
||||
this->setupArrows(setting, width);
|
||||
this->setupInput(setting, width);
|
||||
this->setupSlider(setting, width);
|
||||
return true;
|
||||
void IntSettingNode::onSlider(CCObject* slider) {
|
||||
m_uncommittedValue = valueFromSlider(
|
||||
setting()->castDefinition(),
|
||||
static_cast<SliderThumb*>(slider)->getValue()
|
||||
);
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void IntSettingNode::valueChanged(bool updateText) {
|
||||
GeodeSettingNode::valueChanged(updateText);
|
||||
this->updateLabel(updateText);
|
||||
if (updateText) {
|
||||
this->updateLabel();
|
||||
}
|
||||
this->updateSlider();
|
||||
}
|
||||
|
||||
void IntSettingNode::updateSlider() {
|
||||
if (m_slider) {
|
||||
m_slider->setValue(valueToSlider(
|
||||
setting()->castDefinition(),
|
||||
m_uncommittedValue
|
||||
));
|
||||
m_slider->updateBar();
|
||||
}
|
||||
}
|
||||
|
||||
void IntSettingNode::updateLabel() {
|
||||
if (m_input) {
|
||||
// hacky way to make setString not called textChanged
|
||||
m_input->getInput()->setDelegate(nullptr);
|
||||
m_input->setString(numToString(m_uncommittedValue));
|
||||
m_input->getInput()->setDelegate(this);
|
||||
}
|
||||
else {
|
||||
m_label->setString(numToString(m_uncommittedValue).c_str());
|
||||
m_label->limitLabelWidth(m_width / 2 - 70.f, .5f, .1f);
|
||||
}
|
||||
}
|
||||
|
||||
void IntSettingNode::onArrow(CCObject* sender) {
|
||||
m_uncommittedValue += sender->getTag();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void IntSettingNode::textChanged(CCTextInputNode* input) {
|
||||
m_uncommittedValue = parseNumForInput<ValueType>(input->getString());
|
||||
this->valueChanged(false);
|
||||
}
|
||||
|
||||
bool IntSettingNode::setup(IntSettingValue* setting, float width) {
|
||||
if (setting->castDefinition().controls.input) {
|
||||
m_menu->addChild(m_input = createInput(this, setting, width));
|
||||
}
|
||||
else {
|
||||
m_label = CCLabelBMFont::create("", "bigFont.fnt");
|
||||
m_label->setPosition({
|
||||
-(width / 2 - 70.f) / 2,
|
||||
setting->castDefinition().controls.slider ? 5.f : 0.f
|
||||
});
|
||||
m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f);
|
||||
m_menu->addChild(m_label);
|
||||
}
|
||||
if (setting->castDefinition().controls.slider) {
|
||||
m_menu->addChild(m_slider = createSlider(this, setting, width));
|
||||
}
|
||||
if (setting->castDefinition().controls.arrows) {
|
||||
auto [dec, inc] = createArrows(this, setting, width, false);
|
||||
m_menu->addChild(m_decArrow = dec);
|
||||
m_menu->addChild(m_incArrow = inc);
|
||||
}
|
||||
if (setting->castDefinition().controls.bigArrows) {
|
||||
auto [dec, inc] = createArrows(this, setting, width, true);
|
||||
m_menu->addChild(m_bigDecArrow = dec);
|
||||
m_menu->addChild(m_bigIncArrow = inc);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// FloatSettingNode
|
||||
|
||||
float FloatSettingNode::setupHeight(std::shared_ptr<FloatSetting> setting) const {
|
||||
return setting->hasSlider() ? 55.f : 40.f;
|
||||
float FloatSettingNode::setupHeight(FloatSettingValue* setting) const {
|
||||
return setting->castDefinition().controls.slider ? 55.f : 40.f;
|
||||
}
|
||||
|
||||
bool FloatSettingNode::setup(std::shared_ptr<FloatSetting> setting, float width) {
|
||||
this->setupArrows(setting, width);
|
||||
this->setupInput(setting, width);
|
||||
this->setupSlider(setting, width);
|
||||
return true;
|
||||
void FloatSettingNode::onSlider(CCObject* slider) {
|
||||
m_uncommittedValue = valueFromSlider(
|
||||
setting()->castDefinition(),
|
||||
static_cast<SliderThumb*>(slider)->getValue()
|
||||
);
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void FloatSettingNode::valueChanged(bool updateText) {
|
||||
GeodeSettingNode::valueChanged(updateText);
|
||||
this->updateLabel(updateText);
|
||||
if (updateText) {
|
||||
this->updateLabel();
|
||||
}
|
||||
this->updateSlider();
|
||||
}
|
||||
|
||||
void FloatSettingNode::updateSlider() {
|
||||
if (m_slider) {
|
||||
m_slider->setValue(valueToSlider(
|
||||
setting()->castDefinition(),
|
||||
m_uncommittedValue
|
||||
));
|
||||
m_slider->updateBar();
|
||||
}
|
||||
}
|
||||
|
||||
void FloatSettingNode::updateLabel() {
|
||||
if (m_input) {
|
||||
// hacky way to make setString not called textChanged
|
||||
m_input->getInput()->setDelegate(nullptr);
|
||||
m_input->setString(numToString(m_uncommittedValue));
|
||||
m_input->getInput()->setDelegate(this);
|
||||
}
|
||||
else {
|
||||
m_label->setString(numToString(m_uncommittedValue).c_str());
|
||||
m_label->limitLabelWidth(m_width / 2 - 70.f, .5f, .1f);
|
||||
}
|
||||
}
|
||||
|
||||
void FloatSettingNode::onArrow(CCObject* sender) {
|
||||
m_uncommittedValue += sender->getTag();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void FloatSettingNode::textChanged(CCTextInputNode* input) {
|
||||
m_uncommittedValue = parseNumForInput<ValueType>(input->getString());
|
||||
this->valueChanged(false);
|
||||
}
|
||||
|
||||
bool FloatSettingNode::setup(FloatSettingValue* setting, float width) {
|
||||
if (setting->castDefinition().controls.input) {
|
||||
m_menu->addChild(m_input = createInput(this, setting, width));
|
||||
}
|
||||
else {
|
||||
m_label = CCLabelBMFont::create("", "bigFont.fnt");
|
||||
m_label->setPosition({
|
||||
-(width / 2 - 70.f) / 2,
|
||||
setting->castDefinition().controls.slider ? 5.f : 0.f
|
||||
});
|
||||
m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f);
|
||||
m_menu->addChild(m_label);
|
||||
}
|
||||
if (setting->castDefinition().controls.slider) {
|
||||
m_menu->addChild(m_slider = createSlider(this, setting, width));
|
||||
}
|
||||
if (setting->castDefinition().controls.arrows) {
|
||||
auto [dec, inc] = createArrows(this, setting, width, false);
|
||||
m_menu->addChild(m_decArrow = dec);
|
||||
m_menu->addChild(m_incArrow = inc);
|
||||
}
|
||||
if (setting->castDefinition().controls.bigArrows) {
|
||||
auto [dec, inc] = createArrows(this, setting, width, true);
|
||||
m_menu->addChild(m_bigDecArrow = dec);
|
||||
m_menu->addChild(m_bigIncArrow = inc);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// StringSettingNode
|
||||
|
||||
void StringSettingNode::updateLabel() {
|
||||
|
@ -89,7 +320,7 @@ void StringSettingNode::valueChanged(bool updateText) {
|
|||
this->updateLabel();
|
||||
}
|
||||
|
||||
bool StringSettingNode::setup(std::shared_ptr<StringSetting> setting, float width) {
|
||||
bool StringSettingNode::setup(StringSettingValue* setting, float width) {
|
||||
m_input = InputNode::create(width / 2 - 10.f, "Text", "chatFont.fnt");
|
||||
m_input->setPosition({ -(width / 2 - 70.f) / 2, .0f });
|
||||
m_input->setScale(.65f);
|
||||
|
@ -119,20 +350,19 @@ void FileSettingNode::valueChanged(bool updateText) {
|
|||
}
|
||||
|
||||
void FileSettingNode::onPickFile(CCObject*) {
|
||||
auto setting = std::static_pointer_cast<FileSetting>(m_setting);
|
||||
if (auto path = file::pickFile(
|
||||
file::PickMode::OpenFile,
|
||||
{
|
||||
dirs::getGameDir(),
|
||||
setting->getFileFilters().value_or(std::vector<file::FilePickOptions::Filter>())
|
||||
}
|
||||
)) {
|
||||
file::PickMode::OpenFile,
|
||||
{
|
||||
dirs::getGameDir(),
|
||||
setting()->castDefinition().controls.filters
|
||||
}
|
||||
)) {
|
||||
m_uncommittedValue = path.unwrap();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool FileSettingNode::setup(std::shared_ptr<FileSetting> setting, float width) {
|
||||
bool FileSettingNode::setup(FileSettingValue* setting, float width) {
|
||||
m_input = InputNode::create(width / 2 - 30.f, "Path to File", "chatFont.fnt");
|
||||
m_input->setPosition({ -(width / 2 - 80.f) / 2 - 15.f, .0f });
|
||||
m_input->setScale(.65f);
|
||||
|
@ -142,8 +372,9 @@ bool FileSettingNode::setup(std::shared_ptr<FileSetting> setting, float width) {
|
|||
auto fileBtnSpr = CCSprite::createWithSpriteFrameName("gj_folderBtn_001.png");
|
||||
fileBtnSpr->setScale(.5f);
|
||||
|
||||
auto fileBtn =
|
||||
CCMenuItemSpriteExtra::create(fileBtnSpr, this, menu_selector(FileSettingNode::onPickFile));
|
||||
auto fileBtn = CCMenuItemSpriteExtra::create(
|
||||
fileBtnSpr, this, menu_selector(FileSettingNode::onPickFile)
|
||||
);
|
||||
fileBtn->setPosition(.0f, .0f);
|
||||
m_menu->addChild(fileBtn);
|
||||
|
||||
|
@ -169,7 +400,7 @@ void ColorSettingNode::onSelectColor(CCObject*) {
|
|||
popup->show();
|
||||
}
|
||||
|
||||
bool ColorSettingNode::setup(std::shared_ptr<ColorSetting> setting, float width) {
|
||||
bool ColorSettingNode::setup(ColorSettingValue* setting, float width) {
|
||||
m_colorSpr = ColorChannelSprite::create();
|
||||
m_colorSpr->setColor(m_uncommittedValue);
|
||||
m_colorSpr->setScale(.65f);
|
||||
|
@ -203,7 +434,7 @@ void ColorAlphaSettingNode::onSelectColor(CCObject*) {
|
|||
popup->show();
|
||||
}
|
||||
|
||||
bool ColorAlphaSettingNode::setup(std::shared_ptr<ColorAlphaSetting> setting, float width) {
|
||||
bool ColorAlphaSettingNode::setup(ColorAlphaSettingValue* setting, float width) {
|
||||
m_colorSpr = ColorChannelSprite::create();
|
||||
m_colorSpr->setColor(to3B(m_uncommittedValue));
|
||||
m_colorSpr->updateOpacity(m_uncommittedValue.a / 255.f);
|
||||
|
@ -217,3 +448,53 @@ bool ColorAlphaSettingNode::setup(std::shared_ptr<ColorAlphaSetting> setting, fl
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
// CustomSettingPlaceholderNode
|
||||
|
||||
void CustomSettingPlaceholderNode::commit() {
|
||||
|
||||
}
|
||||
|
||||
bool CustomSettingPlaceholderNode::hasUncommittedChanges() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CustomSettingPlaceholderNode::hasNonDefaultValue() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void CustomSettingPlaceholderNode::resetToDefault() {
|
||||
|
||||
}
|
||||
|
||||
bool CustomSettingPlaceholderNode::init(std::string const& key, float width) {
|
||||
if (!SettingNode::init(nullptr))
|
||||
return false;
|
||||
|
||||
constexpr auto sidePad = 40.f;
|
||||
|
||||
this->setContentSize({ width, 40.f });
|
||||
|
||||
auto info = CCLabelBMFont::create(
|
||||
"You need to enable the mod to modify this setting.",
|
||||
"bigFont.fnt"
|
||||
);
|
||||
info->setAnchorPoint({ .0f, .5f });
|
||||
info->limitLabelWidth(width - sidePad, .5f, .1f);
|
||||
info->setPosition({ sidePad / 2, m_obContentSize.height / 2 });
|
||||
this->addChild(info);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CustomSettingPlaceholderNode* CustomSettingPlaceholderNode::create(
|
||||
std::string const& key, float width
|
||||
) {
|
||||
auto ret = new CustomSettingPlaceholderNode;
|
||||
if (ret && ret->init(key, width)) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
@ -15,435 +15,256 @@
|
|||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
namespace {
|
||||
template <class Num>
|
||||
Num parseNumForInput(std::string const& str) {
|
||||
try {
|
||||
if constexpr (std::is_same_v<Num, int64_t>) {
|
||||
return std::stoll(str);
|
||||
}
|
||||
else if constexpr (std::is_same_v<Num, double>) {
|
||||
return std::stod(str);
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
return 0;
|
||||
}
|
||||
#define IMPL_SETT_CREATE(type_) \
|
||||
static type_##SettingNode* create( \
|
||||
type_##SettingValue* value, float width \
|
||||
) { \
|
||||
auto ret = new type_##SettingNode; \
|
||||
if (ret && ret->init(value, width)) { \
|
||||
ret->autorelease(); \
|
||||
return ret; \
|
||||
} \
|
||||
CC_SAFE_DELETE(ret); \
|
||||
return nullptr; \
|
||||
}
|
||||
|
||||
template <class N, class T>
|
||||
class GeodeSettingNode : public SettingNode {
|
||||
public:
|
||||
using value_t = typename T::value_t;
|
||||
template <class T>
|
||||
class GeodeSettingNode : public SettingNode {
|
||||
public:
|
||||
using ValueType = typename T::ValueType;
|
||||
|
||||
protected:
|
||||
float m_width;
|
||||
float m_height;
|
||||
value_t m_uncommittedValue;
|
||||
CCMenu* m_menu;
|
||||
CCLabelBMFont* m_nameLabel;
|
||||
CCLabelBMFont* m_errorLabel;
|
||||
CCMenuItemSpriteExtra* m_resetBtn;
|
||||
protected:
|
||||
float m_width;
|
||||
float m_height;
|
||||
ValueType m_uncommittedValue;
|
||||
CCMenu* m_menu;
|
||||
CCLabelBMFont* m_nameLabel;
|
||||
CCLabelBMFont* m_errorLabel;
|
||||
CCMenuItemSpriteExtra* m_resetBtn;
|
||||
|
||||
bool init(std::shared_ptr<T> setting, float width) {
|
||||
if (!SettingNode::init(std::static_pointer_cast<Setting>(setting))) return false;
|
||||
bool init(GeodeSettingValue<T>* setting, float width) {
|
||||
if (!SettingNode::init(setting))
|
||||
return false;
|
||||
|
||||
m_width = width;
|
||||
m_height = this->setupHeight(setting);
|
||||
this->setContentSize({ width, m_height });
|
||||
m_width = width;
|
||||
m_height = this->setupHeight(setting);
|
||||
this->setContentSize({ width, m_height });
|
||||
|
||||
constexpr auto sidePad = 40.f;
|
||||
constexpr auto sidePad = 40.f;
|
||||
|
||||
m_uncommittedValue = setting->getValue();
|
||||
m_uncommittedValue = setting->getValue();
|
||||
|
||||
auto name = setting->getDisplayName();
|
||||
auto name = setting->getDefinition().getDisplayName();
|
||||
|
||||
m_nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
|
||||
m_nameLabel->setAnchorPoint({ .0f, .5f });
|
||||
m_nameLabel->limitLabelWidth(width / 2 - 50.f, .5f, .1f);
|
||||
m_nameLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 });
|
||||
this->addChild(m_nameLabel);
|
||||
m_nameLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
|
||||
m_nameLabel->setAnchorPoint({ .0f, .5f });
|
||||
m_nameLabel->limitLabelWidth(width / 2 - 50.f, .5f, .1f);
|
||||
m_nameLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 });
|
||||
this->addChild(m_nameLabel);
|
||||
|
||||
m_errorLabel = CCLabelBMFont::create("", "bigFont.fnt");
|
||||
m_errorLabel->setAnchorPoint({ .0f, .5f });
|
||||
m_errorLabel->limitLabelWidth(width / 2 - 50.f, .25f, .1f);
|
||||
m_errorLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 - 14.f });
|
||||
m_errorLabel->setColor({ 255, 100, 100 });
|
||||
m_errorLabel->setZOrder(5);
|
||||
this->addChild(m_errorLabel);
|
||||
m_errorLabel = CCLabelBMFont::create("", "bigFont.fnt");
|
||||
m_errorLabel->setAnchorPoint({ .0f, .5f });
|
||||
m_errorLabel->limitLabelWidth(width / 2 - 50.f, .25f, .1f);
|
||||
m_errorLabel->setPosition({ sidePad / 2, m_obContentSize.height / 2 - 14.f });
|
||||
m_errorLabel->setColor({ 255, 100, 100 });
|
||||
m_errorLabel->setZOrder(5);
|
||||
this->addChild(m_errorLabel);
|
||||
|
||||
m_menu = CCMenu::create();
|
||||
m_menu->setPosition({ m_obContentSize.width - sidePad / 2, m_obContentSize.height / 2 }
|
||||
m_menu = CCMenu::create();
|
||||
m_menu->setPosition({ m_obContentSize.width - sidePad / 2, m_obContentSize.height / 2 }
|
||||
);
|
||||
this->addChild(m_menu);
|
||||
|
||||
float btnPos = 15.f;
|
||||
|
||||
if (setting->castDefinition().description) {
|
||||
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
|
||||
infoSpr->setScale(.6f);
|
||||
|
||||
auto infoBtn = CCMenuItemSpriteExtra::create(
|
||||
infoSpr, this, menu_selector(GeodeSettingNode::onDescription)
|
||||
);
|
||||
this->addChild(m_menu);
|
||||
infoBtn->setPosition(
|
||||
-m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width +
|
||||
btnPos,
|
||||
0.f
|
||||
);
|
||||
m_menu->addChild(infoBtn);
|
||||
|
||||
float btnPos = 15.f;
|
||||
|
||||
if (setting->getDescription()) {
|
||||
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
|
||||
infoSpr->setScale(.6f);
|
||||
|
||||
auto infoBtn = CCMenuItemSpriteExtra::create(
|
||||
infoSpr, this, menu_selector(GeodeSettingNode::onDescription)
|
||||
);
|
||||
infoBtn->setPosition(
|
||||
-m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width +
|
||||
btnPos,
|
||||
0.f
|
||||
);
|
||||
m_menu->addChild(infoBtn);
|
||||
|
||||
btnPos += 20.f;
|
||||
}
|
||||
|
||||
if (setting->canResetToDefault()) {
|
||||
auto resetBtnSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr);
|
||||
resetBtnSpr->setScale(.5f);
|
||||
|
||||
m_resetBtn = CCMenuItemSpriteExtra::create(
|
||||
resetBtnSpr, this, menu_selector(GeodeSettingNode::onReset)
|
||||
);
|
||||
m_resetBtn->setPosition(
|
||||
-m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width +
|
||||
btnPos,
|
||||
.0f
|
||||
);
|
||||
m_menu->addChild(m_resetBtn);
|
||||
}
|
||||
|
||||
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
|
||||
m_menu->registerWithTouchDispatcher();
|
||||
|
||||
if (!this->setup(setting, width)) return false;
|
||||
|
||||
this->valueChanged();
|
||||
|
||||
return true;
|
||||
btnPos += 20.f;
|
||||
}
|
||||
|
||||
void onDescription(CCObject*) {
|
||||
auto setting = std::static_pointer_cast<T>(m_setting);
|
||||
FLAlertLayer::create(
|
||||
setting->getDisplayName().c_str(), setting->getDescription().value(), "OK"
|
||||
)
|
||||
->show();
|
||||
}
|
||||
auto resetBtnSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr);
|
||||
resetBtnSpr->setScale(.5f);
|
||||
|
||||
void onReset(CCObject*) {
|
||||
auto setting = std::static_pointer_cast<T>(m_setting);
|
||||
createQuickPopup(
|
||||
"Reset",
|
||||
"Are you sure you want to <cr>reset</c> <cl>" + setting->getDisplayName() +
|
||||
"</c> to <cy>default</c>?",
|
||||
"Cancel", "Reset",
|
||||
[this](FLAlertLayer*, bool btn2) {
|
||||
if (btn2) this->resetToDefault();
|
||||
m_resetBtn = CCMenuItemSpriteExtra::create(
|
||||
resetBtnSpr, this, menu_selector(GeodeSettingNode::onReset)
|
||||
);
|
||||
m_resetBtn->setPosition(
|
||||
-m_obContentSize.width + sidePad + m_nameLabel->getScaledContentSize().width +
|
||||
btnPos,
|
||||
.0f
|
||||
);
|
||||
m_menu->addChild(m_resetBtn);
|
||||
|
||||
CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2);
|
||||
m_menu->registerWithTouchDispatcher();
|
||||
|
||||
if (!this->setup(setting, width)) return false;
|
||||
|
||||
this->valueChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onDescription(CCObject*) {
|
||||
FLAlertLayer::create(
|
||||
setting()->getDefinition().getDisplayName().c_str(),
|
||||
setting()->castDefinition().description.value(),
|
||||
"OK"
|
||||
)->show();
|
||||
}
|
||||
|
||||
void onReset(CCObject*) {
|
||||
createQuickPopup(
|
||||
"Reset",
|
||||
"Are you sure you want to <cr>reset</c> <cl>" +
|
||||
setting()->getDefinition().getDisplayName() +
|
||||
"</c> to <cy>default</c>?",
|
||||
"Cancel", "Reset",
|
||||
[this](auto, bool btn2) {
|
||||
if (btn2) {
|
||||
this->resetToDefault();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
virtual float setupHeight(GeodeSettingValue<T>* setting) const {
|
||||
return 40.f;
|
||||
}
|
||||
|
||||
virtual bool setup(GeodeSettingValue<T>* setting, float width) = 0;
|
||||
|
||||
virtual void valueChanged(bool updateText = true) {
|
||||
if (this->hasUncommittedChanges()) {
|
||||
m_nameLabel->setColor(cc3x(0x1d0));
|
||||
}
|
||||
|
||||
virtual float setupHeight(std::shared_ptr<T> setting) const {
|
||||
return 40.f;
|
||||
else {
|
||||
m_nameLabel->setColor(cc3x(0xfff));
|
||||
}
|
||||
|
||||
virtual bool setup(std::shared_ptr<T> setting, float width) = 0;
|
||||
|
||||
virtual void valueChanged(bool updateText = true) {
|
||||
if (m_delegate) m_delegate->settingValueChanged(this);
|
||||
if (this->hasUncommittedChanges()) {
|
||||
m_nameLabel->setColor(cc3x(0x1d0));
|
||||
}
|
||||
else {
|
||||
m_nameLabel->setColor(cc3x(0xfff));
|
||||
}
|
||||
if (m_resetBtn) m_resetBtn->setVisible(this->hasNonDefaultValue());
|
||||
auto isValid = std::static_pointer_cast<T>(m_setting)->isValidValue(m_uncommittedValue);
|
||||
if (!isValid) {
|
||||
m_errorLabel->setVisible(true);
|
||||
m_errorLabel->setString(isValid.unwrapErr().c_str());
|
||||
}
|
||||
else {
|
||||
m_errorLabel->setVisible(false);
|
||||
}
|
||||
if (m_resetBtn) m_resetBtn->setVisible(this->hasNonDefaultValue());
|
||||
auto isValid = setting()->validate(m_uncommittedValue);
|
||||
if (!isValid) {
|
||||
m_errorLabel->setVisible(true);
|
||||
m_errorLabel->setString(isValid.unwrapErr().c_str());
|
||||
}
|
||||
|
||||
public:
|
||||
static N* create(std::shared_ptr<T> setting, float width) {
|
||||
auto ret = new N();
|
||||
if (ret && ret->init(setting, width)) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
else {
|
||||
m_errorLabel->setVisible(false);
|
||||
}
|
||||
this->dispatchChanged();
|
||||
}
|
||||
|
||||
void commit() override {
|
||||
std::static_pointer_cast<T>(m_setting)->setValue(m_uncommittedValue);
|
||||
m_uncommittedValue = std::static_pointer_cast<T>(m_setting)->getValue();
|
||||
this->valueChanged();
|
||||
if (m_delegate) m_delegate->settingValueCommitted(this);
|
||||
}
|
||||
GeodeSettingValue<T>* setting() {
|
||||
return static_cast<GeodeSettingValue<T>*>(m_value);
|
||||
}
|
||||
|
||||
bool hasUncommittedChanges() override {
|
||||
return m_uncommittedValue != std::static_pointer_cast<T>(m_setting)->getValue();
|
||||
}
|
||||
public:
|
||||
void commit() override {
|
||||
setting()->setValue(m_uncommittedValue);
|
||||
m_uncommittedValue = setting()->getValue();
|
||||
this->valueChanged();
|
||||
this->dispatchCommitted();
|
||||
}
|
||||
|
||||
bool hasNonDefaultValue() override {
|
||||
return m_uncommittedValue != std::static_pointer_cast<T>(m_setting)->getDefault();
|
||||
}
|
||||
bool hasUncommittedChanges() override {
|
||||
return m_uncommittedValue != setting()->getValue();
|
||||
}
|
||||
|
||||
void resetToDefault() override {
|
||||
m_uncommittedValue = std::static_pointer_cast<T>(m_setting)->getDefault();
|
||||
this->valueChanged();
|
||||
}
|
||||
};
|
||||
bool hasNonDefaultValue() override {
|
||||
return m_uncommittedValue != setting()->castDefinition().defaultValue;
|
||||
}
|
||||
|
||||
template <class C, class T>
|
||||
class ImplInput : public TextInputDelegate {
|
||||
protected:
|
||||
InputNode* m_input = nullptr;
|
||||
CCLabelBMFont* m_label = nullptr;
|
||||
void resetToDefault() override {
|
||||
m_uncommittedValue = setting()->castDefinition().defaultValue;
|
||||
this->valueChanged();
|
||||
}
|
||||
};
|
||||
|
||||
C* self() {
|
||||
return static_cast<C*>(this);
|
||||
}
|
||||
|
||||
void setupInput(std::shared_ptr<T> setting, float width) {
|
||||
if (setting->hasInput()) {
|
||||
m_input = InputNode::create(width / 2 - 70.f, "Text", "chatFont.fnt");
|
||||
m_input->setPosition({ -(width / 2 - 70.f) / 2, setting->hasSlider() ? 5.f : 0.f });
|
||||
m_input->setScale(.65f);
|
||||
m_input->getInput()->setDelegate(this);
|
||||
self()->m_menu->addChild(m_input);
|
||||
}
|
||||
else {
|
||||
m_label = CCLabelBMFont::create("", "bigFont.fnt");
|
||||
m_label->setPosition({ -(width / 2 - 70.f) / 2, setting->hasSlider() ? 5.f : 0.f });
|
||||
m_label->limitLabelWidth(width / 2 - 70.f, .5f, .1f);
|
||||
self()->m_menu->addChild(m_label);
|
||||
}
|
||||
}
|
||||
|
||||
void updateLabel(bool updateText) {
|
||||
if (!updateText) return;
|
||||
if (m_input) {
|
||||
// hacky way to make setString not called textChanged
|
||||
m_input->getInput()->setDelegate(nullptr);
|
||||
m_input->setString(numToString(self()->m_uncommittedValue));
|
||||
m_input->getInput()->setDelegate(this);
|
||||
}
|
||||
else {
|
||||
m_label->setString(numToString(self()->m_uncommittedValue).c_str());
|
||||
m_label->limitLabelWidth(self()->m_width / 2 - 70.f, .5f, .1f);
|
||||
}
|
||||
}
|
||||
|
||||
void textChanged(CCTextInputNode* input) override {
|
||||
try {
|
||||
self()->m_uncommittedValue =
|
||||
parseNumForInput<typename C::value_t>(input->getString());
|
||||
self()->valueChanged(false);
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class C, class T>
|
||||
class ImplArrows {
|
||||
protected:
|
||||
CCMenuItemSpriteExtra* m_decArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_incArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_bigDecArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_bigIncArrow = nullptr;
|
||||
|
||||
C* self() {
|
||||
return static_cast<C*>(this);
|
||||
}
|
||||
|
||||
void setupArrows(std::shared_ptr<T> setting, float width) {
|
||||
auto yPos = setting->hasSlider() ? 5.f : 0.f;
|
||||
|
||||
if (setting->hasArrows()) {
|
||||
auto decArrowSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png");
|
||||
decArrowSpr->setFlipX(true);
|
||||
decArrowSpr->setScale(.3f);
|
||||
|
||||
m_decArrow = CCMenuItemSpriteExtra::create(
|
||||
decArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onDecrement)
|
||||
);
|
||||
m_decArrow->setPosition(-width / 2 + 80.f, yPos);
|
||||
self()->m_menu->addChild(m_decArrow);
|
||||
|
||||
auto incArrowSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png");
|
||||
incArrowSpr->setScale(.3f);
|
||||
|
||||
m_incArrow = CCMenuItemSpriteExtra::create(
|
||||
incArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onIncrement)
|
||||
);
|
||||
m_incArrow->setPosition(-10.f, yPos);
|
||||
self()->m_menu->addChild(m_incArrow);
|
||||
}
|
||||
|
||||
if (setting->hasBigArrows()) {
|
||||
auto decArrowSpr = CCSprite::createWithSpriteFrameName("double-nav.png"_spr);
|
||||
decArrowSpr->setFlipX(true);
|
||||
decArrowSpr->setScale(.3f);
|
||||
|
||||
m_bigDecArrow = CCMenuItemSpriteExtra::create(
|
||||
decArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onBigDecrement)
|
||||
);
|
||||
m_bigDecArrow->setPosition(-width / 2 + 65.f, yPos);
|
||||
self()->m_menu->addChild(m_bigDecArrow);
|
||||
|
||||
auto incArrowSpr = CCSprite::createWithSpriteFrameName("double-nav.png"_spr);
|
||||
incArrowSpr->setScale(.3f);
|
||||
|
||||
m_bigIncArrow = CCMenuItemSpriteExtra::create(
|
||||
incArrowSpr, self(), menu_selector(ImplArrows::Callbacks::onBigIncrement)
|
||||
);
|
||||
m_bigIncArrow->setPosition(5.f, yPos);
|
||||
self()->m_menu->addChild(m_bigIncArrow);
|
||||
}
|
||||
}
|
||||
|
||||
// intentionally a subclass to make it relatively safe to
|
||||
// use as callbacks for the child class, since we give the
|
||||
// child class as the target to CCMenuItemSpriteExtra
|
||||
struct Callbacks : public C {
|
||||
void onIncrement(CCObject*) {
|
||||
this->m_uncommittedValue +=
|
||||
std::static_pointer_cast<T>(this->m_setting)->getArrowStepSize();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void onDecrement(CCObject*) {
|
||||
this->m_uncommittedValue -=
|
||||
std::static_pointer_cast<T>(this->m_setting)->getArrowStepSize();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void onBigIncrement(CCObject*) {
|
||||
this->m_uncommittedValue +=
|
||||
std::static_pointer_cast<T>(this->m_setting)->getBigArrowStepSize();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
|
||||
void onBigDecrement(CCObject*) {
|
||||
this->m_uncommittedValue -=
|
||||
std::static_pointer_cast<T>(this->m_setting)->getBigArrowStepSize();
|
||||
this->valueChanged(true);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
template <class C, class T>
|
||||
class ImplSlider {
|
||||
protected:
|
||||
Slider* m_slider = nullptr;
|
||||
|
||||
C* self() {
|
||||
return static_cast<C*>(this);
|
||||
}
|
||||
|
||||
static float valueToSlider(std::shared_ptr<T> setting, typename T::value_t num) {
|
||||
auto min = setting->getMin() ? setting->getMin().value() : -100;
|
||||
auto max = setting->getMax() ? setting->getMax().value() : +100;
|
||||
auto range = max - min;
|
||||
return static_cast<float>(clamp(static_cast<double>(num - min) / range, 0.0, 1.0));
|
||||
}
|
||||
|
||||
static typename T::value_t valueFromSlider(std::shared_ptr<T> setting, float num) {
|
||||
auto min = setting->getMin() ? setting->getMin().value() : -100;
|
||||
auto max = setting->getMax() ? setting->getMax().value() : +100;
|
||||
auto range = max - min;
|
||||
auto value = static_cast<typename T::value_t>(num * range + min);
|
||||
if (auto step = setting->getSliderStepSize()) {
|
||||
value =
|
||||
static_cast<typename T::value_t>(round(value / step.value()) * step.value());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void setupSlider(std::shared_ptr<T> setting, float width) {
|
||||
if (setting->hasSlider()) {
|
||||
m_slider =
|
||||
Slider::create(self(), menu_selector(ImplSlider::Callbacks::onSlider), .5f);
|
||||
m_slider->setPosition(-50.f, -15.f);
|
||||
self()->m_menu->addChild(m_slider);
|
||||
|
||||
this->updateSlider();
|
||||
}
|
||||
}
|
||||
|
||||
void updateSlider() {
|
||||
if (m_slider) {
|
||||
auto setting = std::static_pointer_cast<T>(self()->m_setting);
|
||||
m_slider->setValue(valueToSlider(setting, self()->m_uncommittedValue));
|
||||
m_slider->updateBar();
|
||||
}
|
||||
}
|
||||
|
||||
// intentionally a subclass to make it relatively safe to
|
||||
// use as callbacks for the child class, since we give the
|
||||
// child class as the target to CCMenuItemSpriteExtra
|
||||
struct Callbacks : public C {
|
||||
void onSlider(CCObject* slider) {
|
||||
auto setting = std::static_pointer_cast<T>(this->m_setting);
|
||||
|
||||
this->m_uncommittedValue =
|
||||
valueFromSlider(setting, static_cast<SliderThumb*>(slider)->getValue());
|
||||
this->valueChanged(true);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class BoolSettingNode : public GeodeSettingNode<BoolSettingNode, BoolSetting> {
|
||||
class BoolSettingNode : public GeodeSettingNode<BoolSetting> {
|
||||
protected:
|
||||
CCMenuItemToggler* m_toggle;
|
||||
|
||||
void onToggle(CCObject*);
|
||||
void valueChanged(bool updateText) override;
|
||||
bool setup(std::shared_ptr<BoolSetting> setting, float width) override;
|
||||
bool setup(BoolSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
IMPL_SETT_CREATE(Bool);
|
||||
};
|
||||
|
||||
class IntSettingNode :
|
||||
public GeodeSettingNode<IntSettingNode, IntSetting>,
|
||||
public ImplArrows<IntSettingNode, IntSetting>,
|
||||
public ImplInput<IntSettingNode, IntSetting>,
|
||||
public ImplSlider<IntSettingNode, IntSetting> {
|
||||
public GeodeSettingNode<IntSetting>,
|
||||
public TextInputDelegate
|
||||
{
|
||||
protected:
|
||||
friend class ImplArrows<IntSettingNode, IntSetting>;
|
||||
friend class ImplInput<IntSettingNode, IntSetting>;
|
||||
friend class ImplSlider<IntSettingNode, IntSetting>;
|
||||
InputNode* m_input = nullptr;
|
||||
CCLabelBMFont* m_label = nullptr;
|
||||
Slider* m_slider = nullptr;
|
||||
CCMenuItemSpriteExtra* m_decArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_incArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_bigDecArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_bigIncArrow = nullptr;
|
||||
|
||||
void valueChanged(bool updateText) override;
|
||||
void textChanged(CCTextInputNode* input) override;
|
||||
|
||||
float setupHeight(std::shared_ptr<IntSetting> setting) const override;
|
||||
bool setup(std::shared_ptr<IntSetting> setting, float width) override;
|
||||
float setupHeight(IntSettingValue* setting) const override;
|
||||
bool setup(IntSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
void updateSlider();
|
||||
void updateLabel();
|
||||
|
||||
void onSlider(CCObject* slider);
|
||||
void onArrow(CCObject* sender);
|
||||
|
||||
IMPL_SETT_CREATE(Int);
|
||||
};
|
||||
|
||||
class FloatSettingNode :
|
||||
public GeodeSettingNode<FloatSettingNode, FloatSetting>,
|
||||
public ImplArrows<FloatSettingNode, FloatSetting>,
|
||||
public ImplInput<FloatSettingNode, FloatSetting>,
|
||||
public ImplSlider<FloatSettingNode, FloatSetting> {
|
||||
public GeodeSettingNode<FloatSetting>,
|
||||
public TextInputDelegate
|
||||
{
|
||||
protected:
|
||||
friend class ImplArrows<FloatSettingNode, FloatSetting>;
|
||||
friend class ImplInput<FloatSettingNode, FloatSetting>;
|
||||
friend class ImplSlider<FloatSettingNode, FloatSetting>;
|
||||
InputNode* m_input = nullptr;
|
||||
CCLabelBMFont* m_label = nullptr;
|
||||
Slider* m_slider = nullptr;
|
||||
CCMenuItemSpriteExtra* m_decArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_incArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_bigDecArrow = nullptr;
|
||||
CCMenuItemSpriteExtra* m_bigIncArrow = nullptr;
|
||||
|
||||
void valueChanged(bool updateText) override;
|
||||
void textChanged(CCTextInputNode* input) override;
|
||||
|
||||
float setupHeight(std::shared_ptr<FloatSetting> setting) const override;
|
||||
bool setup(std::shared_ptr<FloatSetting> setting, float width) override;
|
||||
float setupHeight(FloatSettingValue* setting) const override;
|
||||
bool setup(FloatSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
void updateSlider();
|
||||
void updateLabel();
|
||||
|
||||
void onSlider(CCObject* slider);
|
||||
void onArrow(CCObject* sender);
|
||||
|
||||
IMPL_SETT_CREATE(Float);
|
||||
};
|
||||
|
||||
class StringSettingNode :
|
||||
public GeodeSettingNode<StringSettingNode, StringSetting>,
|
||||
public TextInputDelegate {
|
||||
public GeodeSettingNode<StringSetting>,
|
||||
public TextInputDelegate
|
||||
{
|
||||
protected:
|
||||
InputNode* m_input;
|
||||
|
||||
|
@ -451,12 +272,16 @@ protected:
|
|||
void valueChanged(bool updateText) override;
|
||||
void updateLabel();
|
||||
|
||||
bool setup(std::shared_ptr<StringSetting> setting, float width) override;
|
||||
bool setup(StringSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
IMPL_SETT_CREATE(String);
|
||||
};
|
||||
|
||||
class FileSettingNode :
|
||||
public GeodeSettingNode<FileSettingNode, FileSetting>,
|
||||
public TextInputDelegate {
|
||||
public GeodeSettingNode<FileSetting>,
|
||||
public TextInputDelegate
|
||||
{
|
||||
protected:
|
||||
InputNode* m_input;
|
||||
|
||||
|
@ -466,11 +291,14 @@ protected:
|
|||
|
||||
void onPickFile(CCObject*);
|
||||
|
||||
bool setup(std::shared_ptr<FileSetting> setting, float width) override;
|
||||
bool setup(FileSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
IMPL_SETT_CREATE(File);
|
||||
};
|
||||
|
||||
class ColorSettingNode :
|
||||
public GeodeSettingNode<ColorSettingNode, ColorSetting>,
|
||||
public GeodeSettingNode<ColorSetting>,
|
||||
public ColorPickPopupDelegate {
|
||||
protected:
|
||||
ColorChannelSprite* m_colorSpr;
|
||||
|
@ -480,11 +308,14 @@ protected:
|
|||
|
||||
void onSelectColor(CCObject*);
|
||||
|
||||
bool setup(std::shared_ptr<ColorSetting> setting, float width) override;
|
||||
bool setup(ColorSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
IMPL_SETT_CREATE(Color);
|
||||
};
|
||||
|
||||
class ColorAlphaSettingNode :
|
||||
public GeodeSettingNode<ColorAlphaSettingNode, ColorAlphaSetting>,
|
||||
public GeodeSettingNode<ColorAlphaSetting>,
|
||||
public ColorPickPopupDelegate {
|
||||
protected:
|
||||
ColorChannelSprite* m_colorSpr;
|
||||
|
@ -494,5 +325,23 @@ protected:
|
|||
|
||||
void onSelectColor(CCObject*);
|
||||
|
||||
bool setup(std::shared_ptr<ColorAlphaSetting> setting, float width) override;
|
||||
bool setup(ColorAlphaSettingValue* setting, float width) override;
|
||||
|
||||
public:
|
||||
IMPL_SETT_CREATE(ColorAlpha);
|
||||
};
|
||||
|
||||
class CustomSettingPlaceholderNode : public SettingNode {
|
||||
protected:
|
||||
void commit() override;
|
||||
bool hasUncommittedChanges() override;
|
||||
bool hasNonDefaultValue() override;
|
||||
void resetToDefault() override;
|
||||
|
||||
bool init(std::string const& key, float width);
|
||||
|
||||
public:
|
||||
static CustomSettingPlaceholderNode* create(
|
||||
std::string const& key, float width
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <Geode/ui/ScrollLayer.hpp>
|
||||
#include <Geode/utils/cocos.hpp>
|
||||
#include <Geode/ui/General.hpp>
|
||||
#include "GeodeSettingNode.hpp"
|
||||
|
||||
bool ModSettingsPopup::setup(Mod* mod) {
|
||||
m_noElasticity = true;
|
||||
|
@ -31,8 +32,14 @@ bool ModSettingsPopup::setup(Mod* mod) {
|
|||
float totalHeight = .0f;
|
||||
std::vector<CCNode*> rendered;
|
||||
bool hasBG = true;
|
||||
for (auto& [_, sett] : mod->getSettings()) {
|
||||
auto node = sett->createNode(layerSize.width);
|
||||
for (auto& key : mod->getSettingKeys()) {
|
||||
SettingNode* node;
|
||||
if (auto sett = mod->getSetting(key)) {
|
||||
node = sett->createNode(layerSize.width);
|
||||
}
|
||||
else {
|
||||
node = CustomSettingPlaceholderNode::create(key, layerSize.width);
|
||||
}
|
||||
node->setDelegate(this);
|
||||
|
||||
totalHeight += node->getScaledContentSize().height;
|
||||
|
@ -144,6 +151,17 @@ void ModSettingsPopup::onResetAll(CCObject*) {
|
|||
);
|
||||
}
|
||||
|
||||
void ModSettingsPopup::settingValueCommitted(SettingNode*) {
|
||||
if (this->hasUncommitted()) {
|
||||
m_applyBtnSpr->setColor(cc3x(0xf));
|
||||
m_applyBtn->setEnabled(true);
|
||||
}
|
||||
else {
|
||||
m_applyBtnSpr->setColor(cc3x(0x4));
|
||||
m_applyBtn->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ModSettingsPopup::settingValueChanged(SettingNode*) {
|
||||
if (this->hasUncommitted()) {
|
||||
m_applyBtnSpr->setColor(cc3x(0xf));
|
||||
|
|
|
@ -13,7 +13,8 @@ protected:
|
|||
CCMenuItemSpriteExtra* m_applyBtn;
|
||||
ButtonSprite* m_applyBtnSpr;
|
||||
|
||||
void settingValueChanged(SettingNode* node) override;
|
||||
void settingValueChanged(SettingNode*) override;
|
||||
void settingValueCommitted(SettingNode*) override;
|
||||
|
||||
bool setup(Mod* mod) override;
|
||||
bool hasUncommitted() const;
|
||||
|
|
|
@ -19,6 +19,11 @@ bool JsonMaybeSomething<Json>::isError() const {
|
|||
return m_checker.isError() || !m_hasValue;
|
||||
}
|
||||
|
||||
template <class Json>
|
||||
std::string JsonMaybeSomething<Json>::getError() const {
|
||||
return m_checker.getError();
|
||||
}
|
||||
|
||||
template <class Json>
|
||||
JsonMaybeSomething<Json>::operator bool() const {
|
||||
return !isError();
|
||||
|
|
|
@ -11,6 +11,14 @@
|
|||
USE_GEODE_NAMESPACE();
|
||||
using namespace geode::utils::file;
|
||||
|
||||
void ghc::filesystem::to_json(nlohmann::json& json, path const& path) {
|
||||
json = path.string();
|
||||
}
|
||||
|
||||
void ghc::filesystem::from_json(nlohmann::json const& json, path& path) {
|
||||
path = json.get<std::string>();
|
||||
}
|
||||
|
||||
Result<std::string> utils::file::readString(ghc::filesystem::path const& path) {
|
||||
#if _WIN32
|
||||
std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);
|
||||
|
@ -45,14 +53,14 @@ Result<nlohmann::json> utils::file::readJson(ghc::filesystem::path const& path)
|
|||
return Err("Unable to open file");
|
||||
}
|
||||
|
||||
Result<byte_array> utils::file::readBinary(ghc::filesystem::path const& path) {
|
||||
Result<ByteVector> utils::file::readBinary(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) {
|
||||
return Ok(byte_array(std::istreambuf_iterator<char>(in), {}));
|
||||
return Ok(ByteVector(std::istreambuf_iterator<char>(in), {}));
|
||||
}
|
||||
return Err("Unable to open file");
|
||||
}
|
||||
|
@ -74,7 +82,7 @@ Result<> utils::file::writeString(ghc::filesystem::path const& path, std::string
|
|||
return Err("Unable to open file");
|
||||
}
|
||||
|
||||
Result<> utils::file::writeBinary(ghc::filesystem::path const& path, byte_array const& data) {
|
||||
Result<> utils::file::writeBinary(ghc::filesystem::path const& path, ByteVector const& data) {
|
||||
std::ofstream file;
|
||||
#if _WIN32
|
||||
file.open(path.wstring(), std::ios::out | std::ios::binary);
|
||||
|
@ -192,7 +200,7 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
Result<byte_array> extract(Path const& name) {
|
||||
Result<ByteVector> extract(Path const& name) {
|
||||
if (!m_entries.count(name)) {
|
||||
return Err("Entry not found");
|
||||
}
|
||||
|
@ -209,7 +217,7 @@ public:
|
|||
if (unzOpenCurrentFile(m_zip) != UNZ_OK) {
|
||||
return Err("Unable to open entry");
|
||||
}
|
||||
byte_array res;
|
||||
ByteVector res;
|
||||
res.resize(entry.uncompressedSize);
|
||||
auto size = unzReadCurrentFile(m_zip, res.data(), entry.uncompressedSize);
|
||||
if (size < 0 || size != entry.uncompressedSize) {
|
||||
|
@ -271,7 +279,7 @@ bool Unzip::hasEntry(Path const& name) {
|
|||
return m_impl->entries().count(name);
|
||||
}
|
||||
|
||||
Result<byte_array> Unzip::extract(Path const& name) {
|
||||
Result<ByteVector> Unzip::extract(Path const& name) {
|
||||
return m_impl->extract(name);
|
||||
}
|
||||
|
||||
|
@ -356,7 +364,7 @@ public:
|
|||
return Ok();
|
||||
}
|
||||
|
||||
Result<> add(Path const& path, byte_array const& data) {
|
||||
Result<> add(Path const& path, ByteVector const& data) {
|
||||
// open entry
|
||||
zip_fileinfo info = { 0 };
|
||||
if (zipOpenNewFileInZip(
|
||||
|
@ -410,12 +418,12 @@ Zip::Path Zip::getPath() const {
|
|||
return m_impl->path();
|
||||
}
|
||||
|
||||
Result<> Zip::add(Path const& path, byte_array const& data) {
|
||||
Result<> Zip::add(Path const& path, ByteVector const& data) {
|
||||
return m_impl->add(path, data);
|
||||
}
|
||||
|
||||
Result<> Zip::add(Path const& path, std::string const& data) {
|
||||
return this->add(path, byte_array(data.begin(), data.end()));
|
||||
return this->add(path, ByteVector(data.begin(), data.end()));
|
||||
}
|
||||
|
||||
Result<> Zip::addFrom(Path const& file, Path const& entryDir) {
|
||||
|
|
|
@ -9,7 +9,7 @@ using namespace web;
|
|||
|
||||
namespace geode::utils::fetch {
|
||||
static size_t writeBytes(char* data, size_t size, size_t nmemb, void* str) {
|
||||
as<byte_array*>(str)->insert(as<byte_array*>(str)->end(), data, data + size * nmemb);
|
||||
as<ByteVector*>(str)->insert(as<ByteVector*>(str)->end(), data, data + size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
|
@ -69,12 +69,12 @@ Result<> web::fetchFile(
|
|||
return Err("Error getting info: " + std::string(curl_easy_strerror(res)));
|
||||
}
|
||||
|
||||
Result<byte_array> web::fetchBytes(std::string const& url) {
|
||||
Result<ByteVector> web::fetchBytes(std::string const& url) {
|
||||
auto curl = curl_easy_init();
|
||||
|
||||
if (!curl) return Err("Curl not initialized!");
|
||||
|
||||
byte_array ret;
|
||||
ByteVector ret;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
|
@ -204,7 +204,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
|
|||
}
|
||||
|
||||
// resulting byte array
|
||||
byte_array ret;
|
||||
ByteVector ret;
|
||||
// output file if downloading to file. unique_ptr because not always
|
||||
// initialized but don't wanna manually managed memory
|
||||
std::unique_ptr<std::ofstream> file = nullptr;
|
||||
|
@ -461,32 +461,32 @@ AsyncWebRequest::~AsyncWebRequest() {
|
|||
|
||||
AsyncWebResult<std::monostate> AsyncWebResponse::into(std::ostream& stream) {
|
||||
m_request.m_target = &stream;
|
||||
return this->as(+[](byte_array const&) -> Result<std::monostate> {
|
||||
return this->as(+[](ByteVector const&) -> Result<std::monostate> {
|
||||
return Ok(std::monostate());
|
||||
});
|
||||
}
|
||||
|
||||
AsyncWebResult<std::monostate> AsyncWebResponse::into(ghc::filesystem::path const& path) {
|
||||
m_request.m_target = path;
|
||||
return this->as(+[](byte_array const&) -> Result<std::monostate> {
|
||||
return this->as(+[](ByteVector const&) -> Result<std::monostate> {
|
||||
return Ok(std::monostate());
|
||||
});
|
||||
}
|
||||
|
||||
AsyncWebResult<std::string> AsyncWebResponse::text() {
|
||||
return this->as(+[](byte_array const& bytes) -> Result<std::string> {
|
||||
return this->as(+[](ByteVector const& bytes) -> Result<std::string> {
|
||||
return Ok(std::string(bytes.begin(), bytes.end()));
|
||||
});
|
||||
}
|
||||
|
||||
AsyncWebResult<byte_array> AsyncWebResponse::bytes() {
|
||||
return this->as(+[](byte_array const& bytes) -> Result<byte_array> {
|
||||
AsyncWebResult<ByteVector> AsyncWebResponse::bytes() {
|
||||
return this->as(+[](ByteVector const& bytes) -> Result<ByteVector> {
|
||||
return Ok(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
AsyncWebResult<nlohmann::json> AsyncWebResponse::json() {
|
||||
return this->as(+[](byte_array const& bytes) -> Result<nlohmann::json> {
|
||||
return this->as(+[](ByteVector const& bytes) -> Result<nlohmann::json> {
|
||||
try {
|
||||
return Ok(nlohmann::json::parse(bytes.begin(), bytes.end()));
|
||||
}
|
||||
|
|
|
@ -3,6 +3,134 @@
|
|||
USE_GEODE_NAMESPACE();
|
||||
|
||||
#include <Geode/modify/MenuLayer.hpp>
|
||||
#include <Geode/loader/SettingNode.hpp>
|
||||
|
||||
enum class Icon {
|
||||
Steve,
|
||||
Mike,
|
||||
LazarithTheDestroyerOfForsakenSouls,
|
||||
Geoff,
|
||||
};
|
||||
constexpr Icon DEFAULT_ICON = Icon::Steve;
|
||||
|
||||
class MySettingValue;
|
||||
|
||||
class MySettingValue : public SettingValue {
|
||||
protected:
|
||||
Icon m_icon;
|
||||
|
||||
public:
|
||||
MySettingValue(std::string const& key, Icon icon)
|
||||
: SettingValue(key), m_icon(icon) {}
|
||||
|
||||
bool load(nlohmann::json const& json) override {
|
||||
try {
|
||||
m_icon = static_cast<Icon>(json.get<int>());
|
||||
return true;
|
||||
} catch(...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool save(nlohmann::json& json) const override {
|
||||
json = static_cast<int>(m_icon);
|
||||
return true;
|
||||
}
|
||||
SettingNode* createNode(float width) override;
|
||||
|
||||
void setIcon(Icon icon) {
|
||||
m_icon = icon;
|
||||
}
|
||||
Icon getIcon() const {
|
||||
return m_icon;
|
||||
}
|
||||
};
|
||||
|
||||
class MySettingNode : public SettingNode {
|
||||
protected:
|
||||
Icon m_currentIcon;
|
||||
std::vector<CCSprite*> m_sprites;
|
||||
|
||||
bool init(MySettingValue* value, float width) {
|
||||
if (!SettingNode::init(value))
|
||||
return false;
|
||||
|
||||
m_currentIcon = value->getIcon();
|
||||
this->setContentSize({ width, 40.f });
|
||||
|
||||
auto menu = CCMenu::create();
|
||||
menu->setPosition(width / 2, 20.f);
|
||||
|
||||
float x = -75.f;
|
||||
|
||||
for (auto& [spr, icon] : {
|
||||
std::pair { "player_01_001.png", Icon::Steve, },
|
||||
std::pair { "player_02_001.png", Icon::Mike, },
|
||||
std::pair { "player_03_001.png", Icon::LazarithTheDestroyerOfForsakenSouls, },
|
||||
std::pair { "player_04_001.png", Icon::Geoff, },
|
||||
}) {
|
||||
auto btnSpr = CCSprite::createWithSpriteFrameName(spr);
|
||||
btnSpr->setScale(.7f);
|
||||
m_sprites.push_back(btnSpr);
|
||||
if (icon == m_currentIcon) {
|
||||
btnSpr->setColor({ 0, 255, 0 });
|
||||
} else {
|
||||
btnSpr->setColor({ 200, 200, 200 });
|
||||
}
|
||||
auto btn = CCMenuItemSpriteExtra::create(
|
||||
btnSpr, this, menu_selector(MySettingNode::onSelect)
|
||||
);
|
||||
btn->setTag(static_cast<int>(icon));
|
||||
btn->setPosition(x, 0);
|
||||
menu->addChild(btn);
|
||||
|
||||
x += 50.f;
|
||||
}
|
||||
|
||||
this->addChild(menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onSelect(CCObject* sender) {
|
||||
for (auto& spr : m_sprites) {
|
||||
spr->setColor({ 200, 200, 200 });
|
||||
}
|
||||
m_currentIcon = static_cast<Icon>(sender->getTag());
|
||||
static_cast<CCSprite*>(
|
||||
static_cast<CCMenuItemSpriteExtra*>(sender)->getNormalImage()
|
||||
)->setColor({ 0, 255, 0 });
|
||||
this->dispatchChanged();
|
||||
}
|
||||
|
||||
public:
|
||||
void commit() override {
|
||||
static_cast<MySettingValue*>(m_value)->setIcon(m_currentIcon);
|
||||
this->dispatchCommitted();
|
||||
}
|
||||
bool hasUncommittedChanges() override {
|
||||
return m_currentIcon != static_cast<MySettingValue*>(m_value)->getIcon();
|
||||
}
|
||||
bool hasNonDefaultValue() override {
|
||||
return m_currentIcon != DEFAULT_ICON;
|
||||
}
|
||||
void resetToDefault() override {
|
||||
m_currentIcon = DEFAULT_ICON;
|
||||
}
|
||||
|
||||
static MySettingNode* create(MySettingValue* value, float width) {
|
||||
auto ret = new MySettingNode;
|
||||
if (ret && ret->init(value, width)) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
SettingNode* MySettingValue::createNode(float width) {
|
||||
return MySettingNode::create(this, width);
|
||||
}
|
||||
|
||||
struct MyMenuLayer : Modify<MyMenuLayer, MenuLayer> {
|
||||
void onMoreGames(CCObject*) {
|
||||
|
@ -10,13 +138,21 @@ struct MyMenuLayer : Modify<MyMenuLayer, MenuLayer> {
|
|||
FLAlertLayer::create("Damn", ":(", "OK")->show();
|
||||
}
|
||||
else {
|
||||
FLAlertLayer::create("Yay", "The weather report said it wouldn't rain today :)", "OK")
|
||||
->show();
|
||||
FLAlertLayer::create(
|
||||
"Yay",
|
||||
"The weather report said it wouldn't rain today :)",
|
||||
"OK"
|
||||
)->show();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
GEODE_API bool GEODE_CALL geode_load(Mod*) {
|
||||
$on_mod(Loaded) {
|
||||
Mod::get()->registerCustomSetting(
|
||||
"overcast-skies",
|
||||
std::make_unique<MySettingValue>("overcast-skies", DEFAULT_ICON)
|
||||
);
|
||||
|
||||
// Dispatcher::get()->addFunction<void(GJGarageLayer*)>("test-garage-open", [](GJGarageLayer*
|
||||
// gl) { auto label = CCLabelBMFont::create("Dispatcher works!", "bigFont.fnt");
|
||||
// label->setPosition(100, 80);
|
||||
|
@ -24,5 +160,4 @@ GEODE_API bool GEODE_CALL geode_load(Mod*) {
|
|||
// label->setZOrder(99999);
|
||||
// gl->addChild(label);
|
||||
// });
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overcast-skies": {
|
||||
"type": "custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"dependencies": [
|
||||
{
|
||||
"id": "geode.testdep",
|
||||
"version": "1.0.*",
|
||||
"version": ">=1.0.0",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue