Merge remote-tracking branch 'refs/remotes/origin/main'
29
CHANGELOG.md
|
@ -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)
|
||||
|
|
2
VERSION
|
@ -1 +1 @@
|
|||
1.1.0
|
||||
1.1.1
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
251
loader/include/Geode/loader/ModMetadata.hpp
Normal 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();
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.7 KiB |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
609
loader/src/loader/ModMetadataImpl.cpp
Normal 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")");
|
||||
}
|
||||
};
|
58
loader/src/loader/ModMetadataImpl.hpp
Normal 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);
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
319
loader/src/ui/internal/list/InstallListCell.cpp
Normal 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 "";
|
||||
}
|
114
loader/src/ui/internal/list/InstallListCell.hpp
Normal 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;
|
||||
};
|
229
loader/src/ui/internal/list/InstallListPopup.cpp
Normal 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;
|
||||
}
|
30
loader/src/ui/internal/list/InstallListPopup.hpp
Normal 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);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
140
loader/src/ui/internal/list/ProblemsListCell.cpp
Normal 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;
|
||||
}
|
33
loader/src/ui/internal/list/ProblemsListCell.hpp
Normal 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);
|
||||
};
|
106
loader/src/ui/internal/list/ProblemsListPopup.cpp
Normal 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;
|
||||
}
|
||||
|
22
loader/src/ui/internal/list/ProblemsListPopup.hpp
Normal 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);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ protected:
|
|||
|
||||
void onPlatformToggle(CCObject*);
|
||||
void onShowInstalled(CCObject*);
|
||||
void onShowInvalid(CCObject*);
|
||||
void onTag(CCObject*);
|
||||
|
||||
void enable(CCMenuItemToggler* toggle, ModListType type);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|