- rewrote Loader to have a public loading API
 - moved logging away from Loader
 - moved texture path handling away from Loader into CCFileUtils, added new functions there for that
 - bumped version to v0.7.0
 - moved ModInfo to its own header
 - added early loading support through mod.json instead of loader stuff
 - wrote a custom Unzip implementation (essentially same as ZipUtils except with a much more simple and clean API)
 - renamed `src/load` to `src/loader`
 - other stuff i prolly forgor
This commit is contained in:
HJfod 2022-11-30 00:48:06 +02:00
parent 8dc67c6631
commit f18353c2af
48 changed files with 1384 additions and 1606 deletions

View file

@ -1 +1 @@
0.6.1
0.7.0

View file

@ -206,6 +206,9 @@ class cocos2d::CCFadeTo {
class cocos2d::CCFileUtils : cocos2d::TypeInfo {
static cocos2d::CCFileUtils* sharedFileUtils() = mac 0x377030, ios 0x159450;
static void purgeFileUtils();
virtual void addSearchPath(const char* path);
virtual void removeSearchPath(const char *path);
virtual std::string fullPathForFilename(const char* filename, bool unk);
}

View file

@ -1,9 +1,9 @@
// included by default in every geode project
#include <Geode/Loader.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
GEODE_API bool GEODE_CALL geode_implicit_load(geode::Mod* m) {
GEODE_API void GEODE_CALL geode_implicit_load(geode::Mod* m) {
geode::Mod::setSharedMod(m);
geode::log::releaseSchedules(m);
geode::Loader::get()->releaseScheduledFunctions(m);
return true;
geode::Loader::get()->dispatchScheduledFunctions(m);
}

View file

@ -19,10 +19,7 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
src/internal/*.cpp
src/platform/mac/*.cpp
src/platform/ios/*.cpp
src/load/*.cpp
src/load/windows/*.cpp
src/load/mac/*.cpp
src/mac/*.cpp
src/loader/*.cpp
src/main.cpp
src/utils/*.cpp
src/index/*.cpp
@ -39,10 +36,6 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
file(GLOB OBJC_SOURCES
src/platform/ios/*.mm
src/platform/mac/*.mm
src/load/ios/*.mm
src/load/mac/*.mm
src/utils/ios/*.mm
src/utils/mac/*.mm
)
set_source_files_properties(${OBJC_SOURCES} PROPERTIES SKIP_PRECOMPILE_HEADERS ON)

View file

@ -40,6 +40,13 @@ class CCArray;
* @{
*/
GEODE_ADD(
struct CCTexturePack {
std::string m_id;
std::vector<std::string> m_paths;
};
)
//! @brief Helper class to handle file operations
class CC_DLL CCFileUtils : public TypeInfo
{
@ -267,13 +274,41 @@ public:
* @lua NA
*/
virtual void setSearchPaths(const gd::vector<gd::string>& searchPaths);
GEODE_ADD(
/**
* Add a texture pack. Texture packs are prioritized over other search
* paths, so if a texture pack has a replacement for a file, it will be
* used over others. Contrary to addSearchPath, this function adds the
* pack to the front of the list. If the pack has already been added,
* it's moved to the front of the list (equivalent to removing and
* re-adding the pack)
* @param pack Pack to add
*/
void addTexturePack(CCTexturePack const& pack);
/**
* Remove texture pack by ID
* @param id ID of the texture pack
*/
void removeTexturePack(std::string const& id);
/**
* Add a search path to the front of the list
* @param path Path to add
*/
void addPriorityPath(const char* path);
/**
* Update search path order; texture packs are added first, then other
* paths
*/
void updatePaths();
)
/**
* Adds a path to search paths.
*
* @since v2.2
*/
virtual void addSearchPath(const char* path);
virtual void addSearchPath(const char* path);
/**
* Removes a path from search paths.

View file

@ -216,6 +216,8 @@ namespace cocos2d
* @since geode v1.0.0
*/
bool isLoaded() const;
bool unzipAllTo(ghc::filesystem::path const& path);
)
/**

View file

@ -1,296 +1,105 @@
#pragma once
#include "Log.hpp"
#include "Types.hpp"
#include <Geode/DefaultInclude.hpp>
#include <Geode/utils/Result.hpp>
#include <fs/filesystem.hpp>
#include <functional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <fs/filesystem.hpp>
#include "Log.hpp"
#include <mutex>
// for some reason std::filesystem::path doesn't have std::hash defined in C++17
// and ghc seems to have inherited this limitation
template<>
struct std::hash<ghc::filesystem::path> {
std::size_t operator()(ghc::filesystem::path const& path) const noexcept {
return ghc::filesystem::hash_value(path);
}
};
namespace geode {
#pragma warning(disable : 4251)
static constexpr std::string_view GEODE_DIRECTORY = "geode";
static constexpr std::string_view GEODE_MOD_DIRECTORY = "mods";
static constexpr std::string_view GEODE_LOG_DIRECTORY = "log";
static constexpr std::string_view GEODE_RESOURCE_DIRECTORY = "resources";
static constexpr std::string_view GEODE_CONFIG_DIRECTORY = "config";
static constexpr std::string_view GEODE_TEMP_DIRECTORY = "temp";
static constexpr std::string_view GEODE_MOD_EXTENSION = ".geode";
static constexpr std::string_view GEODE_INDEX_DIRECTORY = "index";
class Mod;
class Hook;
struct ModInfo;
class VersionInfo;
namespace modifier {
template <class, class>
class FieldIntermediate;
}
}
/**
* The predeclaration of the implicit entry
*/
GEODE_API bool GEODE_CALL geode_implicit_load(geode::Mod*);
namespace geode {
class GEODE_DLL Loader {
public:
struct FailedModInfo {
// todo: change to path
std::string m_file;
std::string m_reason;
};
protected:
struct LoaderSettings {
struct ModSettings {
bool m_enabled = true;
};
std::unordered_map<std::string, ModSettings> m_mods;
// todo: in v1.0.0, make this a customizable option in mod.json
std::unordered_set<ghc::filesystem::path> m_earlyLoadMods;
};
using ScheduledFunction = std::function<void GEODE_CALL(void)>;
std::vector<ScheduledFunction> m_scheduledFunctions;
std::unordered_map<std::string, Mod*> m_mods;
std::vector<log::Log> m_logs;
std::ofstream m_logStream;
std::vector<ghc::filesystem::path> m_modDirectories;
std::vector<FailedModInfo> m_erroredMods;
std::vector<ghc::filesystem::path> m_texturePaths;
LoaderSettings m_loadedSettings;
bool m_isSetup = false;
static std::atomic_bool s_unloading;
mutable std::mutex m_modLoadMutex;
Result<std::string> createTempDirectoryForMod(ModInfo const& info);
Result<Mod*> loadModFromFile(std::string const& file);
size_t loadModsFromDirectory(ghc::filesystem::path const& path, bool recursive);
void createDirectories();
void updateAllDependencies();
friend class Mod;
friend class CustomLoader;
friend struct ModInfo;
size_t getFieldIndexForClass(size_t hash);
template <class, class>
friend class modifier::FieldIntermediate;
void updateModResources(Mod* mod);
// used internally in geode_implicit_load
void releaseScheduledFunctions(Mod* mod);
friend bool GEODE_CALL ::geode_implicit_load(Mod*);
public:
~Loader();
/**
* Get the shared Loader instance
* @returns Shared loader instance
*/
static Loader* get();
static VersionInfo getVersion();
static std::string getVersionType();
Result<> saveSettings();
Result<> loadSettings();
Result<> saveData();
Result<> loadData();
bool didLastLaunchCrash() const;
ghc::filesystem::path getCrashLogDirectory() const;
/**
* Directory where Geometry Dash is
*/
ghc::filesystem::path getGameDirectory() const;
/**
* Directory where GD saves its files
*/
ghc::filesystem::path getSaveDirectory() const;
/**
* Directory where Geode is
*/
ghc::filesystem::path getGeodeDirectory() const;
/**
* Directory where Geode saves its files
*/
ghc::filesystem::path getGeodeSaveDirectory() const;
/**
* Minimum supported mod version
*/
static VersionInfo minModVersion();
/**
* Maximum supported mod version
*/
static VersionInfo maxModVersion();
/**
* Check if a mod's version is within the supported range
*/
static bool supportedModVersion(VersionInfo const& version);
/**
* Whether mod specified with ID is enabled
* @param id The ID of the mod
*/
bool shouldLoadMod(std::string const& id) const;
/**
* Set up the Loader.
* @returns True if setup was succesful or
* has been done before, false if an error
* occurred
*/
bool setup();
/**
* Refresh the mods list. Scans all search
* directories again for unloaded mods
* @returns Amount of new mods loaded
*/
size_t refreshMods();
/**
* Returns true if the Loader is unloading /
* currently unloaded. Used for threading;
* should be thread-safe
* @returns True if the loader is unloading /
* unloaded, false otherwise
*/
static bool isUnloading();
void pushLog(log::Log&& log);
void popLog(log::Log* log);
std::vector<log::Log*> getLogs(std::initializer_list<Severity> severityFilter = {});
void clearLogs();
/**
* Do not call manually unless you know what you're doing.
*/
void updateResourcePaths();
/**
* Do not call manually unless you know what you're doing.
*/
void updateResources();
void addTexturePath(ghc::filesystem::path const& path);
void removeTexturePath(ghc::filesystem::path const& path);
std::vector<ghc::filesystem::path> getTexturePaths() const;
/**
* Check if a mod with an ID is installed. Any
* valid .geode file in the mods directory will
* be listed as installed
* @param id The ID of the mod
* @returns True if the mod is installed
*/
bool isModInstalled(std::string const& id) const;
/**
* Get an installed mod by its ID
* @param id The ID of the mod
* @returns Pointer to Mod if it was found, or nullptr if
* the mod is not installed
*/
Mod* getInstalledMod(std::string const& id) const;
/**
* Check if a mod with an ID is loaded
* @param id The ID of the mod
* @returns True if the mod was found, or false if
* the mod is not loaded nor installed
*/
bool isModLoaded(std::string const& id) const;
/**
* Get a loaded mod by its ID
* @param id The ID of the mod
* @returns Pointer to Mod if it was found, or nullptr if
* the mod is not loaded nor installed
*/
Mod* getLoadedMod(std::string const& id) const;
/**
* Get a list of all installed mods
* @returns List of all installed mods
*/
std::vector<Mod*> getAllMods() const;
/**
* Get all mods that are a serious
* disappointment to their parents
*/
std::vector<FailedModInfo> getFailedMods() const;
/**
* Unload a mod fully. This will remove it
* from the mods list and delete the Mod. If
* the mod does not properly handle unloading,
* this function may cause a crash; Use with
* caution!
*/
void unloadMod(Mod* mod);
/**
* Get Geode's internal representation. Use with
* caution!
* @returns Pointer to InternalMod
*/
static Mod* getInternalMod();
/**
* Run a function in the GD thread. Useful if you're
* doing logic in another thread and need to interact
* with GUI. The function will be run the next time
* `CCScheduler::update` is called
* @param func Function to run
*/
void queueInGDThread(ScheduledFunction func);
/**
* Run a function when the Mod is loaded. Useful if for
* some reason you need to run some function in
* static initialization.
* @param func Function to run
*/
void scheduleOnModLoad(Mod* m, ScheduledFunction func);
/**
* Open the platform-specific external console (if one exists)
*/
static void openPlatformConsole();
/**
* Close the platform-specific external console (if one exists)
*/
static void closePlatfromConsole();
void waitForModsToBeLoaded();
void setEarlyLoadMod(Mod* mod, bool enabled);
bool shouldEarlyLoadMod(Mod* mod) const;
};
}
#pragma once
#include "Types.hpp"
#include "Log.hpp"
#include "../external/filesystem/fs/filesystem.hpp"
#include <mutex>
#include <atomic>
#include "../utils/Result.hpp"
#include "ModInfo.hpp"
namespace geode {
using ScheduledFunction = std::function<void GEODE_CALL(void)>;
struct InvalidGeodeFile {
ghc::filesystem::path m_path;
std::string m_reason;
};
class GEODE_DLL Loader {
protected:
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;
std::vector<ScheduledFunction> m_scheduledFunctions;
mutable std::mutex m_scheduledFunctionsMutex;
bool m_isSetup = false;
std::atomic_bool m_earlyLoadFinished = false;
void createDirectories();
void updateModResources(Mod* mod);
void addSearchPaths();
void dispatchScheduledFunctions(Mod* mod);
friend void GEODE_CALL ::geode_implicit_load(Mod*);
Result<Mod*> loadModFromInfo(ModInfo const& info);
public:
~Loader();
static Loader* get();
Result<> setup();
Result<> saveData();
Result<> loadData();
static VersionInfo getVersion();
static VersionInfo minModVersion();
static VersionInfo maxModVersion();
static bool isModVersionSupported(VersionInfo const& version);
Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
Result<> loadModsFromDirectory(
ghc::filesystem::path const& dir,
bool recursive = true
);
Result<> 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();
static Mod* getInternalMod();
void updateAllDependencies();
std::vector<InvalidGeodeFile> getFailedMods() const;
void updateResources();
void queueInGDThread(ScheduledFunction func);
void scheduleOnModLoad(Mod* mod, ScheduledFunction func);
void waitForModsToBeLoaded();
/**
* Open the platform-specific external console (if one exists)
*/
static void openPlatformConsole();
/**
* Close the platform-specific external console (if one exists)
*/
static void closePlatfromConsole();
bool didLastLaunchCrash() const;
ghc::filesystem::path getCrashLogDirectory() const;
/**
* Directory where Geometry Dash is
*/
ghc::filesystem::path getGameDirectory() const;
/**
* Directory where GD saves its files
*/
ghc::filesystem::path getSaveDirectory() const;
/**
* Directory where Geode is
*/
ghc::filesystem::path getGeodeDirectory() const;
/**
* Directory where Geode saves its files
*/
ghc::filesystem::path getGeodeSaveDirectory() const;
};
}

View file

@ -29,11 +29,11 @@ namespace geode {
using log_clock = std::chrono::system_clock;
GEODE_DLL std::string generateLogName();
GEODE_DLL std::string parse(cocos2d::CCNode*);
template <class T>
requires std::convertible_to<T*, cocos2d::CCNode*> std::string parse(T* node) {
template <class T>
requires std::convertible_to<T*, cocos2d::CCNode*>
std::string parse(T* node) {
return parse(static_cast<cocos2d::CCNode*>(node));
}
@ -45,21 +45,22 @@ namespace geode {
GEODE_DLL std::string parse(cocos2d::ccColor4B const&);
GEODE_DLL std::string parse(cocos2d::ccColor4F const&);
GEODE_DLL std::string parse(cocos2d::CCObject*);
template <class T>
requires(std::convertible_to<T*, cocos2d::CCObject*> && !std::convertible_to<T*, cocos2d::CCNode*>)
std::string parse(T* node) {
template <class T>
requires(
std::convertible_to<T*, cocos2d::CCObject*> &&
!std::convertible_to<T*, cocos2d::CCNode*>
)
std::string parse(T* node) {
return parse(static_cast<cocos2d::CCObject*>(node));
}
GEODE_DLL std::string parse(Mod*);
template <typename T>
requires requires(T b) {
std::stringstream() << b;
}
requires requires(T b) {
std::stringstream() << b;
}
std::string parse(T const& thing) {
std::stringstream buf;
buf << thing;
@ -95,7 +96,7 @@ namespace geode {
}
};*/
class GEODE_DLL Log {
class GEODE_DLL Log final {
private:
static std::vector<std::function<void(Mod*)>>& scheduled();
@ -106,36 +107,19 @@ namespace geode {
Severity m_severity;
public:
Log(Mod* mod,
Severity sev); // : m_sender(mod), m_time(log_clock::now()), m_severity(sev) {}
Log(Mod* mod, Severity sev);
Log(Log&& l) = default;
Log& operator=(Log&& l) = default;
bool operator==(Log const& l);
std::string toString(bool logTime = true) const;
void pushToLoader();
inline std::vector<ComponentTrait*>& getComponents() {
return m_components;
}
std::vector<ComponentTrait*>& getComponents();
log_clock::time_point getTime() const;
Mod* getSender() const;
Severity getSeverity() const;
inline log_clock::time_point getTime() const {
return m_time;
}
inline Mod* getSender() const {
return m_sender;
}
inline Severity getSeverity() const {
return m_severity;
}
inline ~Log() {
for (auto comp : m_components) {
delete comp;
}
}
~Log();
template <typename... Args>
friend void schedule(Severity sev, Args... args);
@ -143,15 +127,30 @@ namespace geode {
friend void GEODE_DLL releaseSchedules(Mod* m);
};
void GEODE_DLL
vlogImpl(Severity, Mod*, std::string_view, std::function<void(Log&)>*, size_t);
class GEODE_DLL Logs {
private:
static inline std::vector<Log> s_logs;
static inline std::ofstream s_logStream;
Logs() = delete;
~Logs() = delete;
public:
static void setup();
static void push(Log&& log);
static void pop(Log* log);
static std::vector<Log*> list();
static void clear();
};
void GEODE_DLL vlogImpl(
Severity, Mod*, std::string_view, std::function<void(Log&)>*, size_t
);
template <typename... Args>
requires requires(Args... b) {
(parse(b), ...);
}
requires requires(Args... b) {
(parse(b), ...);
}
void log(Severity severity, Mod* mod, std::string_view formatStr, Args... args) {
static constexpr auto pushSomething = [](Log& log, auto something) {
// i think this line of code is very sad

View file

@ -1,8 +1,9 @@
#pragma once
#include "Types.hpp"
#include "Hook.hpp"
#include "Setting.hpp"
#include "Types.hpp"
#include "ModInfo.hpp"
#include "../DefaultInclude.hpp"
#include "../utils/Result.hpp"
@ -16,188 +17,7 @@
#include <unordered_map>
#include <vector>
class InternalLoader;
class InternalMod;
namespace geode {
struct PlatformInfo;
class Hook;
class Patch;
class Loader;
class Mod;
class Setting;
class Unknown;
using unknownmemfn_t = void (Unknown::*)();
using unknownfn_t = void (*)();
}
/**
* The predeclaration of the implicit entry
*/
GEODE_API bool GEODE_CALL geode_implicit_load(geode::Mod*);
namespace geode {
struct Dependency {
std::string m_id;
// todo: Dynamic versions (1.*.*)
VersionInfo m_version { 1, 0, 0 };
ModResolveState m_state = ModResolveState::Unloaded;
bool m_required = false;
Mod* m_mod = nullptr;
bool isUnresolved() const;
};
struct IssuesInfo {
std::string m_info;
std::optional<std::string> m_url;
};
/**
* Represents all the data gatherable
* from mod.json
*/
struct GEODE_DLL ModInfo {
/**
* Path to the mod file
*/
ghc::filesystem::path m_path;
/**
* Name of the platform binary within
* the mod zip
*/
// clang-format off
std::string m_binaryName = GEODE_WINDOWS("mod.dll")
GEODE_MACOS("mod.dylib")
GEODE_IOS("mod.dylib")
GEODE_ANDROID("mod.so");
// clang-format on
/**
* Mod Version. Should follow semver.
*/
VersionInfo m_version { 1, 0, 0 };
/**
* Human-readable ID of the Mod.
* Recommended to be in the format
* "com.developer.mod". Not
* guaranteed to be either case-
* nor space-sensitive. Should
* be restricted to the ASCII
* character set.
*/
std::string m_id;
/**
* Name of the mod. May contain
* spaces & punctuation, but should
* be restricted to the ASCII
* character set.
*/
std::string m_name;
/**
* The name of the head developer.
* Should be a single name, like
* "HJfod" or "The Geode Team".
* If the mod has multiple
* developers, this field should
* be one of their name or a team
* name, and the rest of the credits
* should be named in `m_credits`
* instead.
*/
std::string m_developer;
/**
* Short & concise description of the
* mod.
*/
std::optional<std::string> m_description;
/**
* Detailed description of the mod, writtenin Markdown (see
* <Geode/ui/MDTextArea.hpp>) for more info
*/
std::optional<std::string> m_details;
/**
* Changelog for the mod, written in Markdown (see
* <Geode/ui/MDTextArea.hpp>) for more info
*/
std::optional<std::string> m_changelog;
/**
* Support info for the mod; this means anything to show ways to
* support the mod's development, like donations. Written in Markdown
* (see <Geode/ui/MDTextArea.hpp>) for more info
*/
std::optional<std::string> m_supportInfo;
/**
* Git Repository of the mod
*/
std::optional<std::string> m_repository;
/**
* Info about where users should report issues and request help
*/
std::optional<IssuesInfo> m_issues;
/**
* Dependencies
*/
std::vector<Dependency> m_dependencies;
/**
* Mod spritesheet names
*/
std::vector<std::string> m_spritesheets;
/**
* Mod settings
*/
std::vector<std::pair<std::string, std::shared_ptr<Setting>>> m_settings;
/**
* Whether the mod can be disabled or not
*/
bool m_supportsDisabling = true;
/**
* Whether the mod can be unloaded or not
*/
bool m_supportsUnloading = false;
/**
* Create ModInfo from a .geode package
*/
static Result<ModInfo> createFromGeodeFile(ghc::filesystem::path const& path);
/**
* Create ModInfo from a mod.json file
*/
static Result<ModInfo> createFromFile(ghc::filesystem::path const& path);
/**
* Create ModInfo from a parsed json document
*/
static Result<ModInfo> create(ModJson const& json);
/**
* Convert to JSON. Essentially same as getRawJSON except dynamically
* adds runtime fields like path
*/
ModJson toJSON() const;
/**
* Get the raw JSON file
*/
ModJson getRawJSON() const;
private:
ModJson m_rawJSON;
/**
* Version is passed for backwards
* compatibility if we update the mod.json
* format
*/
static Result<ModInfo> createFromSchemaV010(ModJson const& json);
Result<> addSpecialFiles(ghc::filesystem::path const& dir);
Result<> addSpecialFiles(cocos2d::ZipFile& zip);
std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles();
};
// For converting ModInfo back to JSON
void GEODE_DLL to_json(nlohmann::json& json, ModInfo const& info);
template<class T>
struct HandleToSaved : public T {
Mod* m_mod;
@ -242,7 +62,7 @@ namespace geode {
/**
* Whether the mod binary is loaded or not
*/
bool m_loaded = false;
bool m_binaryLoaded = false;
/**
* Whether the mod is loadable or not
*/
@ -262,39 +82,7 @@ namespace geode {
* when their dependency is disabled.
*/
std::vector<Mod*> m_parentDependencies;
/**
* Pointer to the Mod's implicit load function
*/
geode_load m_implicitLoadFunc = nullptr;
/**
* Pointer to the Mod's load function
*/
geode_load m_loadFunc = nullptr;
/**
* Pointer to the Mod's unload function
*/
geode_unload m_unloadFunc = nullptr;
/**
* Pointer to the Mod's enable function
*/
geode_enable m_enableFunc = nullptr;
/**
* Pointer to the Mod's enable function
*/
geode_disable m_disableFunc = nullptr;
geode_load_data m_loadDataFunc = nullptr;
geode_save_data m_saveDataFunc = nullptr;
geode_setting_updated m_settingUpdatedFunc = nullptr;
/**
* Whether temp/<mod id>/resources should be
* added to CCFileUtils search paths
*/
bool m_addResourcesToSearchPath = false;
/**
* Error info in case loading failed
*/
std::string m_loadErrorInfo = "";
decltype(geode_implicit_load)* m_implicitLoadFunc;
/**
* Saved values
*/
@ -305,13 +93,8 @@ namespace geode {
*/
Result<> loadPlatformBinary();
Result<> unloadPlatformBinary();
Result<> saveSettings();
Result<> loadSettings();
Result<> createTempDir();
static bool validateID(std::string const& id);
// no copying
Mod(Mod const&) = delete;
Mod operator=(Mod const&) = delete;
@ -337,7 +120,7 @@ namespace geode {
sharedMod<> = mod;
}
friend bool GEODE_CALL ::geode_implicit_load(Mod*);
friend void GEODE_CALL ::geode_implicit_load(Mod*);
public:
std::string getID() const;
@ -345,7 +128,6 @@ namespace geode {
std::string getDeveloper() const;
std::optional<std::string> getDescription() const;
std::optional<std::string> getDetails() const;
[[deprecated("Use Mod::getPackagePath instead")]] std::string getPath() const;
ghc::filesystem::path getPackagePath() const;
VersionInfo getVersion() const;
bool isEnabled() const;
@ -353,7 +135,6 @@ namespace geode {
bool supportsDisabling() const;
bool supportsUnloading() const;
bool wasSuccesfullyLoaded() const;
std::string getLoadErrorInfo() const;
ModInfo getModInfo() const;
ghc::filesystem::path getTempDir() const;
ghc::filesystem::path getBinaryPath() const;
@ -527,20 +308,20 @@ namespace geode {
Result<> unpatch(Patch* patch);
/**
* Load this mod
* Load & enable this mod
* @returns Successful result on success,
* errorful result with info on error
*/
Result<> load();
Result<> loadBinary();
/**
* Unload this mod
* Disable & unload this mod
* @warning May crash if the mod doesn't
* properly handle unloading!
* @returns Successful result on success,
* errorful result with info on error
*/
Result<> unload();
Result<> unloadBinary();
/**
* Enable this mod
@ -550,15 +331,17 @@ namespace geode {
Result<> enable();
/**
* Disable this mod if it supports doing so
* Disable this mod
* @returns Successful result on success,
* errorful result with info on error
*/
Result<> disable();
/**
* Disable & unload this mod (if supported),
* then delete the mod's .geode file.
* Disable & unload this mod (if supported), then delete the mod's
* .geode package. If unloading isn't supported, the mod's binary
* will stay loaded, and in all cases the Mod* instance will still
* exist and be interactable.
* @returns Successful result on success,
* errorful result with info on error
*/

View file

@ -0,0 +1,179 @@
#pragma once
#include "Types.hpp"
#include "../external/json/json.hpp"
#include "../utils/VersionInfo.hpp"
#include "../utils/Result.hpp"
namespace geode {
namespace utils::file {
class Unzip;
}
using ModJson = nlohmann::ordered_json;
struct Dependency {
std::string m_id;
// todo: Dynamic versions (1.*.*)
VersionInfo m_version { 1, 0, 0 };
ModResolveState m_state = ModResolveState::Unloaded;
bool m_required = false;
Mod* m_mod = nullptr;
bool isUnresolved() const;
};
struct IssuesInfo {
std::string m_info;
std::optional<std::string> m_url;
};
/**
* Represents all the data gatherable
* from mod.json
*/
struct GEODE_DLL ModInfo {
/**
* Path to the mod file
*/
ghc::filesystem::path m_path;
/**
* Name of the platform binary within
* the mod zip
*/
std::string m_binaryName;
/**
* Mod Version. Should follow semver.
*/
VersionInfo m_version { 1, 0, 0 };
/**
* Human-readable ID of the Mod.
* Recommended to be in the format
* "com.developer.mod". Not
* guaranteed to be either case-
* nor space-sensitive. Should
* be restricted to the ASCII
* character set.
*/
std::string m_id;
/**
* Name of the mod. May contain
* spaces & punctuation, but should
* be restricted to the ASCII
* character set.
*/
std::string m_name;
/**
* The name of the head developer.
* Should be a single name, like
* "HJfod" or "The Geode Team".
* If the mod has multiple
* developers, this field should
* be one of their name or a team
* name, and the rest of the credits
* should be named in `m_credits`
* instead.
*/
std::string m_developer;
/**
* Short & concise description of the
* mod.
*/
std::optional<std::string> m_description;
/**
* Detailed description of the mod, writtenin Markdown (see
* <Geode/ui/MDTextArea.hpp>) for more info
*/
std::optional<std::string> m_details;
/**
* Changelog for the mod, written in Markdown (see
* <Geode/ui/MDTextArea.hpp>) for more info
*/
std::optional<std::string> m_changelog;
/**
* Support info for the mod; this means anything to show ways to
* support the mod's development, like donations. Written in Markdown
* (see <Geode/ui/MDTextArea.hpp>) for more info
*/
std::optional<std::string> m_supportInfo;
/**
* Git Repository of the mod
*/
std::optional<std::string> m_repository;
/**
* Info about where users should report issues and request help
*/
std::optional<IssuesInfo> m_issues;
/**
* Dependencies
*/
std::vector<Dependency> m_dependencies;
/**
* Mod spritesheet names
*/
std::vector<std::string> m_spritesheets;
/**
* Mod settings
*/
std::vector<std::pair<std::string, std::shared_ptr<Setting>>> m_settings;
/**
* Whether the mod can be disabled or not
*/
bool m_supportsDisabling = true;
/**
* Whether the mod can be unloaded or not
*/
bool m_supportsUnloading = false;
/**
* Whether this mod has to be loaded before the loading screen or not
*/
bool m_needsEarlyLoad = false;
/**
* Create ModInfo from an unzipped .geode package
*/
static Result<ModInfo> createFromGeodeZip(utils::file::Unzip& zip);
/**
* Create ModInfo from a .geode package
*/
static Result<ModInfo> createFromGeodeFile(ghc::filesystem::path const& path);
/**
* Create ModInfo from a mod.json file
*/
static Result<ModInfo> createFromFile(ghc::filesystem::path const& path);
/**
* Create ModInfo from a parsed json document
*/
static Result<ModInfo> create(ModJson const& json);
/**
* Convert to JSON. Essentially same as getRawJSON except dynamically
* adds runtime fields like path
*/
ModJson toJSON() const;
/**
* Get the raw JSON file
*/
ModJson getRawJSON() const;
bool operator==(ModInfo const& other) const;
static bool validateID(std::string const& id);
private:
ModJson m_rawJSON;
/**
* Version is passed for backwards
* compatibility if we update the mod.json
* format
*/
static Result<ModInfo> createFromSchemaV010(ModJson const& json);
Result<> addSpecialFiles(ghc::filesystem::path const& dir);
Result<> addSpecialFiles(utils::file::Unzip& zip);
std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles();
};
// For converting ModInfo back to JSON
void GEODE_DLL to_json(nlohmann::json& json, ModInfo const& info);
}

View file

@ -7,6 +7,7 @@
#include "../utils/file.hpp"
#include "../utils/ranges.hpp"
#include "../utils/cocos.hpp"
#include "ModInfo.hpp"
#include <optional>
#include <regex>
#include <unordered_set>
@ -15,8 +16,6 @@
#pragma warning(disable : 4275)
namespace geode {
using ModJson = nlohmann::ordered_json;
class Setting;
class SettingNode;
class BoolSetting;

View file

@ -1,9 +1,12 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include <Geode/platform/cplatform.h>
#include "../DefaultInclude.hpp"
#include "../platform/cplatform.h"
#include <string>
class InternalLoader;
class InternalMod;
namespace geode {
/**
* Describes the severity of the log
@ -148,22 +151,33 @@ namespace geode {
Disabled,
};
/**
* These methods will be removed in v1.0.0 and replaced with events
*/
static constexpr std::string_view GEODE_DIRECTORY = "geode";
static constexpr std::string_view GEODE_MOD_DIRECTORY = "mods";
static constexpr std::string_view GEODE_LOG_DIRECTORY = "log";
static constexpr std::string_view GEODE_RESOURCE_DIRECTORY = "resources";
static constexpr std::string_view GEODE_CONFIG_DIRECTORY = "config";
static constexpr std::string_view GEODE_TEMP_DIRECTORY = "temp";
static constexpr std::string_view GEODE_MOD_EXTENSION = ".geode";
static constexpr std::string_view GEODE_INDEX_DIRECTORY = "index";
/**
* Default Geode load method for C++
* mods: The mod receive a pointer to
* its allocated Mod instance. Return
* true on a succesful load,
* return false on error.
*/
typedef bool(GEODE_CALL* geode_load)(Mod*);
typedef void(GEODE_CALL* geode_unload)();
typedef bool(GEODE_CALL* geode_enable)();
typedef bool(GEODE_CALL* geode_disable)();
typedef bool(GEODE_CALL* geode_save_data)(const char*);
typedef bool(GEODE_CALL* geode_load_data)(const char*);
typedef void(GEODE_CALL* geode_setting_updated)(const char*, Setting*);
class Mod;
class Loader;
class Hook;
struct ModInfo;
class VersionInfo;
class Unknown;
using unknownmemfn_t = void (Unknown::*)();
using unknownfn_t = void (*)();
namespace modifier {
template <class, class>
class FieldIntermediate;
}
}
/**
* The predeclaration of the implicit entry
*/
GEODE_API void GEODE_CALL geode_implicit_load(geode::Mod*);

View file

@ -1,7 +1,7 @@
#pragma once
#include "Traits.hpp"
#include <cocos2d.h>
#include <Geode/loader/Loader.hpp>
#include <vector>
@ -47,6 +47,11 @@ namespace geode::modifier {
// Padding used for guaranteeing any member of parents
// will be in between sizeof(Intermediate) and sizeof(Parent)
uintptr_t m_padding;
static inline std::unordered_map<size_t, size_t> nextIndex;
static size_t getFieldIndexForClass(size_t hash) {
return nextIndex[hash]++;
}
public:
static void fieldConstructor(void* offsetField) {
@ -82,7 +87,7 @@ namespace geode::modifier {
reinterpret_cast<Parent*>(reinterpret_cast<std::byte*>(this) - sizeof(Base));
static_assert(sizeof(Base) == offsetof(Parent, m_fields), "offsetof not correct");
auto container = FieldContainer::from(node);
static size_t index = Loader::get()->getFieldIndexForClass(typeid(Base).hash_code());
static size_t index = getFieldIndexForClass(typeid(Base).hash_code());
// this pointer is offset
auto offsetField = container->getField(index);
if (!offsetField) {

View file

@ -55,7 +55,7 @@ namespace geode {
if (isErr()) {
return Result(fmt::format(str, std::forward<Args>(args)...));
} else {
return this;
return *this;
}
}

View file

@ -20,6 +20,59 @@ namespace geode::utils::file {
GEODE_DLL Result<std::vector<std::string>> listFiles(std::string const& path);
GEODE_DLL Result<std::vector<std::string>> listFilesRecursively(std::string const& path);
class UnzipImpl;
class GEODE_DLL Unzip final {
private:
UnzipImpl* m_impl;
public:
Unzip() = delete;
Unzip(Unzip const&) = delete;
Unzip(Unzip&& other);
Unzip(UnzipImpl* impl);
~Unzip();
using Path = ghc::filesystem::path;
/**
* Create unzipper for file
*/
static Result<Unzip> create(Path const& file);
/**
* Path to the opened zip
*/
Path getPath() const;
/**
* Get all entries in zip
*/
std::vector<Path> getEntries() const;
/**
* Check if zip has entry
* @param name Entry path in zip
*/
bool hasEntry(Path const& name);
/**
* Extract entry to memory
* @param name Entry path in zip
*/
Result<byte_array> extract(Path const& name);
/**
* Extract entry to file
* @param name Entry path in zip
* @param path Target file path
*/
Result<> extractTo(Path const& name, Path const& path);
/**
* Extract all entries to directory
* @param dir Directory to unzip the contents to
*/
Result<> extractAllTo(Path const& dir);
};
/**
* Unzip file to directory
* @param from File to unzip

View file

@ -8,9 +8,19 @@
#include <sstream>
#include <string>
#include <vector>
#include <fs/filesystem.hpp>
#undef snprintf
// for some reason std::filesystem::path doesn't have std::hash defined in C++17
// and ghc seems to have inherited this limitation
template<>
struct std::hash<ghc::filesystem::path> {
std::size_t operator()(ghc::filesystem::path const& path) const noexcept {
return ghc::filesystem::hash_value(path);
}
};
namespace geode {
using byte_array = std::vector<uint8_t>;

View file

@ -0,0 +1,96 @@
#include <cocos2d.h>
#include <Geode/modify/CCFileUtils.hpp>
#include <Geode/utils/ranges.hpp>
USE_GEODE_NAMESPACE();
static std::vector<CCTexturePack> PACKS;
static std::vector<std::string> PATHS;
static bool DONT_ADD_PATHS = false;
#pragma warning(push)
#pragma warning(disable : 4273)
void CCFileUtils::addTexturePack(CCTexturePack const& pack) {
// remove pack if it has already been added
ranges::remove(PACKS, [pack](CCTexturePack const& other) {
return pack.m_id == other.m_id;
});
// add pack to start
PACKS.insert(PACKS.begin(), pack);
this->updatePaths();
}
void CCFileUtils::removeTexturePack(std::string const& id) {
ranges::remove(PACKS, [id](CCTexturePack const& pack) {
return pack.m_id == id;
});
this->updatePaths();
}
void CCFileUtils::addPriorityPath(const char* path) {
PATHS.insert(PATHS.begin(), path);
this->updatePaths();
}
void CCFileUtils::updatePaths() {
// add search paths that aren't in PATHS or PACKS to PATHS
for (auto& path : m_searchPathArray) {
bool isKnown = false;
for (auto& pack : PACKS) {
for (auto& packPath : pack.m_paths) {
if (ghc::filesystem::path(path.c_str()) == packPath) {
isKnown = true;
break;
}
}
if (isKnown) break;
}
if (isKnown) break;
for (auto& p : PATHS) {
if (ghc::filesystem::path(path.c_str()) == p) {
isKnown = true;
break;
}
}
if (!isKnown) {
PATHS.push_back(path);
}
}
// clear old paths
m_searchPathArray.clear();
// make sure addSearchPath doesn't add to PACKS or PATHS
DONT_ADD_PATHS = true;
// add texture packs first
for (auto& pack : PACKS) {
for (auto& path : pack.m_paths) {
this->addSearchPath(path.c_str());
}
}
// add other paths after
for (auto& path : PATHS) {
this->addSearchPath(path.c_str());
}
DONT_ADD_PATHS = false;
log::debug("Search Paths: {}", m_searchPathArray.size());
for (auto& path : m_searchPathArray) {
log::debug("Path: {}", path.c_str());
}
}
#pragma warning(pop)
class $modify(CCFileUtils) {
static CCFileUtils* sharedFileUtils() {
auto doAddPaths = !s_sharedFileUtils;
auto ret = CCFileUtils::sharedFileUtils();
if (doAddPaths) {
s_sharedFileUtils->updatePaths();
}
return ret;
}
};

View file

@ -17,4 +17,4 @@ struct SaveLoader : Modify<SaveLoader, AppDelegate> {
return AppDelegate::trySaveGame();
}
};
};

View file

@ -9,4 +9,4 @@ struct FunctionQueue : Modify<FunctionQueue, CCScheduler> {
InternalLoader::get()->executeGDThreadQueue();
return CCScheduler::update(dt);
}
};
};

View file

@ -8,7 +8,7 @@ struct ResourcesUpdate : Modify<ResourcesUpdate, LoadingLayer> {
void loadAssets() {
LoadingLayer::loadAssets();
// this is in case the user refreshes texture quality at runtime
if (this->m_loadStep == 10) {
if (m_loadStep == 10) {
Loader::get()->updateResources();
}
}

View file

@ -580,7 +580,7 @@ void InstallItems::finish(bool replaceFiles) {
}
// load mods
Loader::get()->refreshMods();
(void)Loader::get()->refreshModsList();
// finished
this->post(UpdateStatus::Finished, "", 100);

View file

@ -45,10 +45,7 @@ InternalMod::InternalMod() : Mod(getInternalModInfo()) {
ghc::filesystem::create_directories(m_saveDirPath);
// make sure spritesheets get added
m_addResourcesToSearchPath = true;
auto sett = this->loadSettings();
auto sett = this->loadData();
if (!sett) {
log::log(Severity::Error, this, "{}", sett.unwrapErr());
}

View file

@ -1,546 +0,0 @@
#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 <Geode/utils/map.hpp>
#include <Geode/utils/ranges.hpp>
#include <InternalLoader.hpp>
#include <InternalMod.hpp>
#include <about.hpp>
#include <crashlog.hpp>
#include <mutex>
USE_GEODE_NAMESPACE();
std::atomic_bool Loader::s_unloading = false;
std::mutex g_unloadMutex;
VersionInfo Loader::getVersion() {
return LOADER_VERSION;
}
std::string Loader::getVersionType() {
return LOADER_VERSION_TYPE;
}
Loader* Loader::get() {
return InternalLoader::get();
}
void Loader::createDirectories() {
auto modDir = this->getGeodeDirectory() / GEODE_MOD_DIRECTORY;
auto logDir = this->getGeodeDirectory() / GEODE_LOG_DIRECTORY;
auto resDir = this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY;
auto tempDir = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
auto confDir = this->getGeodeDirectory() / GEODE_CONFIG_DIRECTORY;
#ifdef GEODE_IS_MACOS
ghc::filesystem::create_directory(this->getSaveDirectory());
#endif
ghc::filesystem::create_directories(resDir);
ghc::filesystem::create_directory(confDir);
ghc::filesystem::create_directory(modDir);
ghc::filesystem::create_directory(logDir);
ghc::filesystem::create_directory(tempDir);
if (!ranges::contains(m_modDirectories, modDir)) {
m_modDirectories.push_back(modDir);
}
// files too
m_logStream = std::ofstream(logDir / log::generateLogName());
}
void Loader::updateResourcePaths() {
log::debug("Setting resource paths");
// reset search paths
CCFileUtils::get()->removeAllPaths();
// add custom texture paths first (priority)
for (auto const& path : m_texturePaths) {
CCFileUtils::get()->addSearchPath(path.string().c_str());
}
// add own paths next
CCFileUtils::get()->addSearchPath(
(this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY).string().c_str()
);
CCFileUtils::get()->addSearchPath(
(this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY).string().c_str()
);
// add mods' search paths
for (auto const& [_, mod] : m_mods) {
auto searchPath =
this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY / mod->getID() / "resources";
// add search path
CCFileUtils::get()->addSearchPath(searchPath.string().c_str());
}
// add GD's search path
CCFileUtils::get()->addSearchPath("Resources");
}
void Loader::updateModResources(Mod* mod) {
if (!mod->m_addResourcesToSearchPath) {
log::debug("Mod {} doesn't have resources, skipping", mod->getID());
return;
}
auto searchPath = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY / mod->getID() / "resources";
log::debug("Adding resources for {}", mod->getID());
// add spritesheets
for (auto const& sheet : mod->m_info.m_spritesheets) {
auto png = sheet + ".png";
auto plist = sheet + ".plist";
auto ccfu = CCFileUtils::sharedFileUtils();
if (png == std::string(ccfu->fullPathForFilename(png.c_str(), false)) ||
plist == std::string(ccfu->fullPathForFilename(plist.c_str(), false))) {
log::warn(
"The resource dir of \"{}\" is missing \"{}\" png and/or plist files",
mod->m_info.m_id, sheet
);
}
else {
CCTextureCache::sharedTextureCache()->addImage(png.c_str(), false);
CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile(plist.c_str());
}
}
}
void Loader::updateResources() {
log::debug("Adding resources");
this->updateResourcePaths();
// add own spritesheets
this->updateModResources(InternalMod::get());
// add mods' spritesheets
for (auto const& [_, mod] : m_mods) {
this->updateModResources(mod);
}
}
void Loader::addTexturePath(ghc::filesystem::path const& path) {
// remove path if it already exists
this->removeTexturePath(path);
m_texturePaths.push_back(path);
}
void Loader::removeTexturePath(ghc::filesystem::path const& path) {
ranges::remove(m_texturePaths, path);
}
std::vector<ghc::filesystem::path> Loader::getTexturePaths() const {
return m_texturePaths;
}
size_t Loader::loadModsFromDirectory(
ghc::filesystem::path const& dir, bool recursive
) {
log::debug("Searching {}", dir);
size_t loadedCount = 0;
for (auto const& entry : ghc::filesystem::directory_iterator(dir)) {
// recursively search directories
if (ghc::filesystem::is_directory(entry) && recursive) {
loadedCount += this->loadModsFromDirectory(entry.path(), true);
continue;
}
// skip this entry if it's not a file
if (!ghc::filesystem::is_regular_file(entry)) {
continue;
}
// skip this entry if its extension is not .geode
if (entry.path().extension() != GEODE_MOD_EXTENSION) {
continue;
}
// skip this entry if it's already loaded
if (utils::map::contains<std::string, Mod*>(m_mods, [entry](Mod* p) -> bool {
return p->m_info.m_path == entry.path();
}))
continue;
// load mod
log::debug("Loading {}", entry.path().string());
auto res = this->loadModFromFile(entry.path().string());
if (res) {
// succesfully loaded
loadedCount++;
// check for dependencies
if (!res.unwrap()->hasUnresolvedDependencies()) {
log::debug("Successfully loaded {}", res.unwrap());
}
else {
log::error("{} has unresolved dependencies", res.unwrap());
}
}
else {
// something went wrong
log::error("{}", res.unwrapErr());
m_erroredMods.push_back({ entry.path().string(), res.unwrapErr() });
}
}
return loadedCount;
}
size_t Loader::refreshMods() {
log::debug("Loading mods...");
// load early load mods
m_modLoadMutex.lock();
for (auto const& path : m_loadedSettings.m_earlyLoadMods) {
if (!ranges::contains(m_mods, [path](auto mod) {
return mod.second->m_info.m_path == path;
})) {
auto res = this->loadModFromFile(path.string());
if (!res) {
// something went wrong
log::error("{}", res.unwrapErr());
m_erroredMods.push_back({ path.string(), res.unwrapErr() });
}
}
}
m_modLoadMutex.unlock();
// clear errored mods since that list will be
// reconstructed from scratch
m_erroredMods.clear();
// make sure mod directory exists
this->createDirectories();
size_t loadedCount = 0;
// find all mods in search directories
for (auto const& dir : m_modDirectories) {
// find all mods in this search directory
loadedCount += loadModsFromDirectory(dir, true);
}
log::debug("Loaded {} mods", loadedCount);
return loadedCount;
}
Result<> Loader::saveData() {
auto res = this->saveSettings();
if (!res) return res;
for (auto& [_, mod] : m_mods) {
auto r = mod->saveData();
if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
return Ok();
}
Result<> Loader::loadData() {
auto res = this->loadSettings();
if (!res) return res;
for (auto& [_, mod] : m_mods) {
auto r = mod->loadData();
if (!r) {
log::warn("Unable to load data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
return Ok();
}
Result<> Loader::saveSettings() {
auto json = nlohmann::json::object();
// save mod enabled / disabled states
json["mods"] = nlohmann::json::object();
for (auto [id, mod] : m_mods) {
if (mod->isUninstalled()) continue;
auto value = nlohmann::json::object();
value["enabled"] = mod->m_enabled;
json["mods"][id] = value;
}
json["early-load"] = m_loadedSettings.m_earlyLoadMods;
// save loader settings
GEODE_UNWRAP(InternalMod::get()->saveSettings());
// save info alerts
InternalLoader::get()->saveInfoAlerts(json);
// mark the game as not having crashed
json["succesfully-closed"] = true;
return utils::file::writeString(this->getGeodeSaveDirectory() / "mods.json", json.dump(4));
}
Result<> Loader::loadSettings() {
auto path = this->getGeodeSaveDirectory() / "mods.json";
if (!ghc::filesystem::exists(path)) {
return Ok();
}
// read mods.json
GEODE_UNWRAP_INTO(auto read, utils::file::readString(path));
try {
auto json = nlohmann::json::parse(read);
auto checker = JsonChecker(json);
auto root = checker.root("[loader settings]").obj();
root.has("early-load").into(m_loadedSettings.m_earlyLoadMods);
for (auto [key, val] : root.has("mods").items()) {
auto obj = val.obj();
LoaderSettings::ModSettings mod;
obj.has("enabled").into(mod.m_enabled);
m_loadedSettings.m_mods.insert({ key, mod });
}
if (checker.isError()) {
log::error("Error loading global mod settings: {}", checker.getError());
}
InternalLoader::get()->loadInfoAlerts(json);
return Ok();
}
catch (std::exception const& e) {
return Err(e.what());
}
}
bool Loader::didLastLaunchCrash() const {
return crashlog::didLastLaunchCrash();
}
ghc::filesystem::path Loader::getCrashLogDirectory() const {
return crashlog::getCrashLogDirectory();
}
bool Loader::shouldLoadMod(std::string const& id) const {
if (m_loadedSettings.m_mods.count(id)) {
return m_loadedSettings.m_mods.at(id).m_enabled;
}
return true;
}
bool Loader::isModInstalled(std::string const& id) const {
return m_mods.count(id) && !m_mods.at(id)->isUninstalled();
}
Mod* Loader::getInstalledMod(std::string const& id) const {
if (m_mods.count(id) && !m_mods.at(id)->isUninstalled()) {
return m_mods.at(id);
}
return nullptr;
}
bool Loader::isModLoaded(std::string const& id) const {
return m_mods.count(id) && m_mods.at(id)->isLoaded();
}
Mod* Loader::getLoadedMod(std::string const& id) const {
if (m_mods.count(id)) {
auto mod = m_mods.at(id);
if (mod->isLoaded()) {
return mod;
}
}
return nullptr;
}
std::vector<Mod*> Loader::getAllMods() const {
return utils::map::getValues(m_mods);
}
std::vector<Loader::FailedModInfo> Loader::getFailedMods() const {
return m_erroredMods;
}
void Loader::updateAllDependencies() {
for (auto const& [_, mod] : m_mods) {
mod->updateDependencyStates();
}
}
void Loader::unloadMod(Mod* mod) {
m_mods.erase(mod->m_info.m_id);
// ~Mod will call FreeLibrary
// automatically
delete mod;
}
bool Loader::setup() {
if (m_isSetup) return true;
if (crashlog::setupPlatformHandler()) {
log::debug("Set up platform crash logger");
}
else {
log::debug("Unable to set up platform crash logger");
}
log::debug("Setting up Loader...");
this->createDirectories();
auto sett = this->loadData();
if (!sett) {
log::warn("Unable to load loader settings: {}", sett.unwrapErr());
}
this->refreshMods();
m_isSetup = true;
return true;
}
Loader::~Loader() {
g_unloadMutex.lock();
s_unloading = true;
g_unloadMutex.unlock();
for (auto const& [_, mod] : m_mods) {
delete mod;
}
m_mods.clear();
m_logs.clear();
auto tempDir = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
ghc::filesystem::remove_all(tempDir);
}
void Loader::pushLog(log::Log&& log) {
std::string logStr = log.toString(true);
InternalLoader::get()->logConsoleMessage(logStr);
m_logStream << logStr << std::endl;
m_logs.push_back(std::move(log));
}
void Loader::popLog(log::Log* log) {
ranges::remove(m_logs, *log);
}
std::vector<log::Log*> Loader::getLogs(std::initializer_list<Severity> severityFilter) {
std::vector<log::Log*> logs;
for (auto& log : m_logs) {
if (ranges::contains(severityFilter, log.getSeverity()) || !severityFilter.size()) {
logs.push_back(&log);
}
}
return logs;
}
void Loader::clearLogs() {
m_logs.clear();
}
void Loader::queueInGDThread(ScheduledFunction func) {
InternalLoader::get()->queueInGDThread(func);
}
Mod* Loader::getInternalMod() {
return InternalMod::get();
}
bool Loader::isUnloading() {
return Loader::s_unloading;
}
ghc::filesystem::path Loader::getGameDirectory() const {
return ghc::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str());
}
#ifdef GEODE_IS_WINDOWS
#include <filesystem>
#endif
ghc::filesystem::path Loader::getSaveDirectory() const {
// not using ~/Library/Caches
#ifdef GEODE_IS_MACOS
return ghc::filesystem::path("/Users/Shared/Geode");
#elif defined(GEODE_IS_WINDOWS)
return ghc::filesystem::path(
std::filesystem::weakly_canonical(CCFileUtils::sharedFileUtils()->getWritablePath().c_str())
.string()
);
#else
return ghc::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath().c_str());
#endif
}
ghc::filesystem::path Loader::getGeodeDirectory() const {
return geode::utils::file::geodeRoot() / GEODE_DIRECTORY;
}
ghc::filesystem::path Loader::getGeodeSaveDirectory() const {
return this->getSaveDirectory() / GEODE_DIRECTORY;
}
size_t Loader::getFieldIndexForClass(size_t hash) {
static std::unordered_map<size_t, size_t> nextIndex;
return nextIndex[hash]++;
}
VersionInfo Loader::minModVersion() {
// Remember to update when deleting features!
return VersionInfo { 0, 4, 0 };
}
VersionInfo Loader::maxModVersion() {
// patches are always backwards-compatible. if not, we have failed
return VersionInfo {
Loader::getVersion().getMajor(),
Loader::getVersion().getMinor(),
99999999,
};
}
bool Loader::supportedModVersion(VersionInfo const& version) {
return version >= Loader::minModVersion() && version <= Loader::maxModVersion();
}
void Loader::openPlatformConsole() {
InternalLoader::get()->openPlatformConsole();
}
void Loader::closePlatfromConsole() {
InternalLoader::get()->closePlatformConsole();
}
void Loader::scheduleOnModLoad(Mod* m, ScheduledFunction func) {
if (m) return func();
m_scheduledFunctions.push_back(func);
}
void Loader::releaseScheduledFunctions(Mod* mod) {
for (auto& func : m_scheduledFunctions) {
func();
}
m_scheduledFunctions.clear();
}
void Loader::waitForModsToBeLoaded() {
std::lock_guard _(m_modLoadMutex);
}
void Loader::setEarlyLoadMod(Mod* mod, bool enabled) {
if (enabled) {
m_loadedSettings.m_earlyLoadMods.insert(mod->getPackagePath());
} else {
m_loadedSettings.m_earlyLoadMods.erase(mod->getPackagePath());
}
}
bool Loader::shouldEarlyLoadMod(Mod* mod) const {
return m_loadedSettings.m_earlyLoadMods.count(mod->getPackagePath());
}

View file

@ -1,52 +0,0 @@
#include <Geode/DefaultInclude.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#undef snprintf
USE_GEODE_NAMESPACE();
using namespace std::string_literals;
bool Mod::validateID(std::string const& id) {
// ids may not be empty
if (!id.size()) return false;
for (auto const& c : id) {
if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') ||
(c == '-') || (c == '_') || (c == '.')))
return false;
}
return true;
}
Result<Mod*> Loader::loadModFromFile(std::string const& path) {
// load mod.json
GEODE_UNWRAP_INTO(auto json, ModInfo::createFromGeodeFile(path));
// check that a duplicate has not been loaded
if (m_mods.count(json.m_id)) {
return Err("Mod with ID \"" + json.m_id + "\" has already been loaded!");
}
// create and set up Mod instance
auto mod = new Mod(json);
mod->m_saveDirPath =
Loader::get()->getGeodeSaveDirectory() / GEODE_MOD_DIRECTORY / json.m_id;
ghc::filesystem::create_directories(mod->m_saveDirPath);
auto sett = mod->loadSettings();
if (!sett) {
log::log(Severity::Error, mod, "{}", sett.unwrapErr());
}
// enable mod if needed
mod->m_enabled = Loader::get()->shouldLoadMod(mod->m_info.m_id);
m_mods.insert({ json.m_id, mod });
mod->updateDependencyStates();
// add mod resources
this->queueInGDThread([this, mod]() {
this->updateResourcePaths();
this->updateModResources(mod);
});
return Ok(mod);
}

View file

@ -0,0 +1,419 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <InternalLoader.hpp>
#include <InternalMod.hpp>
#include <about.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/map.hpp>
#include <crashlog.hpp>
USE_GEODE_NAMESPACE();
Loader* Loader::get() {
return InternalLoader::get();
}
Loader::~Loader() {
for (auto& [_, mod] : m_mods) {
delete mod;
}
m_mods.clear();
log::Logs::clear();
ghc::filesystem::remove_all(
this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY
);
}
VersionInfo Loader::getVersion() {
return LOADER_VERSION;
}
VersionInfo Loader::minModVersion() {
return VersionInfo { 0, 7, 0 };
}
VersionInfo Loader::maxModVersion() {
return VersionInfo {
Loader::getVersion().getMajor(),
Loader::getVersion().getMinor(),
// todo: dynamic version info (vM.M.*)
99999999,
};
}
bool Loader::isModVersionSupported(VersionInfo const& version) {
return
version >= Loader::minModVersion() &&
version <= Loader::maxModVersion();
}
void Loader::createDirectories() {
auto modDir = this->getGeodeDirectory() / GEODE_MOD_DIRECTORY;
auto logDir = this->getGeodeDirectory() / GEODE_LOG_DIRECTORY;
auto resDir = this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY;
auto tempDir = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
auto confDir = this->getGeodeDirectory() / GEODE_CONFIG_DIRECTORY;
#ifdef GEODE_IS_MACOS
ghc::filesystem::create_directory(this->getSaveDirectory());
#endif
ghc::filesystem::create_directories(resDir);
ghc::filesystem::create_directory(confDir);
ghc::filesystem::create_directory(modDir);
ghc::filesystem::create_directory(logDir);
ghc::filesystem::create_directory(tempDir);
if (!ranges::contains(m_modSearchDirectories, modDir)) {
m_modSearchDirectories.push_back(modDir);
}
}
Result<> Loader::saveData() {
// save mods' data
for (auto& [_, mod] : m_mods) {
auto r = mod->saveData();
if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
// save loader data
GEODE_UNWRAP(InternalMod::get()->saveData());
return Ok();
}
Result<> Loader::loadData() {
auto e = InternalMod::get()->loadData();
if (!e) {
log::warn("Unable to load loader settings: {}", e.unwrapErr());
}
for (auto& [_, mod] : m_mods) {
auto r = mod->loadData();
if (!r) {
log::warn("Unable to load data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
}
}
return Ok();
}
Result<> Loader::setup() {
if (m_isSetup) {
return Ok();
}
log::Logs::setup();
if (crashlog::setupPlatformHandler()) {
log::debug("Set up platform crash logger");
}
else {
log::debug("Unable to set up platform crash logger");
}
log::debug("Setting up Loader...");
this->createDirectories();
auto sett = this->loadData();
if (!sett) {
log::warn("Unable to load loader settings: {}", sett.unwrapErr());
}
GEODE_UNWRAP(this->refreshModsList());
this->queueInGDThread([]() {
Loader::get()->addSearchPaths();
});
m_isSetup = true;
return Ok();
}
Result<Mod*> Loader::loadModFromInfo(ModInfo const& info) {
if (m_mods.count(info.m_id)) {
return Err(fmt::format("Mod with ID '{}' already loaded", info.m_id));
}
// create Mod instance
auto mod = new Mod(info);
m_mods.insert({ info.m_id, mod });
mod->m_enabled = InternalMod::get()->getSavedValue<bool>(
"should-load-" + info.m_id, true
);
// this loads the mod if its dependencies are resolved
mod->updateDependencyStates();
// add mod resources
this->queueInGDThread([this, mod]() {
auto searchPath = this->getGeodeDirectory() /
GEODE_TEMP_DIRECTORY / mod->getID() / "resources";
CCFileUtils::get()->addSearchPath(searchPath.string().c_str());
this->updateModResources(mod);
});
return Ok(mod);
}
Result<Mod*> Loader::loadModFromFile(ghc::filesystem::path const& file) {
auto res = ModInfo::createFromGeodeFile(file);
if (!res) {
m_invalidMods.push_back(InvalidGeodeFile {
.m_path = file,
.m_reason = res.unwrapErr(),
});
return Err(res.unwrapErr());
}
return this->loadModFromInfo(res.unwrap());
}
Result<> Loader::loadModsFromDirectory(
ghc::filesystem::path const& dir,
bool recursive
) {
log::debug("Searching {}", dir);
for (auto const& entry : ghc::filesystem::directory_iterator(dir)) {
// recursively search directories
if (ghc::filesystem::is_directory(entry) && recursive) {
GEODE_UNWRAP(this->loadModsFromDirectory(entry.path(), true));
continue;
}
// skip this entry if it's not a file
if (!ghc::filesystem::is_regular_file(entry)) {
continue;
}
// skip this entry if its extension is not .geode
if (entry.path().extension() != GEODE_MOD_EXTENSION) {
continue;
}
// skip this entry if it's already loaded
if (map::contains<std::string, Mod*>(m_mods, [entry](Mod* p) -> bool {
return p->m_info.m_path == entry.path();
})) {
continue;
}
// if mods should be loaded immediately, do that
if (m_earlyLoadFinished) {
GEODE_UNWRAP(this->loadModFromFile(entry));
}
// otherwise collect mods to load first to make sure the correct
// versions of the mods are loaded and that early-loaded mods are
// loaded early
else {
auto res = ModInfo::createFromGeodeFile(entry.path());
if (!res) {
m_invalidMods.push_back(InvalidGeodeFile {
.m_path = entry.path(),
.m_reason = res.unwrapErr(),
});
continue;
}
auto info = res.unwrap();
// skip this entry if it's already set to be loaded
if (ranges::contains(m_modsToLoad, info)) {
continue;
}
// add to list of mods to load
m_modsToLoad.push_back(info);
}
}
return Ok();
}
Result<> Loader::refreshModsList() {
log::debug("Loading mods...");
// find mods
for (auto& dir : m_modSearchDirectories) {
GEODE_UNWRAP(this->loadModsFromDirectory(dir));
}
// load early-load mods first
for (auto& mod : m_modsToLoad) {
if (mod.m_needsEarlyLoad) {
GEODE_UNWRAP(this->loadModFromInfo(mod));
}
}
// UI can be loaded now
m_earlyLoadFinished = true;
// load the rest of the mods
for (auto& mod : m_modsToLoad) {
if (!mod.m_needsEarlyLoad) {
GEODE_UNWRAP(this->loadModFromInfo(mod));
}
}
m_modsToLoad.clear();
return Ok();
}
bool Loader::isModInstalled(std::string const& id) const {
return m_mods.count(id) && !m_mods.at(id)->isUninstalled();
}
Mod* Loader::getInstalledMod(std::string const& id) const {
if (m_mods.count(id) && !m_mods.at(id)->isUninstalled()) {
return m_mods.at(id);
}
return nullptr;
}
bool Loader::isModLoaded(std::string const& id) const {
return m_mods.count(id) && m_mods.at(id)->isLoaded();
}
Mod* Loader::getLoadedMod(std::string const& id) const {
if (m_mods.count(id)) {
auto mod = m_mods.at(id);
if (mod->isLoaded()) {
return mod;
}
}
return nullptr;
}
std::vector<Mod*> Loader::getAllMods() {
return map::getValues(m_mods);
}
Mod* Loader::getInternalMod() {
return InternalMod::get();
}
std::vector<InvalidGeodeFile> Loader::getFailedMods() const {
return m_invalidMods;
}
void Loader::updateAllDependencies() {
for (auto const& [_, mod] : m_mods) {
mod->updateDependencyStates();
}
}
void Loader::waitForModsToBeLoaded() {
while (!m_earlyLoadFinished) {}
}
void Loader::dispatchScheduledFunctions(Mod* mod) {
std::lock_guard _(m_scheduledFunctionsMutex);
for (auto& func : m_scheduledFunctions) {
func();
}
m_scheduledFunctions.clear();
}
void Loader::queueInGDThread(ScheduledFunction func) {
InternalLoader::get()->queueInGDThread(func);
}
void Loader::scheduleOnModLoad(Mod* mod, ScheduledFunction func) {
std::lock_guard _(m_scheduledFunctionsMutex);
if (mod) {
return func();
}
m_scheduledFunctions.push_back(func);
}
bool Loader::didLastLaunchCrash() const {
return crashlog::didLastLaunchCrash();
}
ghc::filesystem::path Loader::getCrashLogDirectory() const {
return crashlog::getCrashLogDirectory();
}
void Loader::openPlatformConsole() {
InternalLoader::get()->openPlatformConsole();
}
void Loader::closePlatfromConsole() {
InternalLoader::get()->closePlatformConsole();
}
void Loader::updateModResources(Mod* mod) {
if (!mod->m_info.m_spritesheets.size()) {
return;
}
auto searchPath = this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY / mod->getID() / "resources";
log::debug("Adding resources for {}", mod->getID());
// add spritesheets
for (auto const& sheet : mod->m_info.m_spritesheets) {
log::debug("Adding sheet {}", sheet);
auto png = sheet + ".png";
auto plist = sheet + ".plist";
auto ccfu = CCFileUtils::get();
if (png == std::string(ccfu->fullPathForFilename(png.c_str(), false)) ||
plist == std::string(ccfu->fullPathForFilename(plist.c_str(), false))) {
log::warn(
"The resource dir of \"{}\" is missing \"{}\" png and/or plist files",
mod->m_info.m_id, sheet
);
}
else {
CCTextureCache::get()->addImage(png.c_str(), false);
CCSpriteFrameCache::get()->addSpriteFramesWithFile(plist.c_str());
}
}
}
void Loader::addSearchPaths() {
CCFileUtils::get()->addPriorityPath(
(this->getGeodeDirectory() / GEODE_RESOURCE_DIRECTORY).string().c_str()
);
CCFileUtils::get()->addPriorityPath(
(this->getGeodeDirectory() / GEODE_TEMP_DIRECTORY).string().c_str()
);
}
void Loader::updateResources() {
log::debug("Adding resources");
// add own spritesheets
this->updateModResources(InternalMod::get());
// add mods' spritesheets
for (auto const& [_, mod] : m_mods) {
this->updateModResources(mod);
}
}
ghc::filesystem::path Loader::getGameDirectory() const {
return ghc::filesystem::path(CCFileUtils::sharedFileUtils()->getWritablePath2().c_str());
}
ghc::filesystem::path Loader::getSaveDirectory() const {
#ifdef GEODE_IS_MACOS
// not using ~/Library/Caches
return ghc::filesystem::path("/Users/Shared/Geode");
#elif defined(GEODE_IS_WINDOWS)
return ghc::filesystem::path(
ghc::filesystem::weakly_canonical(
CCFileUtils::sharedFileUtils()->getWritablePath().c_str()
).string()
);
#else
return ghc::filesystem::path(
CCFileUtils::sharedFileUtils()->getWritablePath().c_str()
);
#endif
}
ghc::filesystem::path Loader::getGeodeDirectory() const {
return geode::utils::file::geodeRoot() / GEODE_DIRECTORY;
}
ghc::filesystem::path Loader::getGeodeSaveDirectory() const {
return this->getSaveDirectory() / GEODE_DIRECTORY;
}

View file

@ -89,7 +89,10 @@ std::string log::parse(cocos2d::ccColor4B const& col) {
return fmt::format("rgba({}, {}, {}, {})", col.r, col.g, col.b, col.a);
}
Log::Log(Mod* mod, Severity sev) : m_sender(mod), m_time(log_clock::now()), m_severity(sev) {}
Log::Log(Mod* mod, Severity sev)
: m_sender(mod),
m_time(log_clock::now()),
m_severity(sev) {}
bool Log::operator==(Log const& l) {
return this == &l;
@ -111,8 +114,60 @@ std::string Log::toString(bool logTime) const {
return res;
}
void Log::pushToLoader() {
Loader::get()->pushLog(std::move(*this));
void Logs::setup() {
s_logStream = std::ofstream(
Loader::get()->getGeodeDirectory() /
GEODE_LOG_DIRECTORY /
log::generateLogName()
);
}
void Logs::push(Log&& log) {
std::string logStr = log.toString(true);
InternalLoader::get()->logConsoleMessage(logStr);
s_logStream << logStr << std::endl;
s_logs.emplace_back(std::forward<Log>(log));
}
void Logs::pop(Log* log) {
ranges::remove(s_logs, *log);
}
std::vector<Log*> Logs::list() {
std::vector<Log*> logs;
logs.reserve(s_logs.size());
for (auto& log : s_logs) {
logs.push_back(&log);
}
return logs;
}
void Logs::clear() {
s_logs.clear();
}
std::vector<ComponentTrait*>& Log::getComponents() {
return m_components;
}
log_clock::time_point Log::getTime() const {
return m_time;
}
Mod* Log::getSender() const {
return m_sender;
}
Severity Log::getSeverity() const {
return m_severity;
}
Log::~Log() {
for (auto comp : m_components) {
delete comp;
}
}
std::string geode::log::generateLogName() {
@ -176,5 +231,5 @@ void geode::log::vlogImpl(
// (l.getComponents().push_back(new ComponentBase(args)), ...);
log.pushToLoader();
Logs::push(std::move(log));
}

View file

@ -1,3 +1,4 @@
#include <Geode/cocos/support/zip_support/ZipUtils.h>
#include <Geode/loader/Hook.hpp>
#include <Geode/loader/Loader.hpp>
@ -11,20 +12,25 @@
#include <Geode/utils/string.hpp>
#include <InternalMod.hpp>
#include <about.hpp>
#include <zlib.h>
USE_GEODE_NAMESPACE();
Mod::Mod(ModInfo const& info) {
m_info = info;
m_saveDirPath = Loader::get()->getGeodeSaveDirectory() /
GEODE_MOD_DIRECTORY / info.m_id;
ghc::filesystem::create_directories(m_saveDirPath);
}
Mod::~Mod() {
(void)this->unload();
(void)this->unloadBinary();
}
Result<> Mod::loadSettings() {
// settings
Result<> Mod::loadData() {
ModStateEvent(this, ModEventType::DataLoaded).post();
// settings
// Check if settings exist
auto settPath = m_saveDirPath / "settings.json";
if (ghc::filesystem::exists(settPath)) {
@ -72,7 +78,9 @@ Result<> Mod::loadSettings() {
return Ok();
}
Result<> Mod::saveSettings() {
Result<> Mod::saveData() {
ModStateEvent(this, ModEventType::DataSaved).post();
// settings
auto settPath = m_saveDirPath / "settings.json";
auto json = nlohmann::json::object();
@ -98,133 +106,73 @@ Result<> Mod::saveSettings() {
return Ok();
}
Result<> Mod::loadData() {
if (m_loadDataFunc) {
if (!m_loadDataFunc(m_saveDirPath.string().c_str())) {
log::log(Severity::Error, this, "Mod load data function returned false");
}
}
ModStateEvent(this, ModEventType::DataLoaded).post();
return this->loadSettings();
}
Result<> Mod::saveData() {
if (m_saveDataFunc) {
if (!m_saveDataFunc(m_saveDirPath.string().c_str())) {
log::log(Severity::Error, this, "Mod save data function returned false");
}
}
ModStateEvent(this, ModEventType::DataSaved).post();
return this->saveSettings();
}
Result<> Mod::createTempDir() {
ZipFile unzip(m_info.m_path.string());
if (!unzip.isLoaded()) {
return Err("Unable to unzip " + m_info.m_path.string());
}
if (!unzip.fileExists(m_info.m_binaryName)) {
return Err(
"Unable to find platform binary under the name \"" + m_info.m_binaryName + "\""
);
// Check if temp dir already exists
if (m_tempDirName.string().size()) {
return Ok();
}
// Create geode/temp
auto tempDir = Loader::get()->getGeodeDirectory() / GEODE_TEMP_DIRECTORY;
if (!ghc::filesystem::exists(tempDir)) {
if (!ghc::filesystem::create_directory(tempDir)) {
return Err("Unable to create temp directory for mods!");
}
if (!file::createDirectoryAll(tempDir)) {
return Err("Unable to create Geode temp directory");
}
auto tempPath = ghc::filesystem::path(tempDir) / m_info.m_id;
if (!ghc::filesystem::exists(tempPath) && !ghc::filesystem::create_directories(tempPath)) {
return Err("Unable to create temp directory");
// Create geode/temp/mod.id
auto tempPath = tempDir / m_info.m_id;
if (!file::createDirectoryAll(tempPath)) {
return Err("Unable to create mod temp directory");
}
// Unzip .geode file into temp dir
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_info.m_path));
if (!unzip.hasEntry(m_info.m_binaryName)) {
return Err(fmt::format(
"Unable to find platform binary under the name \"{}\"",
m_info.m_binaryName
));
}
GEODE_UNWRAP(unzip.extractAllTo(tempPath));
// Mark temp dir creation as succesful
m_tempDirName = tempPath;
for (auto file : unzip.getAllFiles()) {
auto path = ghc::filesystem::path(file);
if (path.has_parent_path()) {
if (!ghc::filesystem::exists(tempPath / path.parent_path()) &&
!ghc::filesystem::create_directories(tempPath / path.parent_path())) {
return Err(
"Unable to create directories \"" + path.parent_path().string() + "\""
);
}
}
unsigned long size;
auto data = unzip.getFileData(file, &size);
if (!data || !size) {
return Err("Unable to read \"" + std::string(file) + "\"");
}
auto wrt = utils::file::writeBinary(tempPath / file, byte_array(data, data + size));
if (!wrt) return Err("Unable to write \"" + file + "\": " + wrt.unwrapErr());
}
m_addResourcesToSearchPath = true;
return Ok();
}
Result<> Mod::load() {
if (m_loaded) {
Result<> Mod::loadBinary() {
if (m_binaryLoaded) {
return Ok();
}
#define RETURN_LOAD_ERR(str) \
{ \
m_loadErrorInfo = str; \
return Err(m_loadErrorInfo); \
}
if (!m_tempDirName.string().size()) {
auto err = this->createTempDir();
if (!err) RETURN_LOAD_ERR("Unable to create temp directory: " + err.unwrapErr());
}
GEODE_UNWRAP(this->createTempDir().expect("Unable to create temp directory"));
if (this->hasUnresolvedDependencies()) {
RETURN_LOAD_ERR("Mod has unresolved dependencies");
}
auto err = this->loadPlatformBinary();
if (!err) RETURN_LOAD_ERR(err.unwrapErr());
if (m_implicitLoadFunc) {
auto r = m_implicitLoadFunc(this);
if (!r) {
(void)this->unloadPlatformBinary();
RETURN_LOAD_ERR("Implicit mod entry point returned an error");
}
}
if (m_loadFunc) {
auto r = m_loadFunc(this);
if (!r) {
(void)this->unloadPlatformBinary();
RETURN_LOAD_ERR("Mod entry point returned an error");
}
}
m_loaded = true;
if (m_loadDataFunc) {
if (!m_loadDataFunc(m_saveDirPath.string().c_str())) {
log::log(Severity::Error, this, "Mod load data function returned false");
}
return Err("Mod has unresolved dependencies");
}
GEODE_UNWRAP(this->loadPlatformBinary());
// Call implicit entry point to place hooks etc.
m_implicitLoadFunc(this);
m_binaryLoaded = true;
ModStateEvent(this, ModEventType::Loaded).post();
auto loadRes = this->loadData();
if (!loadRes) {
log::warn("Unable to load data for \"{}\": {}", m_info.m_id, loadRes.unwrapErr());
}
m_loadErrorInfo = "";
Loader::get()->updateAllDependencies();
GEODE_UNWRAP(this->enable());
return Ok();
}
Result<> Mod::unload() {
if (!m_loaded) {
Result<> Mod::unloadBinary() {
if (!m_binaryLoaded) {
return Ok();
}
if (!m_info.m_supportsUnloading) {
return Err("Mod does not support unloading");
}
@ -234,22 +182,16 @@ Result<> Mod::unload() {
return saveRes;
}
if (m_unloadFunc) {
m_unloadFunc();
}
GEODE_UNWRAP(this->disable());
ModStateEvent(this, ModEventType::Unloaded).post();
// Disabling unhooks and unpatches already
for (auto const& hook : m_hooks) {
auto d = this->disableHook(hook);
if (!d) return d;
delete hook;
}
m_hooks.clear();
for (auto const& patch : m_patches) {
if (!patch->restore()) {
return Err("Unable to restore patch at " + std::to_string(patch->getAddress()));
}
delete patch;
}
m_patches.clear();
@ -258,24 +200,18 @@ Result<> Mod::unload() {
if (!res) {
return res;
}
m_loaded = false;
m_binaryLoaded = false;
Loader::get()->updateAllDependencies();
return Ok();
}
Result<> Mod::enable() {
if (!m_loaded) {
return Err("Mod is not loaded");
if (!m_binaryLoaded) {
return this->loadBinary();
}
if (m_enableFunc) {
if (!m_enableFunc()) {
return Err("Mod enable function returned false");
}
}
ModStateEvent(this, ModEventType::Enabled).post();
for (auto const& hook : m_hooks) {
auto d = this->enableHook(hook);
if (!d) return d;
@ -287,22 +223,21 @@ Result<> Mod::enable() {
}
}
ModStateEvent(this, ModEventType::Enabled).post();
m_enabled = true;
return Ok();
}
Result<> Mod::disable() {
if (!m_enabled) {
return Ok();
}
if (!m_info.m_supportsDisabling) {
return Err("Mod does not support disabling");
}
if (m_disableFunc) {
if (!m_disableFunc()) {
return Err("Mod disable function returned false");
}
}
ModStateEvent(this, ModEventType::Disabled).post();
for (auto const& hook : m_hooks) {
@ -323,11 +258,9 @@ Result<> Mod::disable() {
Result<> Mod::uninstall() {
if (m_info.m_supportsDisabling) {
auto r = this->disable();
if (!r) return r;
GEODE_UNWRAP(this->disable());
if (m_info.m_supportsUnloading) {
auto ur = this->unload();
if (!ur) return ur;
GEODE_UNWRAP(this->unloadBinary());
}
}
if (!ghc::filesystem::remove(m_info.m_path)) {
@ -366,18 +299,11 @@ bool Mod::updateDependencyStates() {
if (!dep.m_mod->m_resolved) {
dep.m_mod->m_resolved = true;
dep.m_state = ModResolveState::Resolved;
auto r = dep.m_mod->load();
auto r = dep.m_mod->loadBinary();
if (!r) {
dep.m_state = ModResolveState::Unloaded;
log::log(Severity::Error, dep.m_mod, "{}", r.unwrapErr());
}
else {
auto r = dep.m_mod->enable();
if (!r) {
dep.m_state = ModResolveState::Disabled;
log::log(Severity::Error, dep.m_mod, "{}", r.unwrapErr());
}
}
}
else {
if (dep.m_mod->isEnabled()) {
@ -394,7 +320,7 @@ bool Mod::updateDependencyStates() {
}
if (dep.isUnresolved()) {
m_resolved = false;
(void)this->unload();
(void)this->unloadBinary();
hasUnresolved = true;
}
}
@ -403,16 +329,10 @@ bool Mod::updateDependencyStates() {
m_resolved = true;
if (m_enabled) {
log::debug("Resolved & loading {}", m_info.m_id);
auto r = this->load();
auto r = this->loadBinary();
if (!r) {
log::error("{} Error loading: {}", this, r.unwrapErr());
}
else {
auto r = this->enable();
if (!r) {
log::error("{} Error enabling: {}", this, r.unwrapErr());
}
}
}
else {
log::debug("Resolved {}, however not loading it as it is disabled", m_info.m_id);
@ -444,23 +364,23 @@ ghc::filesystem::path Mod::getSaveDir() const {
return m_saveDirPath;
}
decltype(ModInfo::m_id) Mod::getID() const {
std::string Mod::getID() const {
return m_info.m_id;
}
decltype(ModInfo::m_name) Mod::getName() const {
std::string Mod::getName() const {
return m_info.m_name;
}
decltype(ModInfo::m_developer) Mod::getDeveloper() const {
std::string Mod::getDeveloper() const {
return m_info.m_developer;
}
decltype(ModInfo::m_description) Mod::getDescription() const {
std::optional<std::string> Mod::getDescription() const {
return m_info.m_description;
}
decltype(ModInfo::m_details) Mod::getDetails() const {
std::optional<std::string> Mod::getDetails() const {
return m_info.m_details;
}
@ -476,10 +396,6 @@ ghc::filesystem::path Mod::getBinaryPath() const {
return m_tempDirName / m_info.m_binaryName;
}
std::string Mod::getPath() const {
return m_info.m_path.string();
}
ghc::filesystem::path Mod::getPackagePath() const {
return m_info.m_path;
}
@ -501,7 +417,7 @@ bool Mod::isEnabled() const {
}
bool Mod::isLoaded() const {
return m_loaded;
return m_binaryLoaded;
}
bool Mod::supportsDisabling() const {
@ -526,8 +442,8 @@ bool Mod::depends(std::string const& id) const {
});
}
char const* Mod::expandSpriteName(char const* name) {
static std::unordered_map<std::string, char const*> expanded = {};
const char* Mod::expandSpriteName(const char* name) {
static std::unordered_map<std::string, const char*> expanded = {};
if (expanded.count(name)) {
return expanded[name];
}
@ -564,10 +480,6 @@ bool Mod::hasSetting(std::string const& key) const {
return false;
}
std::string Mod::getLoadErrorInfo() const {
return m_loadErrorInfo;
}
ModJson Mod::getRuntimeInfo() const {
auto json = m_info.toJSON();
@ -581,7 +493,7 @@ ModJson Mod::getRuntimeInfo() const {
obj["patches"].push_back(ModJson(patch->getRuntimeInfo()));
}
obj["enabled"] = m_enabled;
obj["loaded"] = m_loaded;
obj["loaded"] = m_binaryLoaded;
obj["temp-dir"] = this->getTempDir();
obj["save-dir"] = this->getSaveDir();
obj["config-dir"] = this->getConfigDir(false);

View file

@ -11,6 +11,17 @@ static std::string sanitizeDetailsData(std::string const& str) {
return utils::string::replace(str, "\r", "");
}
bool ModInfo::validateID(std::string const& id) {
// ids may not be empty
if (!id.size()) return false;
for (auto const& c : id) {
if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') ||
(c == '-') || (c == '_') || (c == '.')))
return false;
}
return true;
}
Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) {
ModInfo info;
@ -25,7 +36,7 @@ Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) {
using nlohmann::detail::value_t;
root.needs("id").validate(&Mod::validateID).into(info.m_id);
root.needs("id").validate(&ModInfo::validateID).into(info.m_id);
root.needs("version").validate(&VersionInfo::validate).intoAs<std::string>(info.m_version);
root.needs("name").into(info.m_name);
root.needs("developer").into(info.m_developer);
@ -33,12 +44,13 @@ Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& rawJson) {
root.has("repository").into(info.m_repository);
root.has("toggleable").into(info.m_supportsDisabling);
root.has("unloadable").into(info.m_supportsUnloading);
root.has("early-load").into(info.m_needsEarlyLoad);
for (auto& dep : root.has("dependencies").iterate()) {
auto obj = dep.obj();
auto depobj = Dependency {};
obj.needs("id").validate(&Mod::validateID).into(depobj.m_id);
obj.needs("id").validate(&ModInfo::validateID).into(depobj.m_id);
obj.needs("version").validate(&VersionInfo::validate).intoAs<std::string>(depobj.m_version);
obj.has("required").into(depobj.m_required);
obj.checkUnknownKeys();
@ -143,83 +155,61 @@ Result<ModInfo> ModInfo::create(ModJson const& json) {
}
Result<ModInfo> ModInfo::createFromFile(ghc::filesystem::path const& path) {
GEODE_UNWRAP_INTO(auto read, utils::file::readString(path));
try {
GEODE_UNWRAP_INTO(auto read, utils::file::readString(path));
try {
GEODE_UNWRAP_INTO(auto info, ModInfo::create(ModJson::parse(read)));
info.m_path = path;
if (path.has_parent_path()) {
GEODE_UNWRAP(info.addSpecialFiles(path.parent_path()));
}
return Ok(info);
}
catch (std::exception& e) {
return Err("Unable to parse mod.json: " + std::string(e.what()));
GEODE_UNWRAP_INTO(auto info, ModInfo::create(ModJson::parse(read)));
info.m_path = path;
if (path.has_parent_path()) {
GEODE_UNWRAP(info.addSpecialFiles(path.parent_path()));
}
return Ok(info);
}
catch (std::exception const& e) {
return Err(e.what());
catch (std::exception& e) {
return Err("Unable to parse mod.json: " + std::string(e.what()));
}
}
Result<ModInfo> ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) {
ZipFile unzip(path.string());
if (!unzip.isLoaded()) {
return Err("\"" + path.string() + "\": Unable to unzip");
}
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(path));
return ModInfo::createFromGeodeZip(unzip);
}
Result<ModInfo> ModInfo::createFromGeodeZip(file::Unzip& unzip) {
// Check if mod.json exists in zip
if (!unzip.fileExists("mod.json")) {
return Err("\"" + path.string() + "\" is missing mod.json");
if (!unzip.hasEntry("mod.json")) {
return Err("\"" + unzip.getPath().string() + "\" is missing mod.json");
}
// Read mod.json & parse if possible
unsigned long readSize = 0;
auto read = unzip.getFileData("mod.json", &readSize);
if (!read || !readSize) {
return Err("\"" + path.string() + "\": Unable to read mod.json");
}
GEODE_UNWRAP_INTO(auto jsonData, unzip.extract("mod.json"));
ModJson json;
try {
json = ModJson::parse(std::string(read, read + readSize));
json = ModJson::parse(std::string(jsonData.begin(), jsonData.end()));
}
catch (std::exception const& e) {
delete[] read;
return Err(e.what());
}
delete[] read;
if (!json.is_object()) {
return Err(
"\"" + path.string() +
"/mod.json\" does not have an "
"object at root despite expected"
);
}
auto res = ModInfo::create(json);
if (!res) {
return Err("\"" + path.string() + "\" - " + res.unwrapErr());
return Err("\"" + unzip.getPath().string() + "\" - " + res.unwrapErr());
}
auto info = res.unwrap();
info.m_path = path;
info.m_path = unzip.getPath();
GEODE_UNWRAP(info.addSpecialFiles(unzip));
return Ok(info);
}
Result<> ModInfo::addSpecialFiles(ZipFile& unzip) {
Result<> ModInfo::addSpecialFiles(file::Unzip& unzip) {
// unzip known MD files
for (auto& [file, target] : getSpecialFiles()) {
if (unzip.fileExists(file)) {
unsigned long readSize = 0;
auto fileData = unzip.getFileData(file, &readSize);
if (!fileData || !readSize) {
return Err("Unable to read \"" + file + "\"");
}
else {
*target = sanitizeDetailsData(std::string(fileData, fileData + readSize));
}
if (unzip.hasEntry(file)) {
GEODE_UNWRAP_INTO(auto data, unzip.extract(file).expect(
"Unable to extract \"{}\"", file
));
*target = sanitizeDetailsData(std::string(data.begin(), data.end()));
}
}
return Ok();
@ -258,6 +248,10 @@ ModJson ModInfo::getRawJSON() const {
return m_rawJSON;
}
bool ModInfo::operator==(ModInfo const& other) const {
return m_id == other.m_id;
}
void geode::to_json(nlohmann::json& json, ModInfo const& info) {
json = info.toJSON();
}

View file

@ -21,27 +21,10 @@ Result<> Mod::loadPlatformBinary() {
if (dylib) {
this->m_implicitLoadFunc =
findSymbolOrMangled<geode_load>(dylib, "geode_implicit_load", "_geode_implicit_load");
this->m_loadFunc = findSymbolOrMangled<geode_load>(dylib, "geode_load", "_geode_load");
this->m_unloadFunc =
findSymbolOrMangled<geode_unload>(dylib, "geode_unload", "_geode_unload");
this->m_enableFunc =
findSymbolOrMangled<geode_enable>(dylib, "geode_enable", "_geode_enable");
this->m_disableFunc =
findSymbolOrMangled<geode_disable>(dylib, "geode_disable", "_geode_disable");
this->m_saveDataFunc =
findSymbolOrMangled<geode_save_data>(dylib, "geode_save_data", "_geode_save_data");
this->m_loadDataFunc =
findSymbolOrMangled<geode_load_data>(dylib, "geode_load_data", "_geode_load_data");
this->m_settingUpdatedFunc = findSymbolOrMangled<geode_setting_updated>(
dylib, "geode_setting_updated", "_geode_setting_updated"
);
if (!this->m_implicitLoadFunc && !this->m_loadFunc) {
return Err(
"Unable to find mod entry point (lacking both implicit & explicit definition)"
);
if (!this->m_implicitLoadFunc) {
return Err("Unable to find mod entry point");
}
if (this->m_platformInfo) {
delete this->m_platformInfo;
}
@ -58,14 +41,7 @@ Result<> Mod::unloadPlatformBinary() {
delete this->m_platformInfo;
this->m_platformInfo = nullptr;
if (dlclose(dylib) == 0) {
this->m_unloadFunc = nullptr;
this->m_loadFunc = nullptr;
this->m_implicitLoadFunc = nullptr;
this->m_enableFunc = nullptr;
this->m_disableFunc = nullptr;
this->m_saveDataFunc = nullptr;
this->m_loadDataFunc = nullptr;
this->m_settingUpdatedFunc = nullptr;
return Ok();
}
else {

View file

@ -22,25 +22,9 @@ Result<> Mod::loadPlatformBinary() {
if (dylib) {
this->m_implicitLoadFunc =
findSymbolOrMangled<geode_load>(dylib, "geode_implicit_load", "_geode_implicit_load");
this->m_loadFunc = findSymbolOrMangled<geode_load>(dylib, "geode_load", "_geode_load");
this->m_unloadFunc =
findSymbolOrMangled<geode_unload>(dylib, "geode_unload", "_geode_unload");
this->m_enableFunc =
findSymbolOrMangled<geode_enable>(dylib, "geode_enable", "_geode_enable");
this->m_disableFunc =
findSymbolOrMangled<geode_disable>(dylib, "geode_disable", "_geode_disable");
this->m_saveDataFunc =
findSymbolOrMangled<geode_save_data>(dylib, "geode_save_data", "_geode_save_data");
this->m_loadDataFunc =
findSymbolOrMangled<geode_load_data>(dylib, "geode_load_data", "_geode_load_data");
this->m_settingUpdatedFunc = findSymbolOrMangled<geode_setting_updated>(
dylib, "geode_setting_updated", "_geode_setting_updated"
);
if (!this->m_implicitLoadFunc && !this->m_loadFunc) {
return Err(
"Unable to find mod entry point (lacking both implicit & explicit definition)"
);
if (!this->m_implicitLoadFunc) {
return Err("Unable to find mod entry point");
}
if (this->m_platformInfo) {
@ -59,14 +43,7 @@ Result<> Mod::unloadPlatformBinary() {
delete this->m_platformInfo;
this->m_platformInfo = nullptr;
if (dlclose(dylib) == 0) {
this->m_unloadFunc = nullptr;
this->m_loadFunc = nullptr;
this->m_implicitLoadFunc = nullptr;
this->m_enableFunc = nullptr;
this->m_disableFunc = nullptr;
this->m_saveDataFunc = nullptr;
this->m_loadDataFunc = nullptr;
this->m_settingUpdatedFunc = nullptr;
return Ok();
}
else {

View file

@ -24,7 +24,7 @@ void InternalLoader::openPlatformConsole() {
m_platformConsoleOpen = true;
for (auto const& log : Loader::get()->getLogs()) {
for (auto const& log : log::Logs::list()) {
std::cout << log->toString(true) << "\n";
}
}
@ -103,7 +103,7 @@ ipc_done:
void InternalLoader::setupIPC() {
std::thread([]() {
while (!Loader::get()->isUnloading()) {
while (true) {
auto pipe = CreateNamedPipeA(
IPC_PIPE_NAME,
PIPE_ACCESS_DUPLEX,

View file

@ -2,7 +2,8 @@
#ifdef GEODE_IS_WINDOWS
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/Mod.hpp>
USE_GEODE_NAMESPACE();
template <typename T>
@ -71,48 +72,27 @@ std::string getLastWinError() {
}
Result<> Mod::loadPlatformBinary() {
auto load = LoadLibraryW((this->m_tempDirName / this->m_info.m_binaryName).wstring().c_str());
auto load = LoadLibraryW((m_tempDirName / m_info.m_binaryName).wstring().c_str());
if (load) {
this->m_implicitLoadFunc =
findSymbolOrMangled<geode_load>(load, "geode_implicit_load", "_geode_implicit_load@4");
this->m_loadFunc = findSymbolOrMangled<geode_load>(load, "geode_load", "_geode_load@4");
this->m_unloadFunc =
findSymbolOrMangled<geode_unload>(load, "geode_unload", "_geode_unload@0");
this->m_enableFunc =
findSymbolOrMangled<geode_enable>(load, "geode_enable", "_geode_enable@0");
this->m_disableFunc =
findSymbolOrMangled<geode_disable>(load, "geode_disable", "_geode_disable@0");
this->m_saveDataFunc =
findSymbolOrMangled<geode_save_data>(load, "geode_save_data", "_geode_save_data@4");
this->m_loadDataFunc =
findSymbolOrMangled<geode_load_data>(load, "geode_load_data", "_geode_load_data@4");
this->m_settingUpdatedFunc = findSymbolOrMangled<geode_setting_updated>(
load, "geode_setting_updated", "_geode_setting_updated@8"
);
if (!this->m_implicitLoadFunc && !this->m_loadFunc) {
return Err(
"Unable to find mod entry point (lacking both implicit & explicit definition)"
);
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 (this->m_platformInfo) {
delete this->m_platformInfo;
if (m_platformInfo) {
delete m_platformInfo;
}
this->m_platformInfo = new PlatformInfo { load };
m_platformInfo = new PlatformInfo { load };
return Ok();
}
return Err("Unable to load the DLL: " + getLastWinError());
}
Result<> Mod::unloadPlatformBinary() {
auto hmod = this->m_platformInfo->m_hmod;
delete this->m_platformInfo;
auto hmod = m_platformInfo->m_hmod;
delete m_platformInfo;
if (FreeLibrary(hmod)) {
this->m_implicitLoadFunc = nullptr;
this->m_unloadFunc = nullptr;
this->m_loadFunc = nullptr;
m_implicitLoadFunc = nullptr;
return Ok();
}
else {

View file

@ -199,8 +199,7 @@ static void printInfo(std::ostream& stream, LPEXCEPTION_POINTERS info, Mod* faul
}
static void printGeodeInfo(std::ostream& stream) {
stream << "Loader Version: " << Loader::get()->getVersion().toString() << " "
<< Loader::get()->getVersionType() << "\n"
stream << "Loader Version: " << Loader::get()->getVersion().toString() << "\n"
<< "Installed mods: " << Loader::get()->getAllMods().size() << "\n"
<< "Failed mods: " << Loader::get()->getFailedMods().size() << "\n";
}

View file

@ -445,23 +445,21 @@ void ModInfoLayer::onEnableMod(CCObject* pSender) {
return;
}
if (as<CCMenuItemToggler*>(pSender)->isToggled()) {
auto res = m_mod->load();
auto res = m_mod->loadBinary();
if (!res) {
FLAlertLayer::create(nullptr, "Error Loading Mod", res.unwrapErr(), "OK", nullptr)->show();
}
else {
auto res = m_mod->enable();
if (!res) {
FLAlertLayer::create(nullptr, "Error Enabling Mod", res.unwrapErr(), "OK", nullptr)
->show();
}
FLAlertLayer::create(
nullptr, "Error Loading Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
else {
auto res = m_mod->disable();
if (!res) {
FLAlertLayer::create(nullptr, "Error Disabling Mod", res.unwrapErr(), "OK", nullptr)
->show();
FLAlertLayer::create(
nullptr, "Error Disabling Mod",
res.unwrapErr(), "OK", nullptr
)->show();
}
}
if (m_list) m_list->updateAllStates(nullptr);

View file

@ -368,7 +368,7 @@ void ModListLayer::onExit(CCObject*) {
}
void ModListLayer::onReload(CCObject*) {
Loader::get()->refreshMods();
(void)Loader::get()->refreshModsList();
this->reloadList();
}

View file

@ -33,39 +33,37 @@ void ModCell::draw() {
void ModCell::onFailedInfo(CCObject*) {
FLAlertLayer::create(
this, "Error Info",
m_obj->m_info.m_reason.size() ? m_obj->m_info.m_reason : m_obj->m_mod->getLoadErrorInfo(),
m_obj->m_info.m_reason.size() ?
m_obj->m_info.m_reason :
"Unable to load mod",
"OK", "Remove file", 360.f
)
->show();
)->show();
}
void ModCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
if (btn2) {
try {
if (ghc::filesystem::remove(m_obj->m_info.m_file)) {
if (ghc::filesystem::remove(m_obj->m_info.m_path)) {
FLAlertLayer::create(
"File removed", "Removed <cy>" + m_obj->m_info.m_file + "</c>", "OK"
)
->show();
"File removed", "Removed <cy>" + m_obj->m_info.m_path.string() + "</c>", "OK"
)->show();
}
else {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_obj->m_info.m_file + "</c>", "OK"
)
->show();
"Unable to remove <cy>" + m_obj->m_info.m_path.string() + "</c>", "OK"
)->show();
}
}
catch (std::exception& e) {
FLAlertLayer::create(
"Unable to remove file",
"Unable to remove <cy>" + m_obj->m_info.m_file + "</c>: <cr>" +
"Unable to remove <cy>" + m_obj->m_info.m_path.string() + "</c>: <cr>" +
std::string(e.what()) + "</c>",
"OK"
)
->show();
)->show();
}
Loader::get()->refreshMods();
(void)Loader::get()->refreshModsList();
m_list->refreshList();
}
}
@ -83,7 +81,10 @@ void ModCell::setupUnloaded() {
titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f);
m_mainLayer->addChild(titleLabel);
auto pathLabel = CCLabelBMFont::create(m_obj->m_info.m_file.c_str(), "chatFont.fnt");
auto pathLabel = CCLabelBMFont::create(
m_obj->m_info.m_path.string().c_str(),
"chatFont.fnt"
);
pathLabel->setAnchorPoint({ .0f, .5f });
pathLabel->setScale(.43f);
pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f);
@ -297,15 +298,12 @@ void ModCell::onEnable(CCObject* pSender) {
"still see some effects of the mod left however, and you may "
"need to <cg>restart</c> the game to have it fully unloaded.",
"OK"
)
->show();
)->show();
m_list->updateAllStates(this);
return;
}
if (!as<CCMenuItemToggler*>(pSender)->isToggled()) {
if (tryOrAlert(m_obj->m_mod->load(), "Error loading mod")) {
tryOrAlert(m_obj->m_mod->enable(), "Error enabling mod");
}
tryOrAlert(m_obj->m_mod->enable(), "Error enabling mod");
}
else {
tryOrAlert(m_obj->m_mod->disable(), "Error disabling mod");

View file

@ -28,14 +28,14 @@ class ModListLayer;
struct ModObject : public CCObject {
ModObjectType m_type;
Mod* m_mod;
Loader::FailedModInfo m_info;
InvalidGeodeFile m_info;
IndexItem m_index;
inline ModObject(Mod* mod) : m_mod(mod), m_type(ModObjectType::Mod) {
this->autorelease();
};
inline ModObject(Loader::FailedModInfo const& info) :
inline ModObject(InvalidGeodeFile const& info) :
m_info(info), m_type(ModObjectType::Unloaded) {
this->autorelease();
};

View file

@ -8,27 +8,9 @@ bool AdvancedSettingsPopup::setup(Mod* mod) {
this->setTitle("Advanced Settings for " + mod->getName());
auto enableBtn = CCMenuItemToggler::createWithStandardSprites(
this, menu_selector(AdvancedSettingsPopup::onEnableEarlyLoad), .75f
);
enableBtn->setPosition(-100.f, 0.f);
m_buttonMenu->addChild(enableBtn);
auto enableText = CCLabelBMFont::create("Enable Early Load", "bigFont.fnt");
enableText->setPosition(-70.f, 0.f);
enableText->setAnchorPoint({ .0f, .5f });
enableText->setScale(.65f);
m_buttonMenu->addChild(enableText);
return true;
}
void AdvancedSettingsPopup::onEnableEarlyLoad(CCObject* sender) {
Loader::get()->setEarlyLoadMod(
m_mod, !static_cast<CCMenuItemToggler*>(sender)->isToggled()
);
}
AdvancedSettingsPopup* AdvancedSettingsPopup::create(Mod* mod) {
auto ret = new AdvancedSettingsPopup;
if (ret && ret->init(356.f, 220.f, mod)) {

View file

@ -11,8 +11,6 @@ protected:
bool setup(Mod* mod) override;
void onEnableEarlyLoad(CCObject*);
public:
static AdvancedSettingsPopup* create(Mod* mod);
};

View file

@ -5,7 +5,7 @@ USE_GEODE_NAMESPACE();
bool BasedButtonSprite::init(CCNode* ontop, int type, int size, int color) {
if (!CCSprite::initWithSpriteFrameName(Mod::get()->expandSpriteName(
fmt::format("GEODE_blank%02d_%02d_%02d.png", type, size, color).c_str()
fmt::format("GEODE_blank{:02}_{:02}_{:02}.png", type, size, color).c_str()
)))
return false;

View file

@ -1,9 +1,13 @@
#include <Geode/utils/file.hpp>
#include <Geode/utils/string.hpp>
#include <Geode/utils/map.hpp>
#include <fstream>
#include <../support/zip_support/ZipUtils.h>
#include <../support/zip_support/ioapi.h>
#include <../support/zip_support/unzip.h>
USE_GEODE_NAMESPACE();
using namespace geode::utils::file;
Result<std::string> utils::file::readString(ghc::filesystem::path const& path) {
#if _WIN32
@ -112,46 +116,153 @@ Result<std::vector<std::string>> utils::file::listFilesRecursively(std::string c
}
Result<> utils::file::unzipTo(ghc::filesystem::path const& from, ghc::filesystem::path const& to) {
// unzip downloaded
auto unzip = ZipFile(from.string());
if (!unzip.isLoaded()) {
return Err("Unable to unzip index.zip");
}
GEODE_UNWRAP_INTO(auto unzip, Unzip::create(from));
return unzip.extractAllTo(to);
}
if (!ghc::filesystem::exists(to) && !ghc::filesystem::create_directories(to)) {
return Err("Unable to create directories \"" + to.string() + "\"");
}
static constexpr auto MAX_ENTRY_PATH_LEN = 256;
for (auto file : unzip.getAllFiles()) {
// this is a very bad check for seeing
// if file is a directory. it seems to
// work on windows at least. idk why
// getAllFiles returns the directories
// aswell now
if (utils::string::endsWith(file, "\\") || utils::string::endsWith(file, "/")) continue;
struct ZipEntry {
unz_file_pos m_pos;
ZPOS64_T m_compressedSize;
ZPOS64_T m_uncompressedSize;
};
auto zipPath = file;
class file::UnzipImpl final {
public:
using Path = Unzip::Path;
// dont include the github repo folder
file = file.substr(file.find_first_of("/") + 1);
private:
unzFile m_zip;
Path m_zipPath;
std::unordered_map<Path, ZipEntry> m_entries;
auto path = ghc::filesystem::path(file);
if (path.has_parent_path()) {
auto dir = to / path.parent_path();
if (!ghc::filesystem::exists(dir) && !ghc::filesystem::create_directories(dir)) {
return Err("Unable to create directories \"" + dir.string() + "\"");
public:
bool loadEntries() {
// Clear old entries
m_entries.clear();
char fileName[MAX_ENTRY_PATH_LEN + 1];
unz_file_info64 fileInfo;
// Read first file
if (unzGoToFirstFile64(
m_zip, &fileInfo, fileName, sizeof(fileName) - 1
)) {
return false;
}
// Loop over all files
while (true) {
// Read file and add to entries
unz_file_pos pos;
if (unzGetFilePos(m_zip, &pos)) {
m_entries.insert({
fileName, ZipEntry {
.m_pos = pos,
.m_compressedSize = fileInfo.compressed_size,
.m_uncompressedSize = fileInfo.uncompressed_size,
}
});
}
// Read next file, or break on error
if (unzGoToNextFile64(
m_zip, &fileInfo, fileName, sizeof(fileName) - 1)
) {
break;
}
}
unsigned long size;
auto data = unzip.getFileData(zipPath, &size);
if (!data || !size) {
return Err("Unable to read \"" + std::string(zipPath) + "\"");
}
auto wrt = utils::file::writeBinary(to / file, byte_array(data, data + size));
if (!wrt) {
return Err("Unable to write \"" + (to / file).string() + "\": " + wrt.unwrapErr());
}
return true;
}
Result<byte_array> extract(Path const& name) {
if (!m_entries.count(name)) {
return Err("Entry not found");
}
auto entry = m_entries.at(name);
if (!unzGoToFilePos(m_zip, &entry.m_pos)) {
return Err("Unable to navigate to entry");
}
if (!unzOpenCurrentFile(m_zip)) {
return Err("Unable to open entry");
}
byte_array res;
res.reserve(entry.m_uncompressedSize);
auto size = unzReadCurrentFile(m_zip, res.data(), entry.m_uncompressedSize);
if (size == 0 || size == entry.m_uncompressedSize) {
return Err("Unable to extract entry");
}
unzCloseCurrentFile(m_zip);
return Ok(res);
}
std::unordered_map<Path, ZipEntry>& entries() {
return m_entries;
}
Path& path() {
return m_zipPath;
}
UnzipImpl(unzFile zip, Path const& path) : m_zip(zip), m_zipPath(path) {}
~UnzipImpl() {
unzClose(m_zip);
}
};
Unzip::Unzip(UnzipImpl* impl) : m_impl(impl) {}
Unzip::~Unzip() {
if (m_impl) {
delete m_impl;
}
}
Unzip::Unzip(Unzip&& other) : m_impl(other.m_impl) {
other.m_impl = nullptr;
}
Result<Unzip> Unzip::create(Path const& file) {
// todo: make sure unicode paths work
auto zip = unzOpen(file.generic_string().c_str());
if (!zip) {
return Err("Unable to open zip file");
}
auto impl = new UnzipImpl(zip, file);
if (!impl->loadEntries()) {
return Err("Unable to read zip file");
}
return Ok(Unzip(impl));
}
ghc::filesystem::path Unzip::getPath() const {
return m_impl->path();
}
std::vector<ghc::filesystem::path> Unzip::getEntries() const {
return map::getKeys(m_impl->entries());
}
bool Unzip::hasEntry(Path const& name) {
return m_impl->entries().count(name);
}
Result<byte_array> Unzip::extract(Path const& name) {
return m_impl->extract(name);
}
Result<> Unzip::extractTo(Path const& name, Path const& path) {
GEODE_UNWRAP_INTO(auto bytes, m_impl->extract(name));
GEODE_UNWRAP(file::writeBinary(path, bytes));
return Ok();
}
Result<> Unzip::extractAllTo(Path const& dir) {
GEODE_UNWRAP(file::createDirectoryAll(dir));
for (auto& [entry, _] : m_impl->entries()) {
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
}
return Ok();
}