Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
camila314 2023-08-21 11:51:35 -05:00
commit 70584e46dd
68 changed files with 3967 additions and 1385 deletions

View file

@ -1,9 +1,38 @@
# Geode Changelog
## v1.1.1
* Improve installation confirmation popup (9192769)
* Remove unnecessary main thread queues for mod events (38cc38c)
* Fix search and filter buttons being not clickable when over the view/restart button of a mod (ef1f1d1)
* Improve tab textures (108f56a)
* Properly align the borders
* Make the selected and unselected tabs the same height
## v1.1.0
* Fix json library not actually being dynamically exported/imported (5f65d97)
* Update TulipHook, gets rid of keystone dependency and adds stdcall support (efcbf58, 7b90903)
* Make resources.zip platform dependent (e41784e)
* Add utils::string::join (82e128b)
* Add logger nesting: log::pushNest, log::pushNest (7d74f16)
* Add "any" version to comparable versions (2b1dc17)
* Deprecate ModInfo, replaced with ModMetadata (53b52ea)
* Add utils::game::restart (7f449b9, 0e1d639)
* Rework the way dependencies and mod loading works (5200128)
* Early load loads mods before the game starts, while non-early load loads on the loading screen now (e718069)
* Add support for specifying incompatibilities (5200128, 8908235)
* Add support for specifying suggested and recommended optional dependencies (5200128)
* Add UI to select which mods to install (3707418, 73169fb, cd772bd)
* Dependencies/dependants automatically get toggled on toggle (5200128, 6ab542d)
* Add problems list (5200128, aee84c0)
* Show amount of currently loaded mods on the loading screen while they're loading (e718069, 1d5fae8)
* Improve index-related UI (73169fb)
* Remove Android and iOS filters for now
* Add filter to show non-installable mods
* API in quick popups to distinguish between pressing button 1 and the Escape key
* Add "API" label to API mods (cb8759b)
* Fix index not displaying tags (ed5b5c9)
* Change "Cancel" to say "Keep" in the remove mod data on mod uninstall dialogue (cd772bd)
* Fix typos in the word "successfully" (5200128, f316c86)
* Fix MacOS HSV button missing in `CustomizeObjectLayer` (d98cb2d)
* Make missing functions and members private (d98cb2d)
* Update Broma to latest version (0a58432)

View file

@ -1 +1 @@
1.1.0
1.1.1

View file

