Merge branch 'main' into tulip-hook

This commit is contained in:
altalk23 2022-12-14 15:38:38 +03:00
commit bcfe6a6914
56 changed files with 2567 additions and 1971 deletions

View file

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

View file

@ -1 +1 @@
0.6.1
1.0.0-alpha

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -105,6 +105,7 @@ namespace geode {
);
GEODE_DLL bool isError() const;
GEODE_DLL std::string getError() const;
GEODE_DLL operator bool() const;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
#include "../loader/LoaderImpl.hpp"
#include <loader/LoaderImpl.hpp>
USE_GEODE_NAMESPACE();

View file

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

View file

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

View file

@ -29,4 +29,4 @@ void Event::postFrom(Mod* m) {
std::unordered_set<EventListenerProtocol*>& Event::listeners() {
static std::unordered_set<EventListenerProtocol*> listeners;
return listeners;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -76,6 +76,9 @@
}
]
}
},
"overcast-skies": {
"type": "custom"
}
}
}

View file

@ -8,7 +8,7 @@
"dependencies": [
{
"id": "geode.testdep",
"version": "1.0.*",
"version": ">=1.0.0",
"required": true
}
]