mirror of
https://github.com/geode-sdk/geode.git
synced 2024-11-27 01:45:35 -05:00
505 lines
18 KiB
C++
505 lines
18 KiB
C++
#pragma once
|
||
|
||
#include "../DefaultInclude.hpp"
|
||
#include "../cocos/support/zip_support/ZipUtils.h"
|
||
#include "../utils/Result.hpp"
|
||
#include "../utils/VersionInfo.hpp"
|
||
#include "../utils/general.hpp"
|
||
|
||
#include "Loader.hpp" // very nice circular dependency fix
|
||
#include "Hook.hpp"
|
||
#include "ModMetadata.hpp"
|
||
#include "Setting.hpp"
|
||
#include "Types.hpp"
|
||
#include "Loader.hpp"
|
||
|
||
#include <matjson.hpp>
|
||
#include <matjson/stl_serialize.hpp>
|
||
#include <optional>
|
||
#include <string_view>
|
||
#include <tulip/TulipHook.hpp>
|
||
#include <type_traits>
|
||
#include <unordered_map>
|
||
#include <vector>
|
||
|
||
namespace geode {
|
||
template <class T>
|
||
struct HandleToSaved : public T {
|
||
Mod* m_mod;
|
||
std::string m_key;
|
||
|
||
HandleToSaved(std::string const& key, Mod* mod, T const& value) :
|
||
T(value), m_key(key), m_mod(mod) {}
|
||
|
||
HandleToSaved(HandleToSaved const&) = delete;
|
||
HandleToSaved(HandleToSaved&&) = delete;
|
||
~HandleToSaved();
|
||
};
|
||
|
||
enum class ModRequestedAction {
|
||
None,
|
||
Enable,
|
||
Disable,
|
||
Uninstall,
|
||
UninstallWithSaveData
|
||
};
|
||
|
||
static constexpr bool modRequestedActionIsToggle(ModRequestedAction action) {
|
||
return action == ModRequestedAction::Enable || action == ModRequestedAction::Disable;
|
||
}
|
||
static constexpr bool modRequestedActionIsUninstall(ModRequestedAction action) {
|
||
return action == ModRequestedAction::Uninstall || action == ModRequestedAction::UninstallWithSaveData;
|
||
}
|
||
|
||
template <class T>
|
||
static consteval bool typeImplementsIsJSON() {
|
||
using namespace matjson;
|
||
if constexpr (requires(const Value& json) { Serialize<std::decay_t<T>>::is_json(json); })
|
||
return true;
|
||
if constexpr (std::is_same_v<T, Value>) return true;
|
||
if constexpr (std::is_same_v<T, Array>) return true;
|
||
if constexpr (std::is_same_v<T, Object>) return true;
|
||
if constexpr (std::is_constructible_v<std::string, T>) return true;
|
||
if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) return true;
|
||
if constexpr (std::is_same_v<T, bool>) return true;
|
||
if constexpr (std::is_same_v<T, std::nullptr_t>) return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
GEODE_HIDDEN Mod* takeNextLoaderMod();
|
||
|
||
class ModImpl;
|
||
|
||
/**
|
||
* Represents a Mod ingame.
|
||
* @class Mod
|
||
*/
|
||
class GEODE_DLL Mod final {
|
||
protected:
|
||
class Impl;
|
||
std::unique_ptr<Impl> m_impl;
|
||
|
||
friend class Loader;
|
||
|
||
template <class = void>
|
||
static inline GEODE_HIDDEN Mod* sharedMod = nullptr;
|
||
|
||
// used internally in geode_implicit_load
|
||
template <class = void>
|
||
static inline GEODE_HIDDEN void setSharedMod(Mod* mod) {
|
||
sharedMod<> = mod;
|
||
}
|
||
|
||
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(ModMetadata const& metadata);
|
||
~Mod();
|
||
|
||
std::string getID() const;
|
||
std::string getName() const;
|
||
[[deprecated("Use Mod::getDevelopers instead")]]
|
||
std::string getDeveloper() const;
|
||
std::vector<std::string> getDevelopers() const;
|
||
std::optional<std::string> getDescription() const;
|
||
std::optional<std::string> getDetails() const;
|
||
std::filesystem::path getPackagePath() const;
|
||
VersionInfo getVersion() const;
|
||
bool isEnabled() const;
|
||
bool isOrWillBeEnabled() const;
|
||
bool isInternal() const;
|
||
bool needsEarlyLoad() const;
|
||
ModMetadata getMetadata() const;
|
||
std::filesystem::path getTempDir() const;
|
||
/**
|
||
* Get the path to the mod's platform binary (.dll on Windows, .dylib
|
||
* on Mac & iOS, .so on Android)
|
||
*/
|
||
std::filesystem::path getBinaryPath() const;
|
||
/**
|
||
* Get the path to the mod's runtime resources directory (contains all
|
||
* of its resources)
|
||
*/
|
||
std::filesystem::path getResourcesDir() const;
|
||
|
||
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
|
||
void setMetadata(ModMetadata const& metadata);
|
||
std::vector<Mod*> getDependants() const;
|
||
#endif
|
||
|
||
/**
|
||
* Check if this Mod has updates available on the mods index. If
|
||
* you're using this for automatic update checking, use
|
||
* `openInfoPopup` or `openIndexPopup` from the `ui/GeodeUI.hpp`
|
||
* header to open the Mod's page to let the user install the update
|
||
* @returns The latest available version on the index if there are
|
||
* updates for this mod
|
||
*/
|
||
[[deprecated("Use Mod::checkUpdates instead; this function always returns nullopt")]]
|
||
std::optional<VersionInfo> hasAvailableUpdate() const;
|
||
|
||
using CheckUpdatesTask = Task<Result<std::optional<VersionInfo>, std::string>>;
|
||
/**
|
||
* Check if this Mod has updates available on the mods index. If
|
||
* you're using this for automatic update checking, use
|
||
* `openInfoPopup` from the `ui/GeodeUI.hpp` header to open the Mod's
|
||
* page to let the user install the update
|
||
* @returns A task that resolves to an option, either the latest
|
||
* available version on the index if there are updates available, or
|
||
* `std::nullopt` if there are no updates. On error, the Task returns
|
||
* an error
|
||
*/
|
||
CheckUpdatesTask checkUpdates() const;
|
||
|
||
Result<> saveData();
|
||
Result<> loadData();
|
||
|
||
/**
|
||
* Get the mod's save directory path
|
||
*/
|
||
std::filesystem::path getSaveDir() const;
|
||
/**
|
||
* Get the mod's config directory path
|
||
*/
|
||
std::filesystem::path getConfigDir(bool create = true) const;
|
||
|
||
bool hasSettings() const;
|
||
std::vector<std::string> getSettingKeys() const;
|
||
bool hasSetting(std::string_view const key) const;
|
||
std::optional<Setting> getSettingDefinition(std::string_view const key) const;
|
||
SettingValue* getSetting(std::string_view const key) const;
|
||
|
||
/**
|
||
* Register a custom setting's value class. See Mod::addCustomSetting
|
||
* for a convenience wrapper that creates the value in-place to avoid
|
||
* code duplication. Also see
|
||
* [the tutorial page](https://docs.geode-sdk.org/mods/settings) for
|
||
* more information about custom settings
|
||
* @param key The setting's key
|
||
* @param value The SettingValue class that shall handle this setting
|
||
* @see addCustomSetting
|
||
*/
|
||
void registerCustomSetting(std::string_view const key, std::unique_ptr<SettingValue> value);
|
||
/**
|
||
* Register a custom setting's value class. The new SettingValue class
|
||
* will be created in-place using `std::make_unique`. See
|
||
* [the tutorial page](https://docs.geode-sdk.org/mods/settings) for
|
||
* more information about custom settings
|
||
* @param key The setting's key
|
||
* @param value The value of the custom setting
|
||
* @example
|
||
* $on_mod(Loaded) {
|
||
* Mod::get()->addCustomSetting<MySettingValue>("setting-key", DEFAULT_VALUE);
|
||
* }
|
||
*/
|
||
template <class T, class V>
|
||
void addCustomSetting(std::string_view const key, V const& value) {
|
||
this->registerCustomSetting(key, std::make_unique<T>(std::string(key), this->getID(), value));
|
||
}
|
||
|
||
/**
|
||
* Returns a prefixed launch argument name. See `Mod::getLaunchArgument`
|
||
* for details about mod-specific launch arguments.
|
||
*/
|
||
std::string getLaunchArgumentName(std::string_view const name) const;
|
||
/**
|
||
* Returns the names of the available mod-specific launch arguments.
|
||
*/
|
||
std::vector<std::string> getLaunchArgumentNames() const;
|
||
/**
|
||
* Equivalent to a prefixed `Loader::hasLaunchArgument` call. See `Mod::getLaunchArgument`
|
||
* for details about mod-specific launch arguments.
|
||
* @param name The argument name
|
||
*/
|
||
bool hasLaunchArgument(std::string_view const name) const;
|
||
/**
|
||
* Get a mod-specific launch argument. This is equivalent to `Loader::getLaunchArgument`
|
||
* with the argument name prefixed by the mod ID. For example, a launch argument named
|
||
* `mod-arg` for the mod `author.test` would be specified with `--geode:author.test.mod-arg=value`.
|
||
* @param name The argument name
|
||
* @return The value, if present
|
||
*/
|
||
std::optional<std::string> getLaunchArgument(std::string_view const name) const;
|
||
/**
|
||
* Equivalent to a prefixed `Loader::getLaunchFlag` call. See `Mod::getLaunchArgument`
|
||
* for details about mod-specific launch arguments.
|
||
* @param name The argument name
|
||
*/
|
||
bool getLaunchFlag(std::string_view const name) const;
|
||
/**
|
||
* Equivalent to a prefixed `Loader::parseLaunchArgument` call. See `Mod::getLaunchArgument`
|
||
* for details about mod-specific launch arguments.
|
||
* @param name The argument name
|
||
*/
|
||
template <class T>
|
||
std::optional<T> parseLaunchArgument(std::string_view const name) const {
|
||
return Loader::get()->parseLaunchArgument<T>(this->getLaunchArgumentName(name));
|
||
}
|
||
|
||
matjson::Value& getSaveContainer();
|
||
matjson::Value& getSavedSettingsData();
|
||
|
||
template <class T>
|
||
T getSettingValue(std::string_view const key) const {
|
||
if (auto sett = this->getSetting(key)) {
|
||
return SettingValueSetter<T>::get(sett);
|
||
}
|
||
return T();
|
||
}
|
||
|
||
template <class T>
|
||
T setSettingValue(std::string_view const key, T const& value) {
|
||
if (auto sett = this->getSetting(key)) {
|
||
auto old = this->getSettingValue<T>(key);
|
||
SettingValueSetter<T>::set(sett, value);
|
||
return old;
|
||
}
|
||
return T();
|
||
}
|
||
|
||
bool hasSavedValue(std::string_view const key);
|
||
|
||
template <class T>
|
||
T getSavedValue(std::string_view const key) {
|
||
static_assert(geode::typeImplementsIsJSON<T>(), "T must implement is_json in matjson::Serialize<T>, otherwise this always returns default value.");
|
||
auto& saved = this->getSaveContainer();
|
||
if (saved.contains(key)) {
|
||
if (auto value = saved.try_get<T>(key)) {
|
||
return *value;
|
||
}
|
||
}
|
||
return T();
|
||
}
|
||
|
||
template <class T>
|
||
T getSavedValue(std::string_view const key, T const& defaultValue) {
|
||
static_assert(geode::typeImplementsIsJSON<T>(), "T must implement is_json in matjson::Serialize<T>, otherwise this always returns default value.");
|
||
auto& saved = this->getSaveContainer();
|
||
if (saved.contains(key)) {
|
||
if (auto value = saved.try_get<T>(key)) {
|
||
return *value;
|
||
}
|
||
}
|
||
saved[key] = defaultValue;
|
||
return defaultValue;
|
||
}
|
||
|
||
/**
|
||
* Set the value of an automatically saved variable. When the game is
|
||
* closed, the value is automatically saved under the key
|
||
* @param key Key of the saved value
|
||
* @param value Value
|
||
* @returns The old value
|
||
*/
|
||
template <class T>
|
||
T setSavedValue(std::string_view const key, T const& value) {
|
||
auto& saved = this->getSaveContainer();
|
||
auto old = this->getSavedValue<T>(key);
|
||
saved[key] = value;
|
||
return old;
|
||
}
|
||
|
||
/**
|
||
* Get the Mod of the current mod being developed
|
||
* @returns The current mod
|
||
*/
|
||
template <class = void>
|
||
static inline GEODE_HIDDEN Mod* get() {
|
||
if (!sharedMod<>) {
|
||
sharedMod<> = takeNextLoaderMod();
|
||
}
|
||
return sharedMod<>;
|
||
}
|
||
|
||
/**
|
||
* Create a hook at an address. Call the original
|
||
* function by calling the original function –
|
||
* no trampoline needed
|
||
* @param address The absolute address of
|
||
* the function to hook, i.e. gd_base + 0xXXXX
|
||
* @param detour Pointer to your detour function
|
||
* @param displayName Name of the hook that will be
|
||
* displayed in the hook list
|
||
* @param convention Calling convention of the hook
|
||
* @param hookMetadata Metadata of the hook
|
||
* @returns Successful result containing the
|
||
* Hook pointer, errorful result with info on
|
||
* error
|
||
*/
|
||
template<class DetourType>
|
||
Result<Hook*> hook(
|
||
void* address, DetourType detour, std::string const& displayName = "",
|
||
tulip::hook::TulipConvention convention = tulip::hook::TulipConvention::Default,
|
||
tulip::hook::HookMetadata const& hookMetadata = tulip::hook::HookMetadata()
|
||
) {
|
||
auto hook = Hook::create(address, detour, displayName, convention, hookMetadata);
|
||
GEODE_UNWRAP_INTO(auto ptr, this->claimHook(std::move(hook)));
|
||
return Ok(ptr);
|
||
}
|
||
|
||
Result<Hook*> hook(
|
||
void* address, void* detour, std::string const& displayName,
|
||
tulip::hook::HandlerMetadata const& handlerMetadata,
|
||
tulip::hook::HookMetadata const& hookMetadata
|
||
) {
|
||
auto hook = Hook::create(address, detour, displayName, handlerMetadata, hookMetadata);
|
||
GEODE_UNWRAP_INTO(auto ptr, this->claimHook(std::move(hook)));
|
||
return Ok(ptr);
|
||
}
|
||
|
||
/**
|
||
* Claims an existing hook object, marking this mod as its owner.
|
||
* If the hook has "auto enable" set, this will enable the hook.
|
||
* @returns Returns a pointer to the hook, or an error if the
|
||
* hook already has an owner, or was unable to enable the hook.
|
||
*/
|
||
Result<Hook*> claimHook(std::shared_ptr<Hook> hook);
|
||
|
||
/**
|
||
* Disowns a hook which this mod owns, making this mod no longer its owner.
|
||
* If the hook has "auto enable" set, this will disable the hook.
|
||
* @returns Returns an error if this mod doesn't own the hook, or
|
||
* if disabling the hook failed.
|
||
*/
|
||
Result<> disownHook(Hook* hook);
|
||
|
||
/**
|
||
* Get all hooks owned by this Mod
|
||
* @returns Vector of hooks
|
||
*/
|
||
[[nodiscard]] std::vector<Hook*> getHooks() const;
|
||
|
||
/**
|
||
* Write a patch at an address
|
||
* @param address The address to write into
|
||
* @param data The data to write there
|
||
* @returns Successful result on success,
|
||
* errorful result with info on error
|
||
*/
|
||
Result<Patch*> patch(void* address, ByteVector const& data) {
|
||
auto patch = Patch::create(address, data);
|
||
GEODE_UNWRAP_INTO(auto ptr, this->claimPatch(std::move(patch)));
|
||
return Ok(ptr);
|
||
}
|
||
|
||
/**
|
||
* Claims an existing patch object, marking this mod as its owner.
|
||
* If the patch has "auto enable" set, this will enable the patch.
|
||
* @returns Returns a pointer to the patch, or an error if the
|
||
* patch already has an owner, or was unable to enable the patch.
|
||
*/
|
||
Result<Patch*> claimPatch(std::shared_ptr<Patch> patch);
|
||
|
||
/**
|
||
* Disowns a patch which this mod owns, making this mod no longer its owner.
|
||
* If the patch has "auto enable" set, this will disable the patch.
|
||
* @returns Returns an error if this mod doesn't own the patch, or
|
||
* if disabling the patch failed.
|
||
*/
|
||
Result<> disownPatch(Patch* patch);
|
||
|
||
/**
|
||
* Get all patches owned by this Mod
|
||
* @returns Vector of patches
|
||
*/
|
||
[[nodiscard]] std::vector<Patch*> getPatches() const;
|
||
|
||
/**
|
||
* Enable this mod
|
||
* @returns Successful result on success,
|
||
* errorful result with info on error
|
||
*/
|
||
Result<> enable();
|
||
|
||
/**
|
||
* Disable this mod
|
||
* @returns Successful result on success,
|
||
* errorful result with info on error
|
||
*/
|
||
Result<> disable();
|
||
|
||
/**
|
||
* Delete the mod's .geode package.
|
||
* @param deleteSaveData Whether should also delete the mod's save data
|
||
* @returns Successful result on success,
|
||
* errorful result with info on error
|
||
*/
|
||
Result<> uninstall(bool deleteSaveData = false);
|
||
bool isUninstalled() const;
|
||
|
||
ModRequestedAction getRequestedAction() const;
|
||
|
||
/**
|
||
* Check whether or not this Mod
|
||
* depends on another mod
|
||
*/
|
||
bool depends(std::string_view const id) const;
|
||
|
||
/**
|
||
* Check whether all the required
|
||
* dependencies for this mod have
|
||
* been loaded or not
|
||
* @returns True if the mod has unresolved
|
||
* dependencies, false if not.
|
||
*/
|
||
bool hasUnresolvedDependencies() const;
|
||
/**
|
||
* Check whether none of the
|
||
* incompatibilities with this mod are loaded
|
||
* @returns True if the mod has unresolved
|
||
* incompatibilities, false if not.
|
||
*/
|
||
bool hasUnresolvedIncompatibilities() const;
|
||
|
||
std::string_view expandSpriteName(std::string_view name);
|
||
|
||
/**
|
||
* Get info about the mod as JSON
|
||
* @note For IPC
|
||
*/
|
||
ModJson getRuntimeInfo() const;
|
||
|
||
bool isLoggingEnabled() const;
|
||
void setLoggingEnabled(bool enabled);
|
||
|
||
bool hasProblems() const;
|
||
std::vector<LoadProblem> getAllProblems() const;
|
||
std::vector<LoadProblem> getProblems() const;
|
||
std::vector<LoadProblem> getRecommendations() const;
|
||
bool shouldLoad() const;
|
||
bool isCurrentlyLoading() const;
|
||
|
||
friend class ModImpl;
|
||
};
|
||
}
|
||
|
||
namespace geode::geode_internal {
|
||
// this impl relies on the GEODE_MOD_ID macro set by cmake
|
||
template <size_t N>
|
||
struct StringConcatModIDSlash {
|
||
static constexpr size_t extra = sizeof(GEODE_MOD_ID);
|
||
char buffer[extra + N]{};
|
||
constexpr StringConcatModIDSlash(const char (&pp)[N]) {
|
||
char id[] = GEODE_MOD_ID;
|
||
for (int i = 0; i < sizeof(id); ++i) {
|
||
buffer[i] = id[i];
|
||
}
|
||
buffer[extra - 1] = '/';
|
||
for (int i = 0; i < N; ++i) {
|
||
buffer[extra + i] = pp[i];
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
template <geode::geode_internal::StringConcatModIDSlash Str>
|
||
constexpr auto operator""_spr() {
|
||
return Str.buffer;
|
||
}
|