@ -2330,7 +2330,7 @@ class GJGameLevel : cocos2d::CCNode {
int m_chk;
bool m_isChkValid;
bool m_isCompletionLegitimate;
geode::SeedValueVRS m_normalPercent;
geode::SeedValueVSR m_normalPercent;
geode::SeedValueRSV m_orbCompletion;
geode::SeedValueRSV m_newNormalPercent2;
int m_practicePercent;
@ -2597,6 +2597,7 @@ class GJScoreCell : TableViewCell {
void loadFromScore(GJUserScore* score) = win 0x61440;
void onViewProfile(cocos2d::CCObject* sender) = win 0x62380;
void updateBGColor(int index) = win 0x5c6b0;
GJScoreCell(char const* key, float width, float height) = win 0x613C0;
}
class GJSearchObject : cocos2d::CCNode {
@ -3761,6 +3762,7 @@ class LevelCell : TableViewCell {
void loadCustomLevelCell() = mac 0x1183b0, win 0x5a020;
void updateBGColor(int index) = win 0x5c6b0;
void loadFromLevel(GJGameLevel* level) = win 0x59FD0;
LevelCell(char const* key, float width, float height) = win 0x59F40;
}
class LevelCommentDelegate {

View file

@ -49,7 +49,6 @@ elseif (GEODE_TARGET_PLATFORM STREQUAL "MacOS")
${CURL_LIBRARIES}
${GEODE_LOADER_PATH}/include/link/libfmod.dylib
)
target_compile_options(${PROJECT_NAME} INTERFACE -fms-extensions #[[-Wno-deprecated]] -Wno-ignored-attributes -Os #[[-flto]] #[[-fvisibility=internal]])
set(GEODE_PLATFORM_BINARY "Geode.dylib")

View file

@ -165,7 +165,10 @@ endif()
target_compile_definitions(${PROJECT_NAME} PUBLIC GEODE_EXPORTING MAT_JSON_EXPORTING)
target_compile_definitions(${PROJECT_NAME} PRIVATE _CRT_SECURE_NO_WARNINGS)
target_compile_definitions(${PROJECT_NAME} PRIVATE
GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE
_CRT_SECURE_NO_WARNINGS
)
# These are only needed for building source :-)
if (NOT GEODE_BUILDING_DOCS)

View file

@ -2,6 +2,7 @@
#include "Types.hpp"
#include "ModInfo.hpp"
#include "ModMetadata.hpp"
#include "Event.hpp"
#include "../utils/Result.hpp"
#include "../utils/web.hpp"
@ -107,12 +108,23 @@ namespace geode {
public:
ghc::filesystem::path getPath() const;
ModInfo getModInfo() const;
[[deprecated("use getMetadata instead")]] ModInfo getModInfo() const;
ModMetadata getMetadata() const;
std::string getDownloadURL() const;
std::string getPackageHash() const;
std::unordered_set<PlatformID> getAvailablePlatforms() const;
bool isFeatured() const;
std::unordered_set<std::string> getTags() const;
bool isInstalled() const;
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void setMetadata(ModMetadata const& value);
void setDownloadURL(std::string const& value);
void setPackageHash(std::string const& value);
void setAvailablePlatforms(std::unordered_set<PlatformID> const& value);
void setIsFeatured(bool const& value);
void setTags(std::unordered_set<std::string> const& value);
#endif
IndexItem();
~IndexItem();
@ -204,8 +216,15 @@ namespace geode {
* Get an item from the index by its mod.json
* @param info The mod's info
* @returns The item, or nullptr if the item was not found
* @deprecated Use the ModMetadata overload instead
*/
IndexItemHandle getItem(ModInfo const& info) const;
[[deprecated]] IndexItemHandle getItem(ModInfo const& info) const;
/**
* Get an item from the index by its mod.json
* @param info The mod's metadata
* @returns The item, or nullptr if the item was not found
*/
IndexItemHandle getItem(ModMetadata const& metadata) const;
/**
* Get an item from the index that corresponds to an installed mod
* @param mod An installed mod
@ -224,6 +243,12 @@ namespace geode {
* Check if any of the mods on the index have updates available
*/
bool areUpdatesAvailable() const;
/**
* Checks if the mod and its required dependencies can be installed
* @param item Item to get the list for
* @returns Success if the mod and its required dependencies can be installed, an error otherwise
*/
Result<> canInstall(IndexItemHandle item) const;
/**
* Get the list of items needed to install this item (dependencies, etc.)
* @param item Item to get the list for

View file

@ -4,7 +4,9 @@
#include "../utils/Result.hpp"
#include "../utils/MiniFunction.hpp"
#include "Log.hpp"
#include "ModEvent.hpp"
#include "ModInfo.hpp"
#include "ModMetadata.hpp"
#include "Types.hpp"
#include <atomic>
@ -18,6 +20,25 @@ namespace geode {
std::string reason;
};
struct LoadProblem {
enum class Type : uint8_t {
Unknown,
Suggestion,
Recommendation,
Conflict,
InvalidFile,
Duplicate,
SetupFailed,
LoadFailed,
EnableFailed,
MissingDependency,
PresentIncompatibility
};
Type type;
std::variant<ghc::filesystem::path, ModMetadata, Mod*> cause;
std::string message;
};
class LoaderImpl;
class GEODE_DLL Loader {
@ -36,14 +57,25 @@ namespace geode {
void dispatchScheduledFunctions(Mod* mod);
friend void GEODE_CALL ::geode_implicit_load(Mod*);
Result<Mod*> loadModFromInfo(ModInfo const& info);
[[deprecated]] Result<Mod*> loadModFromInfo(ModInfo const& info);
Mod* takeNextMod();
public:
// TODO: do we want to expose all of these functions?
static Loader* get();
enum class LoadingState : uint8_t {
None,
Queue,
List,
Graph,
EarlyMods,
Mods,
Problems,
Done
};
Result<> saveData();
Result<> loadData();
@ -52,17 +84,19 @@ namespace geode {
VersionInfo maxModVersion();
bool isModVersionSupported(VersionInfo const& version);
Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true);
void refreshModsList();
[[deprecated]] Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
[[deprecated]] void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true);
[[deprecated]] void refreshModsList();
LoadingState getLoadingState();
bool isModInstalled(std::string const& id) const;
Mod* getInstalledMod(std::string const& id) const;
bool isModLoaded(std::string const& id) const;
Mod* getLoadedMod(std::string const& id) const;
std::vector<Mod*> getAllMods();
Mod* getModImpl();
void updateAllDependencies();
std::vector<InvalidGeodeFile> getFailedMods() const;
[[deprecated("use Mod::get instead")]] Mod* getModImpl();
[[deprecated]] void updateAllDependencies();
[[deprecated("use getProblems instead")]] std::vector<InvalidGeodeFile> getFailedMods() const;
std::vector<LoadProblem> getProblems() const;
void updateResources();
void updateResources(bool forceReload);

View file

@ -154,6 +154,7 @@ namespace geode {
bool operator==(Log const& l);
std::string toString(bool logTime = true) const;
std::string toString(bool logTime, uint32_t nestLevel) const;
std::vector<ComponentTrait*>& getComponents();
log_clock::time_point getTime() const;
@ -170,6 +171,7 @@ namespace geode {
private:
static std::vector<Log>& logs();
static std::ofstream& logStream();
static uint32_t& nestLevel();
Logger() = delete;
~Logger() = delete;
@ -179,9 +181,11 @@ namespace geode {
static void setup();
static void push(Log&& log);
static void pop(Log* log);
static void pushNest();
static void popNest();
static std::vector<Log*> list();
static void clear();
};
@ -223,5 +227,12 @@ namespace geode {
void error(Args... args) {
internalLog(Severity::Error, getMod(), args...);
}
static void pushNest() {
Logger::pushNest();
}
static void popNest() {
Logger::popNest();
}
}
}

View file

@ -7,6 +7,7 @@
#include "../utils/general.hpp"
#include "Hook.hpp"
#include "ModInfo.hpp"
#include "ModMetadata.hpp"
#include "Setting.hpp"
#include "Types.hpp"
@ -46,7 +47,6 @@ namespace geode {
std::unique_ptr<Impl> m_impl;
friend class Loader;
friend struct ModInfo;
template <class = void>
static inline GEODE_HIDDEN Mod* sharedMod = nullptr;
@ -66,7 +66,8 @@ namespace geode {
// Protected constructor/destructor
Mod() = delete;
Mod(ModInfo const& info);
[[deprecated]] Mod(ModInfo const& info);
Mod(ModMetadata const& metadata);
~Mod();
std::string getID() const;
@ -79,9 +80,14 @@ namespace geode {
bool isEnabled() const;
bool isLoaded() const;
bool supportsDisabling() const;
bool supportsUnloading() const;
bool wasSuccesfullyLoaded() const;
ModInfo getModInfo() const;
bool canDisable() const;
bool canEnable() const;
bool needsEarlyLoad() const;
[[deprecated]] bool supportsUnloading() const;
[[deprecated("use wasSuccessfullyLoaded instead")]] bool wasSuccesfullyLoaded() const;
bool wasSuccessfullyLoaded() const;
[[deprecated("use getMetadata instead")]] ModInfo getModInfo() const;
ModMetadata getMetadata() const;
ghc::filesystem::path getTempDir() const;
/**
* Get the path to the mod's platform binary (.dll on Windows, .dylib
@ -94,6 +100,11 @@ namespace geode {
*/
ghc::filesystem::path getResourcesDir() const;
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void setMetadata(ModMetadata const& metadata);
std::vector<Mod*> getDependants() const;
#endif
Result<> saveData();
Result<> loadData();
@ -293,7 +304,7 @@ namespace geode {
* @returns Successful result on success,
* errorful result with info on error
*/
Result<> loadBinary();
[[deprecated]] Result<> loadBinary();
/**
* Disable & unload this mod
@ -302,7 +313,7 @@ namespace geode {
* @returns Successful result on success,
* errorful result with info on error
*/
Result<> unloadBinary();
[[deprecated]] Result<> unloadBinary();
/**
* Enable this mod
@ -319,10 +330,7 @@ namespace geode {
Result<> disable();
/**
* 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.
* Disable this mod (if supported), then delete the mod's .geode package.
* @returns Successful result on success,
* errorful result with info on error
*/
@ -335,6 +343,16 @@ namespace geode {
*/
bool depends(std::string const& id) const;
/**
* Update the state of each of the
* dependencies. Depending on if the
* mod has unresolved dependencies,
* it will either be loaded or unloaded
* @returns Error.
* @deprecated No longer needed.
*/
[[deprecated("no longer needed")]] Result<> updateDependencies();
/**
* Check whether all the required
* dependencies for this mod have
@ -344,21 +362,20 @@ namespace geode {
*/
bool hasUnresolvedDependencies() const;
/**
* Update the state of each of the
* dependencies. Depending on if the
* mod has unresolved dependencies,
* it will either be loaded or unloaded
* Check whether none of the
* incompatibilities with this mod are loaded
* @returns True if the mod has unresolved
* dependencies, false if not.
* incompatibilities, false if not.
*/
Result<> updateDependencies();
bool hasUnresolvedIncompatibilities() const;
/**
* Get a list of all the unresolved
* dependencies this mod has
* @returns List of all the unresolved
* dependencies
* @deprecated Use Loader::getProblems instead.
*/
std::vector<Dependency> getUnresolvedDependencies();
[[deprecated("use Loader::getProblems instead")]] std::vector<Dependency> getUnresolvedDependencies();
char const* expandSpriteName(char const* name);

View file

@ -13,7 +13,9 @@ namespace geode {
class Unzip;
}
struct GEODE_DLL Dependency {
class ModMetadata;
struct GEODE_DLL [[deprecated("use ModMetadata::Dependency instead")]] Dependency {
std::string id;
ComparableVersionInfo version;
bool required = false;
@ -21,7 +23,7 @@ namespace geode {
bool isResolved() const;
};
struct IssuesInfo {
struct [[deprecated("use ModMetadata::IssuesInfo instead")]] IssuesInfo {
std::string info;
std::optional<std::string> url;
};
@ -29,11 +31,12 @@ namespace geode {
class ModInfoImpl;
/**
* Represents all the data gatherable
* Represents all the data gather-able
* from mod.json
*/
class GEODE_DLL ModInfo {
class GEODE_DLL [[deprecated("use ModMetadata instead")]] ModInfo {
class Impl;
#pragma warning(suppress : 4996)
std::unique_ptr<Impl> m_impl;
public:
@ -82,7 +85,7 @@ namespace geode {
/**
* The name of the head developer.
* Should be a single name, like
* "HJfod" or "The Geode Team".
* "HJfod" or "Geode Team".
* If the mod has multiple
* developers, this field should
* be one of their name or a team
@ -194,6 +197,9 @@ namespace geode {
static bool validateID(std::string const& id);
operator ModMetadata();
operator ModMetadata() const;
private:
ModJson& rawJSON();
ModJson const& rawJSON() const;
@ -210,11 +216,13 @@ namespace geode {
std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles();
friend class ModInfoImpl;
friend class ModMetadata;
};
}
template <>
struct json::Serialize<geode::ModInfo> {
struct [[deprecated]] json::Serialize<geode::ModInfo> {
static json::Value to_json(geode::ModInfo const& info) {
return info.toJSON();
}

View file

@ -0,0 +1,251 @@
#pragma once
#include "../utils/Result.hpp"
#include "../utils/VersionInfo.hpp"
#include "ModInfo.hpp"
#include "Setting.hpp"
#include "Types.hpp"
#include <json.hpp>
#include <memory>
namespace geode {
namespace utils::file {
class Unzip;
}
struct GEODE_DLL [[deprecated("use ModMetadata::Dependency instead")]] Dependency;
struct [[deprecated("use ModMetadata::IssuesInfo instead")]] IssuesInfo;
class ModMetadataImpl;
/**
* Represents all the data gather-able
* from mod.json
*/
class GEODE_DLL ModMetadata {
class Impl;
std::unique_ptr<Impl> m_impl;
public:
ModMetadata();
explicit ModMetadata(std::string id);
ModMetadata(ModMetadata const& other);
ModMetadata(ModMetadata&& other) noexcept;
ModMetadata& operator=(ModMetadata const& other);
ModMetadata& operator=(ModMetadata&& other) noexcept;
~ModMetadata();
struct GEODE_DLL Dependency {
enum class Importance : uint8_t { Required, Recommended, Suggested };
std::string id;
ComparableVersionInfo version;
Importance importance = Importance::Required;
Mod* mod = nullptr;
[[nodiscard]] bool isResolved() const;
[[deprecated]] operator geode::Dependency();
[[deprecated]] operator geode::Dependency() const;
[[deprecated]] static Dependency fromDeprecated(geode::Dependency const& value);
};
struct GEODE_DLL Incompatibility {
enum class Importance : uint8_t {
Breaking,
Conflicting
};
std::string id;
ComparableVersionInfo version;
Importance importance = Importance::Breaking;
Mod* mod = nullptr;
[[nodiscard]] bool isResolved() const;
};
struct IssuesInfo {
std::string info;
std::optional<std::string> url;
[[deprecated]] operator geode::IssuesInfo();
[[deprecated]] operator geode::IssuesInfo() const;
[[deprecated]] static IssuesInfo fromDeprecated(geode::IssuesInfo const& value);
};
/**
* Path to the mod file
*/
[[nodiscard]] ghc::filesystem::path getPath() const;
/**
* Name of the platform binary within
* the mod zip
*/
[[nodiscard]] std::string getBinaryName() const;
/**
* Mod Version. Should follow semantic versioning.
*/
[[nodiscard]] VersionInfo getVersion() const;
/**
* Human-readable ID of the Mod.
* Recommended to be in the format
* "developer.mod". Not
* guaranteed to be either case-
* nor space-sensitive. Should
* be restricted to the ASCII
* character set.
*/
[[nodiscard]] std::string getID() const;
/**
* Name of the mod. May contain
* spaces & punctuation, but should
* be restricted to the ASCII
* character set.
*/
[[nodiscard]] std::string getName() const;
/**
* The name of the head developer.
* Should be a single name, like
* "HJfod" or "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.
*/
[[nodiscard]] std::string getDeveloper() const;
/**
* Short & concise description of the
* mod.
*/
[[nodiscard]] std::optional<std::string> getDescription() const;
/**
* Detailed description of the mod, written in Markdown (see
* <Geode/ui/MDTextArea.hpp>) for more info
*/
[[nodiscard]] std::optional<std::string> getDetails() const;
/**
* Changelog for the mod, written in Markdown (see
* <Geode/ui/MDTextArea.hpp>) for more info
*/
[[nodiscard]] std::optional<std::string> getChangelog() const;
/**
* Support info for the mod; this means anything to show ways to
* support the mod's development, like donations. Written in Markdown
* (see MDTextArea for more info)
*/
[[nodiscard]] std::optional<std::string> getSupportInfo() const;
/**
* Git Repository of the mod
*/
[[nodiscard]] std::optional<std::string> getRepository() const;
/**
* Info about where users should report issues and request help
*/
[[nodiscard]] std::optional<IssuesInfo> getIssues() const;
/**
* Dependencies
*/
[[nodiscard]] std::vector<Dependency> getDependencies() const;
/**
* Incompatibilities
*/
[[nodiscard]] std::vector<Incompatibility> getIncompatibilities() const;
/**
* Mod spritesheet names
*/
[[nodiscard]] std::vector<std::string> getSpritesheets() const;
/**
* Mod settings
* @note Not a map because insertion order must be preserved
*/
[[nodiscard]] std::vector<std::pair<std::string, Setting>> getSettings() const;
/**
* Whether this mod has to be loaded before the loading screen or not
*/
[[nodiscard]] bool needsEarlyLoad() const;
/**
* Whether this mod is an API or not
*/
[[nodiscard]] bool isAPI() const;
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void setPath(ghc::filesystem::path const& value);
void setBinaryName(std::string const& value);
void setVersion(VersionInfo const& value);
void setID(std::string const& value);
void setName(std::string const& value);
void setDeveloper(std::string const& value);
void setDescription(std::optional<std::string> const& value);
void setDetails(std::optional<std::string> const& value);
void setChangelog(std::optional<std::string> const& value);
void setSupportInfo(std::optional<std::string> const& value);
void setRepository(std::optional<std::string> const& value);
void setIssues(std::optional<IssuesInfo> const& value);
void setDependencies(std::vector<Dependency> const& value);
void setIncompatibilities(std::vector<Incompatibility> const& value);
void setSpritesheets(std::vector<std::string> const& value);
void setSettings(std::vector<std::pair<std::string, Setting>> const& value);
void setNeedsEarlyLoad(bool const& value);
void setIsAPI(bool const& value);
#endif
/**
* Create ModInfo from an unzipped .geode package
*/
static Result<ModMetadata> createFromGeodeZip(utils::file::Unzip& zip);
/**
* Create ModInfo from a .geode package
*/
static Result<ModMetadata> createFromGeodeFile(ghc::filesystem::path const& path);
/**
* Create ModInfo from a mod.json file
*/
static Result<ModMetadata> createFromFile(ghc::filesystem::path const& path);
/**
* Create ModInfo from a parsed json document
*/
static Result<ModMetadata> create(ModJson const& json);
/**
* Convert to JSON. Essentially same as getRawJSON except dynamically
* adds runtime fields like path
*/
[[nodiscard]] ModJson toJSON() const;
/**
* Get the raw JSON file
*/
[[nodiscard]] ModJson getRawJSON() const;
bool operator==(ModMetadata const& other) const;
static bool validateID(std::string const& id);
[[deprecated]] operator ModInfo();
[[deprecated]] operator ModInfo() const;
private:
/**
* Version is passed for backwards
* compatibility if we update the mod.json
* format
*/
static Result<ModMetadata> 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();
friend class Loader;
friend class ModMetadataImpl;
};
}
template <>
struct json::Serialize<geode::ModMetadata> {
static json::Value to_json(geode::ModMetadata const& info) {
return info.toJSON();
}
};

View file

@ -92,7 +92,7 @@ namespace geode {
std::optional<std::string> description;
ValueType defaultValue;
/**
* A regex the string must succesfully match against
* A regex the string must successfully match against
*/
std::optional<std::string> match;

View file

@ -96,4 +96,14 @@ namespace geode {
char const* title, std::string const& content, char const* btn1, char const* btn2,
float width, utils::MiniFunction<void(FLAlertLayer*, bool)> selected, bool doShow = true
);
GEODE_DLL FLAlertLayer* createQuickPopup(
char const* title, std::string const& content, char const* btn1, char const* btn2,
utils::MiniFunction<void(FLAlertLayer*, bool)> selected, bool doShow, bool cancelledByEscape
);
GEODE_DLL FLAlertLayer* createQuickPopup(
char const* title, std::string const& content, char const* btn1, char const* btn2,
float width, utils::MiniFunction<void(FLAlertLayer*, bool)> selected, bool doShow, bool cancelledByEscape
);
}

View file

@ -281,10 +281,10 @@ namespace geode {
into = std::move(GEODE_CONCAT(unwrap_res_, __LINE__).unwrap())
#define GEODE_UNWRAP(...) \
{ \
do { \
auto GEODE_CONCAT(unwrap_res_, __LINE__) = (__VA_ARGS__); \
if (GEODE_CONCAT(unwrap_res_, __LINE__).isErr()) { \
return geode::Err(std::move(GEODE_CONCAT(unwrap_res_, __LINE__).unwrapErr())); \
} \
}
} while(false)
}

View file

@ -13,6 +13,7 @@ namespace geode {
MoreEq,
Less,
More,
Any
};
/**
@ -185,7 +186,7 @@ namespace geode {
protected:
VersionInfo m_version;
VersionCompare m_compare = VersionCompare::Exact;
public:
constexpr ComparableVersionInfo() = default;
constexpr ComparableVersionInfo(
@ -196,12 +197,16 @@ namespace geode {
static Result<ComparableVersionInfo> parse(std::string const& string);
constexpr bool compare(VersionInfo const& version) const {
if (m_compare == VersionCompare::Any) {
return true;
}
// opposing major versions never match
if (m_version.getMajor() != version.getMajor()) {
return false;
}
// the comparison works invertedly as a version like "v1.2.0"
// the comparison works invertedly as a version like "v1.2.0"
// should return true for "<=v1.3.0"
switch (m_compare) {
case VersionCompare::LessEq:

View file

@ -127,3 +127,7 @@ namespace geode::utils::clipboard {
GEODE_DLL bool write(std::string const& data);
GEODE_DLL std::string read();
}
namespace geode::utils::game {
GEODE_DLL void restart();
}

View file

@ -48,6 +48,9 @@ namespace geode::utils::string {
GEODE_DLL std::vector<std::string> split(std::string const& str, std::string const& split);
GEODE_DLL std::vector<std::wstring> split(std::wstring const& str, std::wstring const& split);
GEODE_DLL std::string join(std::vector<std::string> const& strs, std::string const& separator);
GEODE_DLL std::wstring join(std::vector<std::wstring> const& strs, std::wstring const& separator);
GEODE_DLL std::vector<char> split(std::string const& str);
GEODE_DLL std::vector<wchar_t> split(std::wstring const& str);

View file

@ -107,6 +107,11 @@ int main(int argc, char* argv[]) {
if (argc < 2)
return 0;
if (!waitForFile(workingDir / argv[1])) {
showError("There was an error restarting GD. Please, restart the game manually.");
return 0;
}
// restart gd using the provided path
ShellExecuteA(NULL, "open", (workingDir / argv[1]).string().c_str(), "", workingDir.string().c_str(), TRUE);
return 0;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -8,46 +8,48 @@
using namespace geode::prelude;
struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
CCLabelBMFont* m_loadedModsLabel;
bool m_updatingResources;
CustomLoadingLayer() : m_updatingResources(false) {}
CustomLoadingLayer() : m_loadedModsLabel(nullptr), m_updatingResources(false) {}
void updateLoadedModsLabel() {
auto allMods = Loader::get()->getAllMods();
auto count = std::count_if(allMods.begin(), allMods.end(), [&](auto& item) {
return item->isLoaded();
});
auto str = fmt::format("Geode: Loaded {}/{} mods", count, allMods.size());
m_fields->m_loadedModsLabel->setCString(str.c_str());
}
bool init(bool fromReload) {
if (!fromReload) {
Loader::get()->waitForModsToBeLoaded();
}
CCFileUtils::get()->updatePaths();
if (!LoadingLayer::init(fromReload)) return false;
if (!fromReload) {
auto winSize = CCDirector::sharedDirector()->getWinSize();
auto count = Loader::get()->getAllMods().size();
if (fromReload) return true;
auto label = CCLabelBMFont::create(
fmt::format("Geode: Loaded {} mods", count).c_str(),
"goldFont.fnt"
);
label->setPosition(winSize.width / 2, 30.f);
label->setScale(.45f);
label->setID("geode-loaded-info");
this->addChild(label);
auto winSize = CCDirector::sharedDirector()->getWinSize();
// fields have unpredictable destructors
this->addChild(EventListenerNode<ResourceDownloadFilter>::create(
this, &CustomLoadingLayer::updateResourcesProgress
));
m_fields->m_loadedModsLabel = CCLabelBMFont::create("Geode: Loaded 0/0 mods", "goldFont.fnt");
m_fields->m_loadedModsLabel->setPosition(winSize.width / 2, 30.f);
m_fields->m_loadedModsLabel->setScale(.45f);
m_fields->m_loadedModsLabel->setID("geode-loaded-info");
this->addChild(m_fields->m_loadedModsLabel);
this->updateLoadedModsLabel();
// verify loader resources
if (!LoaderImpl::get()->verifyLoaderResources()) {
m_fields->m_updatingResources = true;
this->setUpdateText("Downloading Resources");
}
else {
LoaderImpl::get()->updateSpecialFiles();
}
// fields have unpredictable destructors
this->addChild(EventListenerNode<ResourceDownloadFilter>::create(
this, &CustomLoadingLayer::updateResourcesProgress
));
// verify loader resources
if (!LoaderImpl::get()->verifyLoaderResources()) {
m_fields->m_updatingResources = true;
this->setUpdateText("Downloading Resources");
}
else {
LoaderImpl::get()->updateSpecialFiles();
}
return true;
@ -87,6 +89,13 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
}
void loadAssets() {
if (Loader::get()->getLoadingState() != Loader::LoadingState::Done) {
this->updateLoadedModsLabel();
Loader::get()->queueInGDThread([this]() {
this->loadAssets();
});
return;
}
if (m_fields->m_updatingResources) {
return;
}

View file

@ -23,34 +23,34 @@ class CustomMenuLayer;
static Ref<Notification> INDEX_UPDATE_NOTIF = nullptr;
$execute {
new EventListener<IndexUpdateFilter>(+[](IndexUpdateEvent* event) {
if (!INDEX_UPDATE_NOTIF) return;
std::visit(makeVisitor {
[](UpdateProgress const& prog) {},
[](UpdateFinished const&) {
INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Success);
INDEX_UPDATE_NOTIF->setString("Index Up-to-Date");
INDEX_UPDATE_NOTIF->waitAndHide();
INDEX_UPDATE_NOTIF = nullptr;
},
[](UpdateFailed const& info) {
INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Error);
INDEX_UPDATE_NOTIF->setString(info);
INDEX_UPDATE_NOTIF->setTime(NOTIFICATION_LONG_TIME);
INDEX_UPDATE_NOTIF = nullptr;
},
}, event->status);
});
new EventListener<IndexUpdateFilter>(+[](IndexUpdateEvent* event) {
if (!INDEX_UPDATE_NOTIF) return;
std::visit(makeVisitor {
[](UpdateProgress const& prog) {},
[](UpdateFinished const&) {
INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Success);
INDEX_UPDATE_NOTIF->setString("Index Up-to-Date");
INDEX_UPDATE_NOTIF->waitAndHide();
INDEX_UPDATE_NOTIF = nullptr;
},
[](UpdateFailed const& info) {
INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Error);
INDEX_UPDATE_NOTIF->setString(info);
INDEX_UPDATE_NOTIF->setTime(NOTIFICATION_LONG_TIME);
INDEX_UPDATE_NOTIF = nullptr;
},
}, event->status);
});
};
struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
static void onModify(auto& self) {
if (!self.setHookPriority("MenuLayer::init", GEODE_ID_PRIORITY)) {
static void onModify(auto& self) {
if (!self.setHookPriority("MenuLayer::init", GEODE_ID_PRIORITY)) {
log::warn("Failed to set MenuLayer::init hook priority, node IDs may not work properly");
}
}
CCSprite* m_geodeButton;
CCSprite* m_geodeButton;
bool init() {
if (!MenuLayer::init()) return false;
@ -61,28 +61,28 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
auto winSize = CCDirector::sharedDirector()->getWinSize();
// add geode button
m_fields->m_geodeButton = CircleButtonSprite::createWithSpriteFrameName(
"geode-logo-outline-gold.png"_spr,
1.0f,
CircleBaseColor::Green,
CircleBaseSize::MediumAlt
);
auto geodeBtnSelector = &CustomMenuLayer::onGeode;
if (!m_fields->m_geodeButton) {
geodeBtnSelector = &CustomMenuLayer::onMissingTextures;
m_fields->m_geodeButton = ButtonSprite::create("!!");
}
// add geode button
m_fields->m_geodeButton = CircleButtonSprite::createWithSpriteFrameName(
"geode-logo-outline-gold.png"_spr,
1.0f,
CircleBaseColor::Green,
CircleBaseSize::MediumAlt
);
auto geodeBtnSelector = &CustomMenuLayer::onGeode;
if (!m_fields->m_geodeButton) {
geodeBtnSelector = &CustomMenuLayer::onMissingTextures;
m_fields->m_geodeButton = ButtonSprite::create("!!");
}
auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu"));
auto btn = CCMenuItemSpriteExtra::create(
m_fields->m_geodeButton, this,
static_cast<SEL_MenuHandler>(geodeBtnSelector)
);
btn->setID("geode-button"_spr);
bottomMenu->addChild(btn);
auto btn = CCMenuItemSpriteExtra::create(
m_fields->m_geodeButton, this,
static_cast<SEL_MenuHandler>(geodeBtnSelector)
);
btn->setID("geode-button"_spr);
bottomMenu->addChild(btn);
bottomMenu->updateLayout();
@ -96,54 +96,57 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
static bool shownFailedNotif = false;
if (!shownFailedNotif) {
shownFailedNotif = true;
if (Loader::get()->getFailedMods().size()) {
Notification::create("Some mods failed to load", NotificationIcon::Error)->show();
auto problems = Loader::get()->getProblems();
if (std::any_of(problems.begin(), problems.end(), [&](auto& item) {
return item.type != LoadProblem::Type::Suggestion && item.type != LoadProblem::Type::Recommendation;
})) {
Notification::create("There were problems loading some mods", NotificationIcon::Error)->show();
}
}
// show if the user tried to be naughty and load arbitary DLLs
static bool shownTriedToLoadDlls = false;
if (!shownTriedToLoadDlls) {
shownTriedToLoadDlls = true;
if (Loader::get()->userTriedToLoadDLLs()) {
auto popup = FLAlertLayer::create(
"Hold up!",
"It appears that you have tried to <cr>load DLLs</c> with Geode. "
"Please note that <cy>Geode is incompatible with ALL DLLs</c>, "
"as they can cause Geode mods to <cr>error</c>, or even "
"<cr>crash</c>.\n\n"
"Remove the DLLs / other mod loaders you have, or <cr>proceed at "
"your own risk.</c>",
"OK"
);
popup->m_scene = this;
popup->m_noElasticity = true;
popup->show();
}
}
// show if the user tried to be naughty and load arbitrary DLLs
static bool shownTriedToLoadDlls = false;
if (!shownTriedToLoadDlls) {
shownTriedToLoadDlls = true;
if (Loader::get()->userTriedToLoadDLLs()) {
auto popup = FLAlertLayer::create(
"Hold up!",
"It appears that you have tried to <cr>load DLLs</c> with Geode. "
"Please note that <cy>Geode is incompatible with ALL DLLs</c>, "
"as they can cause Geode mods to <cr>error</c>, or even "
"<cr>crash</c>.\n\n"
"Remove the DLLs / other mod loaders you have, or <cr>proceed at "
"your own risk.</c>",
"OK"
);
popup->m_scene = this;
popup->m_noElasticity = true;
popup->show();
}
}
// show auto update message
static bool shownUpdateInfo = false;
if (LoaderImpl::get()->isNewUpdateDownloaded() && !shownUpdateInfo) {
shownUpdateInfo = true;
auto popup = FLAlertLayer::create(
"Update downloaded",
"A new <cy>update</c> for Geode has been installed! "
"Please <cy>restart the game</c> to apply.",
"OK"
);
popup->m_scene = this;
popup->m_noElasticity = true;
popup->show();
}
// show auto update message
static bool shownUpdateInfo = false;
if (LoaderImpl::get()->isNewUpdateDownloaded() && !shownUpdateInfo) {
shownUpdateInfo = true;
auto popup = FLAlertLayer::create(
"Update downloaded",
"A new <cy>update</c> for Geode has been installed! "
"Please <cy>restart the game</c> to apply.",
"OK"
);
popup->m_scene = this;
popup->m_noElasticity = true;
popup->show();
}
// show crash info
static bool shownLastCrash = false;
if (
Loader::get()->didLastLaunchCrash() &&
!shownLastCrash &&
!Mod::get()->template getSettingValue<bool>("disable-last-crashed-popup")
) {
Loader::get()->didLastLaunchCrash() &&
!shownLastCrash &&
!Mod::get()->template getSettingValue<bool>("disable-last-crashed-popup")
) {
shownLastCrash = true;
auto popup = createQuickPopup(
"Crashed",
@ -163,94 +166,94 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
popup->show();
}
// update mods index
if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) {
this->addChild(EventListenerNode<IndexUpdateFilter>::create(
this, &CustomMenuLayer::onIndexUpdate
));
INDEX_UPDATE_NOTIF = Notification::create(
"Updating Index", NotificationIcon::Loading, 0
);
INDEX_UPDATE_NOTIF->show();
Index::get()->update();
}
// update mods index
if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) {
this->addChild(EventListenerNode<IndexUpdateFilter>::create(
this, &CustomMenuLayer::onIndexUpdate
));
INDEX_UPDATE_NOTIF = Notification::create(
"Updating Index", NotificationIcon::Loading, 0
);
INDEX_UPDATE_NOTIF->show();
Index::get()->update();
}
this->addUpdateIndicator();
return true;
}
this->addUpdateIndicator();
return true;
}
void onIndexUpdate(IndexUpdateEvent* event) {
if (
std::holds_alternative<UpdateFinished>(event->status) ||
std::holds_alternative<UpdateFailed>(event->status)
) {
this->addUpdateIndicator();
}
}
void onIndexUpdate(IndexUpdateEvent* event) {
if (
std::holds_alternative<UpdateFinished>(event->status) ||
std::holds_alternative<UpdateFailed>(event->status)
) {
this->addUpdateIndicator();
}
}
void addUpdateIndicator() {
if (Index::get()->areUpdatesAvailable()) {
auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
icon->setPosition(
m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f }
);
icon->setZOrder(99);
icon->setScale(.5f);
m_fields->m_geodeButton->addChild(icon);
}
}
void addUpdateIndicator() {
if (Index::get()->areUpdatesAvailable()) {
auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
icon->setPosition(
m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f }
);
icon->setZOrder(99);
icon->setScale(.5f);
m_fields->m_geodeButton->addChild(icon);
}
}
void onMissingTextures(CCObject*) {
#ifdef GEODE_IS_DESKTOP
void onMissingTextures(CCObject*) {
#ifdef GEODE_IS_DESKTOP
(void) utils::file::createDirectoryAll(dirs::getGeodeDir() / "update" / "resources");
(void) utils::file::createDirectoryAll(dirs::getGeodeDir() / "update" / "resources");
createQuickPopup(
"Missing Textures",
"You appear to be missing textures, and the automatic texture fixer "
"hasn't fixed the issue.\n"
"Download <cy>resources.zip</c> from the latest release on GitHub, "
"and <cy>unzip its contents</c> into <cb>geode/update/resources</c>.\n"
"Afterwards, <cg>restart the game</c>.\n"
"You may also continue without installing resources, but be aware that "
"you won't be able to open <cr>the Geode menu</c>.",
"Dismiss", "Open Github",
[](auto, bool btn2) {
if (btn2) {
web::openLinkInBrowser("https://github.com/geode-sdk/geode/releases/latest");
file::openFolder(dirs::getGeodeDir() / "update" / "resources");
FLAlertLayer::create(
"Info",
"Opened GitHub in your browser and the destination in "
"your file browser.\n"
"Download <cy>resources.zip</c>, "
"and <cy>unzip its contents</c> into the destination "
"folder.\n"
"<cb>Don't add any new folders to the destination!</c>",
"OK"
)->show();
}
}
);
createQuickPopup(
"Missing Textures",
"You appear to be missing textures, and the automatic texture fixer "
"hasn't fixed the issue.\n"
"Download <cy>resources.zip</c> from the latest release on GitHub, "
"and <cy>unzip its contents</c> into <cb>geode/update/resources</c>.\n"
"Afterwards, <cg>restart the game</c>.\n"
"You may also continue without installing resources, but be aware that "
"you won't be able to open <cr>the Geode menu</c>.",
"Dismiss", "Open Github",
[](auto, bool btn2) {
if (btn2) {
web::openLinkInBrowser("https://github.com/geode-sdk/geode/releases/latest");
file::openFolder(dirs::getGeodeDir() / "update" / "resources");
FLAlertLayer::create(
"Info",
"Opened GitHub in your browser and the destination in "
"your file browser.\n"
"Download <cy>resources.zip</c>, "
"and <cy>unzip its contents</c> into the destination "
"folder.\n"
"<cb>Don't add any new folders to the destination!</c>",
"OK"
)->show();
}
}
);
#else
#else
// dunno if we can auto-create target directory on mobile, nor if the
// user has access to moving stuff there
// dunno if we can auto-create target directory on mobile, nor if the
// user has access to moving stuff there
FLAlertLayer::create(
"Missing Textures",
"You appear to be missing textures, and the automatic texture fixer "
"hasn't fixed the issue.\n"
"**<cy>Report this bug to the Geode developers</c>**. It is very likely "
"that your game <cr>will crash</c> until the issue is resolved.",
"OK"
)->show();
FLAlertLayer::create(
"Missing Textures",
"You appear to be missing textures, and the automatic texture fixer "
"hasn't fixed the issue.\n"
"**<cy>Report this bug to the Geode developers</c>**. It is very likely "
"that your game <cr>will crash</c> until the issue is resolved.",
"OK"
)->show();
#endif
}
#endif
}
void onGeode(CCObject*) {
ModListLayer::scene();

View file

@ -18,7 +18,7 @@ static std::string getDateString(bool filesafe) {
static void printGeodeInfo(std::stringstream& stream) {
stream << "Loader Version: " << Loader::get()->getVersion().toString() << "\n"
<< "Installed mods: " << Loader::get()->getAllMods().size() << "\n"
<< "Failed mods: " << Loader::get()->getFailedMods().size() << "\n";
<< "Problems: " << Loader::get()->getProblems().size() << "\n";
}
static void printMods(std::stringstream& stream) {
@ -84,4 +84,4 @@ std::string crashlog::writeCrashlog(geode::Mod* faultyMod, std::string const& in
actualFile.close();
return file.str();
}
}

View file

@ -10,7 +10,7 @@
namespace crashlog {
/**
* Setup platform-specific crashlog handler
* @returns True if the handler was succesfully installed, false otherwise
* @returns True if the handler was successfully installed, false otherwise
*/
bool GEODE_DLL setupPlatformHandler();
/**

View file

@ -4,7 +4,6 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/Setting.hpp>
#include <Geode/loader/SettingEvent.hpp>
#include <Geode/loader/ModJsonTest.hpp>
#include <Geode/utils/JsonValidation.hpp>
@ -30,7 +29,7 @@ $execute {
});
listenForIPC("loader-info", [](IPCEvent* event) -> json::Value {
return Loader::get()->getModImpl()->getModInfo();
return Mod::get()->getMetadata();
});
listenForIPC("list-mods", [](IPCEvent* event) -> json::Value {
@ -45,13 +44,13 @@ $execute {
if (!dontIncludeLoader) {
res.push_back(
includeRunTimeInfo ? Loader::get()->getModImpl()->getRuntimeInfo() :
Loader::get()->getModImpl()->getModInfo().toJSON()
includeRunTimeInfo ? Mod::get()->getRuntimeInfo() :
Mod::get()->getMetadata().toJSON()
);
}
for (auto& mod : Loader::get()->getAllMods()) {
res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getModInfo().toJSON());
res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getMetadata().toJSON());
}
return res;
@ -67,7 +66,7 @@ int geodeEntry(void* platformData) {
"There was a fatal error setting up "
"the internal mod and Geode can not be loaded: " + internalSetupRes.unwrapErr()
);
LoaderImpl::get()->reset();
LoaderImpl::get()->forceReset();
return 1;
}
@ -85,7 +84,7 @@ int geodeEntry(void* platformData) {
"the loader and Geode can not be loaded. "
"(" + setupRes.unwrapErr() + ")"
);
LoaderImpl::get()->reset();
LoaderImpl::get()->forceReset();
return 1;
}

View file

@ -48,7 +48,7 @@ IndexUpdateFilter::IndexUpdateFilter() {}
class IndexItem::Impl final {
private:
ghc::filesystem::path m_path;
ModInfo m_info;
ModMetadata m_metadata;
std::string m_downloadURL;
std::string m_downloadHash;
std::unordered_set<PlatformID> m_platforms;
@ -64,6 +64,8 @@ public:
static Result<std::shared_ptr<IndexItem>> create(
ghc::filesystem::path const& dir
);
bool isInstalled() const;
};
IndexItem::IndexItem() : m_impl(std::make_unique<Impl>()) {}
@ -74,7 +76,11 @@ ghc::filesystem::path IndexItem::getPath() const {
}
ModInfo IndexItem::getModInfo() const {
return m_impl->m_info;
return this->getMetadata();
}
ModMetadata IndexItem::getMetadata() const {
return m_impl->m_metadata;
}
std::string IndexItem::getDownloadURL() const {
@ -97,13 +103,43 @@ std::unordered_set<std::string> IndexItem::getTags() const {
return m_impl->m_tags;
}
bool IndexItem::isInstalled() const {
return m_impl->isInstalled();
}
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void IndexItem::setMetadata(ModMetadata const& value) {
m_impl->m_metadata = value;
}
void IndexItem::setDownloadURL(std::string const& value) {
m_impl->m_downloadURL = value;
}
void IndexItem::setPackageHash(std::string const& value) {
m_impl->m_downloadHash = value;
}
void IndexItem::setAvailablePlatforms(std::unordered_set<PlatformID> const& value) {
m_impl->m_platforms = value;
}
void IndexItem::setIsFeatured(bool const& value) {
m_impl->m_isFeatured = value;
}
void IndexItem::setTags(std::unordered_set<std::string> const& value) {
m_impl->m_tags = value;
}
#endif
Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir) {
GEODE_UNWRAP_INTO(
auto entry, file::readJson(dir / "entry.json")
.expect("Unable to read entry.json")
);
GEODE_UNWRAP_INTO(
auto info, ModInfo::createFromFile(dir / "mod.json")
auto metadata, ModMetadata::createFromFile(dir / "mod.json")
.expect("Unable to read mod.json: {error}")
);
@ -112,17 +148,22 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir
std::unordered_set<PlatformID> platforms;
for (auto& plat : root.has("platforms").iterate()) {
platforms.insert(PlatformID::from(plat.template get<std::string>()));
platforms.insert(PlatformID::from(plat.get<std::string>()));
}
std::unordered_set<std::string> tags;
for (auto& tag : root.has("tags").iterate()) {
tags.insert(tag.get<std::string>());
}
auto item = std::make_shared<IndexItem>();
item->m_impl->m_path = dir;
item->m_impl->m_info = info;
item->m_impl->m_downloadURL = root.has("mod").obj().has("download").template get<std::string>();
item->m_impl->m_downloadHash = root.has("mod").obj().has("hash").template get<std::string>();
item->m_impl->m_metadata = metadata;
item->m_impl->m_platforms = platforms;
item->m_impl->m_isFeatured = root.has("featured").template get<bool>();
item->m_impl->m_tags = root.has("tags").template get<std::unordered_set<std::string>>();
item->m_impl->m_tags = tags;
root.has("mod").obj().has("download").into(item->m_impl->m_downloadURL);
root.has("mod").obj().has("hash").into(item->m_impl->m_downloadHash);
root.has("featured").into(item->m_impl->m_isFeatured);
if (checker.isError()) {
return Err(checker.getError());
@ -130,6 +171,10 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir
return Ok(item);
}
bool IndexItem::Impl::isInstalled() const {
return ghc::filesystem::exists(dirs::getModsDir() / (m_metadata.getID() + ".geode"));
}
// Helpers
static Result<> flattenGithubRepo(ghc::filesystem::path const& dir) {
@ -333,22 +378,23 @@ void Index::Impl::updateFromLocalTree() {
continue;
}
auto add = addRes.unwrap();
auto info = add->getModInfo();
auto metadata = add->getMetadata();
// check if this major version of this item has already been added
if (m_items[info.id()].count(info.version().getMajor())) {
if (m_items[metadata.getID()].count(metadata.getVersion().getMajor())) {
log::warn(
"Item {}@{} has already been added, skipping",
info.id(), info.version()
metadata.getID(),
metadata.getVersion()
);
continue;
}
// add new major version of this item
m_items[info.id()].insert({
info.version().getMajor(),
m_items[metadata.getID()].insert({metadata.getVersion().getMajor(),
add
});
}
} catch(std::exception& e) {
log::error("Unable to read local index tree: {}", e.what());
IndexUpdateEvent("Unable to read local index tree").post();
return;
}
@ -408,7 +454,7 @@ std::vector<IndexItemHandle> Index::getItemsByDeveloper(
std::vector<IndexItemHandle> res;
for (auto& items : map::values(m_impl->m_items)) {
for (auto& item : items) {
if (item.second->getModInfo().developer() == name) {
if (item.second->getMetadata().getDeveloper() == name) {
res.push_back(item.second);
}
}
@ -441,12 +487,12 @@ IndexItemHandle Index::getItem(
if (version) {
// prefer most major version
for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) {
if (version.value() == item->getModInfo().version()) {
if (version.value() == item->getMetadata().getVersion()) {
return item;
}
}
} else {
if (versions.size()) {
if (!versions.empty()) {
return m_impl->m_items.at(id).rbegin()->second;
}
}
@ -461,7 +507,7 @@ IndexItemHandle Index::getItem(
if (m_impl->m_items.count(id)) {
// prefer most major version
for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) {
if (version.compare(item->getModInfo().version())) {
if (version.compare(item->getMetadata().getVersion())) {
return item;
}
}
@ -473,22 +519,26 @@ IndexItemHandle Index::getItem(ModInfo const& info) const {
return this->getItem(info.id(), info.version());
}
IndexItemHandle Index::getItem(ModMetadata const& metadata) const {
return this->getItem(metadata.getID(), metadata.getVersion());
}
IndexItemHandle Index::getItem(Mod* mod) const {
return this->getItem(mod->getID(), mod->getVersion());
}
bool Index::isUpdateAvailable(IndexItemHandle item) const {
auto installed = Loader::get()->getInstalledMod(item->getModInfo().id());
auto installed = Loader::get()->getInstalledMod(item->getMetadata().getID());
if (!installed) {
return false;
}
return item->getModInfo().version() > installed->getVersion();
return item->getMetadata().getVersion() > installed->getVersion();
}
bool Index::areUpdatesAvailable() const {
for (auto& mod : Loader::get()->getAllMods()) {
auto item = this->getMajorItem(mod->getID());
if (item && item->getModInfo().version() > mod->getVersion()) {
if (item && item->getMetadata().getVersion() > mod->getVersion()) {
return true;
}
}
@ -497,41 +547,84 @@ bool Index::areUpdatesAvailable() const {
// Item installation
Result<> Index::canInstall(IndexItemHandle item) const {
if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) {
return Err("Mod is not available on {}", GEODE_PLATFORM_NAME);
}
for (auto& dep : item->getMetadata().getDependencies()) {
// if the dep is resolved, then all its dependencies must be installed
// already in order for that to have happened
if (dep.isResolved()) continue;
if (dep.importance != ModMetadata::Dependency::Importance::Required) continue;
// check if this dep is available in the index
if (auto depItem = this->getItem(dep.id, dep.version)) {
if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) {
return Err(
"Dependency {} is not available on {}",
dep.id, GEODE_PLATFORM_NAME
);
}
// recursively add dependencies
GEODE_UNWRAP_INTO(auto deps, this->canInstall(depItem));
}
// otherwise user must get this dependency manually from somewhere
else {
return Err(
"Dependency {} version {} not found in the index! Likely "
"reason is that the version of the dependency this mod "
"depends on is not available. Please let the developer "
"of the mod ({}) know!",
dep.id, dep.version.toString(), item->getMetadata().getDeveloper()
);
}
}
return Ok();
}
Result<IndexInstallList> Index::getInstallList(IndexItemHandle item) const {
if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) {
return Err("Mod is not available on {}", GEODE_PLATFORM_NAME);
}
IndexInstallList list;
list.target = item;
for (auto& dep : item->getModInfo().dependencies()) {
if (!dep.isResolved()) {
// check if this dep is available in the index
if (auto depItem = this->getItem(dep.id, dep.version)) {
if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) {
return Err(
"Dependency {} is not available on {}",
dep.id, GEODE_PLATFORM_NAME
);
}
// recursively add dependencies
GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem));
ranges::push(list.list, deps.list);
}
// otherwise user must get this dependency manually from somewhere
// else
else {
for (auto& dep : item->getMetadata().getDependencies()) {
// if the dep is resolved, then all its dependencies must be installed
// already in order for that to have happened
if (dep.isResolved()) continue;
if (dep.importance == ModMetadata::Dependency::Importance::Suggested) continue;
// check if this dep is available in the index
if (auto depItem = this->getItem(dep.id, dep.version)) {
if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) {
// it's fine to not install optional dependencies
if (dep.importance != ModMetadata::Dependency::Importance::Required) continue;
return Err(
"Dependency {} version {} not found in the index! Likely "
"reason is that the version of the dependency this mod "
"depends on is not available. Please let the the developer "
"({}) of the mod know!",
dep.id, dep.version.toString(), item->getModInfo().developer()
"Dependency {} is not available on {}",
dep.id, GEODE_PLATFORM_NAME
);
}
// recursively add dependencies
GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem));
ranges::push(list.list, deps.list);
}
// otherwise user must get this dependency manually from somewhere
else {
// it's fine to not install optional dependencies
if (dep.importance != ModMetadata::Dependency::Importance::Required) continue;
return Err(
"Dependency {} version {} not found in the index! Likely "
"reason is that the version of the dependency this mod "
"depends on is not available. Please let the developer "
"of the mod ({}) know!",
dep.id, dep.version.toString(), item->getMetadata().getDeveloper()
);
}
// if the dep is resolved, then all its dependencies must be installed
// already in order for that to have happened
}
// add this item to the end of the list
list.list.push_back(item);
@ -541,7 +634,7 @@ Result<IndexInstallList> Index::getInstallList(IndexItemHandle item) const {
void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
auto postError = [this, list](std::string const& error) {
m_runningInstallations.erase(list.target);
ModInstallEvent(list.target->getModInfo().id(), error).post();
ModInstallEvent(list.target->getMetadata().getID(), error).post();
};
// If we're at the end of the list, move the downloaded items to mods
@ -550,12 +643,12 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
// Move all downloaded files
for (auto& item : list.list) {
// If the mod is already installed, delete the old .geode file
if (auto mod = Loader::get()->getInstalledMod(item->getModInfo().id())) {
if (auto mod = Loader::get()->getInstalledMod(item->getMetadata().getID())) {
auto res = mod->uninstall();
if (!res) {
return postError(fmt::format(
"Unable to uninstall old version of {}: {}",
item->getModInfo().id(), res.unwrapErr()
item->getMetadata().getID(), res.unwrapErr()
));
}
}
@ -563,21 +656,22 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
// Move the temp file
try {
ghc::filesystem::rename(
dirs::getTempDir() / (item->getModInfo().id() + ".index"),
dirs::getModsDir() / (item->getModInfo().id() + ".geode")
dirs::getTempDir() / (item->getMetadata().getID() + ".index"),
dirs::getModsDir() / (item->getMetadata().getID() + ".geode")
);
} catch(std::exception& e) {
return postError(fmt::format(
"Unable to install {}: {}",
item->getModInfo().id(), e.what()
item->getMetadata().getID(), e.what()
));
}
}
// load mods
Loader::get()->refreshModsList();
ModInstallEvent(list.target->getModInfo().id(), UpdateFinished()).post();
auto const& eventModID = list.target->getMetadata().getID();
Loader::get()->queueInGDThread([eventModID]() {
ModInstallEvent(eventModID, UpdateFinished()).post();
});
return;
}
@ -588,9 +682,9 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
};
auto item = list.list.at(index);
auto tempFile = dirs::getTempDir() / (item->getModInfo().id() + ".index");
auto tempFile = dirs::getTempDir() / (item->getMetadata().getID() + ".index");
m_runningInstallations[list.target] = web::AsyncWebRequest()
.join("install_item_" + item->getModInfo().id())
.join("install_item_" + item->getMetadata().getID())
.fetch(item->getDownloadURL())
.into(tempFile)
.then([=](auto) {
@ -600,25 +694,25 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
return postError(fmt::format(
"Binary file download for {} returned \"404 Not found\". "
"Report this to the Geode development team.",
item->getModInfo().id()
item->getMetadata().getID()
));
}
// Verify checksum
ModInstallEvent(
list.target->getModInfo().id(),
list.target->getMetadata().getID(),
UpdateProgress(
scaledProgress(100),
fmt::format("Verifying {}", item->getModInfo().id())
fmt::format("Verifying {}", item->getMetadata().getID())
)
).post();
if (::calculateHash(tempFile) != item->getPackageHash()) {
return postError(fmt::format(
"Checksum mismatch with {}! (Downloaded file did not match what "
"was expected. Try again, and if the download fails another time, "
"report this to the Geode development team.)",
item->getModInfo().id()
item->getMetadata().getID()
));
}
@ -628,15 +722,15 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) {
.expect([postError, list, item](std::string const& err) {
postError(fmt::format(
"Unable to download {}: {}",
item->getModInfo().id(), err
item->getMetadata().getID(), err
));
})
.progress([this, item, list, scaledProgress](auto&, double now, double total) {
ModInstallEvent(
list.target->getModInfo().id(),
list.target->getMetadata().getID(),
UpdateProgress(
scaledProgress(now / total * 100.0),
fmt::format("Downloading {}", item->getModInfo().id())
fmt::format("Downloading {}", item->getMetadata().getID())
)
).post();
})
@ -671,7 +765,7 @@ void Index::install(IndexItemHandle item) {
this->install(list.unwrap());
} else {
ModInstallEvent(
item->getModInfo().id(),
item->getMetadata().getID(),
UpdateFailed(list.unwrapErr())
).post();
}

View file

@ -63,6 +63,10 @@ void Loader::refreshModsList() {
return m_impl->refreshModsList();
}
Loader::LoadingState Loader::getLoadingState() {
return m_impl->m_loadingState;
}
bool Loader::isModInstalled(std::string const& id) const {
return m_impl->isModInstalled(id);
}
@ -84,7 +88,7 @@ std::vector<Mod*> Loader::getAllMods() {
}
Mod* Loader::getModImpl() {
return m_impl->getModImpl();
return Mod::get();
}
void Loader::updateAllDependencies() {
@ -95,6 +99,10 @@ std::vector<InvalidGeodeFile> Loader::getFailedMods() const {
return m_impl->getFailedMods();
}
std::vector<LoadProblem> Loader::getProblems() const {
return m_impl->getProblems();
}
void Loader::updateResources() {
return m_impl->updateResources();
}

View file

@ -1,29 +1,27 @@
#include "LoaderImpl.hpp"
#include <cocos2d.h>
#include "ModImpl.hpp"
#include "ModMetadataImpl.hpp"
#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/IPC.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/loader/ModJsonTest.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/file.hpp>
#include <Geode/utils/map.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/string.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include "ModImpl.hpp"
#include "ModInfoImpl.hpp"
#include <about.hpp>
#include <crashlog.hpp>
#include <fmt/format.h>
#include <hash.hpp>
#include <iostream>
#include <resources.hpp>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
using namespace geode::prelude;
@ -32,9 +30,9 @@ Loader::Impl* LoaderImpl::get() {
return Loader::get()->m_impl.get();
}
Loader::Impl::Impl() {}
Loader::Impl::Impl() = default;
Loader::Impl::~Impl() {}
Loader::Impl::~Impl() = default;
// Initialization
@ -88,15 +86,10 @@ Result<> Loader::Impl::setup() {
this->setupIPC();
this->createDirectories();
auto sett = this->loadData();
if (!sett) {
log::warn("Unable to load loader settings: {}", sett.unwrapErr());
}
this->refreshModsList();
this->queueInGDThread([]() {
Loader::get()->addSearchPaths();
});
this->addSearchPaths();
this->refreshModGraph();
m_isSetup = true;
@ -128,12 +121,19 @@ std::vector<Mod*> Loader::Impl::getAllMods() {
return map::values(m_mods);
}
Mod* Loader::Impl::getModImpl() {
return Mod::get();
}
std::vector<InvalidGeodeFile> Loader::Impl::getFailedMods() const {
return m_invalidMods;
std::vector<InvalidGeodeFile> inv;
for (auto const& item : this->getProblems()) {
if (item.type != LoadProblem::Type::InvalidFile)
continue;
if (!holds_alternative<ghc::filesystem::path>(item.cause))
continue;
inv.push_back({
std::get<ghc::filesystem::path>(item.cause),
item.message
});
}
return inv;
}
// Version info
@ -166,7 +166,7 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) {
Result<> Loader::Impl::saveData() {
// save mods' data
for (auto& [id, mod] : m_mods) {
Mod::get()->setSavedValue("should-load-" + id, mod->isEnabled());
Mod::get()->setSavedValue("should-load-" + id, mod->isUninstalled() || mod->isEnabled());
auto r = mod->saveData();
if (!r) {
log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr());
@ -174,15 +174,11 @@ Result<> Loader::Impl::saveData() {
}
// save loader data
GEODE_UNWRAP(Mod::get()->saveData());
return Ok();
}
Result<> Loader::Impl::loadData() {
auto e = Mod::get()->loadData();
if (!e) {
log::warn("Unable to load loader settings: {}", e.unwrapErr());
}
for (auto& [_, mod] : m_mods) {
auto r = mod->loadData();
if (!r) {
@ -194,60 +190,6 @@ Result<> Loader::Impl::loadData() {
// Mod loading
Result<Mod*> Loader::Impl::loadModFromInfo(ModInfo const& info) {
if (m_mods.count(info.id())) {
return Err(fmt::format("Mod with ID '{}' already loaded", info.id()));
}
// create Mod instance
auto mod = new Mod(info);
auto setupRes = mod->m_impl->setup();
if (!setupRes) {
// old code artifcat, idk why we are not using unique_ptr TBH
delete mod;
return Err(fmt::format(
"Unable to setup mod '{}': {}",
info.id(), setupRes.unwrapErr()
));
}
m_mods.insert({ info.id(), mod });
mod->m_impl->m_enabled = Mod::get()->getSavedValue<bool>(
"should-load-" + info.id(), true
);
// this loads the mod if its dependencies are resolved
auto dependenciesRes = mod->updateDependencies();
if (!dependenciesRes) {
delete mod;
m_mods.erase(info.id());
return Err(dependenciesRes.unwrapErr());
}
// add mod resources
this->queueInGDThread([this, mod]() {
auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources";
CCFileUtils::get()->addSearchPath(searchPath.string().c_str());
this->updateModResources(mod);
});
return Ok(mod);
}
Result<Mod*> Loader::Impl::loadModFromFile(ghc::filesystem::path const& file) {
auto res = ModInfo::createFromGeodeFile(file);
if (!res) {
m_invalidMods.push_back(InvalidGeodeFile {
.path = file,
.reason = res.unwrapErr(),
});
return Err(res.unwrapErr());
}
return this->loadModFromInfo(res.unwrap());
}
bool Loader::Impl::isModInstalled(std::string const& id) const {
return m_mods.count(id) && !m_mods.at(id)->isUninstalled();
}
@ -260,13 +202,13 @@ Mod* Loader::Impl::getInstalledMod(std::string const& id) const {
}
bool Loader::Impl::isModLoaded(std::string const& id) const {
return m_mods.count(id) && m_mods.at(id)->isLoaded() && m_mods.at(id)->isEnabled();
return m_mods.count(id) && m_mods.at(id)->isLoaded();
}
Mod* Loader::Impl::getLoadedMod(std::string const& id) const {
if (m_mods.count(id)) {
auto mod = m_mods.at(id);
if (mod->isLoaded() && mod->isEnabled()) {
if (mod->isLoaded()) {
return mod;
}
}
@ -274,16 +216,15 @@ Mod* Loader::Impl::getLoadedMod(std::string const& id) const {
}
void Loader::Impl::updateModResources(Mod* mod) {
if (!mod->m_impl->m_info.spritesheets().size()) {
if (mod->getMetadata().getSpritesheets().empty())
return;
}
auto searchPath = mod->getResourcesDir();
log::debug("Adding resources for {}", mod->getID());
// add spritesheets
for (auto const& sheet : mod->m_impl->m_info.spritesheets()) {
for (auto const& sheet : mod->getMetadata().getSpritesheets()) {
log::debug("Adding sheet {}", sheet);
auto png = sheet + ".png";
auto plist = sheet + ".plist";
@ -292,8 +233,8 @@ void Loader::Impl::updateModResources(Mod* mod) {
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_impl->m_info.id(), sheet
R"(The resource dir of "{}" is missing "{}" png and/or plist files)",
mod->getID(), sheet
);
}
else {
@ -305,131 +246,403 @@ void Loader::Impl::updateModResources(Mod* mod) {
// Dependencies and refreshing
void Loader::Impl::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) {
this->loadModsFromDirectory(entry.path(), true);
continue;
}
Result<Mod*> Loader::Impl::loadModFromInfo(ModInfo const& info) {
return Err("Loader::loadModFromInfo is deprecated");
}
// skip this entry if it's not a file
if (!ghc::filesystem::is_regular_file(entry)) {
continue;
}
Result<Mod*> Loader::Impl::loadModFromFile(ghc::filesystem::path const& file) {
return Err("Loader::loadModFromFile is deprecated");
}
// 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_impl->m_info.path() == entry.path();
})) {
continue;
}
// if mods should be loaded immediately, do that
if (m_earlyLoadFinished) {
auto load = this->loadModFromFile(entry);
if (!load) {
log::error("Unable to load {}: {}", entry, load.unwrapErr());
}
}
// 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 {
.path = entry.path(),
.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);
}
}
void Loader::Impl::loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive) {
log::error("Called deprecated stub: Loader::loadModsFromDirectory");
}
void Loader::Impl::refreshModsList() {
log::debug("Loading mods...");
// find mods
for (auto& dir : m_modSearchDirectories) {
this->loadModsFromDirectory(dir);
}
// load early-load mods first
for (auto& mod : m_modsToLoad) {
if (mod.needsEarlyLoad()) {
auto load = this->loadModFromInfo(mod);
if (!load) {
log::error("Unable to load {}: {}", mod.id(), load.unwrapErr());
m_invalidMods.push_back(InvalidGeodeFile {
.path = mod.path(),
.reason = load.unwrapErr(),
});
}
}
}
// UI can be loaded now
m_earlyLoadFinished = true;
m_earlyLoadFinishedCV.notify_all();
// load the rest of the mods
for (auto& mod : m_modsToLoad) {
if (!mod.needsEarlyLoad()) {
auto load = this->loadModFromInfo(mod);
if (!load) {
log::error("Unable to load {}: {}", mod.id(), load.unwrapErr());
m_invalidMods.push_back(InvalidGeodeFile {
.path = mod.path(),
.reason = load.unwrapErr(),
});
}
}
}
m_modsToLoad.clear();
log::error("Called deprecated stub: Loader::refreshModsList");
}
void Loader::Impl::updateAllDependencies() {
for (auto const& [_, mod] : m_mods) {
(void)mod->updateDependencies();
log::error("Called deprecated stub: Loader::updateAllDependencies");
}
void Loader::Impl::queueMods(std::vector<ModMetadata>& modQueue) {
for (auto const& dir : m_modSearchDirectories) {
log::debug("Searching {}", dir);
log::pushNest();
for (auto const& entry : ghc::filesystem::directory_iterator(dir)) {
if (!ghc::filesystem::is_regular_file(entry) ||
entry.path().extension() != GEODE_MOD_EXTENSION)
continue;
log::debug("Found {}", entry.path().filename());
log::pushNest();
auto res = ModMetadata::createFromGeodeFile(entry.path());
if (!res) {
m_problems.push_back({
LoadProblem::Type::InvalidFile,
entry.path(),
res.unwrapErr()
});
log::error("Failed to queue: {}", res.unwrapErr());
log::popNest();
continue;
}
auto modMetadata = res.unwrap();
log::debug("id: {}", modMetadata.getID());
log::debug("version: {}", modMetadata.getVersion());
log::debug("early: {}", modMetadata.needsEarlyLoad() ? "yes" : "no");
if (std::find_if(modQueue.begin(), modQueue.end(), [&](auto& item) {
return modMetadata.getID() == item.getID();
}) != modQueue.end()) {
m_problems.push_back({
LoadProblem::Type::Duplicate,
modMetadata,
"A mod with the same ID is already present."
});
log::error("Failed to queue: a mod with the same ID is already queued");
log::popNest();
continue;
}
modQueue.push_back(modMetadata);
log::popNest();
}
log::popNest();
}
}
void Loader::Impl::waitForModsToBeLoaded() {
auto lock = std::unique_lock(m_earlyLoadFinishedMutex);
log::debug("Waiting for mods to be loaded... {}", bool(m_earlyLoadFinished));
m_earlyLoadFinishedCV.wait(lock, [this] {
return bool(m_earlyLoadFinished);
void Loader::Impl::populateModList(std::vector<ModMetadata>& modQueue) {
std::vector<std::string> toRemove;
for (auto& [id, mod] : m_mods) {
if (id == "geode.loader")
continue;
delete mod;
toRemove.push_back(id);
}
for (auto const& id : toRemove) {
m_mods.erase(id);
}
for (auto const& metadata : modQueue) {
log::debug("{} {}", metadata.getID(), metadata.getVersion());
log::pushNest();
auto mod = new Mod(metadata);
auto res = mod->m_impl->setup();
if (!res) {
m_problems.push_back({
LoadProblem::Type::SetupFailed,
mod,
res.unwrapErr()
});
log::error("Failed to set up: {}", res.unwrapErr());
log::popNest();
continue;
}
m_mods.insert({metadata.getID(), mod});
queueInGDThread([this, mod]() {
auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources";
CCFileUtils::get()->addSearchPath(searchPath.string().c_str());
updateModResources(mod);
});
log::popNest();
}
}
void Loader::Impl::buildModGraph() {
for (auto const& [id, mod] : m_mods) {
log::debug("{}", mod->getID());
log::pushNest();
for (auto& dependency : mod->m_impl->m_metadata.m_impl->m_dependencies) {
log::debug("{}", dependency.id);
if (!m_mods.contains(dependency.id)) {
dependency.mod = nullptr;
continue;
}
dependency.mod = m_mods[dependency.id];
if (!dependency.version.compare(dependency.mod->getVersion())) {
dependency.mod = nullptr;
continue;
}
if (dependency.importance != ModMetadata::Dependency::Importance::Required || dependency.mod == nullptr)
continue;
dependency.mod->m_impl->m_dependants.push_back(mod);
}
for (auto& incompatibility : mod->m_impl->m_metadata.m_impl->m_incompatibilities) {
incompatibility.mod =
m_mods.contains(incompatibility.id) ? m_mods[incompatibility.id] : nullptr;
}
log::popNest();
}
}
void Loader::Impl::loadModGraph(Mod* node, bool early) {
if (early && !node->needsEarlyLoad()) {
m_modsToLoad.push(node);
return;
}
if (node->hasUnresolvedDependencies())
return;
if (node->hasUnresolvedIncompatibilities())
return;
log::debug("{} {}", node->getID(), node->getVersion());
log::pushNest();
if (node->isLoaded()) {
for (auto const& dep : node->m_impl->m_dependants) {
this->loadModGraph(dep, early);
}
log::popNest();
return;
}
log::debug("Load");
auto res = node->m_impl->loadBinary();
if (!res) {
m_problems.push_back({
LoadProblem::Type::LoadFailed,
node,
res.unwrapErr()
});
log::error("Failed to load binary: {}", res.unwrapErr());
log::popNest();
return;
}
if (Mod::get()->getSavedValue<bool>("should-load-" + node->getID(), true)) {
log::debug("Enable");
res = node->m_impl->enable();
if (!res) {
node->m_impl->m_enabled = true;
(void)node->m_impl->disable();
m_problems.push_back({
LoadProblem::Type::EnableFailed,
node,
res.unwrapErr()
});
log::error("Failed to enable: {}", res.unwrapErr());
log::popNest();
return;
}
for (auto const& dep : node->m_impl->m_dependants) {
this->loadModGraph(dep, early);
}
}
log::popNest();
}
void Loader::Impl::findProblems() {
for (auto const& [id, mod] : m_mods) {
log::debug(id);
log::pushNest();
for (auto const& dep : mod->getMetadata().getDependencies()) {
if (dep.mod && dep.mod->isLoaded() && dep.version.compare(dep.mod->getVersion()))
continue;
switch(dep.importance) {
case ModMetadata::Dependency::Importance::Suggested:
m_problems.push_back({
LoadProblem::Type::Suggestion,
mod,
fmt::format("{} {}", dep.id, dep.version.toString())
});
log::info("{} suggests {} {}", id, dep.id, dep.version);
break;
case ModMetadata::Dependency::Importance::Recommended:
m_problems.push_back({
LoadProblem::Type::Recommendation,
mod,
fmt::format("{} {}", dep.id, dep.version.toString())
});
log::warn("{} recommends {} {}", id, dep.id, dep.version);
break;
case ModMetadata::Dependency::Importance::Required:
m_problems.push_back({
LoadProblem::Type::MissingDependency,
mod,
fmt::format("{} {}", dep.id, dep.version.toString())
});
log::error("{} requires {} {}", id, dep.id, dep.version);
break;
}
}
for (auto const& dep : mod->getMetadata().getIncompatibilities()) {
if (!dep.mod || !dep.version.compare(dep.mod->getVersion()))
continue;
switch(dep.importance) {
case ModMetadata::Incompatibility::Importance::Conflicting:
m_problems.push_back({
LoadProblem::Type::Conflict,
mod,
fmt::format("{} {}", dep.id, dep.version.toString())
});
log::warn("{} conflicts with {} {}", id, dep.id, dep.version);
break;
case ModMetadata::Incompatibility::Importance::Breaking:
m_problems.push_back({
LoadProblem::Type::PresentIncompatibility,
mod,
fmt::format("{} {}", dep.id, dep.version.toString())
});
log::error("{} breaks {} {}", id, dep.id, dep.version);
break;
}
}
Mod* myEpicMod = mod; // clang fix
// if the mod is not loaded but there are no problems related to it
if (!mod->isLoaded() && !std::any_of(m_problems.begin(), m_problems.end(), [myEpicMod](auto& item) {
return std::holds_alternative<ModMetadata>(item.cause) &&
std::get<ModMetadata>(item.cause).getID() == myEpicMod->getID() ||
std::holds_alternative<Mod*>(item.cause) &&
std::get<Mod*>(item.cause) == myEpicMod;
})) {
m_problems.push_back({
LoadProblem::Type::Unknown,
mod,
""
});
log::error("{} failed to load for an unknown reason", id);
}
log::popNest();
}
}
void Loader::Impl::refreshModGraph() {
log::info("Refreshing mod graph...");
log::pushNest();
auto begin = std::chrono::high_resolution_clock::now();
if (m_mods.size() > 1) {
log::error("Cannot refresh mod graph after startup");
log::popNest();
return;
}
m_problems.clear();
m_loadingState = LoadingState::Queue;
log::debug("Queueing mods");
log::pushNest();
std::vector<ModMetadata> modQueue;
this->queueMods(modQueue);
log::popNest();
m_loadingState = LoadingState::List;
log::debug("Populating mod list");
log::pushNest();
this->populateModList(modQueue);
modQueue.clear();
log::popNest();
m_loadingState = LoadingState::Graph;
log::debug("Building mod graph");
log::pushNest();
this->buildModGraph();
log::popNest();
m_loadingState = LoadingState::EarlyMods;
log::debug("Loading early mods");
log::pushNest();
for (auto const& dep : Mod::get()->m_impl->m_dependants) {
this->loadModGraph(dep, true);
}
log::popNest();
auto end = std::chrono::high_resolution_clock::now();
auto time = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
log::info("Took {}s. Continuing next frame...", static_cast<float>(time) / 1000.f);
log::popNest();
if (m_modsToLoad.empty())
m_loadingState = LoadingState::Problems;
else
m_loadingState = LoadingState::Mods;
queueInGDThread([]() {
Loader::get()->m_impl->continueRefreshModGraph();
});
}
void Loader::Impl::continueRefreshModGraph() {
log::info("Continuing mod graph refresh...");
log::pushNest();
auto begin = std::chrono::high_resolution_clock::now();
switch (m_loadingState) {
case LoadingState::Mods:
log::debug("Loading mods");
log::pushNest();
this->loadModGraph(m_modsToLoad.front(), false);
log::popNest();
m_modsToLoad.pop();
if (m_modsToLoad.empty())
m_loadingState = LoadingState::Problems;
break;
case LoadingState::Problems:
log::debug("Finding problems");
log::pushNest();
this->findProblems();
log::popNest();
m_loadingState = LoadingState::Done;
break;
default:
m_loadingState = LoadingState::Done;
log::warn("Impossible loading state, resetting to 'Done'! "
"Was Loader::Impl::continueRefreshModGraph() called from the wrong place?");
break;
}
auto end = std::chrono::high_resolution_clock::now();
auto time = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
log::info("Took {}s", static_cast<float>(time) / 1000.f);
if (m_loadingState != LoadingState::Done) {
queueInGDThread([]() {
Loader::get()->m_impl->continueRefreshModGraph();
});
}
log::popNest();
}
std::vector<LoadProblem> Loader::Impl::getProblems() const {
return m_problems;
}
void Loader::Impl::waitForModsToBeLoaded() {
log::debug("Waiting for mods to be loaded...");
// genius
log::warn("waitForModsToBeLoaded() does not wait for mods to be loaded!");
}
bool Loader::Impl::didLastLaunchCrash() const {
return crashlog::didLastLaunchCrash();
}
void Loader::Impl::reset() {
void Loader::Impl::forceReset() {
this->closePlatformConsole();
for (auto& [_, mod] : m_mods) {
delete mod;
}
@ -444,7 +657,7 @@ bool Loader::Impl::isReadyToHook() const {
}
void Loader::Impl::addInternalHook(Hook* hook, Mod* mod) {
m_internalHooks.push_back({hook, mod});
m_internalHooks.emplace_back(hook, mod);
}
bool Loader::Impl::loadHooks() {
@ -560,7 +773,7 @@ void Loader::Impl::tryDownloadLoaderResources(
void Loader::Impl::updateSpecialFiles() {
auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID();
auto res = ModInfoImpl::getImpl(ModImpl::get()->m_info).addSpecialFiles(resourcesDir);
auto res = ModMetadataImpl::getImpl(ModImpl::get()->m_metadata).addSpecialFiles(resourcesDir);
if (res.isErr()) {
log::warn("Unable to add special files: {}", res.unwrapErr());
}

View file

@ -1,3 +1,5 @@
#pragma once
#include "FileWatcher.hpp"
#include <json.hpp>
@ -19,6 +21,7 @@
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <queue>
#include <tulip/TulipHook.hpp>
// TODO: Find a file convention for impl headers
@ -54,9 +57,9 @@ namespace geode {
mutable std::mutex m_mutex;
std::vector<ghc::filesystem::path> m_modSearchDirectories;
std::vector<ModInfo> m_modsToLoad;
std::vector<InvalidGeodeFile> m_invalidMods;
std::vector<LoadProblem> m_problems;
std::unordered_map<std::string, Mod*> m_mods;
std::queue<Mod*> m_modsToLoad;
std::vector<ghc::filesystem::path> m_texturePaths;
bool m_isSetup = false;
@ -65,9 +68,8 @@ namespace geode {
std::optional<json::Value> m_latestGithubRelease;
bool m_isNewUpdateDownloaded = false;
std::condition_variable m_earlyLoadFinishedCV;
std::mutex m_earlyLoadFinishedMutex;
std::atomic_bool m_earlyLoadFinished = false;
LoadingState m_loadingState;
std::vector<utils::MiniFunction<void(void)>> m_gdThreadQueue;
mutable std::mutex m_gdThreadMutex;
bool m_platformConsoleOpen = false;
@ -113,10 +115,10 @@ namespace geode {
friend void GEODE_CALL ::geode_implicit_load(Mod*);
Result<Mod*> loadModFromInfo(ModInfo const& info);
[[deprecated]] Result<Mod*> loadModFromInfo(ModInfo const& info);
Result<> setup();
void reset();
void forceReset();
Result<> saveData();
Result<> loadData();
@ -126,17 +128,26 @@ namespace geode {
VersionInfo maxModVersion();
bool isModVersionSupported(VersionInfo const& version);
Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true);
void refreshModsList();
[[deprecated]] Result<Mod*> loadModFromFile(ghc::filesystem::path const& file);
[[deprecated]] void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true);
[[deprecated]] void refreshModsList();
void queueMods(std::vector<ModMetadata>& modQueue);
void populateModList(std::vector<ModMetadata>& modQueue);
void buildModGraph();
void loadModGraph(Mod* node, bool early);
void findProblems();
void refreshModGraph();
void continueRefreshModGraph();
bool isModInstalled(std::string const& id) const;
Mod* getInstalledMod(std::string const& id) const;
bool isModLoaded(std::string const& id) const;
Mod* getLoadedMod(std::string const& id) const;
std::vector<Mod*> getAllMods();
Mod* getModImpl();
void updateAllDependencies();
std::vector<InvalidGeodeFile> getFailedMods() const;
[[deprecated]] Mod* getModImpl();
[[deprecated]] void updateAllDependencies();
[[deprecated]] std::vector<InvalidGeodeFile> getFailedMods() const;
std::vector<LoadProblem> getProblems() const;
void updateResources();
void updateResources(bool forceReload);
@ -171,7 +182,7 @@ namespace geode {
bool userTriedToLoadDLLs() const;
};
class LoaderImpl {
class LoaderImpl : public Loader::Impl {
public:
static Loader::Impl* get();
};

View file

@ -104,6 +104,9 @@ bool Log::operator==(Log const& l) {
}
std::string Log::toString(bool logTime) const {
return toString(logTime, 0);
}
std::string Log::toString(bool logTime, uint32_t nestLevel) const {
std::string res;
if (logTime) {
@ -112,6 +115,10 @@ std::string Log::toString(bool logTime) const {
res += fmt::format(" [{}]: ", m_sender ? m_sender->getName() : "Geode?");
for (uint32_t i = 0; i < nestLevel; i++) {
res += " ";
}
for (auto& i : m_components) {
res += i->_toString();
}
@ -205,13 +212,17 @@ std::ofstream& Logger::logStream() {
static std::ofstream logStream;
return logStream;
}
uint32_t& Logger::nestLevel() {
static std::uint32_t nestLevel = 0;
return nestLevel;
}
void Logger::setup() {
logStream() = std::ofstream(dirs::getGeodeLogDir() / log::generateLogName());
}
void Logger::push(Log&& log) {
std::string logStr = log.toString(true);
std::string logStr = log.toString(true, nestLevel());
LoaderImpl::get()->logConsoleMessageWithSeverity(logStr, log.getSeverity());
logStream() << logStr << std::endl;
@ -223,6 +234,17 @@ void Logger::pop(Log* log) {
geode::utils::ranges::remove(Logger::logs(), *log);
}
void Logger::pushNest() {
if (nestLevel() == std::numeric_limits<uint32_t>::max())
return;
nestLevel()++;
}
void Logger::popNest() {
if (nestLevel() == 0)
return;
nestLevel()--;
}
std::vector<Log*> Logger::list() {
std::vector<Log*> logs_;
logs_.reserve(logs().size());

View file

@ -4,7 +4,9 @@
using namespace geode::prelude;
#pragma warning(suppress : 4996)
Mod::Mod(ModInfo const& info) : m_impl(std::make_unique<Impl>(this, info)) {}
Mod::Mod(ModMetadata const& metadata) : m_impl(std::make_unique<Impl>(this, metadata)) {}
Mod::~Mod() {}
@ -52,16 +54,35 @@ bool Mod::supportsDisabling() const {
return m_impl->supportsDisabling();
}
bool Mod::canDisable() const {
return m_impl->canDisable();
}
bool Mod::canEnable() const {
return m_impl->canEnable();
}
bool Mod::needsEarlyLoad() const {
return m_impl->needsEarlyLoad();
}
bool Mod::supportsUnloading() const {
return m_impl->supportsUnloading();
return false;
}
bool Mod::wasSuccesfullyLoaded() const {
return m_impl->wasSuccesfullyLoaded();
return this->wasSuccessfullyLoaded();
}
bool Mod::wasSuccessfullyLoaded() const {
return m_impl->wasSuccessfullyLoaded();
}
ModInfo Mod::getModInfo() const {
return m_impl->getModInfo();
return this->getMetadata();
}
ModMetadata Mod::getMetadata() const {
return m_impl->getMetadata();
}
ghc::filesystem::path Mod::getTempDir() const {
@ -76,6 +97,15 @@ ghc::filesystem::path Mod::getResourcesDir() const {
return dirs::getModRuntimeDir() / this->getID() / "resources" / this->getID();
}
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void Mod::setMetadata(ModMetadata const& metadata) {
m_impl->setMetadata(metadata);
}
std::vector<Mod*> Mod::getDependants() const {
return m_impl->getDependants();
}
#endif
Result<> Mod::saveData() {
return m_impl->saveData();
}
@ -145,11 +175,11 @@ Result<> Mod::unpatch(Patch* patch) {
}
Result<> Mod::loadBinary() {
return m_impl->loadBinary();
return Err("Load mod binaries after startup is not supported");
}
Result<> Mod::unloadBinary() {
return m_impl->unloadBinary();
return Err("Unloading mod binaries is not supported");
}
Result<> Mod::enable() {
@ -172,14 +202,19 @@ bool Mod::depends(std::string const& id) const {
return m_impl->depends(id);
}
bool Mod::hasUnresolvedDependencies() const {
return m_impl->hasUnresolvedDependencies();
}
Result<> Mod::updateDependencies() {
return m_impl->updateDependencies();
}
bool Mod::hasUnresolvedDependencies() const {
return m_impl->hasUnresolvedDependencies();
}
bool Mod::hasUnresolvedIncompatibilities() const {
return m_impl->hasUnresolvedIncompatibilities();
}
#pragma warning(suppress : 4996)
std::vector<Dependency> Mod::getUnresolvedDependencies() {
return m_impl->getUnresolvedDependencies();
}

View file

@ -1,6 +1,6 @@
#include "ModImpl.hpp"
#include "LoaderImpl.hpp"
#include "ModInfoImpl.hpp"
#include "ModMetadataImpl.hpp"
#include "about.hpp"
#include <Geode/loader/Dirs.hpp>
@ -25,24 +25,22 @@ Mod::Impl* ModImpl::getImpl(Mod* mod) {
return mod->m_impl.get();
}
Mod::Impl::Impl(Mod* self, ModInfo const& info) : m_self(self), m_info(info) {
Mod::Impl::Impl(Mod* self, ModMetadata const& metadata) : m_self(self), m_metadata(metadata) {
}
Mod::Impl::~Impl() {
(void)this->unloadBinary();
}
Mod::Impl::~Impl() = default;
Result<> Mod::Impl::setup() {
m_saveDirPath = dirs::getModsSaveDir() / m_info.id();
m_saveDirPath = dirs::getModsSaveDir() / m_metadata.getID();
(void) utils::file::createDirectoryAll(m_saveDirPath);
// always create temp dir for all mods, even if disabled, so resources can be loaded
GEODE_UNWRAP(this->createTempDir().expect("Unable to create temp dir: {error}"));
this->setupSettings();
auto loadRes = this->loadData();
if (!loadRes) {
log::warn("Unable to load data for \"{}\": {}", m_info.id(), loadRes.unwrapErr());
log::warn("Unable to load data for \"{}\": {}", m_metadata.getID(), loadRes.unwrapErr());
}
if (LoaderImpl::get()->m_isSetup) {
Loader::get()->updateResources(false);
@ -58,43 +56,52 @@ ghc::filesystem::path Mod::Impl::getSaveDir() const {
}
std::string Mod::Impl::getID() const {
return m_info.id();
return m_metadata.getID();
}
std::string Mod::Impl::getName() const {
return m_info.name();
return m_metadata.getName();
}
std::string Mod::Impl::getDeveloper() const {
return m_info.developer();
return m_metadata.getDeveloper();
}
std::optional<std::string> Mod::Impl::getDescription() const {
return m_info.description();
return m_metadata.getDescription();
}
std::optional<std::string> Mod::Impl::getDetails() const {
return m_info.details();
return m_metadata.getDetails();
}
ModInfo Mod::Impl::getModInfo() const {
return m_info;
ModMetadata Mod::Impl::getMetadata() const {
return m_metadata;
}
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void Mod::Impl::setMetadata(ModMetadata const& metadata) {
m_metadata = metadata;
}
std::vector<Mod*> Mod::Impl::getDependants() const {
return m_dependants;
}
#endif
ghc::filesystem::path Mod::Impl::getTempDir() const {
return m_tempDirName;
}
ghc::filesystem::path Mod::Impl::getBinaryPath() const {
return m_tempDirName / m_info.binaryName();
return m_tempDirName / m_metadata.getBinaryName();
}
ghc::filesystem::path Mod::Impl::getPackagePath() const {
return m_info.path();
return m_metadata.getPath();
}
VersionInfo Mod::Impl::getVersion() const {
return m_info.version();
return m_metadata.getVersion();
}
json::Value& Mod::Impl::getSaveContainer() {
@ -110,14 +117,34 @@ bool Mod::Impl::isLoaded() const {
}
bool Mod::Impl::supportsDisabling() const {
return m_info.supportsDisabling();
return m_metadata.getID() != "geode.loader" && !m_metadata.isAPI();
}
bool Mod::Impl::supportsUnloading() const {
return m_info.supportsUnloading();
bool Mod::Impl::canDisable() const {
auto deps = m_dependants;
return this->supportsDisabling() &&
(deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) {
return item->canDisable();
}));
}
bool Mod::Impl::wasSuccesfullyLoaded() const {
bool Mod::Impl::canEnable() const {
auto deps = m_metadata.getDependencies();
return !this->isUninstalled() &&
(deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) {
return item.isResolved();
}));
}
bool Mod::Impl::needsEarlyLoad() const {
auto deps = m_dependants;
return getMetadata().needsEarlyLoad() ||
!deps.empty() && std::any_of(deps.begin(), deps.end(), [&](auto& item) {
return item->needsEarlyLoad();
});
}
bool Mod::Impl::wasSuccessfullyLoaded() const {
return !this->isEnabled() || this->isLoaded();
}
@ -155,7 +182,7 @@ Result<> Mod::Impl::loadData() {
Severity::Error,
m_self,
"{}: Unable to load value for setting \"{}\"",
m_info.id(),
m_metadata.getID(),
key
);
}
@ -244,7 +271,7 @@ Result<> Mod::Impl::saveData() {
}
void Mod::Impl::setupSettings() {
for (auto& [key, sett] : m_info.settings()) {
for (auto& [key, sett] : m_metadata.getSettings()) {
if (auto value = sett.createDefaultValue()) {
m_settings.emplace(key, std::move(value));
}
@ -262,19 +289,19 @@ void Mod::Impl::registerCustomSetting(std::string const& key, std::unique_ptr<Se
}
bool Mod::Impl::hasSettings() const {
return m_info.settings().size();
return m_metadata.getSettings().size();
}
std::vector<std::string> Mod::Impl::getSettingKeys() const {
std::vector<std::string> keys;
for (auto& [key, _] : m_info.settings()) {
for (auto& [key, _] : m_metadata.getSettings()) {
keys.push_back(key);
}
return keys;
}
std::optional<Setting> Mod::Impl::getSettingDefinition(std::string const& key) const {
for (auto& setting : m_info.settings()) {
for (auto& setting : m_metadata.getSettings()) {
if (setting.first == key) {
return setting.second;
}
@ -290,7 +317,7 @@ SettingValue* Mod::Impl::getSetting(std::string const& key) const {
}
bool Mod::Impl::hasSetting(std::string const& key) const {
for (auto& setting : m_info.settings()) {
for (auto& setting : m_metadata.getSettings()) {
if (setting.first == key) {
return true;
}
@ -301,14 +328,9 @@ bool Mod::Impl::hasSetting(std::string const& key) const {
// Loading, Toggling, Installing
Result<> Mod::Impl::loadBinary() {
log::debug("Loading binary for mod {}", m_info.id());
if (m_binaryLoaded) {
log::debug("Loading binary for mod {}", m_metadata.getID());
if (m_binaryLoaded)
return Ok();
}
if (this->hasUnresolvedDependencies()) {
return Err("Mod has unresolved dependencies");
}
LoaderImpl::get()->provideNextMod(m_self);
@ -316,68 +338,42 @@ Result<> Mod::Impl::loadBinary() {
if (!res) {
// make sure to free up the next mod mutex
LoaderImpl::get()->releaseNextMod();
log::warn("Failed to load binary for mod {}: {}", m_info.id(), res.unwrapErr());
log::error("Failed to load binary for mod {}: {}", m_metadata.getID(), res.unwrapErr());
return res;
}
m_binaryLoaded = true;
LoaderImpl::get()->releaseNextMod();
Loader::get()->queueInGDThread([&]() {
ModStateEvent(m_self, ModEventType::Loaded).post();
});
Loader::get()->updateAllDependencies();
log::debug("Enabling mod {}", m_info.id());
GEODE_UNWRAP(this->enable());
return Ok();
}
Result<> Mod::Impl::unloadBinary() {
if (!m_binaryLoaded) {
return Ok();
}
if (!m_info.supportsUnloading()) {
return Err("Mod does not support unloading");
}
GEODE_UNWRAP(this->saveData());
GEODE_UNWRAP(this->disable());
Loader::get()->queueInGDThread([&]() {
ModStateEvent(m_self, ModEventType::Unloaded).post();
});
// Disabling unhooks and unpatches already
for (auto const& hook : m_hooks) {
delete hook;
}
m_hooks.clear();
for (auto const& patch : m_patches) {
delete patch;
}
m_patches.clear();
GEODE_UNWRAP(this->unloadPlatformBinary());
m_binaryLoaded = false;
Loader::get()->updateAllDependencies();
ModStateEvent(m_self, ModEventType::Loaded).post();
return Ok();
}
Result<> Mod::Impl::enable() {
if (!m_binaryLoaded) {
return this->loadBinary();
if (!m_binaryLoaded)
return Err("Tried to enable {} but its binary is not loaded", m_metadata.getID());
bool enabledDependencies = true;
for (auto const& item : m_metadata.getDependencies()) {
if (!item.isResolved() || !item.mod)
continue;
auto res = item.mod->enable();
if (!res) {
enabledDependencies = false;
log::error("Failed to enable {}: {}", item.id, res.unwrapErr());
}
}
if (!enabledDependencies)
return Err("Mod cannot be enabled because one or more of its dependencies cannot be enabled.");
if (!this->canEnable())
return Err("Mod cannot be enabled because it has unresolved dependencies.");
for (auto const& hook : m_hooks) {
if (!hook) {
log::warn("Hook is null in mod \"{}\"", m_info.name());
log::warn("Hook is null in mod \"{}\"", m_metadata.getName());
continue;
}
if (hook->getAutoEnable()) {
@ -392,50 +388,70 @@ Result<> Mod::Impl::enable() {
}
}
Loader::get()->queueInGDThread([&]() {
ModStateEvent(m_self, ModEventType::Enabled).post();
});
m_enabled = true;
ModStateEvent(m_self, ModEventType::Enabled).post();
return Ok();
}
Result<> Mod::Impl::disable() {
if (!m_enabled) {
if (!m_enabled)
return Ok();
}
if (!m_info.supportsDisabling()) {
return Err("Mod does not support disabling");
if (!this->supportsDisabling())
return Err("Mod does not support disabling.");
if (!this->canDisable())
return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled.");
// disable dependants
bool disabledDependants = true;
for (auto& item : m_dependants) {
auto res = item->disable();
if (res)
continue;
disabledDependants = false;
log::error("Failed to disable {}: {}", item->getID(), res.unwrapErr());
}
Loader::get()->queueInGDThread([&]() {
ModStateEvent(m_self, ModEventType::Disabled).post();
});
if (!disabledDependants)
return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled.");
std::vector<std::string> errors;
for (auto const& hook : m_hooks) {
GEODE_UNWRAP(this->disableHook(hook));
auto res = this->disableHook(hook);
if (!res)
errors.push_back(res.unwrapErr());
}
for (auto const& patch : m_patches) {
if (!patch->restore()) {
return Err("Unable to restore patch at " + std::to_string(patch->getAddress()));
}
auto res = this->unpatch(patch);
if (!res)
errors.push_back(res.unwrapErr());
}
m_enabled = false;
ModStateEvent(m_self, ModEventType::Disabled).post();
if (!errors.empty())
return Err(utils::string::join(errors, "\n"));
return Ok();
}
Result<> Mod::Impl::uninstall() {
if (m_info.supportsDisabling()) {
if (supportsDisabling()) {
GEODE_UNWRAP(this->disable());
if (m_info.supportsUnloading()) {
GEODE_UNWRAP(this->unloadBinary());
}
else {
for (auto& item : m_dependants) {
if (!item->canDisable())
continue;
GEODE_UNWRAP(item->disable());
}
}
try {
ghc::filesystem::remove(m_info.path());
ghc::filesystem::remove(m_metadata.getPath());
}
catch (std::exception& e) {
return Err(
@ -449,57 +465,18 @@ Result<> Mod::Impl::uninstall() {
}
bool Mod::Impl::isUninstalled() const {
return m_self != Mod::get() && !ghc::filesystem::exists(m_info.path());
return m_self != Mod::get() && !ghc::filesystem::exists(m_metadata.getPath());
}
// Dependencies
Result<> Mod::Impl::updateDependencies() {
bool hasUnresolved = false;
for (auto& dep : m_info.dependencies()) {
// set the dependency's loaded mod if such exists
if (!dep.mod) {
dep.mod = Loader::get()->getLoadedMod(dep.id);
// verify loaded dependency version
if (dep.mod && !dep.version.compare(dep.mod->getVersion())) {
dep.mod = nullptr;
}
}
// check if the dependency is loaded
if (dep.mod) {
// update the dependency recursively
GEODE_UNWRAP(dep.mod->updateDependencies());
// enable mod if it's resolved & enabled
if (!dep.mod->hasUnresolvedDependencies()) {
if (dep.mod->isEnabled()) {
GEODE_UNWRAP(dep.mod->loadBinary().expect("Unable to load dependency: {error}"));
}
}
}
// check if the dependency is resolved now
if (!dep.isResolved()) {
GEODE_UNWRAP(this->unloadBinary().expect("Unable to unload mod: {error}"));
hasUnresolved = true;
}
}
// load if there weren't any unresolved dependencies
if (!hasUnresolved && !m_binaryLoaded) {
log::debug("All dependencies for {} found", m_info.id());
if (m_enabled) {
log::debug("Resolved & loading {}", m_info.id());
GEODE_UNWRAP(this->loadBinary());
}
else {
log::debug("Resolved {}, however not loading it as it is disabled", m_info.id());
}
}
return Ok();
return Err("Mod::updateDependencies is no longer needed, "
"as this is handled by Loader::refreshModGraph");
}
bool Mod::Impl::hasUnresolvedDependencies() const {
for (auto const& dep : m_info.dependencies()) {
for (auto const& dep : m_metadata.getDependencies()) {
if (!dep.isResolved()) {
return true;
}
@ -507,10 +484,23 @@ bool Mod::Impl::hasUnresolvedDependencies() const {
return false;
}
std::vector<Dependency> Mod::Impl::getUnresolvedDependencies() {
std::vector<Dependency> unresolved;
for (auto const& dep : m_info.dependencies()) {
bool Mod::Impl::hasUnresolvedIncompatibilities() const {
for (auto const& dep : m_metadata.getIncompatibilities()) {
if (!dep.isResolved()) {
return true;
}
}
return false;
}
// msvc stop fucking screaming please i BEG YOU
#pragma warning(suppress : 4996)
std::vector<Dependency> Mod::Impl::getUnresolvedDependencies() {
#pragma warning(suppress : 4996)
std::vector<Dependency> unresolved;
for (auto const& dep : m_metadata.getDependencies()) {
if (!dep.isResolved()) {
#pragma warning(suppress : 4996)
unresolved.push_back(dep);
}
}
@ -518,7 +508,7 @@ std::vector<Dependency> Mod::Impl::getUnresolvedDependencies() {
}
bool Mod::Impl::depends(std::string const& id) const {
return utils::ranges::contains(m_info.dependencies(), [id](Dependency const& t) {
return utils::ranges::contains(m_metadata.getDependencies(), [id](ModMetadata::Dependency const& t) {
return t.id == id;
});
}
@ -528,7 +518,7 @@ bool Mod::Impl::depends(std::string const& id) const {
Result<> Mod::Impl::enableHook(Hook* hook) {
auto res = hook->enable();
if (!res) {
log::error("Can't enable hook {} for mod {}: {}", hook->getDisplayName(), m_info.id(), res.unwrapErr());
log::error("Can't enable hook {} for mod {}: {}", hook->getDisplayName(), m_metadata.getID(), res.unwrapErr());
}
return res;
@ -541,7 +531,7 @@ Result<> Mod::Impl::disableHook(Hook* hook) {
Result<Hook*> Mod::Impl::addHook(Hook* hook) {
m_hooks.push_back(hook);
if (LoaderImpl::get()->isReadyToHook()) {
if (hook->getAutoEnable()) {
if (this->isEnabled() && hook->getAutoEnable()) {
auto res = this->enableHook(hook);
if (!res) {
delete hook;
@ -582,21 +572,20 @@ Result<Patch*> Mod::Impl::patch(void* address, ByteVector const& data) {
p->m_original = readMemory(address, data.size());
p->m_owner = m_self;
p->m_patch = data;
if (!p->apply()) {
if (this->isEnabled() && !p->apply()) {
delete p;
return Err("Unable to enable patch at " + std::to_string(p->getAddress()));
return Err("Unable to enable patch at " + std::to_string(reinterpret_cast<uintptr_t>(address)));
}
m_patches.push_back(p);
return Ok(p);
}
Result<> Mod::Impl::unpatch(Patch* patch) {
if (patch->restore()) {
ranges::remove(m_patches, patch);
delete patch;
return Ok();
}
return Err("Unable to restore patch!");
if (!patch->restore())
return Err("Unable to restore patch at " + std::to_string(patch->getAddress()));
ranges::remove(m_patches, patch);
delete patch;
return Ok();
}
// Misc.
@ -608,7 +597,7 @@ Result<> Mod::Impl::createTempDir() {
}
// If the info doesn't specify a path, don't do anything
if (m_info.path().string().empty()) {
if (m_metadata.getPath().string().empty()) {
return Ok();
}
@ -619,16 +608,16 @@ Result<> Mod::Impl::createTempDir() {
}
// Create geode/temp/mod.id
auto tempPath = tempDir / m_info.id();
auto tempPath = tempDir / m_metadata.getID();
if (!file::createDirectoryAll(tempPath)) {
return Err("Unable to create mod runtime directory");
}
// Unzip .geode file into temp dir
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_info.path()));
if (!unzip.hasEntry(m_info.binaryName())) {
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_metadata.getPath()));
if (!unzip.hasEntry(m_metadata.getBinaryName())) {
return Err(
fmt::format("Unable to find platform binary under the name \"{}\"", m_info.binaryName())
fmt::format("Unable to find platform binary under the name \"{}\"", m_metadata.getBinaryName())
);
}
GEODE_UNWRAP(unzip.extractAllTo(tempPath));
@ -640,7 +629,7 @@ Result<> Mod::Impl::createTempDir() {
}
ghc::filesystem::path Mod::Impl::getConfigDir(bool create) const {
auto dir = dirs::getModConfigDir() / m_info.id();
auto dir = dirs::getModConfigDir() / m_metadata.getID();
if (create) {
(void)file::createDirectoryAll(dir);
}
@ -651,8 +640,8 @@ char const* Mod::Impl::expandSpriteName(char const* name) {
static std::unordered_map<std::string, char const*> expanded = {};
if (expanded.count(name)) return expanded[name];
auto exp = new char[strlen(name) + 2 + m_info.id().size()];
auto exps = m_info.id() + "/" + name;
auto exp = new char[strlen(name) + 2 + m_metadata.getID().size()];
auto exps = m_metadata.getID() + "/" + name;
memcpy(exp, exps.c_str(), exps.size() + 1);
expanded[name] = exp;
@ -661,7 +650,7 @@ char const* Mod::Impl::expandSpriteName(char const* name) {
}
ModJson Mod::Impl::getRuntimeInfo() const {
auto json = m_info.toJSON();
auto json = m_metadata.toJSON();
auto obj = json::Object();
obj["hooks"] = json::Array();
@ -682,7 +671,7 @@ ModJson Mod::Impl::getRuntimeInfo() const {
return json;
}
static Result<ModInfo> getModImplInfo() {
static Result<ModMetadata> getModImplInfo() {
std::string err;
json::Value json;
try {
@ -691,34 +680,30 @@ static Result<ModInfo> getModImplInfo() {
return Err("Unable to parse mod.json: " + std::string(err.what()));
}
GEODE_UNWRAP_INTO(auto info, ModInfo::create(json));
info.supportsDisabling() = false;
GEODE_UNWRAP_INTO(auto info, ModMetadata::create(json));
return Ok(info);
}
Mod* Loader::Impl::createInternalMod() {
auto& mod = Mod::sharedMod<>;
if (!mod) {
auto infoRes = getModImplInfo();
if (!infoRes) {
LoaderImpl::get()->platformMessageBox(
"Fatal Internal Error",
"Unable to create internal mod info: \"" + infoRes.unwrapErr() +
"\"\n"
"This is a fatal internal error in the loader, please "
"contact Geode developers immediately!"
);
auto info = ModInfo();
info.id() = "geode.loader";
mod = new Mod(info);
}
else {
mod = new Mod(infoRes.unwrap());
}
mod->m_impl->m_binaryLoaded = true;
mod->m_impl->m_enabled = true;
m_mods.insert({ mod->getID(), mod });
if (mod) return mod;
auto infoRes = getModImplInfo();
if (!infoRes) {
LoaderImpl::get()->platformMessageBox(
"Fatal Internal Error",
"Unable to create internal mod info: \"" + infoRes.unwrapErr() +
"\"\n"
"This is a fatal internal error in the loader, please "
"contact Geode developers immediately!"
);
mod = new Mod(ModMetadata("geode.loader"));
}
else {
mod = new Mod(infoRes.unwrap());
}
mod->m_impl->m_binaryLoaded = true;
mod->m_impl->m_enabled = true;
m_mods.insert({ mod->getID(), mod });
return mod;
}

View file

@ -7,9 +7,9 @@ namespace geode {
public:
Mod* m_self;
/**
* Mod info
* Mod metadata
*/
ModInfo m_info;
ModMetadata m_metadata;
/**
* Platform-specific info
*/
@ -39,12 +39,11 @@ namespace geode {
*/
ghc::filesystem::path m_saveDirPath;
/**
* Pointers to mods that depend on
* this Mod. Makes it possible to
* enable / disable them automatically,
* Pointers to mods that depend on this Mod.
* Makes it possible to enable / disable them automatically,
* when their dependency is disabled.
*/
std::vector<Mod*> m_parentDependencies;
std::vector<Mod*> m_dependants;
/**
* Saved values
*/
@ -63,7 +62,7 @@ namespace geode {
*/
bool m_resourcesLoaded = false;
Impl(Mod* self, ModInfo const& info);
Impl(Mod* self, ModMetadata const& metadata);
~Impl();
Result<> setup();
@ -84,14 +83,21 @@ namespace geode {
bool isEnabled() const;
bool isLoaded() const;
bool supportsDisabling() const;
bool supportsUnloading() const;
bool wasSuccesfullyLoaded() const;
ModInfo getModInfo() const;
bool canDisable() const;
bool canEnable() const;
bool needsEarlyLoad() const;
bool wasSuccessfullyLoaded() const;
ModMetadata getMetadata() const;
ghc::filesystem::path getTempDir() const;
ghc::filesystem::path getBinaryPath() const;
json::Value& getSaveContainer();
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void setMetadata(ModMetadata const& metadata);
std::vector<Mod*> getDependants() const;
#endif
Result<> saveData();
Result<> loadData();
@ -112,25 +118,26 @@ namespace geode {
Result<> removeHook(Hook* hook);
Result<Patch*> patch(void* address, ByteVector const& data);
Result<> unpatch(Patch* patch);
Result<> loadBinary();
Result<> unloadBinary();
Result<> enable();
Result<> disable();
Result<> uninstall();
bool isUninstalled() const;
bool depends(std::string const& id) const;
bool hasUnresolvedDependencies() const;
Result<> updateDependencies();
std::vector<Dependency> getUnresolvedDependencies();
bool hasUnresolvedDependencies() const;
bool hasUnresolvedIncompatibilities() const;
[[deprecated]] std::vector<Dependency> getUnresolvedDependencies();
Result<> loadBinary();
char const* expandSpriteName(char const* name);
ModJson getRuntimeInfo() const;
};
class ModImpl : public Mod {
class ModImpl : public Mod::Impl {
public:
static Mod::Impl* get();
static Mod::Impl* getImpl(Mod* mod);
};
}
}

View file

@ -1,18 +1,16 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/VersionInfo.hpp>
#include <Geode/utils/file.hpp>
#include <Geode/utils/string.hpp>
#include <about.hpp>
#include <json.hpp>
#include "ModInfoImpl.hpp"
#pragma warning(disable : 4996) // deprecation
using namespace geode::prelude;
ModInfo::Impl& ModInfoImpl::getImpl(ModInfo& info) {
return *info.m_impl.get();
return *info.m_impl;
}
bool Dependency::isResolved() const {
@ -21,330 +19,85 @@ bool Dependency::isResolved() const {
this->version.compare(this->mod->getVersion()));
}
static std::string sanitizeDetailsData(std::string const& str) {
// delete CRLF
return utils::string::replace(str, "\r", "");
}
bool ModInfo::Impl::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::Impl::createFromSchemaV010(ModJson const& rawJson) {
ModInfo info;
auto impl = info.m_impl.get();
impl->m_rawJSON = rawJson;
JsonChecker checker(impl->m_rawJSON);
auto root = checker.root("[mod.json]").obj();
root.addKnownKey("geode");
// don't think its used locally yet
root.addKnownKey("tags");
root.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModInfo::validateID)).into(impl->m_id);
root.needs("version").into(impl->m_version);
root.needs("name").into(impl->m_name);
root.needs("developer").into(impl->m_developer);
root.has("description").into(impl->m_description);
root.has("repository").into(impl->m_repository);
root.has("toggleable").into(impl->m_supportsDisabling);
root.has("unloadable").into(impl->m_supportsUnloading);
root.has("early-load").into(impl->m_needsEarlyLoad);
if (root.has("api")) {
impl->m_isAPI = true;
}
for (auto& dep : root.has("dependencies").iterate()) {
auto obj = dep.obj();
auto depobj = Dependency{};
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModInfo::validateID)).into(depobj.id);
obj.needs("version").into(depobj.version);
obj.has("required").into(depobj.required);
obj.checkUnknownKeys();
impl->m_dependencies.push_back(depobj);
}
for (auto& [key, value] : root.has("settings").items()) {
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value));
impl->m_settings.push_back({key, sett});
}
if (auto resources = root.has("resources").obj()) {
for (auto& [key, _] : resources.has("spritesheets").items()) {
impl->m_spritesheets.push_back(impl->m_id + "/" + key);
}
}
if (auto issues = root.has("issues").obj()) {
IssuesInfo issuesInfo;
issues.needs("info").into(issuesInfo.info);
issues.has("url").intoAs<std::string>(issuesInfo.url);
impl->m_issues = issuesInfo;
}
// with new cli, binary name is always mod id
impl->m_binaryName = impl->m_id + GEODE_PLATFORM_EXTENSION;
// removed keys
if (root.has("datastore")) {
log::error(
"{}: [mod.json].datastore has been deprecated "
"and removed. Use Saved Values instead (see TODO: DOCS LINK)", impl->m_id
);
}
if (root.has("binary")) {
log::error("{}: [mod.json].binary has been deprecated and removed.", impl->m_id);
}
if (checker.isError()) {
return Err(checker.getError());
}
root.checkUnknownKeys();
return Ok(info);
}
Result<ModInfo> ModInfo::Impl::create(ModJson const& json) {
// Check mod.json target version
auto schema = LOADER_VERSION;
if (json.contains("geode") && json["geode"].is_string()) {
GEODE_UNWRAP_INTO(
schema,
VersionInfo::parse(json["geode"].as_string())
.expect("[mod.json] has invalid target loader version: {error}")
);
}
else {
return Err(
"[mod.json] has no target loader version "
"specified, or it is invalidally formatted (required: \"[v]X.X.X\")!"
);
}
if (schema < Loader::get()->minModVersion()) {
return Err(
"[mod.json] is built for an older version (" + schema.toString() +
") of Geode (current: " + Loader::get()->getVersion().toString() +
"). Please update the mod to the latest version, "
"and if the problem persists, contact the developer "
"to update it."
);
}
if (schema > Loader::get()->maxModVersion()) {
return Err(
"[mod.json] is built for a newer version (" + schema.toString() +
") of Geode (current: " + Loader::get()->getVersion().toString() +
"). You need to update Geode in order to use "
"this mod."
);
}
// Handle mod.json data based on target
if (schema >= VersionInfo(0, 1, 0)) {
return Impl::createFromSchemaV010(json);
}
return Err(
"[mod.json] targets a version (" + schema.toString() +
") that isn't supported by this version (v" +
LOADER_VERSION_STR +
") of geode. This is probably a bug; report it to "
"the Geode Development Team."
);
}
Result<ModInfo> ModInfo::Impl::createFromFile(ghc::filesystem::path const& path) {
GEODE_UNWRAP_INTO(auto read, utils::file::readString(path));
try {
GEODE_UNWRAP_INTO(auto info, ModInfo::create(json::parse(read)));
auto impl = info.m_impl.get();
impl->m_path = path;
if (path.has_parent_path()) {
GEODE_UNWRAP(info.addSpecialFiles(path.parent_path()));
}
return Ok(info);
}
catch (std::exception& err) {
return Err(std::string("Unable to parse mod.json: ") + err.what());
}
}
Result<ModInfo> ModInfo::Impl::createFromGeodeFile(ghc::filesystem::path const& path) {
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(path));
return ModInfo::createFromGeodeZip(unzip);
}
Result<ModInfo> ModInfo::Impl::createFromGeodeZip(file::Unzip& unzip) {
// Check if mod.json exists in zip
if (!unzip.hasEntry("mod.json")) {
return Err("\"" + unzip.getPath().string() + "\" is missing mod.json");
}
// Read mod.json & parse if possible
GEODE_UNWRAP_INTO(
auto jsonData, unzip.extract("mod.json").expect("Unable to read mod.json: {error}")
);
std::string err;
ModJson json;
try {
json = json::parse(std::string(jsonData.begin(), jsonData.end()));
}
catch (std::exception& err) {
return Err(err.what());
}
auto res = ModInfo::create(json);
if (!res) {
return Err("\"" + unzip.getPath().string() + "\" - " + res.unwrapErr());
}
auto info = res.unwrap();
auto impl = info.m_impl.get();
impl->m_path = unzip.getPath();
GEODE_UNWRAP(info.addSpecialFiles(unzip).expect("Unable to add extra files: {error}"));
return Ok(info);
}
Result<> ModInfo::Impl::addSpecialFiles(file::Unzip& unzip) {
// unzip known MD files
for (auto& [file, target] : this->getSpecialFiles()) {
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();
}
Result<> ModInfo::Impl::addSpecialFiles(ghc::filesystem::path const& dir) {
// unzip known MD files
for (auto& [file, target] : this->getSpecialFiles()) {
if (ghc::filesystem::exists(dir / file)) {
auto data = file::readString(dir / file);
if (!data) {
return Err("Unable to read \"" + file + "\": " + data.unwrapErr());
}
*target = sanitizeDetailsData(data.unwrap());
}
}
return Ok();
}
std::vector<std::pair<std::string, std::optional<std::string>*>> ModInfo::Impl::getSpecialFiles() {
return {
{"about.md", &this->m_details},
{"changelog.md", &this->m_changelog},
{"support.md", &this->m_supportInfo},
};
}
ModJson ModInfo::Impl::toJSON() const {
auto json = m_rawJSON;
json["path"] = this->m_path.string();
json["binary"] = this->m_binaryName;
return json;
}
ModJson ModInfo::Impl::getRawJSON() const {
return m_rawJSON;
}
bool ModInfo::Impl::operator==(ModInfo::Impl const& other) const {
return this->m_id == other.m_id;
return this->m_metadata.m_id == other.m_metadata.m_id;
}
ghc::filesystem::path& ModInfo::path() {
return m_impl->m_path;
return m_impl->m_metadata.m_path;
}
ghc::filesystem::path const& ModInfo::path() const {
return m_impl->m_path;
return m_impl->m_metadata.m_path;
}
std::string& ModInfo::binaryName() {
return m_impl->m_binaryName;
return m_impl->m_metadata.m_binaryName;
}
std::string const& ModInfo::binaryName() const {
return m_impl->m_binaryName;
return m_impl->m_metadata.m_binaryName;
}
VersionInfo& ModInfo::version() {
return m_impl->m_version;
return m_impl->m_metadata.m_version;
}
VersionInfo const& ModInfo::version() const {
return m_impl->m_version;
return m_impl->m_metadata.m_version;
}
std::string& ModInfo::id() {
return m_impl->m_id;
return m_impl->m_metadata.m_id;
}
std::string const& ModInfo::id() const {
return m_impl->m_id;
return m_impl->m_metadata.m_id;
}
std::string& ModInfo::name() {
return m_impl->m_name;
return m_impl->m_metadata.m_name;
}
std::string const& ModInfo::name() const {
return m_impl->m_name;
return m_impl->m_metadata.m_name;
}
std::string& ModInfo::developer() {
return m_impl->m_developer;
return m_impl->m_metadata.m_developer;
}
std::string const& ModInfo::developer() const {
return m_impl->m_developer;
return m_impl->m_metadata.m_developer;
}
std::optional<std::string>& ModInfo::description() {
return m_impl->m_description;
return m_impl->m_metadata.m_description;
}
std::optional<std::string> const& ModInfo::description() const {
return m_impl->m_description;
return m_impl->m_metadata.m_description;
}
std::optional<std::string>& ModInfo::details() {
return m_impl->m_details;
return m_impl->m_metadata.m_details;
}
std::optional<std::string> const& ModInfo::details() const {
return m_impl->m_details;
return m_impl->m_metadata.m_details;
}
std::optional<std::string>& ModInfo::changelog() {
return m_impl->m_changelog;
return m_impl->m_metadata.m_changelog;
}
std::optional<std::string> const& ModInfo::changelog() const {
return m_impl->m_changelog;
return m_impl->m_metadata.m_changelog;
}
std::optional<std::string>& ModInfo::supportInfo() {
return m_impl->m_supportInfo;
return m_impl->m_metadata.m_supportInfo;
}
std::optional<std::string> const& ModInfo::supportInfo() const {
return m_impl->m_supportInfo;
return m_impl->m_metadata.m_supportInfo;
}
std::optional<std::string>& ModInfo::repository() {
return m_impl->m_repository;
return m_impl->m_metadata.m_repository;
}
std::optional<std::string> const& ModInfo::repository() const {
return m_impl->m_repository;
return m_impl->m_metadata.m_repository;
}
std::optional<IssuesInfo>& ModInfo::issues() {
@ -362,17 +115,17 @@ std::vector<Dependency> const& ModInfo::dependencies() const {
}
std::vector<std::string>& ModInfo::spritesheets() {
return m_impl->m_spritesheets;
return m_impl->m_metadata.m_spritesheets;
}
std::vector<std::string> const& ModInfo::spritesheets() const {
return m_impl->m_spritesheets;
return m_impl->m_metadata.m_spritesheets;
}
std::vector<std::pair<std::string, Setting>>& ModInfo::settings() {
return m_impl->m_settings;
return m_impl->m_metadata.m_settings;
}
std::vector<std::pair<std::string, Setting>> const& ModInfo::settings() const {
return m_impl->m_settings;
return m_impl->m_metadata.m_settings;
}
bool& ModInfo::supportsDisabling() {
@ -390,73 +143,48 @@ bool const& ModInfo::supportsUnloading() const {
}
bool& ModInfo::needsEarlyLoad() {
return m_impl->m_needsEarlyLoad;
return m_impl->m_metadata.m_needsEarlyLoad;
}
bool const& ModInfo::needsEarlyLoad() const {
return m_impl->m_needsEarlyLoad;
return m_impl->m_metadata.m_needsEarlyLoad;
}
bool& ModInfo::isAPI() {
return m_impl->m_isAPI;
return m_impl->m_metadata.m_isAPI;
}
bool const& ModInfo::isAPI() const {
return m_impl->m_isAPI;
return m_impl->m_metadata.m_isAPI;
}
Result<ModInfo> ModInfo::createFromGeodeZip(utils::file::Unzip& zip) {
return Impl::createFromGeodeZip(zip);
return ModMetadataImpl::createFromGeodeZip(zip);
}
Result<ModInfo> ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) {
return Impl::createFromGeodeFile(path);
return ModMetadataImpl::createFromGeodeFile(path);
}
Result<ModInfo> ModInfo::createFromFile(ghc::filesystem::path const& path) {
return Impl::createFromFile(path);
return ModMetadataImpl::createFromFile(path);
}
Result<ModInfo> ModInfo::create(ModJson const& json) {
return Impl::create(json);
return ModMetadataImpl::create(json);
}
ModJson ModInfo::toJSON() const {
return m_impl->toJSON();
return m_impl->m_metadata.m_rawJSON;
}
ModJson ModInfo::getRawJSON() const {
return m_impl->getRawJSON();
return m_impl->m_metadata.m_rawJSON;
}
bool ModInfo::operator==(ModInfo const& other) const {
return m_impl->operator==(*other.m_impl);
}
bool ModInfo::validateID(std::string const& id) {
return Impl::validateID(id);
}
ModJson& ModInfo::rawJSON() {
return m_impl->m_rawJSON;
}
ModJson const& ModInfo::rawJSON() const {
return m_impl->m_rawJSON;
}
Result<ModInfo> ModInfo::createFromSchemaV010(ModJson const& json) {
return Impl::createFromSchemaV010(json);
}
Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) {
return m_impl->addSpecialFiles(dir);
}
Result<> ModInfo::addSpecialFiles(utils::file::Unzip& zip) {
return m_impl->addSpecialFiles(zip);
}
std::vector<std::pair<std::string, std::optional<std::string>*>> ModInfo::getSpecialFiles() {
return m_impl->getSpecialFiles();
}
#pragma warning(suppress : 4996)
ModInfo::ModInfo() : m_impl(std::make_unique<Impl>()) {}
ModInfo::ModInfo(ModInfo const& other) : m_impl(std::make_unique<Impl>(*other.m_impl)) {}
@ -473,4 +201,44 @@ ModInfo& ModInfo::operator=(ModInfo&& other) noexcept {
return *this;
}
ModInfo::~ModInfo() {}
ModInfo::operator ModMetadata() {
ModMetadata metadata;
ModMetadataImpl::getImpl(metadata) = std::move(m_impl->m_metadata);
auto& metadataImpl = ModMetadataImpl::getImpl(metadata);
metadataImpl.m_issues = m_impl->m_issues ?
ModMetadata::IssuesInfo::fromDeprecated(m_impl->m_issues.value()) :
std::optional<ModMetadata::IssuesInfo>();
for (auto& dep : m_impl->m_dependencies)
metadataImpl.m_dependencies.push_back(ModMetadata::Dependency::fromDeprecated(dep));
return metadata;
}
ModInfo::operator ModMetadata() const {
ModMetadata metadata;
ModMetadataImpl::getImpl(metadata) = std::move(m_impl->m_metadata);
return metadata;
}
ModJson& ModInfo::rawJSON() {
return m_impl->m_metadata.m_rawJSON;
}
ModJson const& ModInfo::rawJSON() const {
return m_impl->m_metadata.m_rawJSON;
}
Result<ModInfo> ModInfo::createFromSchemaV010(geode::ModJson const& json) {
return ModMetadataImpl::createFromSchemaV010(json);
}
Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) {
return m_impl->m_metadata.addSpecialFiles(dir);
}
Result<> ModInfo::addSpecialFiles(utils::file::Unzip& zip) {
return m_impl->m_metadata.addSpecialFiles(zip);
}
std::vector<std::pair<std::string, std::optional<std::string>*>> ModInfo::getSpecialFiles() {
return m_impl->m_metadata.getSpecialFiles();
}
ModInfo::~ModInfo() = default;

View file

@ -1,59 +1,30 @@
#pragma once
#include "ModMetadataImpl.hpp"
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/VersionInfo.hpp>
#pragma warning(disable : 4996) // deprecation
using namespace geode::prelude;
namespace geode {
class ModInfo::Impl {
class [[deprecated]] ModInfo::Impl {
public:
ghc::filesystem::path m_path;
std::string m_binaryName;
VersionInfo m_version{1, 0, 0};
std::string m_id;
std::string m_name;
std::string m_developer;
std::optional<std::string> m_description;
std::optional<std::string> m_details;
std::optional<std::string> m_changelog;
std::optional<std::string> m_supportInfo;
std::optional<std::string> m_repository;
ModMetadataImpl m_metadata;
std::optional<IssuesInfo> m_issues;
std::vector<Dependency> m_dependencies;
std::vector<std::string> m_spritesheets;
std::vector<std::pair<std::string, Setting>> m_settings;
bool m_supportsDisabling = true;
bool m_supportsUnloading = false;
bool m_needsEarlyLoad = false;
bool m_isAPI = false;
ModJson m_rawJSON;
static Result<ModInfo> createFromGeodeZip(utils::file::Unzip& zip);
static Result<ModInfo> createFromGeodeFile(ghc::filesystem::path const& path);
static Result<ModInfo> createFromFile(ghc::filesystem::path const& path);
static Result<ModInfo> create(ModJson const& json);
ModJson toJSON() const;
ModJson getRawJSON() const;
bool operator==(ModInfo::Impl const& other) const;
static bool validateID(std::string const& id);
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();
};
class ModInfoImpl {
class [[deprecated]] ModInfoImpl : public ModInfo::Impl {
public:
static ModInfo::Impl& getImpl(ModInfo& info);
};
}
}

View file

@ -0,0 +1,609 @@
#include <Geode/loader/Loader.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/VersionInfo.hpp>
#include <Geode/utils/file.hpp>
#include <Geode/utils/string.hpp>
#include <about.hpp>
#include <json.hpp>
#include <utility>
#include "ModMetadataImpl.hpp"
#include "ModInfoImpl.hpp"
using namespace geode::prelude;
ModMetadata::Impl& ModMetadataImpl::getImpl(ModMetadata& info) {
return *info.m_impl;
}
bool ModMetadata::Dependency::isResolved() const {
return this->importance != Importance::Required ||
this->mod && this->mod->isLoaded() && this->version.compare(this->mod->getVersion());
}
bool ModMetadata::Incompatibility::isResolved() const {
return this->importance != Importance::Breaking ||
(!this->mod || !this->version.compare(this->mod->getVersion()));
}
ModMetadata::Dependency::operator geode::Dependency() {
return {id, version, importance == Importance::Required, mod};
}
ModMetadata::Dependency::operator geode::Dependency() const {
return {id, version, importance == Importance::Required, mod};
}
ModMetadata::IssuesInfo::operator geode::IssuesInfo() {
return {info, url};
}
ModMetadata::IssuesInfo::operator geode::IssuesInfo() const {
return {info, url};
}
ModMetadata::Dependency ModMetadata::Dependency::fromDeprecated(geode::Dependency const& value) {
return {
value.id,
value.version,
value.required ?
ModMetadata::Dependency::Importance::Required :
ModMetadata::Dependency::Importance::Suggested,
value.mod
};
}
ModMetadata::IssuesInfo ModMetadata::IssuesInfo::fromDeprecated(geode::IssuesInfo const& value) {
return {value.info, value.url};
}
static std::string sanitizeDetailsData(std::string const& str) {
// delete CRLF
return utils::string::replace(str, "\r", "");
}
bool ModMetadata::Impl::validateID(std::string const& id) {
// ids may not be empty
if (id.empty()) 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<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJson) {
ModMetadata info;
auto impl = info.m_impl.get();
impl->m_rawJSON = rawJson;
JsonChecker checker(impl->m_rawJSON);
auto root = checker.root("[mod.json]").obj();
root.addKnownKey("geode");
// don't think its used locally yet
root.addKnownKey("tags");
root.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(impl->m_id);
root.needs("version").into(impl->m_version);
root.needs("name").into(impl->m_name);
root.needs("developer").into(impl->m_developer);
root.has("description").into(impl->m_description);
root.has("repository").into(impl->m_repository);
root.has("early-load").into(impl->m_needsEarlyLoad);
// TODO for 2.0.0: fix this lol
// i think whoever wrote that intended that has would return the value if the key is present and false otherwise
// but the actual behavior here is false if key not present and true if key is present
if (root.has("api")) {
impl->m_isAPI = true;
}
if (root.has("toggleable"))
log::warn("{}: [mod.json].toggleable is deprecated and will be removed in a future update.", impl->m_id);
if (root.has("unloadable"))
log::warn("{}: [mod.json].unloadable is deprecated and will be removed in a future update.", impl->m_id);
// TODO for 2.0.0: specify this in mod.json manually
if (info.getID() != "geode.loader") {
impl->m_dependencies.push_back({
"geode.loader",
{LOADER_VERSION, VersionCompare::Exact},
Dependency::Importance::Required,
Mod::get()
});
}
for (auto& dep : root.has("dependencies").iterate()) {
auto obj = dep.obj();
Dependency dependency;
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(dependency.id);
obj.needs("version").into(dependency.version);
auto required = obj.has("required");
if (required) {
log::warn("{}: [mod.json].required has been deprecated and will be removed "
"in a future update. Use importance instead (see TODO: DOCS LINK)", impl->m_id);
dependency.importance = required.get<bool>() ?
Dependency::Importance::Required :
Dependency::Importance::Suggested;
}
obj.has("importance").into(dependency.importance);
obj.checkUnknownKeys();
impl->m_dependencies.push_back(dependency);
}
for (auto& incompat : root.has("incompatibilities").iterate()) {
auto obj = incompat.obj();
Incompatibility incompatibility;
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(incompatibility.id);
obj.needs("version").into(incompatibility.version);
obj.has("importance").into(incompatibility.importance);
obj.checkUnknownKeys();
impl->m_incompatibilities.push_back(incompatibility);
}
for (auto& [key, value] : root.has("settings").items()) {
GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value));
impl->m_settings.emplace_back(key, sett);
}
if (auto resources = root.has("resources").obj()) {
for (auto& [key, _] : resources.has("spritesheets").items()) {
impl->m_spritesheets.push_back(impl->m_id + "/" + key);
}
}
if (auto issues = root.has("issues").obj()) {
IssuesInfo issuesInfo;
issues.needs("info").into(issuesInfo.info);
issues.has("url").intoAs<std::string>(issuesInfo.url);
impl->m_issues = issuesInfo;
}
// with new cli, binary name is always mod id
impl->m_binaryName = impl->m_id + GEODE_PLATFORM_EXTENSION;
// removed keys
if (root.has("datastore")) {
log::error(
"{}: [mod.json].datastore has been removed. "
"Use Saved Values instead (see TODO: DOCS LINK)", impl->m_id
);
}
if (root.has("binary")) {
log::error("{}: [mod.json].binary has been removed.", impl->m_id);
}
if (checker.isError()) {
return Err(checker.getError());
}
root.checkUnknownKeys();
return Ok(info);
}
Result<ModMetadata> ModMetadata::Impl::create(ModJson const& json) {
// Check mod.json target version
auto schema = LOADER_VERSION;
if (json.contains("geode") && json["geode"].is_string()) {
GEODE_UNWRAP_INTO(
schema,
VersionInfo::parse(json["geode"].as_string())
.expect("[mod.json] has invalid target loader version: {error}")
);
}
else {
return Err(
"[mod.json] has no target loader version "
"specified, or its formatting is invalid (required: \"[v]X.X.X\")!"
);
}
if (schema < Loader::get()->minModVersion()) {
return Err(
"[mod.json] is built for an older version (" + schema.toString() +
") of Geode (current: " + Loader::get()->getVersion().toString() +
"). Please update the mod to the latest version, "
"and if the problem persists, contact the developer "
"to update it."
);
}
if (schema > Loader::get()->maxModVersion()) {
return Err(
"[mod.json] is built for a newer version (" + schema.toString() +
") of Geode (current: " + Loader::get()->getVersion().toString() +
"). You need to update Geode in order to use "
"this mod."
);
}
// Handle mod.json data based on target
if (schema < VersionInfo(0, 1, 0)) {
return Err(
"[mod.json] targets a version (" + schema.toString() +
") that isn't supported by this version (v" +
LOADER_VERSION_STR +
") of geode. This is probably a bug; report it to "
"the Geode Development Team."
);
}
return Impl::createFromSchemaV010(json);
}
Result<ModMetadata> ModMetadata::Impl::createFromFile(ghc::filesystem::path const& path) {
GEODE_UNWRAP_INTO(auto read, utils::file::readString(path));
try {
GEODE_UNWRAP_INTO(auto info, ModMetadata::create(json::parse(read)));
auto impl = info.m_impl.get();
impl->m_path = path;
if (path.has_parent_path()) {
GEODE_UNWRAP(info.addSpecialFiles(path.parent_path()));
}
return Ok(info);
}
catch (std::exception& err) {
return Err(std::string("Unable to parse mod.json: ") + err.what());
}
}
Result<ModMetadata> ModMetadata::Impl::createFromGeodeFile(ghc::filesystem::path const& path) {
GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(path));
return ModMetadata::createFromGeodeZip(unzip);
}
Result<ModMetadata> ModMetadata::Impl::createFromGeodeZip(file::Unzip& unzip) {
// Check if mod.json exists in zip
if (!unzip.hasEntry("mod.json")) {
return Err("\"" + unzip.getPath().string() + "\" is missing mod.json");
}
// Read mod.json & parse if possible
GEODE_UNWRAP_INTO(
auto jsonData, unzip.extract("mod.json").expect("Unable to read mod.json: {error}")
);
std::string err;
ModJson json;
try {
json = json::parse(std::string(jsonData.begin(), jsonData.end()));
}
catch (std::exception& err) {
return Err(err.what());
}
auto res = ModMetadata::create(json);
if (!res) {
return Err("\"" + unzip.getPath().string() + "\" - " + res.unwrapErr());
}
auto info = res.unwrap();
auto impl = info.m_impl.get();
impl->m_path = unzip.getPath();
GEODE_UNWRAP(info.addSpecialFiles(unzip).expect("Unable to add extra files: {error}"));
return Ok(info);
}
Result<> ModMetadata::Impl::addSpecialFiles(file::Unzip& unzip) {
// unzip known MD files
for (auto& [file, target] : this->getSpecialFiles()) {
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();
}
Result<> ModMetadata::Impl::addSpecialFiles(ghc::filesystem::path const& dir) {
// unzip known MD files
for (auto& [file, target] : this->getSpecialFiles()) {
if (ghc::filesystem::exists(dir / file)) {
auto data = file::readString(dir / file);
if (!data) {
return Err("Unable to read \"" + file + "\": " + data.unwrapErr());
}
*target = sanitizeDetailsData(data.unwrap());
}
}
return Ok();
}
std::vector<std::pair<std::string, std::optional<std::string>*>> ModMetadata::Impl::getSpecialFiles() {
return {
{"about.md", &this->m_details},
{"changelog.md", &this->m_changelog},
{"support.md", &this->m_supportInfo},
};
}
ModJson ModMetadata::Impl::toJSON() const {
auto json = m_rawJSON;
json["path"] = this->m_path.string();
json["binary"] = this->m_binaryName;
return json;
}
ModJson ModMetadata::Impl::getRawJSON() const {
return m_rawJSON;
}
bool ModMetadata::Impl::operator==(ModMetadata::Impl const& other) const {
return this->m_id == other.m_id;
}
[[maybe_unused]] ghc::filesystem::path ModMetadata::getPath() const {
return m_impl->m_path;
}
std::string ModMetadata::getBinaryName() const {
return m_impl->m_binaryName;
}
VersionInfo ModMetadata::getVersion() const {
return m_impl->m_version;
}
std::string ModMetadata::getID() const {
return m_impl->m_id;
}
std::string ModMetadata::getName() const {
return m_impl->m_name;
}
std::string ModMetadata::getDeveloper() const {
return m_impl->m_developer;
}
std::optional<std::string> ModMetadata::getDescription() const {
return m_impl->m_description;
}
std::optional<std::string> ModMetadata::getDetails() const {
return m_impl->m_details;
}
std::optional<std::string> ModMetadata::getChangelog() const {
return m_impl->m_changelog;
}
std::optional<std::string> ModMetadata::getSupportInfo() const {
return m_impl->m_supportInfo;
}
std::optional<std::string> ModMetadata::getRepository() const {
return m_impl->m_repository;
}
std::optional<ModMetadata::IssuesInfo> ModMetadata::getIssues() const {
return m_impl->m_issues;
}
std::vector<ModMetadata::Dependency> ModMetadata::getDependencies() const {
return m_impl->m_dependencies;
}
std::vector<ModMetadata::Incompatibility> ModMetadata::getIncompatibilities() const {
return m_impl->m_incompatibilities;
}
std::vector<std::string> ModMetadata::getSpritesheets() const {
return m_impl->m_spritesheets;
}
std::vector<std::pair<std::string, Setting>> ModMetadata::getSettings() const {
return m_impl->m_settings;
}
bool ModMetadata::needsEarlyLoad() const {
return m_impl->m_needsEarlyLoad;
}
bool ModMetadata::isAPI() const {
return m_impl->m_isAPI;
}
#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void ModMetadata::setPath(ghc::filesystem::path const& value) {
m_impl->m_path = value;
}
void ModMetadata::setBinaryName(std::string const& value) {
m_impl->m_binaryName = value;
}
void ModMetadata::setVersion(VersionInfo const& value) {
m_impl->m_version = value;
}
void ModMetadata::setID(std::string const& value) {
m_impl->m_id = value;
}
void ModMetadata::setName(std::string const& value) {
m_impl->m_name = value;
}
void ModMetadata::setDeveloper(std::string const& value) {
m_impl->m_developer = value;
}
void ModMetadata::setDescription(std::optional<std::string> const& value) {
m_impl->m_description = value;
}
void ModMetadata::setDetails(std::optional<std::string> const& value) {
m_impl->m_details = value;
}
void ModMetadata::setChangelog(std::optional<std::string> const& value) {
m_impl->m_changelog = value;
}
void ModMetadata::setSupportInfo(std::optional<std::string> const& value) {
m_impl->m_supportInfo = value;
}
void ModMetadata::setRepository(std::optional<std::string> const& value) {
m_impl->m_repository = value;
}
void ModMetadata::setIssues(std::optional<IssuesInfo> const& value) {
m_impl->m_issues = value;
}
void ModMetadata::setDependencies(std::vector<Dependency> const& value) {
m_impl->m_dependencies = value;
}
void ModMetadata::setIncompatibilities(std::vector<Incompatibility> const& value) {
m_impl->m_incompatibilities = value;
}
void ModMetadata::setSpritesheets(std::vector<std::string> const& value) {
m_impl->m_spritesheets = value;
}
void ModMetadata::setSettings(std::vector<std::pair<std::string, Setting>> const& value) {
m_impl->m_settings = value;
}
void ModMetadata::setNeedsEarlyLoad(bool const& value) {
m_impl->m_needsEarlyLoad = value;
}
void ModMetadata::setIsAPI(bool const& value) {
m_impl->m_isAPI = value;
}
#endif
Result<ModMetadata> ModMetadata::createFromGeodeZip(utils::file::Unzip& zip) {
return Impl::createFromGeodeZip(zip);
}
Result<ModMetadata> ModMetadata::createFromGeodeFile(ghc::filesystem::path const& path) {
return Impl::createFromGeodeFile(path);
}
Result<ModMetadata> ModMetadata::createFromFile(ghc::filesystem::path const& path) {
return Impl::createFromFile(path);
}
Result<ModMetadata> ModMetadata::create(ModJson const& json) {
return Impl::create(json);
}
ModJson ModMetadata::toJSON() const {
return m_impl->toJSON();
}
ModJson ModMetadata::getRawJSON() const {
return m_impl->getRawJSON();
}
bool ModMetadata::operator==(ModMetadata const& other) const {
return m_impl->operator==(*other.m_impl);
}
bool ModMetadata::validateID(std::string const& id) {
return Impl::validateID(id);
}
Result<ModMetadata> ModMetadata::createFromSchemaV010(ModJson const& json) {
return Impl::createFromSchemaV010(json);
}
Result<> ModMetadata::addSpecialFiles(ghc::filesystem::path const& dir) {
return m_impl->addSpecialFiles(dir);
}
Result<> ModMetadata::addSpecialFiles(utils::file::Unzip& zip) {
return m_impl->addSpecialFiles(zip);
}
std::vector<std::pair<std::string, std::optional<std::string>*>> ModMetadata::getSpecialFiles() {
return m_impl->getSpecialFiles();
}
ModMetadata::ModMetadata() : m_impl(std::make_unique<Impl>()) {}
ModMetadata::ModMetadata(std::string id) : m_impl(std::make_unique<Impl>()) { m_impl->m_id = std::move(id); }
ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(std::make_unique<Impl>(*other.m_impl)) {}
ModMetadata::ModMetadata(ModMetadata&& other) noexcept : m_impl(std::move(other.m_impl)) {}
ModMetadata& ModMetadata::operator=(ModMetadata const& other) {
m_impl = std::make_unique<Impl>(*other.m_impl);
return *this;
}
ModMetadata& ModMetadata::operator=(ModMetadata&& other) noexcept {
m_impl = std::move(other.m_impl);
return *this;
}
ModMetadata::operator ModInfo() {
ModInfo info;
auto infoImpl = ModInfoImpl::getImpl(info);
infoImpl.m_metadata.Impl::operator=(*m_impl); // im gonna cry what is this hack why are you not using pointers
infoImpl.m_issues = m_impl->m_issues;
for (auto& dep : m_impl->m_dependencies)
infoImpl.m_dependencies.push_back(dep);
return info;
}
ModMetadata::operator ModInfo() const {
ModInfo info;
auto infoImpl = ModInfoImpl::getImpl(info);
infoImpl.m_metadata.Impl::operator=(*m_impl);
infoImpl.m_issues = m_impl->m_issues;
for (auto& dep : m_impl->m_dependencies)
infoImpl.m_dependencies.push_back(dep);
return info;
}
ModMetadata::~ModMetadata() = default;
template <>
struct json::Serialize<geode::ModMetadata::Dependency::Importance> {
static json::Value GEODE_DLL to_json(geode::ModMetadata::Dependency::Importance const& importance) {
switch (importance) {
case geode::ModMetadata::Dependency::Importance::Required: return {"required"};
case geode::ModMetadata::Dependency::Importance::Recommended: return {"recommended"};
case geode::ModMetadata::Dependency::Importance::Suggested: return {"suggested"};
default: return {"unknown"};
}
}
static geode::ModMetadata::Dependency::Importance GEODE_DLL from_json(json::Value const& importance) {
auto impStr = importance.as_string();
if (impStr == "required")
return geode::ModMetadata::Dependency::Importance::Required;
if (impStr == "recommended")
return geode::ModMetadata::Dependency::Importance::Recommended;
if (impStr == "suggested")
return geode::ModMetadata::Dependency::Importance::Suggested;
throw json::JsonException(R"(Expected importance to be "required", "recommended" or "suggested")");
}
};
template <>
struct json::Serialize<geode::ModMetadata::Incompatibility::Importance> {
static json::Value GEODE_DLL to_json(geode::ModMetadata::Incompatibility::Importance const& importance) {
switch (importance) {
case geode::ModMetadata::Incompatibility::Importance::Breaking: return {"breaking"};
case geode::ModMetadata::Incompatibility::Importance::Conflicting: return {"conflicting"};
default: return {"unknown"};
}
}
static geode::ModMetadata::Incompatibility::Importance GEODE_DLL from_json(json::Value const& importance) {
auto impStr = importance.as_string();
if (impStr == "breaking")
return geode::ModMetadata::Incompatibility::Importance::Breaking;
if (impStr == "conflicting")
return geode::ModMetadata::Incompatibility::Importance::Conflicting;
throw json::JsonException(R"(Expected importance to be "breaking" or "conflicting")");
}
};

View file

@ -0,0 +1,58 @@
#pragma once
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/JsonValidation.hpp>
#include <Geode/utils/VersionInfo.hpp>
using namespace geode::prelude;
namespace geode {
class ModMetadata::Impl {
public:
ghc::filesystem::path m_path;
std::string m_binaryName;
VersionInfo m_version{1, 0, 0};
std::string m_id;
std::string m_name;
std::string m_developer;
std::optional<std::string> m_description;
std::optional<std::string> m_details;
std::optional<std::string> m_changelog;
std::optional<std::string> m_supportInfo;
std::optional<std::string> m_repository;
std::optional<IssuesInfo> m_issues;
std::vector<Dependency> m_dependencies;
std::vector<Incompatibility> m_incompatibilities;
std::vector<std::string> m_spritesheets;
std::vector<std::pair<std::string, Setting>> m_settings;
bool m_needsEarlyLoad = false;
bool m_isAPI = false;
ModJson m_rawJSON;
static Result<ModMetadata> createFromGeodeZip(utils::file::Unzip& zip);
static Result<ModMetadata> createFromGeodeFile(ghc::filesystem::path const& path);
static Result<ModMetadata> createFromFile(ghc::filesystem::path const& path);
static Result<ModMetadata> create(ModJson const& json);
ModJson toJSON() const;
ModJson getRawJSON() const;
bool operator==(ModMetadata::Impl const& other) const;
static bool validateID(std::string const& id);
static Result<ModMetadata> createFromSchemaV010(ModJson const& rawJson);
Result<> addSpecialFiles(ghc::filesystem::path const& dir);
Result<> addSpecialFiles(utils::file::Unzip& zip);
std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles();
};
class ModMetadataImpl : public ModMetadata::Impl {
public:
static ModMetadata::Impl& getImpl(ModMetadata& info);
};
}

View file

@ -5,6 +5,7 @@ using namespace geode::prelude;
#if defined(GEODE_IS_MACOS)
#include "mac/LoaderImpl.mm"
#include "mac/main.mm"
#include "mac/crashlog.mm"
#include "mac/FileWatcher.mm"

View file

@ -3,10 +3,7 @@
#include <iostream>
#include <loader/LoaderImpl.hpp>
#include <loader/ModImpl.hpp>
#ifdef GEODE_IS_MACOS
#include <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
using namespace geode::prelude;
@ -36,6 +33,9 @@ void Loader::Impl::logConsoleMessageWithSeverity(std::string const& msg, Severit
}
void Loader::Impl::openPlatformConsole() {
// it's not possible to redirect stdout to a terminal
// and the console.app is too clunky
m_platformConsoleOpen = true;
for (auto const& log : log::Logger::list()) {
@ -83,5 +83,3 @@ void Loader::Impl::setupIPC() {
bool Loader::Impl::userTriedToLoadDLLs() const {
return false;
}
#endif

View file

@ -2,9 +2,9 @@
#ifdef GEODE_IS_MACOS
#include <Geode/loader/Mod.hpp>
#include <loader/ModImpl.hpp>
#include <dlfcn.h>
#include <Geode/loader/Mod.hpp>
#include <loader/ModImpl.hpp>
#include <dlfcn.h>
using namespace geode::prelude;
@ -19,7 +19,7 @@ T findSymbolOrMangled(void* dylib, char const* name, char const* mangled) {
Result<> Mod::Impl::loadPlatformBinary() {
auto dylib =
dlopen((m_tempDirName / m_info.binaryName()).string().c_str(), RTLD_LAZY);
dlopen((m_tempDirName / m_metadata.getBinaryName()).string().c_str(), RTLD_LAZY);
if (dylib) {
if (m_platformInfo) {
delete m_platformInfo;

View file

@ -10,6 +10,7 @@ using namespace geode::prelude;
#include <Geode/utils/web.hpp>
#include <Geode/utils/file.hpp>
#include <Geode/utils/cocos.hpp>
#include <Geode/binding/GameManager.hpp>
bool utils::clipboard::write(std::string const& data) {
[[NSPasteboard generalPasteboard] clearContents];
@ -180,7 +181,7 @@ ghc::filesystem::path dirs::getGameDir() {
_NSGetExecutablePath(gddir.data(), &out);
ghc::filesystem::path gdpath = gddir.data();
auto currentPath = gdpath.parent_path().parent_path();
auto currentPath = ghc::filesystem::canonical(gdpath.parent_path().parent_path());
return currentPath;
}();
@ -200,4 +201,38 @@ ghc::filesystem::path dirs::getSaveDir() {
return path;
}
void geode::utils::game::restart() {
if (CCApplication::sharedApplication() &&
(GameManager::get()->m_playLayer || GameManager::get()->m_levelEditorLayer)) {
log::error("Cannot restart in PlayLayer or LevelEditorLayer!");
return;
}
auto restart = +[] {
log::info("Restarting game...");
auto gdExec = dirs::getGameDir() / "MacOS" / "Geometry Dash";
NSTask *task = [NSTask new];
[task setLaunchPath: [NSString stringWithUTF8String: gdExec.string().c_str()]];
[task launch];
};
class Exit : public CCObject {
public:
void shutdown() {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-method-access"
[[[NSClassFromString(@"AppControllerManager") sharedInstance] controller] shutdownGame];
#pragma clang diagnostic pop
}
};
std::atexit(restart);
CCDirector::get()->getActionManager()->addAction(CCSequence::create(
CCDelayTime::create(0.5f),
CCCallFunc::create(nullptr, callfunc_selector(Exit::shutdown)),
nullptr
), CCDirector::get()->getRunningScene(), false);
}
#endif

View file

@ -101,7 +101,7 @@ void ipcPipeThread(HANDLE pipe) {
}
void Loader::Impl::setupIPC() {
std::thread([]() {
std::thread ipcThread([]() {
while (true) {
auto pipe = CreateNamedPipeA(
IPC_PIPE_NAME,
@ -125,14 +125,18 @@ void Loader::Impl::setupIPC() {
// log::debug("Waiting for pipe connections");
if (ConnectNamedPipe(pipe, nullptr)) {
// log::debug("Got connection, creating thread");
std::thread(&ipcPipeThread, pipe).detach();
std::thread pipeThread(&ipcPipeThread, pipe);
SetThreadDescription(pipeThread.native_handle(), L"Geode IPC Pipe");
pipeThread.detach();
}
else {
// log::debug("No connection, cleaning pipe");
CloseHandle(pipe);
}
}
}).detach();
});
SetThreadDescription(ipcThread.native_handle(), L"Geode Main IPC");
ipcThread.detach();
log::debug("IPC set up");
}

View file

@ -73,7 +73,7 @@ std::string getLastWinError() {
}
Result<> Mod::Impl::loadPlatformBinary() {
auto load = LoadLibraryW((m_tempDirName / m_info.binaryName()).wstring().c_str());
auto load = LoadLibraryW((m_tempDirName / m_metadata.getBinaryName()).wstring().c_str());
if (load) {
if (m_platformInfo) {
delete m_platformInfo;

View file

@ -24,16 +24,7 @@ void updateGeode() {
ghc::filesystem::exists(updatesDir / "GeodeUpdater.exe"))
ghc::filesystem::rename(updatesDir / "GeodeUpdater.exe", workingDir / "GeodeUpdater.exe");
wchar_t buffer[MAX_PATH];
GetModuleFileNameW(nullptr, buffer, MAX_PATH);
const auto gdName = ghc::filesystem::path(buffer).filename().string();
// launch updater
const auto updaterPath = (workingDir / "GeodeUpdater.exe").string();
ShellExecuteA(nullptr, "open", updaterPath.c_str(), gdName.c_str(), workingDir.string().c_str(), false);
// quit gd before it can even start
exit(0);
utils::game::restart();
}
int WINAPI gdMainHook(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) {

View file

@ -158,4 +158,30 @@ ghc::filesystem::path dirs::getSaveDir() {
return path;
}
void geode::utils::game::restart() {
if (CCApplication::sharedApplication() &&
(GameManager::get()->m_playLayer || GameManager::get()->m_levelEditorLayer)) {
log::error("Cannot restart in PlayLayer or LevelEditorLayer!");
return;
}
const auto workingDir = dirs::getGameDir();
wchar_t buffer[MAX_PATH];
GetModuleFileNameW(nullptr, buffer, MAX_PATH);
const auto gdName = ghc::filesystem::path(buffer).filename().string();
// launch updater
const auto updaterPath = (workingDir / "GeodeUpdater.exe").string();
ShellExecuteA(nullptr, "open", updaterPath.c_str(), gdName.c_str(), workingDir.string().c_str(), false);
if (CCApplication::sharedApplication())
// please forgive me..
// manually set the closed flag
// TODO: actually call glfwSetWindowShouldClose
*reinterpret_cast<bool*>(reinterpret_cast<uintptr_t>(CCEGLView::sharedOpenGLView()->getWindow()) + 0xa) = true;
else
exit(0);
}
#endif

View file

@ -14,19 +14,19 @@ void geode::openModsList() {
}
void geode::openIssueReportPopup(Mod* mod) {
if (mod->getModInfo().issues()) {
if (mod->getMetadata().getIssues()) {
MDPopup::create(
"Issue Report",
mod->getModInfo().issues().value().info +
mod->getMetadata().getIssues().value().info +
"\n\n"
"If your issue relates to a <cr>game crash</c>, <cb>please include</c> the "
"latest crash log(s) from `" +
dirs::getCrashlogsDir().string() + "`",
"OK", (mod->getModInfo().issues().value().url ? "Open URL" : ""),
"OK", (mod->getMetadata().getIssues().value().url ? "Open URL" : ""),
[mod](bool btn2) {
if (btn2) {
web::openLinkInBrowser(
mod->getModInfo().issues().value().url.value()
mod->getMetadata().getIssues().value().url.value()
);
}
}
@ -73,13 +73,9 @@ CCNode* geode::createDefaultLogo(CCSize const& size) {
}
CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
CCNode* spr = nullptr;
if (mod == Loader::get()->getModImpl()) {
spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
}
else {
spr = CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str());
}
CCNode* spr = mod == Mod::get() ?
CCSprite::createWithSpriteFrameName("geode-logo.png"_spr) :
CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str());
if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
if (!spr) spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
limitNodeSize(spr, size, 1.f, .1f);
@ -87,9 +83,8 @@ CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
}
CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) {
CCNode* spr = nullptr;
auto logoPath = ghc::filesystem::absolute(item->getPath() / "logo.png");
spr = CCSprite::create(logoPath.string().c_str());
CCNode* spr = CCSprite::create(logoPath.string().c_str());
if (!spr) {
spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
}

View file

@ -27,7 +27,7 @@ bool DevProfilePopup::setup(std::string const& developer) {
// index mods
for (auto& item : Index::get()->getItemsByDeveloper(developer)) {
if (Loader::get()->isModInstalled(item->getModInfo().id())) {
if (Loader::get()->isModInstalled(item->getMetadata().getID())) {
continue;
}
auto cell = IndexItemCell::create(

View file

@ -11,24 +11,23 @@
#include <Geode/binding/Slider.hpp>
#include <Geode/binding/SliderThumb.hpp>
#include <Geode/binding/SliderTouchLogic.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/ui/BasedButton.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/ui/IconButtonSprite.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <Geode/ui/MDPopup.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/web.hpp>
#include <loader/LoaderImpl.hpp>
#include <ui/internal/list/InstallListPopup.hpp>
static constexpr int const TAG_CONFIRM_UNINSTALL = 5;
static constexpr int const TAG_CONFIRM_UPDATE = 6;
static constexpr int const TAG_DELETE_SAVEDATA = 7;
static const CCSize LAYER_SIZE = {440.f, 290.f};
bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
bool ModInfoPopup::init(ModMetadata const& metadata, ModListLayer* list) {
m_noElasticity = true;
m_layer = list;
@ -50,7 +49,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
constexpr float logoSize = 40.f;
constexpr float logoOffset = 10.f;
auto nameLabel = CCLabelBMFont::create(info.name().c_str(), "bigFont.fnt");
auto nameLabel = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt");
nameLabel->setAnchorPoint({ .0f, .5f });
nameLabel->limitLabelWidth(200.f, .7f, .1f);
m_mainLayer->addChild(nameLabel, 2);
@ -58,7 +57,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
auto logoSpr = this->createLogo({logoSize, logoSize});
m_mainLayer->addChild(logoSpr);
auto developerStr = "by " + info.developer();
auto developerStr = "by " + metadata.getDeveloper();
auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt");
developerLabel->setScale(.5f);
developerLabel->setAnchorPoint({.0f, .5f});
@ -78,8 +77,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 105.f
);
auto versionLabel = CCLabelBMFont::create(
info.version().toString().c_str(),
auto versionLabel = CCLabelBMFont::create(metadata.getVersion().toString().c_str(),
"bigFont.fnt"
);
versionLabel->setAnchorPoint({ .0f, .5f });
@ -94,7 +92,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
this->setTouchEnabled(true);
m_detailsArea = MDTextArea::create(
(info.details() ? info.details().value() : "### No description provided."),
(metadata.getDetails() ? metadata.getDetails().value() : "### No description provided."),
{ 350.f, 137.5f }
);
m_detailsArea->setPosition(
@ -111,7 +109,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
m_mainLayer->addChild(m_scrollbar);
// changelog
if (info.changelog()) {
if (metadata.getChangelog()) {
// m_changelogArea is only created if the changelog button is clicked
// because changelogs can get really long and take a while to load
@ -142,7 +140,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
m_buttonMenu->addChild(changelogBtn);
}
// mod info
// mod metadata
auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png");
infoSpr->setScale(.85f);
@ -151,7 +149,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
m_buttonMenu->addChild(m_infoBtn);
// repo button
if (info.repository()) {
if (metadata.getRepository()) {
auto repoBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("github.png"_spr),
this,
@ -162,7 +160,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
}
// support button
if (info.supportInfo()) {
if (metadata.getSupportInfo()) {
auto supportBtn = CCMenuItemSpriteExtra::create(
CCSprite::createWithSpriteFrameName("gift.png"_spr),
this,
@ -188,30 +186,30 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) {
void ModInfoPopup::onSupport(CCObject*) {
MDPopup::create(
"Support " + this->getModInfo().name(),
this->getModInfo().supportInfo().value(),
"Support " + this->getMetadata().getName(),
this->getMetadata().getSupportInfo().value(),
"OK"
)->show();
}
void ModInfoPopup::onRepository(CCObject*) {
web::openLinkInBrowser(this->getModInfo().repository().value());
web::openLinkInBrowser(this->getMetadata().getRepository().value());
}
void ModInfoPopup::onInfo(CCObject*) {
auto info = this->getModInfo();
auto info = this->getMetadata();
FLAlertLayer::create(
nullptr,
("About " + info.name()).c_str(),
("About " + info.getName()).c_str(),
fmt::format(
"<cr>ID: {}</c>\n"
"<cg>Version: {}</c>\n"
"<cp>Developer: {}</c>\n"
"<cb>Path: {}</c>\n",
info.id(),
info.version().toString(),
info.developer(),
info.path().string()
info.getID(),
info.getVersion().toString(),
info.getDeveloper(),
info.getPath().string()
),
"OK",
nullptr,
@ -224,7 +222,7 @@ void ModInfoPopup::onChangelog(CCObject* sender) {
auto winSize = CCDirector::get()->getWinSize();
if (!m_changelogArea) {
m_changelogArea = MDTextArea::create(this->getModInfo().changelog().value(), { 350.f, 137.5f });
m_changelogArea = MDTextArea::create(this->getMetadata().getChangelog().value(), { 350.f, 137.5f });
m_changelogArea->setPosition(
-5000.f, winSize.height / 2 - m_changelogArea->getScaledContentSize().height / 2 - 20.f
);
@ -288,12 +286,12 @@ LocalModInfoPopup::LocalModInfoPopup()
bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
m_item = Index::get()->getMajorItem(mod->getModInfo().id());
m_item = Index::get()->getMajorItem(mod->getMetadata().getID());
if (m_item)
m_installListener.setFilter(m_item->getModInfo().id());
m_installListener.setFilter(m_item->getMetadata().getID());
m_mod = mod;
if (!ModInfoPopup::init(mod->getModInfo(), list)) return false;
if (!ModInfoPopup::init(mod->getMetadata(), list)) return false;
auto winSize = CCDirector::sharedDirector()->getWinSize();
@ -344,10 +342,9 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
disableBtnSpr->setColor({150, 150, 150});
}
if (mod != Loader::get()->getModImpl()) {
auto uninstallBtnSpr = ButtonSprite::create(
"Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f
);
if (mod != Mod::get()) {
auto uninstallBtnSpr =
ButtonSprite::create("Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f);
uninstallBtnSpr->setScale(.6f);
auto uninstallBtn = CCMenuItemSpriteExtra::create(
@ -376,16 +373,16 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
m_mainLayer->addChild(m_installStatus);
auto minorIndexItem = Index::get()->getItem(
mod->getModInfo().id(),
ComparableVersionInfo(mod->getModInfo().version(), VersionCompare::MoreEq)
mod->getMetadata().getID(),
ComparableVersionInfo(mod->getMetadata().getVersion(), VersionCompare::MoreEq)
);
// TODO: use column layout here?
if (m_item->getModInfo().version().getMajor() > minorIndexItem->getModInfo().version().getMajor()) {
if (m_item->getMetadata().getVersion().getMajor() > minorIndexItem->getMetadata().getVersion().getMajor()) {
// has major update
m_latestVersionLabel = CCLabelBMFont::create(
("Available: " + m_item->getModInfo().version().toString()).c_str(),
("Available: " + m_item->getMetadata().getVersion().toString()).c_str(),
"bigFont.fnt"
);
m_latestVersionLabel->setScale(.35f);
@ -395,10 +392,10 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
m_mainLayer->addChild(m_latestVersionLabel);
}
if (minorIndexItem->getModInfo().version() > mod->getModInfo().version()) {
if (minorIndexItem->getMetadata().getVersion() > mod->getMetadata().getVersion()) {
// has minor update
m_minorVersionLabel = CCLabelBMFont::create(
("Available: " + minorIndexItem->getModInfo().version().toString()).c_str(),
("Available: " + minorIndexItem->getMetadata().getVersion().toString()).c_str(),
"bigFont.fnt"
);
m_minorVersionLabel->setScale(.35f);
@ -429,7 +426,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
}
// issue report button
if (mod->getModInfo().issues()) {
if (mod->getMetadata().getIssues()) {
auto issuesBtnSpr = ButtonSprite::create(
"Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f
);
@ -449,8 +446,8 @@ CCNode* LocalModInfoPopup::createLogo(CCSize const& size) {
return geode::createModLogo(m_mod, size);
}
ModInfo LocalModInfoPopup::getModInfo() const {
return m_mod->getModInfo();
ModMetadata LocalModInfoPopup::getMetadata() const {
return m_mod->getMetadata();
}
void LocalModInfoPopup::onIssues(CCObject*) {
@ -464,7 +461,7 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) {
FLAlertLayer::create(
"Update complete",
"Mod succesfully updated! :) "
"Mod successfully updated! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect)",
"OK"
@ -516,8 +513,8 @@ void LocalModInfoPopup::onUpdate(CCObject*) {
[](IndexItemHandle handle) {
return fmt::format(
" - <cr>{}</c> (<cy>{}</c>)",
handle->getModInfo().name(),
handle->getModInfo().id()
handle->getMetadata().getName(),
handle->getMetadata().getID()
);
}
),
@ -577,9 +574,9 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) {
)->show();
}
if (as<CCMenuItemToggler*>(sender)->isToggled()) {
auto res = m_mod->loadBinary();
auto res = m_mod->enable();
if (!res) {
FLAlertLayer::create(nullptr, "Error Loading Mod", res.unwrapErr(), "OK", nullptr)->show();
FLAlertLayer::create(nullptr, "Error Enabling Mod", res.unwrapErr(), "OK", nullptr)->show();
}
}
else {
@ -589,7 +586,7 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) {
}
}
if (m_layer) {
m_layer->updateAllStates(nullptr);
m_layer->updateAllStates();
}
as<CCMenuItemToggler*>(sender)->toggle(m_mod->isEnabled());
}
@ -651,12 +648,12 @@ void LocalModInfoPopup::doUninstall() {
auto layer = FLAlertLayer::create(
this,
"Uninstall complete",
"Mod was succesfully uninstalled! :) "
"Mod was successfully uninstalled! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect). "
"<co>Would you also like to delete the mod's "
"save data?</c>",
"Cancel",
"Keep",
"Delete",
350.f
);
@ -684,11 +681,13 @@ IndexItemInfoPopup::IndexItemInfoPopup()
bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) {
m_item = item;
m_installListener.setFilter(m_item->getModInfo().id());
m_installListener.setFilter(m_item->getMetadata().getID());
auto winSize = CCDirector::sharedDirector()->getWinSize();
if (!ModInfoPopup::init(item->getModInfo(), list)) return false;
if (!ModInfoPopup::init(item->getMetadata(), list)) return false;
if (item->isInstalled()) return true;
m_installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr,
@ -719,7 +718,7 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
FLAlertLayer::create(
"Install complete",
"Mod succesfully installed! :) "
"Mod successfully installed! :) "
"(You may need to <cy>restart the game</c> "
"for the mod to take full effect)",
"OK"
@ -751,43 +750,78 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) {
}
void IndexItemInfoPopup::onInstall(CCObject*) {
auto list = Index::get()->getInstallList(m_item);
if (!list) {
return FLAlertLayer::create(
"Unable to Install",
list.unwrapErr(),
"OK"
)->show();
auto deps = m_item->getMetadata().getDependencies();
enum class DepState {
None,
HasOnlyRequired,
HasOptional
} depState = DepState::None;
for (auto const& item : deps) {
// resolved means it's already installed, so
// no need to ask the user whether they want to install it
if (Loader::get()->isModLoaded(item.id))
continue;
if (item.importance != ModMetadata::Dependency::Importance::Required) {
depState = DepState::HasOptional;
break;
}
depState = DepState::HasOnlyRequired;
}
FLAlertLayer::create(
this,
"Confirm Install",
fmt::format(
"The following mods will be installed:\n {}",
// le nest
ranges::join(
ranges::map<std::vector<std::string>>(
list.unwrap().list,
[](IndexItemHandle handle) {
return fmt::format(
" - <cr>{}</c> (<cy>{}</c>)",
handle->getModInfo().name(),
handle->getModInfo().id()
);
}
),
"\n "
)
),
"Cancel", "OK"
)->show();
std::string content;
char const* btn1;
char const* btn2;
switch (depState) {
case DepState::None:
content = fmt::format(
"Are you sure you want to install <cg>{}</c>?",
m_item->getMetadata().getName()
);
btn1 = "Info";
btn2 = "Install";
break;
case DepState::HasOnlyRequired:
content =
"Installing this mod requires other mods to be installed. "
"Would you like to <cy>proceed</c> with the installation or "
"<cb>view</c> which mods are going to be installed?";
btn1 = "View";
btn2 = "Proceed";
break;
case DepState::HasOptional:
content =
"This mod recommends installing other mods alongside it. "
"Would you like to continue with <cy>recommended settings</c> or "
"<cb>customize</c> which mods to install?";
btn1 = "Customize";
btn2 = "Recommended";
break;
}
createQuickPopup("Confirm Install", content, btn1, btn2, 320.f, [&](FLAlertLayer*, bool btn2) {
if (btn2) {
auto canInstall = Index::get()->canInstall(m_item);
if (!canInstall) {
FLAlertLayer::create(
"Unable to Install",
canInstall.unwrapErr(),
"OK"
)->show();
return;
}
this->preInstall();
Index::get()->install(m_item);
}
else {
InstallListPopup::create(m_item, [&](IndexInstallList const& list) {
this->preInstall();
Index::get()->install(list);
})->show();
}
}, true, true);
}
void IndexItemInfoPopup::onCancel(CCObject*) {
Index::get()->cancelInstall(m_item);
}
void IndexItemInfoPopup::doInstall() {
void IndexItemInfoPopup::preInstall() {
if (m_latestVersionLabel) {
m_latestVersionLabel->setVisible(false);
}
@ -798,22 +832,18 @@ void IndexItemInfoPopup::doInstall() {
);
m_installBtnSpr->setString("Cancel");
m_installBtnSpr->setBG("GJ_button_06.png", false);
Index::get()->install(m_item);
}
void IndexItemInfoPopup::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
if (btn2) {
this->doInstall();
}
void IndexItemInfoPopup::onCancel(CCObject*) {
Index::get()->cancelInstall(m_item);
}
CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {
return geode::createIndexItemLogo(m_item, size);
}
ModInfo IndexItemInfoPopup::getModInfo() const {
return m_item->getModInfo();
ModMetadata IndexItemInfoPopup::getMetadata() const {
return m_item->getMetadata();
}
IndexItemInfoPopup* IndexItemInfoPopup::create(

View file

@ -44,7 +44,7 @@ protected:
void onSupport(CCObject*);
void onInfo(CCObject*);
bool init(ModInfo const& info, ModListLayer* list);
bool init(ModMetadata const& metadata, ModListLayer* list);
void keyDown(cocos2d::enumKeyCodes) override;
void onClose(cocos2d::CCObject*);
@ -52,7 +52,7 @@ protected:
void setInstallStatus(std::optional<UpdateProgress> const& progress);
virtual CCNode* createLogo(CCSize const& size) = 0;
virtual ModInfo getModInfo() const = 0;
virtual ModMetadata getMetadata() const = 0;
};
class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
@ -62,7 +62,7 @@ protected:
Mod* m_mod;
bool init(Mod* mod, ModListLayer* list);
void onIssues(CCObject*);
void onSettings(CCObject*);
void onNoSettings(CCObject*);
@ -81,7 +81,7 @@ protected:
void FLAlert_Clicked(FLAlertLayer*, bool) override;
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
ModMetadata getMetadata() const override;
LocalModInfoPopup();
@ -89,22 +89,21 @@ public:
static LocalModInfoPopup* create(Mod* mod, ModListLayer* list);
};
class IndexItemInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol {
class IndexItemInfoPopup : public ModInfoPopup {
protected:
IndexItemHandle m_item;
EventListener<ModInstallFilter> m_installListener;
bool init(IndexItemHandle item, ModListLayer* list);
void onInstallProgress(ModInstallEvent* event);
void onInstall(CCObject*);
void onCancel(CCObject*);
void doInstall();
void FLAlert_Clicked(FLAlertLayer*, bool) override;
void preInstall();
CCNode* createLogo(CCSize const& size) override;
ModInfo getModInfo() const override;
ModMetadata getMetadata() const override;
IndexItemInfoPopup();

View file

@ -0,0 +1,319 @@
#include "InstallListCell.hpp"
#include "InstallListPopup.hpp"
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/binding/FLAlertLayer.hpp>
#include <Geode/binding/StatsCell.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <loader/LoaderImpl.hpp>
#include <utility>
#include "../info/TagNode.hpp"
#include "../info/DevProfilePopup.hpp"
// InstallListCell
void InstallListCell::draw() {
reinterpret_cast<StatsCell*>(this)->StatsCell::draw();
}
float InstallListCell::getLogoSize() const {
return m_height / 1.5f;
}
void InstallListCell::setupInfo(
std::string name,
std::optional<std::string> developer,
std::variant<VersionInfo, ComparableVersionInfo> version,
bool inactive
) {
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 10.f, m_height / 2);
this->addChild(m_menu);
auto logoSize = this->getLogoSize();
auto logoSpr = this->createLogo({ logoSize, logoSize });
logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 });
auto logoSprColor = typeinfo_cast<CCRGBAProtocol*>(logoSpr);
if (inactive && logoSprColor) {
logoSprColor->setColor({ 163, 163, 163 });
}
this->addChild(logoSpr);
auto titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
titleLabel->setPositionY(m_height / 2);
titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f);
if (inactive) {
titleLabel->setColor({ 163, 163, 163 });
}
this->addChild(titleLabel);
m_developerBtn = nullptr;
if (developer) {
auto creatorStr = "by " + *developer;
auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt");
creatorLabel->setScale(.34f);
if (inactive) {
creatorLabel->setColor({ 163, 163, 163 });
}
m_developerBtn = CCMenuItemSpriteExtra::create(
creatorLabel, this, menu_selector(InstallListCell::onViewDev)
);
m_developerBtn->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f +
creatorLabel->getScaledContentSize().width / 2 -
m_menu->getPositionX(),
-0.5f
);
m_menu->addChild(m_developerBtn);
}
auto versionLabel = CCLabelBMFont::create(
std::holds_alternative<VersionInfo>(version) ?
std::get<VersionInfo>(version).toString(false).c_str() :
std::get<ComparableVersionInfo>(version).toString().c_str(),
"bigFont.fnt"
);
versionLabel->setAnchorPoint({ .0f, .5f });
versionLabel->setScale(.2f);
versionLabel->setPosition(
titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f +
(m_developerBtn ? m_developerBtn->getScaledContentSize().width + 3.f : 0.f),
titleLabel->getPositionY() - 1.f
);
versionLabel->setColor({ 0, 255, 0 });
if (inactive) {
versionLabel->setColor({ 0, 163, 0 });
}
this->addChild(versionLabel);
if (!std::holds_alternative<VersionInfo>(version)) return;
if (auto tag = std::get<VersionInfo>(version).getTag()) {
auto tagLabel = TagNode::create(tag->toString());
tagLabel->setAnchorPoint({.0f, .5f});
tagLabel->setScale(.2f);
tagLabel->setPosition(
versionLabel->getPositionX() + versionLabel->getScaledContentSize().width + 3.f,
versionLabel->getPositionY()
);
this->addChild(tagLabel);
}
}
void InstallListCell::setupInfo(ModMetadata const& metadata, bool inactive) {
this->setupInfo(metadata.getName(), metadata.getDeveloper(), metadata.getVersion(), inactive);
}
void InstallListCell::onViewDev(CCObject*) {
DevProfilePopup::create(getDeveloper())->show();
}
bool InstallListCell::init(InstallListPopup* list, CCSize const& size) {
m_width = size.width;
m_height = size.height;
m_layer = list;
this->setContentSize(size);
this->setID("install-list-cell");
return true;
}
bool InstallListCell::isIncluded() {
return m_toggle && m_toggle->isOn();
}
// ModInstallListCell
bool ModInstallListCell::init(Mod* mod, InstallListPopup* list, CCSize const& size) {
if (!InstallListCell::init(list, size))
return false;
m_mod = mod;
this->setupInfo(mod->getMetadata(), true);
auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
message->setAnchorPoint({ 1.f, .5f });
message->setPositionX(m_menu->getPositionX());
message->setPositionY(16.f);
message->setScale(0.4f);
message->setColor({ 163, 163, 163 });
this->addChild(message);
return true;
}
ModInstallListCell* ModInstallListCell::create(Mod* mod, InstallListPopup* list, CCSize const& size) {
auto ret = new ModInstallListCell();
if (ret->init(mod, list, size)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
CCNode* ModInstallListCell::createLogo(CCSize const& size) {
return geode::createModLogo(m_mod, size);
}
std::string ModInstallListCell::getID() const {
return m_mod->getID();
}
std::string ModInstallListCell::getDeveloper() const {
return m_mod->getDeveloper();
}
// IndexItemInstallListCell
bool IndexItemInstallListCell::init(
IndexItemHandle item,
ModMetadata::Dependency::Importance importance,
InstallListPopup* list,
CCSize const& size,
std::optional<bool> selected
) {
if (!InstallListCell::init(list, size))
return false;
m_item = item;
this->setupInfo(item->getMetadata(), item->isInstalled());
if (item->isInstalled()) {
auto message = CCLabelBMFont::create("Installed", "bigFont.fnt");
message->setAnchorPoint({ 1.f, .5f });
message->setPositionX(m_menu->getPositionX());
message->setPositionY(16.f);
message->setScale(0.4f);
message->setColor({ 163, 163, 163 });
this->addChild(message);
return true;
}
m_toggle = CCMenuItemToggler::createWithStandardSprites(
m_layer,
menu_selector(InstallListPopup::onCellToggle),
.6f
);
m_toggle->setPosition(-m_toggle->getScaledContentSize().width / 2, 0.f);
switch (importance) {
case ModMetadata::Dependency::Importance::Required:
m_toggle->setClickable(false);
m_toggle->toggle(true);
break;
case ModMetadata::Dependency::Importance::Recommended:
m_toggle->setClickable(true);
m_toggle->toggle(true);
break;
case ModMetadata::Dependency::Importance::Suggested:
m_toggle->setClickable(true);
m_toggle->toggle(false);
break;
}
if (m_item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) {
m_toggle->setClickable(false);
m_toggle->toggle(false);
auto message = CCLabelBMFont::create("N/A", "bigFont.fnt");
message->setAnchorPoint({ 1.f, .5f });
message->setPositionX(m_menu->getPositionX() - m_toggle->getScaledContentSize().width - 5.f);
message->setPositionY(16.f);
message->setScale(0.4f);
message->setColor({ 240, 31, 31 });
this->addChild(message);
if (importance != ModMetadata::Dependency::Importance::Required) {
message->setCString("N/A (Optional)");
message->setColor({ 163, 24, 24 });
}
}
if (m_toggle->m_notClickable) {
m_toggle->m_offButton->setOpacity(100);
m_toggle->m_offButton->setColor(cc3x(155));
m_toggle->m_onButton->setOpacity(100);
m_toggle->m_onButton->setColor(cc3x(155));
}
if (!m_toggle->m_notClickable && selected) {
m_toggle->toggle(*selected);
}
m_menu->addChild(m_toggle);
return true;
}
IndexItemInstallListCell* IndexItemInstallListCell::create(
IndexItemHandle item,
ModMetadata::Dependency::Importance importance,
InstallListPopup* list,
CCSize const& size,
std::optional<bool> selected
) {
auto ret = new IndexItemInstallListCell();
if (ret->init(std::move(item), importance, list, size, selected)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
CCNode* IndexItemInstallListCell::createLogo(CCSize const& size) {
return geode::createIndexItemLogo(m_item, size);
}
std::string IndexItemInstallListCell::getID() const {
return m_item->getMetadata().getID();
}
std::string IndexItemInstallListCell::getDeveloper() const {
return m_item->getMetadata().getDeveloper();
}
IndexItemHandle IndexItemInstallListCell::getItem() {
return m_item;
}
// UnknownInstallListCell
bool UnknownInstallListCell::init(
ModMetadata::Dependency const& dependency,
InstallListPopup* list,
CCSize const& size
) {
if (!InstallListCell::init(list, size))
return false;
m_dependency = dependency;
bool optional = dependency.importance != ModMetadata::Dependency::Importance::Required;
this->setupInfo(dependency.id, std::nullopt, dependency.version, optional);
auto message = CCLabelBMFont::create("Missing", "bigFont.fnt");
message->setAnchorPoint({ 1.f, .5f });
message->setPositionX(m_menu->getPositionX());
message->setPositionY(16.f);
message->setScale(0.4f);
message->setColor({ 240, 31, 31 });
if (optional) {
message->setCString("Missing (Optional)");
message->setColor({ 163, 24, 24 });
}
this->addChild(message);
return true;
}
UnknownInstallListCell* UnknownInstallListCell::create(
ModMetadata::Dependency const& dependency,
InstallListPopup* list,
CCSize const& size
) {
auto ret = new UnknownInstallListCell();
if (ret->init(dependency, list, size)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
CCNode* UnknownInstallListCell::createLogo(CCSize const& size) {
return geode::createDefaultLogo(size);
}
std::string UnknownInstallListCell::getID() const {
return m_dependency.id;
}
std::string UnknownInstallListCell::getDeveloper() const {
return "";
}

View file

@ -0,0 +1,114 @@
#pragma once
#include <Geode/binding/TableViewCell.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModMetadata.hpp>
#include <Geode/loader/Index.hpp>
using namespace geode::prelude;
class InstallListPopup;
/**
* Base class for install list items
*/
class InstallListCell : public CCLayer {
protected:
float m_width;
float m_height;
InstallListPopup* m_layer;
CCMenu* m_menu;
CCMenuItemSpriteExtra* m_developerBtn;
CCMenuItemToggler* m_toggle = nullptr;
void setupInfo(
std::string name,
std::optional<std::string> developer,
std::variant<VersionInfo, ComparableVersionInfo> version,
bool inactive
);
bool init(InstallListPopup* list, CCSize const& size);
void setupInfo(ModMetadata const& metadata, bool inactive);
void draw() override;
float getLogoSize() const;
void onViewDev(CCObject*);
public:
bool isIncluded();
virtual CCNode* createLogo(CCSize const& size) = 0;
[[nodiscard]] virtual std::string getID() const = 0;
[[nodiscard]] virtual std::string getDeveloper() const = 0;
};
/**
* Install list item for a mod
*/
class ModInstallListCell : public InstallListCell {
protected:
Mod* m_mod;
bool init(Mod* mod, InstallListPopup* list, CCSize const& size);
public:
static ModInstallListCell* create(Mod* mod, InstallListPopup* list, CCSize const& size);
CCNode* createLogo(CCSize const& size) override;
[[nodiscard]] std::string getID() const override;
[[nodiscard]] std::string getDeveloper() const override;
};
/**
* Install list item for an index item
*/
class IndexItemInstallListCell : public InstallListCell {
protected:
IndexItemHandle m_item;
bool init(
IndexItemHandle item,
ModMetadata::Dependency::Importance importance,
InstallListPopup* list,
CCSize const& size,
std::optional<bool> selected
);
public:
static IndexItemInstallListCell* create(
IndexItemHandle item,
ModMetadata::Dependency::Importance importance,
InstallListPopup* list,
CCSize const& size,
std::optional<bool> selected
);
CCNode* createLogo(CCSize const& size) override;
[[nodiscard]] std::string getID() const override;
[[nodiscard]] std::string getDeveloper() const override;
IndexItemHandle getItem();
};
/**
* Install list item for an unknown item
*/
class UnknownInstallListCell : public InstallListCell {
protected:
ModMetadata::Dependency m_dependency;
bool init(ModMetadata::Dependency const& dependency, InstallListPopup* list, CCSize const& size);
public:
static UnknownInstallListCell* create(
ModMetadata::Dependency const& dependency,
InstallListPopup* list,
CCSize const& size
);
CCNode* createLogo(CCSize const& size) override;
[[nodiscard]] std::string getID() const override;
[[nodiscard]] std::string getDeveloper() const override;
};

View file

@ -0,0 +1,229 @@
#include "InstallListPopup.hpp"
#include "InstallListCell.hpp"
#include <utility>
#include <queue>
bool InstallListPopup::setup(IndexItemHandle item, MiniFunction<void(IndexInstallList const&)> callback) {
m_noElasticity = true;
m_item = item;
m_callback = callback;
this->setTitle("Select Mods to Install");
this->createList();
auto installBtnSpr = IconButtonSprite::create(
"GE_button_01.png"_spr,
CCSprite::createWithSpriteFrameName("install.png"_spr),
"Install",
"bigFont.fnt"
);
installBtnSpr->setScale(.6f);
auto installBtn = CCMenuItemSpriteExtra::create(
installBtnSpr,
this,
menu_selector(InstallListPopup::onInstall)
);
installBtn->setPositionY(-m_bgSprite->getScaledContentSize().height / 2 + 22.f);
m_buttonMenu->addChild(installBtn);
return true;
}
void InstallListPopup::createList() {
auto winSize = CCDirector::sharedDirector()->getWinSize();
std::unordered_map<std::string, InstallListCell*> oldCells;
bool oldScrollAtBottom;
std::optional<float> oldScroll;
if (m_list) {
CCArray* oldEntries = m_list->m_entries;
for (size_t i = 0; i < oldEntries->count(); i++) {
auto* itemCell = typeinfo_cast<InstallListCell*>(oldEntries->objectAtIndex(i));
oldCells[itemCell->getID()] = itemCell;
}
auto content = m_list->m_tableView->m_contentLayer;
oldScroll = content->getPositionY();
oldScrollAtBottom = oldScroll >= 0.f;
if (!oldScrollAtBottom)
*oldScroll += content->getScaledContentSize().height;
m_list->removeFromParent();
}
if (m_listParent) {
m_listParent->removeFromParent();
}
m_listParent = CCNode::create();
m_mainLayer->addChild(m_listParent);
auto items = this->createCells(oldCells);
m_list = ListView::create(
items,
this->getCellSize().height,
this->getListSize().width,
this->getListSize().height
);
m_list->setPosition(winSize / 2 - m_list->getScaledContentSize() / 2);
m_listParent->addChild(m_list);
// restore scroll on list recreation
// it's stored from the top unless was scrolled all the way to the bottom
if (oldScroll) {
auto content = m_list->m_tableView->m_contentLayer;
if (oldScrollAtBottom)
content->setPositionY(*oldScroll);
else
content->setPositionY(*oldScroll - content->getScaledContentSize().height);
}
addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize());
}
CCArray* InstallListPopup::createCells(std::unordered_map<std::string, InstallListCell*> const& oldCells) {
std::vector<InstallListCell*> top;
std::vector<InstallListCell*> middle;
std::vector<InstallListCell*> bottom;
std::queue<ModMetadata::Dependency> queue;
std::unordered_set<std::string> queued;
auto id = m_item->getMetadata().getID();
middle.push_back(IndexItemInstallListCell::create(
m_item,
ModMetadata::Dependency::Importance::Required,
this,
this->getCellSize(),
oldCells.contains(id) ? std::make_optional(oldCells.at(id)->isIncluded()) : std::nullopt
));
for (auto const& dep : m_item->getMetadata().getDependencies()) {
queue.push(dep);
}
auto index = Index::get();
while (!queue.empty()) {
auto const& item = queue.front();
if (queued.contains(item.id)) {
queue.pop();
continue;
}
queued.insert(item.id);
// installed
if (item.mod && !item.mod->isUninstalled()) {
bottom.push_back(ModInstallListCell::create(item.mod, this, this->getCellSize()));
for (auto const& dep : item.mod->getMetadata().getDependencies()) {
queue.push(dep);
}
queue.pop();
continue;
}
// on index
if (auto depItem = index->getItem(item.id, item.version)) {
auto cell = IndexItemInstallListCell::create(
depItem,
item.importance,
this,
this->getCellSize(),
oldCells.contains(item.id) ?
std::make_optional(oldCells.at(item.id)->isIncluded()) :
std::nullopt
);
// put missing dependencies at the top
if (depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0)
top.push_back(cell);
// put installed dependencies at the bottom
else if (depItem->isInstalled())
bottom.push_back(cell);
else
middle.push_back(cell);
if (!cell->isIncluded()) {
queue.pop();
continue;
}
for (auto const& dep : depItem->getMetadata().getDependencies()) {
queue.push(dep);
}
queue.pop();
continue;
}
// unknown (aka not installed and missing from index)
auto unknownCell = UnknownInstallListCell::create(item, this, this->getCellSize());
top.push_back(unknownCell);
queue.pop();
}
auto mods = CCArray::create();
for (auto const& item : top) {
mods->addObject(item);
}
for (auto const& item : middle) {
mods->addObject(item);
}
for (auto const& item : bottom) {
mods->addObject(item);
}
return mods;
}
// Getters
CCSize InstallListPopup::getListSize() const {
return { 340.f, 170.f };
}
CCSize InstallListPopup::getCellSize() const {
return { getListSize().width, 30.f };
}
// Callbacks
void InstallListPopup::onCellToggle(cocos2d::CCObject* obj) {
auto* toggler = typeinfo_cast<CCMenuItemToggler*>(obj);
if (toggler && !toggler->m_notClickable)
toggler->toggle(!toggler->isOn());
this->createList();
}
void InstallListPopup::onInstall(cocos2d::CCObject* obj) {
this->onBtn2(obj);
if (!m_callback)
return;
IndexInstallList list;
list.target = m_item;
CCArray* entries = m_list->m_entries;
for (size_t i = entries->count(); i > 0; i--) {
auto* itemCell = typeinfo_cast<IndexItemInstallListCell*>(entries->objectAtIndex(i - 1));
if (!itemCell || !itemCell->isIncluded())
continue;
IndexItemHandle item = itemCell->getItem();
list.list.push_back(item);
}
m_callback(list);
}
// Static
InstallListPopup* InstallListPopup::create(
IndexItemHandle item,
MiniFunction<void(IndexInstallList const&)> onInstall
) {
auto ret = new InstallListPopup();
if (!ret->init(380.f, 250.f, std::move(item), std::move(onInstall))) {
CC_SAFE_DELETE(ret);
return nullptr;
}
ret->autorelease();
return ret;
}

View file

@ -0,0 +1,30 @@
#pragma once
#include <Geode/ui/Popup.hpp>
#include <Geode/loader/Index.hpp>
#include "InstallListCell.hpp"
using namespace geode::prelude;
class InstallListPopup : public Popup<IndexItemHandle, MiniFunction<void(IndexInstallList const&)>> {
protected:
IndexItemHandle m_item;
CCNode* m_listParent;
ListView* m_list;
MiniFunction<void(IndexInstallList const&)> m_callback;
bool setup(IndexItemHandle item, MiniFunction<void(IndexInstallList const&)> callback) override;
void createList();
CCArray* createCells(std::unordered_map<std::string, InstallListCell*> const& oldCells);
CCSize getCellSize() const;
CCSize getListSize() const;
void onInstall(CCObject* obj);
public:
void onCellToggle(CCObject* obj);
static InstallListPopup* create(IndexItemHandle item, MiniFunction<void(IndexInstallList const&)> onInstall);
};

View file

@ -1,4 +1,3 @@
#include "ModListCell.hpp"
#include "ModListLayer.hpp"
#include "../info/ModInfoPopup.hpp"
@ -11,6 +10,7 @@
#include <loader/LoaderImpl.hpp>
#include "../info/TagNode.hpp"
#include "../info/DevProfilePopup.hpp"
#include "ProblemsListPopup.hpp"
template <class T>
static bool tryOrAlert(Result<T> const& res, char const* title) {
@ -29,9 +29,10 @@ float ModListCell::getLogoSize() const {
}
void ModListCell::setupInfo(
ModInfo const& info,
ModMetadata const& metadata,
bool spaceForTags,
ModListDisplay display
ModListDisplay display,
bool inactive
) {
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
@ -41,13 +42,17 @@ void ModListCell::setupInfo(
auto logoSpr = this->createLogo({ logoSize, logoSize });
logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 });
auto logoSprColor = typeinfo_cast<CCRGBAProtocol*>(logoSpr);
if (inactive && logoSprColor) {
logoSprColor->setColor({ 163, 163, 163 });
}
this->addChild(logoSpr);
bool hasDesc =
display == ModListDisplay::Expanded &&
info.description().has_value();
metadata.getDescription().has_value();
auto titleLabel = CCLabelBMFont::create(info.name().c_str(), "bigFont.fnt");
auto titleLabel = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f);
if (hasDesc && spaceForTags) {
@ -63,10 +68,13 @@ void ModListCell::setupInfo(
titleLabel->setPositionY(m_height / 2 + 7.f);
}
titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f);
if (inactive) {
titleLabel->setColor({ 163, 163, 163 });
}
this->addChild(titleLabel);
auto versionLabel = CCLabelBMFont::create(
info.version().toString(false).c_str(),
metadata.getVersion().toString(false).c_str(),
"bigFont.fnt"
);
versionLabel->setAnchorPoint({ .0f, .5f });
@ -76,23 +84,52 @@ void ModListCell::setupInfo(
titleLabel->getPositionY() - 1.f
);
versionLabel->setColor({ 0, 255, 0 });
if (inactive) {
versionLabel->setColor({ 0, 163, 0 });
}
this->addChild(versionLabel);
if (auto tag = info.version().getTag()) {
TagNode* apiLabel = nullptr;
if (metadata.isAPI()) {
apiLabel = TagNode::create("API");
apiLabel->setAnchorPoint({ .0f, .5f });
apiLabel->setScale(.3f);
apiLabel->setPosition(
versionLabel->getPositionX() +
versionLabel->getScaledContentSize().width + 5.f,
versionLabel->getPositionY()
);
}
if (auto tag = metadata.getVersion().getTag()) {
auto tagLabel = TagNode::create(tag.value().toString().c_str());
tagLabel->setAnchorPoint({ .0f, .5f });
tagLabel->setScale(.3f);
tagLabel->setPosition(
versionLabel->getPositionX() +
versionLabel->getPositionX() +
versionLabel->getScaledContentSize().width + 5.f,
versionLabel->getPositionY()
);
this->addChild(tagLabel);
if (apiLabel) {
apiLabel->setPosition(
tagLabel->getPositionX() +
tagLabel->getScaledContentSize().width + 5.f,
tagLabel->getPositionY()
);
}
}
auto creatorStr = "by " + info.developer();
if (apiLabel)
this->addChild(apiLabel);
auto creatorStr = "by " + metadata.getDeveloper();
auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt");
creatorLabel->setScale(.43f);
if (inactive) {
creatorLabel->setColor({ 163, 163, 163 });
}
m_developerBtn = CCMenuItemSpriteExtra::create(
creatorLabel, this, menu_selector(ModListCell::onViewDev)
@ -129,10 +166,13 @@ void ModListCell::setupInfo(
descBG->setScale(.25f);
this->addChild(descBG);
m_description = CCLabelBMFont::create(info.description().value().c_str(), "chatFont.fnt");
m_description = CCLabelBMFont::create(metadata.getDescription().value().c_str(), "chatFont.fnt");
m_description->setAnchorPoint({ .0f, .5f });
m_description->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY());
m_description->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f);
if (inactive) {
m_description->setColor({ 163, 163, 163 });
}
this->addChild(m_description);
}
}
@ -187,30 +227,25 @@ void ModCell::onEnable(CCObject* sender) {
else {
tryOrAlert(m_mod->disable(), "Error disabling mod");
}
if (m_layer) {
m_layer->updateAllStates(this);
}
Loader::get()->queueInGDThread([this]() {
if (m_layer) {
m_layer->updateAllStates();
}
});
}
void ModCell::onUnresolvedInfo(CCObject*) {
std::string info =
"This mod has the following "
"<cr>unresolved dependencies</c>: ";
for (auto const& dep : m_mod->getUnresolvedDependencies()) {
info += fmt::format(
"<cg>{}</c> (<cy>{}</c>), ",
dep.id, dep.version.toString()
);
}
info.pop_back();
info.pop_back();
FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show();
ProblemsListPopup::create(m_mod)->show();
}
void ModCell::onInfo(CCObject*) {
LocalModInfoPopup::create(m_mod, m_layer)->show();
}
void ModCell::onRestart(CCObject*) {
utils::game::restart();
}
void ModCell::updateState() {
bool unresolved = m_mod->hasUnresolvedDependencies();
if (m_enableToggle) {
@ -221,7 +256,16 @@ void ModCell::updateState() {
m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255);
m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255));
}
m_unresolvedExMark->setVisible(unresolved);
bool hasProblems = false;
for (auto const& item : Loader::get()->getProblems()) {
if (!std::holds_alternative<Mod*>(item.cause) ||
std::get<Mod*>(item.cause) != m_mod ||
item.type <= LoadProblem::Type::Recommendation)
continue;
hasProblems = true;
break;
}
m_unresolvedExMark->setVisible(hasProblems);
}
bool ModCell::init(
@ -232,18 +276,50 @@ bool ModCell::init(
) {
if (!ModListCell::init(list, size))
return false;
m_mod = mod;
this->setupInfo(mod->getModInfo(), false, display);
this->setupInfo(mod->getMetadata(), false, display, m_mod->isUninstalled());
auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
if (mod->isUninstalled()) {
auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f);
restartSpr->setScale(.65f);
auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo));
m_menu->addChild(viewBtn);
auto restartBtn = CCMenuItemSpriteExtra::create(restartSpr, this, menu_selector(ModCell::onRestart));
restartBtn->setPositionX(-16.f);
m_menu->addChild(restartBtn);
}
else {
auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
if (m_mod->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) {
auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo));
m_menu->addChild(viewBtn);
if (m_mod->wasSuccessfullyLoaded()) {
auto latestIndexItem = Index::get()->getMajorItem(
mod->getMetadata().getID()
);
if (latestIndexItem && Index::get()->isUpdateAvailable(latestIndexItem)) {
viewSpr->updateBGImage("GE_button_01.png"_spr);
auto minorIndexItem = Index::get()->getItem(
mod->getMetadata().getID(),
ComparableVersionInfo(mod->getMetadata().getVersion(), VersionCompare::MoreEq)
);
if (latestIndexItem->getMetadata().getVersion().getMajor() > minorIndexItem->getMetadata().getVersion().getMajor()) {
auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f });
updateIcon->setZOrder(99);
updateIcon->setScale(.5f);
viewSpr->addChild(updateIcon);
}
}
}
}
if (m_mod->wasSuccessfullyLoaded() && m_mod->supportsDisabling() && !m_mod->isUninstalled()) {
m_enableToggle =
CCMenuItemToggler::createWithStandardSprites(this, menu_selector(ModCell::onEnable), .7f);
m_enableToggle->setPosition(-45.f, 0.f);
@ -259,30 +335,6 @@ bool ModCell::init(
m_unresolvedExMark->setVisible(false);
m_menu->addChild(m_unresolvedExMark);
if (m_mod->wasSuccesfullyLoaded()) {
auto latestIndexItem = Index::get()->getMajorItem(
mod->getModInfo().id()
);
if (latestIndexItem && Index::get()->isUpdateAvailable(latestIndexItem)) {
viewSpr->updateBGImage("GE_button_01.png"_spr);
auto minorIndexItem = Index::get()->getItem(
mod->getModInfo().id(),
ComparableVersionInfo(mod->getModInfo().version(), VersionCompare::MoreEq)
);
if (latestIndexItem->getModInfo().version().getMajor() > minorIndexItem->getModInfo().version().getMajor()) {
auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f });
updateIcon->setZOrder(99);
updateIcon->setScale(.5f);
viewSpr->addChild(updateIcon);
}
}
}
this->updateState();
return true;
@ -302,6 +354,10 @@ void IndexItemCell::onInfo(CCObject*) {
IndexItemInfoPopup::create(m_item, m_layer)->show();
}
void IndexItemCell::onRestart(CCObject*) {
utils::game::restart();
}
IndexItemCell* IndexItemCell::create(
IndexItemHandle item,
ModListLayer* list,
@ -327,15 +383,26 @@ bool IndexItemCell::init(
m_item = item;
this->setupInfo(item->getModInfo(), item->getTags().size(), display);
auto viewSpr = ButtonSprite::create(
"View", "bigFont.fnt", "GJ_button_01.png", .8f
);
viewSpr->setScale(.65f);
bool justInstalled = item->isInstalled() && !Loader::get()->isModInstalled(item->getMetadata().getID());
auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo));
m_menu->addChild(viewBtn);
this->setupInfo(item->getMetadata(), item->getTags().size(), display, justInstalled);
if (justInstalled) {
auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f);
restartSpr->setScale(.65f);
auto restartBtn = CCMenuItemSpriteExtra::create(restartSpr, this, menu_selector(IndexItemCell::onRestart));
restartBtn->setPositionX(-16.f);
m_menu->addChild(restartBtn);
}
else {
auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn =
CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo));
m_menu->addChild(viewBtn);
}
if (item->getTags().size()) {
float x = m_height / 2 + this->getLogoSize() / 2 + 13.f;
@ -355,7 +422,7 @@ bool IndexItemCell::init(
x += node->getScaledContentSize().width + 5.f;
}
}
this->updateState();
return true;
@ -364,7 +431,7 @@ bool IndexItemCell::init(
void IndexItemCell::updateState() {}
std::string IndexItemCell::getDeveloper() const {
return m_item->getModInfo().developer();
return m_item->getMetadata().getDeveloper();
}
CCNode* IndexItemCell::createLogo(CCSize const& size) {
@ -405,7 +472,6 @@ void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) {
)
->show();
}
Loader::get()->refreshModsList();
if (m_layer) {
m_layer->reloadList();
}
@ -443,7 +509,7 @@ bool InvalidGeodeFileCell::init(
pathLabel->setColor({ 255, 255, 0 });
this->addChild(pathLabel);
auto whySpr = ButtonSprite::create("Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f);
auto whySpr = ButtonSprite::create("Info", 0, false, "bigFont.fnt", "GJ_button_01.png", 0, .8f);
whySpr->setScale(.65f);
auto viewBtn =
@ -477,3 +543,112 @@ std::string InvalidGeodeFileCell::getDeveloper() const {
CCNode* InvalidGeodeFileCell::createLogo(CCSize const& size) {
return nullptr;
}
// ProblemsCell
void ProblemsCell::onInfo(CCObject*) {
ProblemsListPopup::create(nullptr)->show();
}
bool ProblemsCell::init(
ModListLayer* list,
ModListDisplay display,
CCSize const& size
) {
if (!ModListCell::init(list, size))
return false;
LoadProblem::Type problemType = LoadProblem::Type::Unknown;
// iterate problems to find the most important severity
for (auto const& problem : Loader::get()->getProblems()) {
if (problemType < problem.type)
problemType = problem.type;
// already found the most important one (error)
if (problemType > LoadProblem::Type::Conflict)
break;
}
std::string icon;
std::string title;
switch (problemType) {
case LoadProblem::Type::Unknown:
title = "?????";
break;
case LoadProblem::Type::Suggestion:
icon = "GJ_infoIcon_001.png";
title = "You have suggested mods";
m_color = { 66, 135, 245 };
break;
case LoadProblem::Type::Recommendation:
icon = "GJ_infoIcon_001.png";
title = "You have recommended mods";
m_color = { 66, 135, 245 };
break;
case LoadProblem::Type::Conflict:
icon = "info-warning.png"_spr;
title = "Some mods had warnings when loading";
m_color = { 250, 176, 37 };
break;
default:
icon = "info-alert.png"_spr;
title = "Some mods had problems loading";
m_color = { 245, 66, 66 };
break;
}
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
this->addChild(m_menu);
auto logoSize = this->getLogoSize();
if (!icon.empty()) {
auto logoSpr = CCSprite::createWithSpriteFrameName(icon.c_str());
limitNodeSize(logoSpr, size, 1.f, .1f);
logoSpr->setPosition({logoSize / 2 + 12.f, m_height / 2});
this->addChild(logoSpr);
}
auto titleLabel = CCLabelBMFont::create(title.c_str(), "bigFont.fnt");
titleLabel->setAnchorPoint({ .0f, .5f });
titleLabel->setPosition(m_height / 2 + logoSize / 2 + 13.f, m_height / 2);
titleLabel->limitLabelWidth(m_width - 120.f, 1.f, .1f);
this->addChild(titleLabel);
auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn =
CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ProblemsCell::onInfo));
m_menu->addChild(viewBtn);
return true;
}
std::optional<ccColor3B> ProblemsCell::getColor() {
return m_color;
}
ProblemsCell* ProblemsCell::create(
ModListLayer* list,
ModListDisplay display,
CCSize const& size
) {
auto ret = new ProblemsCell();
if (ret->init(list, display, size)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void ProblemsCell::updateState() {}
std::string ProblemsCell::getDeveloper() const {
return "";
}
CCNode* ProblemsCell::createLogo(CCSize const& size) {
return nullptr;
}

View file

@ -3,7 +3,7 @@
#include <Geode/binding/TableViewCell.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModInfo.hpp>
#include <Geode/loader/ModMetadata.hpp>
#include <Geode/loader/Index.hpp>
using namespace geode::prelude;
@ -26,7 +26,7 @@ protected:
CCMenuItemSpriteExtra* m_developerBtn;
bool init(ModListLayer* list, CCSize const& size);
void setupInfo(ModInfo const& info, bool spaceForTags, ModListDisplay display);
void setupInfo(ModMetadata const& metadata, bool spaceForTags, ModListDisplay display, bool inactive);
void draw() override;
float getLogoSize() const;
@ -55,6 +55,7 @@ protected:
);
void onInfo(CCObject*);
void onRestart(CCObject*);
void onEnable(CCObject*);
void onUnresolvedInfo(CCObject*);
@ -86,6 +87,7 @@ protected:
);
void onInfo(CCObject*);
void onRestart(CCObject*);
public:
static IndexItemCell* create(
@ -129,3 +131,32 @@ public:
CCNode* createLogo(CCSize const& size) override;
std::string getDeveloper() const override;
};
/**
* Mod list item for an invalid Geode package
*/
class ProblemsCell : public ModListCell {
protected:
std::optional<ccColor3B> m_color;
bool init(
ModListLayer* list,
ModListDisplay display,
CCSize const& size
);
void onInfo(CCObject*);
public:
static ProblemsCell* create(
ModListLayer* list,
ModListDisplay display,
CCSize const& size
);
std::optional<ccColor3B> getColor();
void updateState() override;
CCNode* createLogo(CCSize const& size) override;
std::string getDeveloper() const override;
};

View file

@ -55,18 +55,18 @@ static std::optional<int> fuzzyMatch(std::string const& kw, std::string const& s
static std::optional<int> queryMatchKeywords(
ModListQuery const& query,
ModInfo const& info
ModMetadata const& metadata
) {
double weighted = 0;
// fuzzy match keywords
if (query.keywords) {
bool someMatched = false;
WEIGHTED_MATCH_MAX(info.name(), 2);
WEIGHTED_MATCH_MAX(info.id(), 1);
WEIGHTED_MATCH_MAX(info.developer(), 0.5);
WEIGHTED_MATCH_MAX(info.details().value_or(""), 0.05);
WEIGHTED_MATCH_MAX(info.description().value_or(""), 0.2);
WEIGHTED_MATCH_MAX(metadata.getName(), 2);
WEIGHTED_MATCH_MAX(metadata.getID(), 1);
WEIGHTED_MATCH_MAX(metadata.getDeveloper(), 0.5);
WEIGHTED_MATCH_MAX(metadata.getDetails().value_or(""), 0.05);
WEIGHTED_MATCH_MAX(metadata.getDescription().value_or(""), 0.2);
if (!someMatched) {
return std::nullopt;
}
@ -77,7 +77,7 @@ static std::optional<int> queryMatchKeywords(
// sorted, at least enough so that if you're scrolling it based on
// alphabetical order you will find the part you're looking for easily
// so it's fine
return static_cast<int>(-tolower(info.name()[0]));
return static_cast<int>(-tolower(metadata.getName()[0]));
}
// if the weight is relatively small we can ignore it
@ -93,13 +93,12 @@ static std::optional<int> queryMatch(ModListQuery const& query, Mod* mod) {
// Only checking keywords makes sense for mods since their
// platform always matches, they are always visible and they don't
// currently list their tags
return queryMatchKeywords(query, mod->getModInfo());
return queryMatchKeywords(query, mod->getMetadata());
}
static std::optional<int> queryMatch(ModListQuery const& query, IndexItemHandle item) {
// if no force visibility was provided and item is already installed, don't
// show it
if (!query.forceVisibility && Loader::get()->isModInstalled(item->getModInfo().id())) {
// if no force visibility was provided and item is already installed, don't show it
if (!query.forceVisibility && Loader::get()->isModInstalled(item->getMetadata().getID())) {
return std::nullopt;
}
// make sure all tags match
@ -114,8 +113,18 @@ static std::optional<int> queryMatch(ModListQuery const& query, IndexItemHandle
})) {
return std::nullopt;
}
// if no force visibility was provided and item is already installed, don't show it
auto canInstall = Index::get()->canInstall(item);
if (!query.forceInvalid && !canInstall) {
log::warn(
"Removing {} from the list because it cannot be installed: {}",
item->getMetadata().getID(),
canInstall.unwrapErr()
);
return std::nullopt;
}
// otherwise match keywords
if (auto match = queryMatchKeywords(query, item->getModInfo())) {
if (auto match = queryMatchKeywords(query, item->getMetadata())) {
auto weighted = match.value();
// add extra weight on tag matches
if (query.keywords) {
@ -136,7 +145,7 @@ static std::optional<int> queryMatch(ModListQuery const& query, IndexItemHandle
static std::optional<int> queryMatch(ModListQuery const& query, InvalidGeodeFile const& info) {
// if any explicit filters were provided, no match
if (query.tags.size() || query.keywords.has_value()) {
if (!query.tags.empty() || query.keywords.has_value()) {
return std::nullopt;
}
return 0;
@ -147,34 +156,40 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer
switch (type) {
default:
case ModListType::Installed: {
// failed mods first
for (auto const& mod : Loader::get()->getFailedMods()) {
if (!queryMatch(query, mod)) continue;
mods->addObject(InvalidGeodeFileCell::create(
mod, this, m_display, this->getCellSize()
));
// problems first
if (!Loader::get()->getProblems().empty()) {
mods->addObject(ProblemsCell::create(this, m_display, this->getCellSize()));
}
// sort the mods by match score
std::multimap<int, Mod*> sorted;
// sort the mods by match score
std::multimap<int, ModListCell*> sorted;
// then other mods
// newly installed
for (auto const& item : Index::get()->getItems()) {
if (!item->isInstalled() ||
Loader::get()->isModInstalled(item->getMetadata().getID()) ||
Loader::get()->isModLoaded(item->getMetadata().getID()))
continue;
// match the same as other installed mods
if (auto match = queryMatchKeywords(query, item->getMetadata())) {
auto cell = IndexItemCell::create(item, this, m_display, this->getCellSize());
sorted.insert({ match.value(), cell });
}
}
// loaded
for (auto const& mod : Loader::get()->getAllMods()) {
// if the mod is no longer installed nor
// loaded, it's as good as not existing
// (because it doesn't)
if (mod->isUninstalled() && !mod->isLoaded()) continue;
// only show mods that match query in list
if (auto match = queryMatch(query, mod)) {
sorted.insert({ match.value(), mod });
auto cell = ModCell::create(mod, this, m_display, this->getCellSize());
sorted.insert({ match.value(), cell });
}
}
// add the mods sorted
for (auto& [score, mod] : ranges::reverse(sorted)) {
mods->addObject(ModCell::create(
mod, this, m_display, this->getCellSize()
));
for (auto& [score, cell] : ranges::reverse(sorted)) {
mods->addObject(cell);
}
} break;
@ -182,7 +197,8 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer
// sort the mods by match score
std::multimap<int, IndexItemHandle> sorted;
for (auto const& item : Index::get()->getItems()) {
auto index = Index::get();
for (auto const& item : index->getItems()) {
if (auto match = queryMatch(query, item)) {
sorted.insert({ match.value(), item });
}
@ -412,6 +428,9 @@ void ModListLayer::createSearchControl() {
inputBG->setScale(.5f);
m_searchBG->addChild(inputBG);
if (m_searchInput)
return;
m_searchInput =
CCTextInputNode::create(310.f - buttonSpace, 20.f, "Search Mods...", "bigFont.fnt");
m_searchInput->setLabelPlaceholderColor({ 150, 150, 150 });
@ -441,10 +460,7 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
std::nullopt;
// remove old list
if (m_list) {
if (m_searchBG) m_searchBG->retain();
m_list->removeFromParent();
}
if (m_list) m_list->removeFromParent();
auto items = this->createModCells(g_tab, m_query);
@ -455,6 +471,15 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
this->getListSize().width,
this->getListSize().height
);
// please forgive me for this code
auto problemsCell = typeinfo_cast<ProblemsCell*>(list->m_entries->objectAtIndex(0));
if (problemsCell) {
auto cellView =
typeinfo_cast<TableViewCell*>(list->m_tableView->m_cellArray->objectAtIndex(0));
if (cellView && problemsCell->getColor()) {
cellView->m_backgroundLayer->setColor(*problemsCell->getColor());
}
}
// set list status
if (!items->count()) {
@ -497,13 +522,7 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
m_tabsGradientSprite->setPosition(m_list->getPosition() + CCPoint{179.f, 235.f});
// add search input to list
if (!m_searchInput) {
this->createSearchControl();
}
else {
m_list->addChild(m_searchBG);
m_searchBG->release();
}
this->createSearchControl();
// enable filter button
m_filterBtn->setEnabled(g_tab != ModListType::Installed);
@ -546,14 +565,11 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) {
}
}
void ModListLayer::updateAllStates(ModListCell* toggled) {
void ModListLayer::updateAllStates() {
for (auto cell : CCArrayExt<GenericListCell>(
m_list->m_listView->m_tableView->m_cellArray
)) {
auto node = static_cast<ModListCell*>(cell->getChildByID("mod-list-cell"));
if (toggled != node) {
node->updateState();
}
static_cast<ModListCell*>(cell->getChildByID("mod-list-cell"))->updateState();
}
}
@ -612,7 +628,6 @@ void ModListLayer::onExit(CCObject*) {
}
void ModListLayer::onReload(CCObject*) {
Loader::get()->refreshModsList();
this->reloadList();
}

View file

@ -25,10 +25,15 @@ struct ModListQuery {
*/
std::optional<std::string> keywords;
/**
* Force mods to be shown on the list unless they explicitly mismatch some
* Force already installed mods to be shown on the list unless they explicitly mismatch some
* tags (used to show installed mods on index)
*/
bool forceVisibility;
/**
* Force not installable mods to be shown on the list unless they explicitly mismatch some
* tags (used to show installed mods on index)
*/
bool forceInvalid;
/**
* Empty means current platform
*/
@ -84,7 +89,7 @@ protected:
public:
static ModListLayer* create();
static ModListLayer* scene();
void updateAllStates(ModListCell* except = nullptr);
void updateAllStates();
ModListDisplay getDisplay() const;
ModListQuery& getQuery();

View file

@ -0,0 +1,140 @@
#include "ProblemsListCell.hpp"
#include "ProblemsListPopup.hpp"
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/binding/FLAlertLayer.hpp>
#include <Geode/binding/StatsCell.hpp>
#include <Geode/ui/GeodeUI.hpp>
#include <loader/LoaderImpl.hpp>
#include <utility>
void ProblemsListCell::draw() {
reinterpret_cast<StatsCell*>(this)->StatsCell::draw();
}
float ProblemsListCell::getLogoSize() const {
return m_height / 1.5f;
}
bool ProblemsListCell::init(LoadProblem problem, ProblemsListPopup* list, CCSize const& size) {
m_width = size.width;
m_height = size.height;
m_layer = list;
this->setContentSize(size);
this->setID("problems-list-cell");
std::string cause = "unknown";
if (std::holds_alternative<ghc::filesystem::path>(problem.cause)) {
cause = std::get<ghc::filesystem::path>(problem.cause).filename().string();
}
else if (std::holds_alternative<ModMetadata>(problem.cause)) {
cause = std::get<ModMetadata>(problem.cause).getName();
}
else if (std::holds_alternative<Mod*>(problem.cause)) {
cause = std::get<Mod*>(problem.cause)->getName();
}
std::string icon;
std::string message;
switch (problem.type) {
case LoadProblem::Type::Unknown:
message = fmt::format("Unknown error in {}", cause);
m_longMessage = problem.message;
break;
case LoadProblem::Type::Suggestion:
icon = "GJ_infoIcon_001.png";
message = fmt::format("{} suggests {}", cause, problem.message);
break;
case LoadProblem::Type::Recommendation:
icon = "GJ_infoIcon_001.png";
message = fmt::format("{} recommends {}", cause, problem.message);
break;
case LoadProblem::Type::Conflict:
icon = "info-warning.png"_spr;
message = fmt::format("{} conflicts with {}", cause, problem.message);
break;
case LoadProblem::Type::InvalidFile:
icon = "info-alert.png"_spr;
message = fmt::format("{} is an invalid .geode file", cause);
m_longMessage = problem.message;
break;
case LoadProblem::Type::Duplicate:
icon = "info-alert.png"_spr;
message = fmt::format("{} is installed more than once", cause);
m_longMessage = problem.message;
break;
case LoadProblem::Type::SetupFailed:
icon = "info-alert.png"_spr;
message = fmt::format("{} has failed setting up", cause);
m_longMessage = problem.message;
break;
case LoadProblem::Type::LoadFailed:
icon = "info-alert.png"_spr;
message = fmt::format("{} has failed loading", cause);
m_longMessage = problem.message;
break;
case LoadProblem::Type::EnableFailed:
icon = "info-alert.png"_spr;
message = fmt::format("{} has failed enabling", cause);
m_longMessage = problem.message;
break;
case LoadProblem::Type::MissingDependency:
icon = "info-alert.png"_spr;
message = fmt::format("{} depends on {}", cause, problem.message);
break;
case LoadProblem::Type::PresentIncompatibility:
icon = "info-alert.png"_spr;
message = fmt::format("{} is incompatible with {}", cause, problem.message);
break;
}
m_problem = std::move(problem);
m_menu = CCMenu::create();
m_menu->setPosition(m_width - 40.f, m_height / 2);
this->addChild(m_menu);
auto logoSize = this->getLogoSize();
if (!icon.empty()) {
auto logoSpr = CCSprite::createWithSpriteFrameName(icon.c_str());
limitNodeSize(logoSpr, size, 1.f, .1f);
logoSpr->setPosition({logoSize / 2 + 12.f, m_height / 2});
this->addChild(logoSpr);
}
auto messageLabel = CCLabelBMFont::create(message.c_str(), "bigFont.fnt");
messageLabel->setAnchorPoint({ .0f, .5f });
messageLabel->setPosition(m_height / 2 + logoSize / 2 + 13.f, m_height / 2);
messageLabel->limitLabelWidth(m_width - 120.f, 1.f, .1f);
this->addChild(messageLabel);
if (!m_longMessage.empty()) {
auto viewSpr = ButtonSprite::create("More", "bigFont.fnt", "GJ_button_01.png", .8f);
viewSpr->setScale(.65f);
auto viewBtn =
CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ProblemsListCell::onMore));
m_menu->addChild(viewBtn);
}
return true;
}
void ProblemsListCell::onMore(cocos2d::CCObject*) {
FLAlertLayer::create("Problem Info", m_longMessage, "OK")->show();
}
LoadProblem ProblemsListCell::getProblem() const {
return m_problem;
}
ProblemsListCell* ProblemsListCell::create(LoadProblem problem, ProblemsListPopup* list, CCSize const& size) {
auto ret = new ProblemsListCell();
if (ret->init(problem, list, size)) {
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}

View file

@ -0,0 +1,33 @@
#pragma once
#include <Geode/binding/TableViewCell.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/ModMetadata.hpp>
#include <Geode/loader/Index.hpp>
using namespace geode::prelude;
class ProblemsListPopup;
class ProblemsListCell : public CCLayer {
protected:
float m_width;
float m_height;
ProblemsListPopup* m_layer;
CCMenu* m_menu;
LoadProblem m_problem;
std::string m_longMessage;
bool init(LoadProblem problem, ProblemsListPopup* list, CCSize const& size);
void draw() override;
void onMore(CCObject*);
float getLogoSize() const;
public:
LoadProblem getProblem() const;
static ProblemsListCell* create(LoadProblem problem, ProblemsListPopup* list, CCSize const& size);
};

View file

@ -0,0 +1,106 @@
#include "ProblemsListPopup.hpp"
#include "ProblemsListCell.hpp"
#include <utility>
#include <queue>
bool ProblemsListPopup::setup(Mod* scrollTo) {
m_noElasticity = true;
this->setTitle("Problems");
this->createList(scrollTo);
return true;
}
void ProblemsListPopup::createList(Mod* scrollTo) {
auto winSize = CCDirector::sharedDirector()->getWinSize();
m_listParent = CCNode::create();
m_listParent->setPositionY(-7.f);
m_mainLayer->addChild(m_listParent);
float scroll = 0.f;
auto items = this->createCells(scrollTo, scroll);
m_list = ListView::create(
items,
this->getCellSize().height,
this->getListSize().width,
this->getListSize().height
);
m_list->setPosition(winSize / 2 - m_list->getScaledContentSize() / 2);
m_listParent->addChild(m_list);
m_list->m_tableView->m_contentLayer->setPositionY(m_list->m_tableView->m_contentLayer->getPositionY() + scroll);
addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize());
}
CCArray* ProblemsListPopup::createCells(Mod* scrollTo, float& scrollValue) {
std::vector<ProblemsListCell*> top;
std::vector<ProblemsListCell*> middle;
std::vector<ProblemsListCell*> bottom;
for (auto const& problem : Loader::get()->getProblems()) {
switch (problem.type) {
case geode::LoadProblem::Type::Suggestion:
bottom.push_back(ProblemsListCell::create(problem, this, this->getCellSize()));
break;
case geode::LoadProblem::Type::Recommendation:
middle.push_back(ProblemsListCell::create(problem, this, this->getCellSize()));
break;
default:
top.push_back(ProblemsListCell::create(problem, this, this->getCellSize()));
break;
}
}
auto final = CCArray::create();
// find the highest scrollTo element
bool scrollFound = false;
auto tryFindScroll = [&](auto const& item) {
if (!scrollTo || scrollFound ||
!std::holds_alternative<Mod*>(item->getProblem().cause) ||
std::get<Mod*>(item->getProblem().cause) != scrollTo)
return;
scrollValue = (float)final->count() * this->getCellSize().height;
scrollFound = true;
};
for (auto const& item : top) {
tryFindScroll(item);
final->addObject(item);
}
for (auto const& item : middle) {
tryFindScroll(item);
final->addObject(item);
}
for (auto const& item : bottom) {
tryFindScroll(item);
final->addObject(item);
}
return final;
}
// Getters
CCSize ProblemsListPopup::getListSize() const {
return { 340.f, 190.f };
}
CCSize ProblemsListPopup::getCellSize() const {
return { getListSize().width, 40.f };
}
// Static
ProblemsListPopup* ProblemsListPopup::create(Mod* scrollTo) {
auto ret = new ProblemsListPopup();
if (!ret->init(380.f, 250.f, scrollTo)) {
CC_SAFE_DELETE(ret);
return nullptr;
}
ret->autorelease();
return ret;
}

View file

@ -0,0 +1,22 @@
#pragma once
#include <Geode/ui/Popup.hpp>
#include <Geode/loader/Loader.hpp>
using namespace geode::prelude;
class ProblemsListPopup : public Popup<Mod*> {
protected:
CCNode* m_listParent;
ListView* m_list;
bool setup(Mod* scrollTo) override;
void createList(Mod* scrollTo);
CCArray* createCells(Mod* scrollTo, float& scrollValue);
CCSize getCellSize() const;
CCSize getListSize() const;
public:
static ProblemsListPopup* create(Mod* scrollTo);
};

View file

@ -5,7 +5,9 @@
#include <Geode/binding/GameToolbox.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/ui/SelectList.hpp>
// re-add when we actually add the platforms
const float iosAndAndroidSize = 45.f;
bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
m_noElasticity = true;
@ -14,66 +16,77 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) {
this->setTitle("Search Filters");
auto winSize = CCDirector::sharedDirector()->getWinSize();
auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f };
auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f };
// platforms
auto platformTitle = CCLabelBMFont::create("Platforms", "goldFont.fnt");
platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 75.f);
platformTitle->setAnchorPoint({ 0.5f, 1.f });
platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f);
platformTitle->setScale(.5f);
m_mainLayer->addChild(platformTitle);
auto platformBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
platformBG->setColor({ 0, 0, 0 });
platformBG->setOpacity(90);
platformBG->setContentSize({ 290.f, 205.f });
platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 11.f);
platformBG->setContentSize({ 290.f, 205.f - iosAndAndroidSize * 2.f });
platformBG->setAnchorPoint({ 0.5f, 1.f });
platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 62.25f - iosAndAndroidSize * 0.25f);
platformBG->setScale(.5f);
m_mainLayer->addChild(platformBG);
this->enable(this->addPlatformToggle("Windows", PlatformID::Windows, pos), type);
this->enable(this->addPlatformToggle("macOS", PlatformID::MacOS, pos), type);
this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type);
this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type);
//this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type);
//this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type);
// show installed
auto installedTitle = CCLabelBMFont::create("Other", "goldFont.fnt");
installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 57.f);
installedTitle->setAnchorPoint({ 0.5f, 1.f });
installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 50.5f + iosAndAndroidSize - iosAndAndroidSize * 0.25f);
installedTitle->setScale(.5f);
m_mainLayer->addChild(installedTitle);
auto installedBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
installedBG->setColor({ 0, 0, 0 });
installedBG->setOpacity(90);
installedBG->setContentSize({ 290.f, 65.f });
installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 85.f);
installedBG->setContentSize({ 290.f, 110.f });
installedBG->setAnchorPoint({ 0.5f, 1.f });
installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 68.75f + iosAndAndroidSize - iosAndAndroidSize * 0.25f);
installedBG->setScale(.5f);
m_mainLayer->addChild(installedBG);
pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f };
pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f + iosAndAndroidSize - iosAndAndroidSize * 0.25f };
this->addToggle(
"Show Installed", menu_selector(SearchFilterPopup::onShowInstalled),
m_modLayer->getQuery().forceVisibility, 0, pos
);
this->addToggle(
"Show Invalid", menu_selector(SearchFilterPopup::onShowInvalid),
m_modLayer->getQuery().forceInvalid, 1, pos
);
// tags
auto tagsTitle = CCLabelBMFont::create("Tags", "goldFont.fnt");
tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 75.f);
tagsTitle->setAnchorPoint({ 0.5f, 1.f });
tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f);
tagsTitle->setScale(.5f);
m_mainLayer->addChild(tagsTitle);
auto tagsBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f });
tagsBG->setColor({ 0, 0, 0 });
tagsBG->setOpacity(90);
tagsBG->setContentSize({ 290.f, 328.f });
tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 - 19.5f);
tagsBG->setContentSize({ 290.f, 328.f - iosAndAndroidSize });
tagsBG->setAnchorPoint({ 0.5f, 1.f });
tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 62.5f - iosAndAndroidSize * 0.25f);
tagsBG->setScale(.5f);
m_mainLayer->addChild(tagsBG);
pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f };
pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f };
for (auto& tag : Index::get()->getTags()) {
auto toggle = CCMenuItemToggler::createWithStandardSprites(
@ -116,6 +129,11 @@ void SearchFilterPopup::onShowInstalled(CCObject* sender) {
m_modLayer->getQuery().forceVisibility = !toggle->isToggled();
}
void SearchFilterPopup::onShowInvalid(CCObject* sender) {
auto toggle = static_cast<CCMenuItemToggler*>(sender);
m_modLayer->getQuery().forceInvalid = !toggle->isToggled();
}
void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) {
if (type == ModListType::Installed) {
toggle->setEnabled(false);
@ -162,7 +180,7 @@ void SearchFilterPopup::onClose(CCObject* sender) {
SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) {
auto ret = new SearchFilterPopup();
if (ret && ret->init(350.f, 240.f, layer, type)) {
if (ret && ret->init(350.f, 240.f - iosAndAndroidSize * 0.5f, layer, type)) {
ret->autorelease();
return ret;
}

View file

@ -19,6 +19,7 @@ protected:
void onPlatformToggle(CCObject*);
void onShowInstalled(CCObject*);
void onShowInvalid(CCObject*);
void onTag(CCObject*);
void enable(CCMenuItemToggler* toggle, ModListType type);

View file

@ -5,8 +5,18 @@ using namespace geode::prelude;
class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol {
protected:
MiniFunction<void(FLAlertLayer*, bool)> m_selected;
bool m_cancelledByEscape;
bool m_usedEscape = false;
void keyBackClicked() override {
m_usedEscape = true;
FLAlertLayer::keyBackClicked();
}
void FLAlert_Clicked(FLAlertLayer* layer, bool btn2) override {
if (m_cancelledByEscape && m_usedEscape) {
return;
}
if (m_selected) {
m_selected(layer, btn2);
}
@ -15,10 +25,11 @@ protected:
public:
static QuickPopup* create(
char const* title, std::string const& content, char const* btn1, char const* btn2,
float width, MiniFunction<void(FLAlertLayer*, bool)> selected
float width, MiniFunction<void(FLAlertLayer*, bool)> selected, bool cancelledByEscape
) {
auto inst = new QuickPopup;
inst->m_selected = selected;
inst->m_cancelledByEscape = cancelledByEscape;
if (inst && inst->init(inst, title, content, btn1, btn2, width, false, .0f)) {
inst->autorelease();
return inst;
@ -32,7 +43,7 @@ FLAlertLayer* geode::createQuickPopup(
char const* title, std::string const& content, char const* btn1, char const* btn2, float width,
MiniFunction<void(FLAlertLayer*, bool)> selected, bool doShow
) {
auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected);
auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, false);
if (doShow) {
ret->show();
}
@ -45,3 +56,21 @@ FLAlertLayer* geode::createQuickPopup(
) {
return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow);
}
FLAlertLayer* geode::createQuickPopup(
char const* title, std::string const& content, char const* btn1, char const* btn2, float width,
MiniFunction<void(FLAlertLayer*, bool)> selected, bool doShow, bool cancelledByEscape
) {
auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, cancelledByEscape);
if (doShow) {
ret->show();
}
return ret;
}
FLAlertLayer* geode::createQuickPopup(
char const* title, std::string const& content, char const* btn1, char const* btn2,
MiniFunction<void(FLAlertLayer*, bool)> selected, bool doShow, bool cancelledByEscape
) {
return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow, cancelledByEscape);
}

View file

@ -133,6 +133,11 @@ std::ostream& geode::operator<<(std::ostream& stream, VersionInfo const& version
Result<ComparableVersionInfo> ComparableVersionInfo::parse(std::string const& rawStr) {
VersionCompare compare;
auto string = rawStr;
if (string == "*") {
return Ok(ComparableVersionInfo({0, 0, 0}, VersionCompare::Any));
}
if (string.starts_with("<=")) {
compare = VersionCompare::LessEq;
string.erase(0, 2);
@ -162,13 +167,14 @@ Result<ComparableVersionInfo> ComparableVersionInfo::parse(std::string const& ra
}
std::string ComparableVersionInfo::toString() const {
std::string prefix = "";
std::string prefix;
switch (m_compare) {
case VersionCompare::Exact: prefix = "="; break;
case VersionCompare::Exact: prefix = "="; break;
case VersionCompare::LessEq: prefix = "<="; break;
case VersionCompare::MoreEq: prefix = ">="; break;
case VersionCompare::Less: prefix = "<"; break;
case VersionCompare::More: prefix = ">"; break;
case VersionCompare::Less: prefix = "<"; break;
case VersionCompare::More: prefix = ">"; break;
case VersionCompare::Any: return "*";
}
return prefix + m_version.toString();
}

View file

@ -127,29 +127,61 @@ std::wstring utils::string::replace(
std::vector<std::string> utils::string::split(std::string const& str, std::string const& split) {
std::vector<std::string> res;
if (str.size()) {
auto s = str;
size_t pos = 0;
while ((pos = s.find(split)) != std::string::npos) {
res.push_back(s.substr(0, pos));
s.erase(0, pos + split.length());
}
res.push_back(s);
if (str.empty()) return res;
auto s = str;
size_t pos;
while ((pos = s.find(split)) != std::string::npos) {
res.push_back(s.substr(0, pos));
s.erase(0, pos + split.length());
}
res.push_back(s);
return res;
}
std::vector<std::wstring> utils::string::split(std::wstring const& str, std::wstring const& split) {
std::vector<std::wstring> res;
if (str.size()) {
auto s = str;
size_t pos = 0;
while ((pos = s.find(split)) != std::wstring::npos) {
res.push_back(s.substr(0, pos));
s.erase(0, pos + split.length());
}
res.push_back(s);
if (str.empty()) return res;
auto s = str;
size_t pos;
while ((pos = s.find(split)) != std::wstring::npos) {
res.push_back(s.substr(0, pos));
s.erase(0, pos + split.length());
}
res.push_back(s);
return res;
}
std::string utils::string::join(std::vector<std::string> const& strs, std::string const& separator) {
std::string res;
if (strs.empty())
return res;
if (strs.size() == 1)
return strs[0];
// idk if less allocations but an extra loop is faster but
size_t size = 0;
for (auto const& str : strs)
size += str.size() + separator.size();
res.reserve(size);
for (auto const& str : strs)
res += str + separator;
res.erase(res.size() - separator.size());
return res;
}
std::wstring utils::string::join(std::vector<std::wstring> const& strs, std::wstring const& separator) {
std::wstring res;
if (strs.empty())
return res;
if (strs.size() == 1)
return strs[0];
// idk if less allocations but an extra loop is faster but
size_t size = 0;
for (auto const& str : strs)
size += str.size() + separator.size();
res.reserve(size);
for (auto const& str : strs)
res += str + separator;
res.erase(res.size() - separator.size());
return res;
}