mirror of
https://github.com/geode-sdk/geode.git
synced 2025-04-24 05:14:40 -04:00
new blank + move ModInfo definitions to their own file + mod settings
are now saved
This commit is contained in:
parent
71bd0fec83
commit
c29443c0d7
10 changed files with 394 additions and 294 deletions
loader
include/Geode
resources/blanks
src/load
|
@ -278,8 +278,8 @@ namespace geode {
|
|||
Result<> loadPlatformBinary();
|
||||
Result<> unloadPlatformBinary();
|
||||
|
||||
Result<> saveDataStore();
|
||||
Result<> loadDataStore();
|
||||
Result<> saveSettings();
|
||||
Result<> loadSettings();
|
||||
|
||||
void postDSUpdate();
|
||||
|
||||
|
@ -323,13 +323,22 @@ namespace geode {
|
|||
|
||||
bool hasSettings() const;
|
||||
decltype(ModInfo::m_settings) getSettings() const;
|
||||
std::shared_ptr<Setting> getSetting(std::string const& key) const;
|
||||
template<class T>
|
||||
T getSettingValue(std::string const& key) const {
|
||||
if (m_info.m_settings.count(key)) {
|
||||
return geode::getSettingValue<T>(m_info.m_settings.at(key));
|
||||
return geode::getBuiltInSettingValue<T>(m_info.m_settings.at(key));
|
||||
}
|
||||
return T();
|
||||
}
|
||||
template<class T>
|
||||
bool setSettingValue(std::string const& key, T const& value) {
|
||||
if (m_info.m_settings.count(key)) {
|
||||
geode::setBuiltInSettingValue<T>(m_info.m_settings[key], value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mod container stored in the Interface
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace geode {
|
|||
};
|
||||
|
||||
namespace {
|
||||
#define GEODE_PARSE_SETTING_IMPL(obj, func) \
|
||||
#define GEODE_INT_PARSE_SETTING_IMPL(obj, func) \
|
||||
if constexpr (requires(JsonMaybeObject& obj) {\
|
||||
{res->func(obj)} -> std::same_as<Result<>>;\
|
||||
}) {\
|
||||
|
@ -63,7 +63,7 @@ namespace geode {
|
|||
if (!r) return Err(r.error());\
|
||||
}
|
||||
|
||||
#define GEODE_CONSTRAIN_SETTING_IMPL(func) \
|
||||
#define GEODE_INT_CONSTRAIN_SETTING_IMPL(func) \
|
||||
if constexpr (requires(ValueType& value) {\
|
||||
this->func(value);\
|
||||
}) {\
|
||||
|
@ -91,17 +91,17 @@ namespace geode {
|
|||
obj.needs("default").into(res->m_default);
|
||||
obj.has("name").into(res->m_name);
|
||||
obj.has("description").into(res->m_description);
|
||||
GEODE_PARSE_SETTING_IMPL(obj, Class::parseMinMax);
|
||||
GEODE_PARSE_SETTING_IMPL(obj, Class::parseOneOf);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(obj, Class::parseMinMax);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(obj, Class::parseOneOf);
|
||||
res->setValue(res->m_default);
|
||||
|
||||
if (auto controls = obj.has("control").obj()) {
|
||||
// every built-in setting type has a reset button
|
||||
// by default
|
||||
controls.has("can-reset").into(res->m_canResetToDefault);
|
||||
GEODE_PARSE_SETTING_IMPL(controls, Class::parseArrows);
|
||||
GEODE_PARSE_SETTING_IMPL(controls, Class::parseSlider);
|
||||
GEODE_PARSE_SETTING_IMPL(controls, Class::parseInput);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, Class::parseArrows);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, Class::parseSlider);
|
||||
GEODE_INT_PARSE_SETTING_IMPL(controls, Class::parseInput);
|
||||
}
|
||||
|
||||
return Ok(res);
|
||||
|
@ -128,17 +128,20 @@ namespace geode {
|
|||
|
||||
void setValue(ValueType const& value) {
|
||||
m_value = value;
|
||||
GEODE_CONSTRAIN_SETTING_IMPL(Class::constrainMinMax);
|
||||
GEODE_CONSTRAIN_SETTING_IMPL(Class::constrainOneOf);
|
||||
GEODE_INT_CONSTRAIN_SETTING_IMPL(Class::constrainMinMax);
|
||||
GEODE_INT_CONSTRAIN_SETTING_IMPL(Class::constrainOneOf);
|
||||
}
|
||||
|
||||
bool load(nlohmann::json const& json) override {
|
||||
m_value = json["value"];
|
||||
auto rawJson = json;
|
||||
JsonChecker(rawJson)
|
||||
.root("[setting value]")
|
||||
.into(m_value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool save(nlohmann::json& json) const override {
|
||||
json["value"] = m_value;
|
||||
json = m_value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -213,7 +216,7 @@ namespace geode {
|
|||
}
|
||||
};
|
||||
|
||||
#define GEODE_DECL_SETTING_CONTROL(Name, name, default, json) \
|
||||
#define GEODE_INT_DECL_SETTING_CONTROL(Name, name, default, json) \
|
||||
class IC##Name {\
|
||||
protected:\
|
||||
bool m_##name = default;\
|
||||
|
@ -227,9 +230,9 @@ namespace geode {
|
|||
}\
|
||||
}
|
||||
|
||||
GEODE_DECL_SETTING_CONTROL(Arrows, hasArrows, true, "arrows");
|
||||
GEODE_DECL_SETTING_CONTROL(Slider, hasSlider, true, "slider");
|
||||
GEODE_DECL_SETTING_CONTROL(Input, hasInput, true, "input");
|
||||
GEODE_INT_DECL_SETTING_CONTROL(Arrows, hasArrows, true, "arrows");
|
||||
GEODE_INT_DECL_SETTING_CONTROL(Slider, hasSlider, true, "slider");
|
||||
GEODE_INT_DECL_SETTING_CONTROL(Input, hasInput, true, "input");
|
||||
}
|
||||
|
||||
class GEODE_DLL BoolSetting :
|
||||
|
@ -269,31 +272,35 @@ namespace geode {
|
|||
SettingNode* createNode(float width) override;
|
||||
};
|
||||
|
||||
// these can't be member functions because C++ is single-pass >:(
|
||||
|
||||
#define GEODE_INT_BUILTIN_SETTING_IF(type, action, ...) \
|
||||
if constexpr (__VA_ARGS__) {\
|
||||
if (setting->getType() == SettingType::type) {\
|
||||
return std::static_pointer_cast<type##Setting>(setting)->action;\
|
||||
}\
|
||||
}
|
||||
|
||||
template<class T>
|
||||
T getSettingValue(const std::shared_ptr<Setting> setting) {
|
||||
if constexpr (std::is_same_v<T, bool>) {
|
||||
if (setting->getType() == SettingType::Bool) {
|
||||
return std::static_pointer_cast<BoolSetting>(setting)->getValue();
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_floating_point_v<T>) {
|
||||
if (setting->getType() == SettingType::Float) {
|
||||
return std::static_pointer_cast<FloatSetting>(setting)->getValue();
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_integral_v<T>) {
|
||||
if (setting->getType() == SettingType::Int) {
|
||||
return std::static_pointer_cast<IntSetting>(setting)->getValue();
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
if (setting->getType() == SettingType::String) {
|
||||
return std::static_pointer_cast<StringSetting>(setting)->getValue();
|
||||
}
|
||||
}
|
||||
T getBuiltInSettingValue(const std::shared_ptr<Setting> setting) {
|
||||
GEODE_INT_BUILTIN_SETTING_IF(Bool, getValue(), std::is_same_v<T, bool>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Float, getValue(), std::is_floating_point_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Int, getValue(), std::is_integral_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(String, getValue(), std::is_same_v<T, std::string>)
|
||||
else {
|
||||
static_assert(!std::is_same_v<T, T>, "todo: implement");
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
void setBuiltInSettingValue(const std::shared_ptr<Setting> setting, T const& value) {
|
||||
GEODE_INT_BUILTIN_SETTING_IF(Bool, setValue(value), std::is_same_v<T, bool>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Float, setValue(value), std::is_floating_point_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(Int, setValue(value), std::is_integral_v<T>)
|
||||
else GEODE_INT_BUILTIN_SETTING_IF(String, setValue(value), std::is_same_v<T, std::string>)
|
||||
else {
|
||||
static_assert(!std::is_same_v<T, T>, "todo: implement");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,13 +58,22 @@ namespace geode {
|
|||
};
|
||||
|
||||
enum class BaseType {
|
||||
// Circle buttons
|
||||
Circle = 0,
|
||||
// Like the buttons in the main menu
|
||||
Cross = 1,
|
||||
Account = 2,
|
||||
// Like the friend request / account buttons
|
||||
BevelledSquare = 2,
|
||||
// Like the icon select button
|
||||
IconSelect = 3,
|
||||
GlobalThing = 4,
|
||||
// Like the leaderboard buttons
|
||||
BevelledSquare2 = 4,
|
||||
// Like the buttons in the editor sidebar
|
||||
Editor = 5,
|
||||
// Like a list view tab
|
||||
Tab = 6,
|
||||
// Like the buttons in CreatorLayer
|
||||
Category = 7,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -290,8 +290,8 @@ namespace geode {
|
|||
}
|
||||
|
||||
JsonMaybeValue has(std::string const& key) {
|
||||
if (this->isError()) return emptyValue();
|
||||
this->addKnownKey(key);
|
||||
if (this->isError()) return emptyValue();
|
||||
if (!m_json.contains(key) || m_json[key].is_null()) {
|
||||
return emptyValue();
|
||||
}
|
||||
|
@ -299,8 +299,8 @@ namespace geode {
|
|||
}
|
||||
|
||||
JsonMaybeValue needs(std::string const& key) {
|
||||
if (this->isError()) return emptyValue();
|
||||
this->addKnownKey(key);
|
||||
if (this->isError()) return emptyValue();
|
||||
if (!m_json.contains(key)) {
|
||||
this->setError(
|
||||
m_hierarchy + " is missing required key \"" + key + "\""
|
||||
|
@ -335,12 +335,8 @@ namespace geode {
|
|||
return std::get<std::string>(m_result);
|
||||
}
|
||||
|
||||
JsonMaybeObject root(std::string const& hierarchy) {
|
||||
if (!m_json.is_object()) {
|
||||
m_result = hierarchy + ": Root is not an object";
|
||||
return JsonMaybeObject(*this, m_json, hierarchy, false);
|
||||
}
|
||||
return JsonMaybeObject(*this, m_json, hierarchy, true);
|
||||
JsonMaybeValue root(std::string const& hierarchy) {
|
||||
return JsonMaybeValue(*this, m_json, hierarchy, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
BIN
loader/resources/blanks/GEODE_blank07_00_00-uhd.png
Normal file
BIN
loader/resources/blanks/GEODE_blank07_00_00-uhd.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 111 KiB |
|
@ -188,7 +188,8 @@ Result<> Loader::saveSettings() {
|
|||
if (mod->isUninstalled()) continue;
|
||||
auto value = nlohmann::json::object();
|
||||
value["enabled"] = mod->m_enabled;
|
||||
mod->saveDataStore();
|
||||
mod->saveSettings();
|
||||
|
||||
json["mods"][id] = value;
|
||||
}
|
||||
json["succesfully-closed"] = true;
|
||||
|
|
|
@ -51,22 +51,82 @@ Mod::~Mod() {
|
|||
this->unload();
|
||||
}
|
||||
|
||||
Result<> Mod::loadDataStore() {
|
||||
auto dsPath = this->m_saveDirPath / "ds.json";
|
||||
Result<> Mod::loadSettings() {
|
||||
// settings
|
||||
|
||||
// Check if settings exist
|
||||
auto settPath = m_saveDirPath / "settings.json";
|
||||
if (ghc::filesystem::exists(settPath)) {
|
||||
auto settData = file_utils::readString(settPath);
|
||||
if (!settData) return settData;
|
||||
try {
|
||||
// parse settings.json
|
||||
auto data = nlohmann::json::parse(settData.value());
|
||||
JsonChecker checker(data);
|
||||
auto root = checker.root("[settings.json]");
|
||||
|
||||
for (auto& [key, value] : root.items()) {
|
||||
// check if this is a known setting
|
||||
if (auto sett = this->getSetting(key)) {
|
||||
// load its value
|
||||
if (!sett->load(value.json())) {
|
||||
return Err(
|
||||
"Unable to load value for setting \"" +
|
||||
key + "\""
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this->logInfo(
|
||||
"Encountered unknown setting \"" + key + "\" while "
|
||||
"loading settings",
|
||||
Severity::Warning
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} catch(std::exception& e) {
|
||||
return Err(std::string("Unable to parse settings: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// datastore
|
||||
auto dsPath = m_saveDirPath / "ds.json";
|
||||
if (!ghc::filesystem::exists(dsPath)) {
|
||||
this->m_dataStore = this->m_info.m_defaultDataStore;
|
||||
m_dataStore = m_info.m_defaultDataStore;
|
||||
} else {
|
||||
auto dsData = file_utils::readString(dsPath);
|
||||
if (!dsData) return dsData;
|
||||
|
||||
this->m_dataStore = nlohmann::json::parse(dsData.value());
|
||||
try {
|
||||
m_dataStore = nlohmann::json::parse(dsData.value());
|
||||
} catch(std::exception& e) {
|
||||
return Err(std::string("Unable to parse datastore: ") + e.what());
|
||||
}
|
||||
}
|
||||
return Ok<>();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
Result<> Mod::saveDataStore() {
|
||||
auto dsPath = this->m_saveDirPath / "ds.json";
|
||||
return file_utils::writeString(dsPath, m_dataStore.dump(4));
|
||||
Result<> Mod::saveSettings() {
|
||||
// settings
|
||||
auto settPath = m_saveDirPath / "settings.json";
|
||||
auto json = nlohmann::json::object();
|
||||
for (auto& [key, sett] : m_info.m_settings) {
|
||||
if (!sett->save(json[key])) {
|
||||
return Err("Unable to save setting \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
auto sw = file_utils::writeString(settPath, json.dump(4));
|
||||
if (!sw) {
|
||||
return sw;
|
||||
}
|
||||
|
||||
// datastore
|
||||
auto dsPath = m_saveDirPath / "ds.json";
|
||||
auto dw = file_utils::writeString(dsPath, m_dataStore.dump(4));
|
||||
if (!dw) {
|
||||
return dw;
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
DataStore Mod::getDataStore() {
|
||||
|
@ -497,239 +557,13 @@ decltype(ModInfo::m_settings) Mod::getSettings() const {
|
|||
return m_info.m_settings;
|
||||
}
|
||||
|
||||
std::shared_ptr<Setting> Mod::getSetting(std::string const& key) const {
|
||||
if (m_info.m_settings.count(key)) {
|
||||
return m_info.m_settings.at(key);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string Mod::getLoadErrorInfo() const {
|
||||
return m_loadErrorInfo;
|
||||
}
|
||||
|
||||
static std::string sanitizeDetailsData(unsigned char* start, unsigned char* end) {
|
||||
// delete CRLF
|
||||
return string_utils::replace(std::string(start, end), "\r", "");
|
||||
}
|
||||
|
||||
Result<ModInfo> ModInfo::createFromSchemaV010(nlohmann::json const& rawJson) {
|
||||
ModInfo info;
|
||||
|
||||
auto json = rawJson;
|
||||
|
||||
#define PROPAGATE(err) \
|
||||
{ auto err__ = err; if (!err__) return Err(err__.error()); }
|
||||
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("[mod.json]");
|
||||
|
||||
root.addKnownKey("geode");
|
||||
root.addKnownKey("binary");
|
||||
|
||||
using nlohmann::detail::value_t;
|
||||
|
||||
root
|
||||
.needs("id")
|
||||
.validate(&Mod::validateID)
|
||||
.into(info.m_id);
|
||||
root
|
||||
.needs("version")
|
||||
.validate(&VersionInfo::validate)
|
||||
.intoAs<std::string>(info.m_version);
|
||||
root.needs("name").into(info.m_name);
|
||||
root.needs("developer").into(info.m_developer);
|
||||
root.has("description").into(info.m_description);
|
||||
root.has("repository").into(info.m_repository);
|
||||
root.has("datastore").intoRaw(info.m_defaultDataStore);
|
||||
root.has("toggleable").into(info.m_supportsDisabling);
|
||||
root.has("unloadable").into(info.m_supportsUnloading);
|
||||
|
||||
for (auto& dep : root.has("dependencies").iterate()) {
|
||||
auto obj = dep.obj();
|
||||
|
||||
auto depobj = Dependency {};
|
||||
obj
|
||||
.needs("id")
|
||||
.validate(&Mod::validateID)
|
||||
.into(depobj.m_id);
|
||||
obj
|
||||
.needs("version")
|
||||
.validate(&VersionInfo::validate)
|
||||
.intoAs<std::string>(depobj.m_version);
|
||||
obj
|
||||
.has("required")
|
||||
.into(depobj.m_required);
|
||||
obj.checkUnknownKeys();
|
||||
|
||||
info.m_dependencies.push_back(depobj);
|
||||
}
|
||||
|
||||
for (auto& [key, value] : root.has("settings").items()) {
|
||||
auto sett = Setting::parse(key, value.json());
|
||||
PROPAGATE(sett);
|
||||
info.m_settings.insert({ key, sett.value() });
|
||||
}
|
||||
|
||||
if (auto resources = root.has("resources").obj()) {
|
||||
for (auto& [key, _] : resources.has("spritesheets").items()) {
|
||||
info.m_spritesheets.push_back(info.m_id + "/" + key);
|
||||
}
|
||||
}
|
||||
|
||||
root.has("binary").asOneOf<value_t::string, value_t::object>();
|
||||
|
||||
bool autoEndBinaryName = true;
|
||||
|
||||
root.has("binary").is<value_t::string>().into(info.m_binaryName);
|
||||
|
||||
if (auto bin = root.has("binary").is<value_t::object>().obj()) {
|
||||
bin.has("*").into(info.m_binaryName);
|
||||
bin.has("auto").into(autoEndBinaryName);
|
||||
|
||||
#if defined(GEODE_IS_WINDOWS)
|
||||
bin.has("windows").into(info.m_binaryName);
|
||||
#elif defined(GEODE_IS_MACOS)
|
||||
bin.has("macos").into(info.m_binaryName);
|
||||
#elif defined(GEODE_IS_ANDROID)
|
||||
bin.has("android").into(info.m_binaryName);
|
||||
#elif defined(GEODE_IS_IOS)
|
||||
bin.has("ios").into(info.m_binaryName);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (
|
||||
root.has("binary") &&
|
||||
autoEndBinaryName &&
|
||||
!string_utils::endsWith(info.m_binaryName, GEODE_PLATFORM_EXTENSION)
|
||||
) {
|
||||
info.m_binaryName += GEODE_PLATFORM_EXTENSION;
|
||||
}
|
||||
|
||||
if (checker.isError()) {
|
||||
return Err(checker.getError());
|
||||
}
|
||||
root.checkUnknownKeys();
|
||||
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
Result<ModInfo> ModInfo::create(nlohmann::json const& json) {
|
||||
// Check mod.json target version
|
||||
auto schema = LOADER_VERSION;
|
||||
if (json.contains("geode") && json["geode"].is_string()) {
|
||||
auto ver = json["geode"];
|
||||
if (VersionInfo::validate(ver)) {
|
||||
schema = VersionInfo(ver);
|
||||
} else {
|
||||
return Err(
|
||||
"[mod.json] has no target loader version "
|
||||
"specified, or it is invalidally formatted (required: \"[v]X.X.X\")!"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Err(
|
||||
"[mod.json] has no target loader version "
|
||||
"specified, or it is invalidally formatted (required: \"[v]X.X.X\")!"
|
||||
);
|
||||
}
|
||||
if (schema < Loader::s_supportedVersionMin) {
|
||||
return Err(
|
||||
"[mod.json] is built for an older version (" +
|
||||
schema.toString() + ") of Geode (current: " +
|
||||
Loader::s_supportedVersionMin.toString() +
|
||||
"). Please update the mod to the latest version, "
|
||||
"and if the problem persists, contact the developer "
|
||||
"to update it."
|
||||
);
|
||||
}
|
||||
if (schema > Loader::s_supportedVersionMax) {
|
||||
return Err(
|
||||
"[mod.json] is built for a newer version (" +
|
||||
schema.toString() + ") of Geode (current: " +
|
||||
Loader::s_supportedVersionMax.toString() +
|
||||
"). You need to update Geode in order to use "
|
||||
"this mod."
|
||||
);
|
||||
}
|
||||
|
||||
// Handle mod.json data based on target
|
||||
if (schema <= VersionInfo(0, 2, 0)) {
|
||||
return ModInfo::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::createFromFile(ghc::filesystem::path const& path) {
|
||||
try {
|
||||
auto read = file_utils::readString(path);
|
||||
if (!read) return Err(read.error());
|
||||
auto res = ModInfo::create(nlohmann::json::parse(read.value()));
|
||||
if (!res) return res;
|
||||
auto info = res.value();
|
||||
info.m_path = path;
|
||||
return Ok(info);
|
||||
} catch(std::exception const& e) {
|
||||
return Err(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
Result<ModInfo> ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) {
|
||||
ZipFile unzip(path.string());
|
||||
if (!unzip.isLoaded()) {
|
||||
return Err<>("\"" + path.string() + "\": Unable to unzip");
|
||||
}
|
||||
// Check if mod.json exists in zip
|
||||
if (!unzip.fileExists("mod.json")) {
|
||||
return Err<>("\"" + path.string() + "\" is missing mod.json");
|
||||
}
|
||||
// Read mod.json & parse if possible
|
||||
unsigned long readSize = 0;
|
||||
auto read = unzip.getFileData("mod.json", &readSize);
|
||||
if (!read || !readSize) {
|
||||
return Err("\"" + path.string() + "\": Unable to read mod.json");
|
||||
}
|
||||
nlohmann::json json;
|
||||
try {
|
||||
json = nlohmann::json::parse(std::string(read, read + readSize));
|
||||
} catch(std::exception const& e) {
|
||||
delete[] read;
|
||||
return Err<>(e.what());
|
||||
}
|
||||
|
||||
delete[] read;
|
||||
|
||||
if (!json.is_object()) {
|
||||
return Err(
|
||||
"\"" + path.string() + "/mod.json\" does not have an "
|
||||
"object at root despite expected"
|
||||
);
|
||||
}
|
||||
|
||||
auto res = ModInfo::create(json);
|
||||
if (!res) {
|
||||
return Err("\"" + path.string() + "\" - " + res.error());
|
||||
}
|
||||
auto info = res.value();
|
||||
info.m_path = path;
|
||||
|
||||
// unzip known MD files
|
||||
using God = std::initializer_list<std::pair<std::string, std::string*>>;
|
||||
for (auto [file, target] : God {
|
||||
{ "about.md", &info.m_details },
|
||||
{ "changelog.md", &info.m_changelog },
|
||||
}) {
|
||||
if (unzip.fileExists(file)) {
|
||||
unsigned long readSize = 0;
|
||||
auto fileData = unzip.getFileData(file, &readSize);
|
||||
if (!fileData || !readSize) {
|
||||
return Err("Unable to read \"" + path.string() + "\"/" + file);
|
||||
} else {
|
||||
*target = sanitizeDetailsData(fileData, fileData + readSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(info);
|
||||
}
|
||||
|
|
240
loader/src/load/ModInfo.cpp
Normal file
240
loader/src/load/ModInfo.cpp
Normal file
|
@ -0,0 +1,240 @@
|
|||
#include <about.hpp>
|
||||
#include <Geode/loader/Loader.hpp>
|
||||
#include <Geode/loader/Mod.hpp>
|
||||
#include <Geode/utils/string.hpp>
|
||||
#include <Geode/utils/file.hpp>
|
||||
|
||||
USE_GEODE_NAMESPACE();
|
||||
|
||||
static std::string sanitizeDetailsData(unsigned char* start, unsigned char* end) {
|
||||
// delete CRLF
|
||||
return string_utils::replace(std::string(start, end), "\r", "");
|
||||
}
|
||||
|
||||
Result<ModInfo> ModInfo::createFromSchemaV010(nlohmann::json const& rawJson) {
|
||||
ModInfo info;
|
||||
|
||||
auto json = rawJson;
|
||||
|
||||
#define PROPAGATE(err) \
|
||||
{ auto err__ = err; if (!err__) return Err(err__.error()); }
|
||||
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("[mod.json]").obj();
|
||||
|
||||
root.addKnownKey("geode");
|
||||
root.addKnownKey("binary");
|
||||
|
||||
using nlohmann::detail::value_t;
|
||||
|
||||
root
|
||||
.needs("id")
|
||||
.validate(&Mod::validateID)
|
||||
.into(info.m_id);
|
||||
root
|
||||
.needs("version")
|
||||
.validate(&VersionInfo::validate)
|
||||
.intoAs<std::string>(info.m_version);
|
||||
root.needs("name").into(info.m_name);
|
||||
root.needs("developer").into(info.m_developer);
|
||||
root.has("description").into(info.m_description);
|
||||
root.has("repository").into(info.m_repository);
|
||||
root.has("datastore").intoRaw(info.m_defaultDataStore);
|
||||
root.has("toggleable").into(info.m_supportsDisabling);
|
||||
root.has("unloadable").into(info.m_supportsUnloading);
|
||||
|
||||
for (auto& dep : root.has("dependencies").iterate()) {
|
||||
auto obj = dep.obj();
|
||||
|
||||
auto depobj = Dependency {};
|
||||
obj
|
||||
.needs("id")
|
||||
.validate(&Mod::validateID)
|
||||
.into(depobj.m_id);
|
||||
obj
|
||||
.needs("version")
|
||||
.validate(&VersionInfo::validate)
|
||||
.intoAs<std::string>(depobj.m_version);
|
||||
obj
|
||||
.has("required")
|
||||
.into(depobj.m_required);
|
||||
obj.checkUnknownKeys();
|
||||
|
||||
info.m_dependencies.push_back(depobj);
|
||||
}
|
||||
|
||||
for (auto& [key, value] : root.has("settings").items()) {
|
||||
auto sett = Setting::parse(key, value.json());
|
||||
PROPAGATE(sett);
|
||||
info.m_settings.insert({ key, sett.value() });
|
||||
}
|
||||
|
||||
if (auto resources = root.has("resources").obj()) {
|
||||
for (auto& [key, _] : resources.has("spritesheets").items()) {
|
||||
info.m_spritesheets.push_back(info.m_id + "/" + key);
|
||||
}
|
||||
}
|
||||
|
||||
root.has("binary").asOneOf<value_t::string, value_t::object>();
|
||||
|
||||
bool autoEndBinaryName = true;
|
||||
|
||||
root.has("binary").is<value_t::string>().into(info.m_binaryName);
|
||||
|
||||
if (auto bin = root.has("binary").is<value_t::object>().obj()) {
|
||||
bin.has("*").into(info.m_binaryName);
|
||||
bin.has("auto").into(autoEndBinaryName);
|
||||
|
||||
#if defined(GEODE_IS_WINDOWS)
|
||||
bin.has("windows").into(info.m_binaryName);
|
||||
#elif defined(GEODE_IS_MACOS)
|
||||
bin.has("macos").into(info.m_binaryName);
|
||||
#elif defined(GEODE_IS_ANDROID)
|
||||
bin.has("android").into(info.m_binaryName);
|
||||
#elif defined(GEODE_IS_IOS)
|
||||
bin.has("ios").into(info.m_binaryName);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (
|
||||
root.has("binary") &&
|
||||
autoEndBinaryName &&
|
||||
!string_utils::endsWith(info.m_binaryName, GEODE_PLATFORM_EXTENSION)
|
||||
) {
|
||||
info.m_binaryName += GEODE_PLATFORM_EXTENSION;
|
||||
}
|
||||
|
||||
if (checker.isError()) {
|
||||
return Err(checker.getError());
|
||||
}
|
||||
root.checkUnknownKeys();
|
||||
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
Result<ModInfo> ModInfo::create(nlohmann::json const& json) {
|
||||
// Check mod.json target version
|
||||
auto schema = LOADER_VERSION;
|
||||
if (json.contains("geode") && json["geode"].is_string()) {
|
||||
auto ver = json["geode"];
|
||||
if (VersionInfo::validate(ver)) {
|
||||
schema = VersionInfo(ver);
|
||||
} else {
|
||||
return Err(
|
||||
"[mod.json] has no target loader version "
|
||||
"specified, or it is invalidally formatted (required: \"[v]X.X.X\")!"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Err(
|
||||
"[mod.json] has no target loader version "
|
||||
"specified, or it is invalidally formatted (required: \"[v]X.X.X\")!"
|
||||
);
|
||||
}
|
||||
if (schema < Loader::s_supportedVersionMin) {
|
||||
return Err(
|
||||
"[mod.json] is built for an older version (" +
|
||||
schema.toString() + ") of Geode (current: " +
|
||||
Loader::s_supportedVersionMin.toString() +
|
||||
"). Please update the mod to the latest version, "
|
||||
"and if the problem persists, contact the developer "
|
||||
"to update it."
|
||||
);
|
||||
}
|
||||
if (schema > Loader::s_supportedVersionMax) {
|
||||
return Err(
|
||||
"[mod.json] is built for a newer version (" +
|
||||
schema.toString() + ") of Geode (current: " +
|
||||
Loader::s_supportedVersionMax.toString() +
|
||||
"). You need to update Geode in order to use "
|
||||
"this mod."
|
||||
);
|
||||
}
|
||||
|
||||
// Handle mod.json data based on target
|
||||
if (schema <= VersionInfo(0, 2, 0)) {
|
||||
return ModInfo::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::createFromFile(ghc::filesystem::path const& path) {
|
||||
try {
|
||||
auto read = file_utils::readString(path);
|
||||
if (!read) return Err(read.error());
|
||||
auto res = ModInfo::create(nlohmann::json::parse(read.value()));
|
||||
if (!res) return res;
|
||||
auto info = res.value();
|
||||
info.m_path = path;
|
||||
return Ok(info);
|
||||
} catch(std::exception const& e) {
|
||||
return Err(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
Result<ModInfo> ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) {
|
||||
ZipFile unzip(path.string());
|
||||
if (!unzip.isLoaded()) {
|
||||
return Err<>("\"" + path.string() + "\": Unable to unzip");
|
||||
}
|
||||
// Check if mod.json exists in zip
|
||||
if (!unzip.fileExists("mod.json")) {
|
||||
return Err<>("\"" + path.string() + "\" is missing mod.json");
|
||||
}
|
||||
// Read mod.json & parse if possible
|
||||
unsigned long readSize = 0;
|
||||
auto read = unzip.getFileData("mod.json", &readSize);
|
||||
if (!read || !readSize) {
|
||||
return Err("\"" + path.string() + "\": Unable to read mod.json");
|
||||
}
|
||||
nlohmann::json json;
|
||||
try {
|
||||
json = nlohmann::json::parse(std::string(read, read + readSize));
|
||||
} catch(std::exception const& e) {
|
||||
delete[] read;
|
||||
return Err<>(e.what());
|
||||
}
|
||||
|
||||
delete[] read;
|
||||
|
||||
if (!json.is_object()) {
|
||||
return Err(
|
||||
"\"" + path.string() + "/mod.json\" does not have an "
|
||||
"object at root despite expected"
|
||||
);
|
||||
}
|
||||
|
||||
auto res = ModInfo::create(json);
|
||||
if (!res) {
|
||||
return Err("\"" + path.string() + "\" - " + res.error());
|
||||
}
|
||||
auto info = res.value();
|
||||
info.m_path = path;
|
||||
|
||||
// unzip known MD files
|
||||
using God = std::initializer_list<std::pair<std::string, std::string*>>;
|
||||
for (auto [file, target] : God {
|
||||
{ "about.md", &info.m_details },
|
||||
{ "changelog.md", &info.m_changelog },
|
||||
}) {
|
||||
if (unzip.fileExists(file)) {
|
||||
unsigned long readSize = 0;
|
||||
auto fileData = unzip.getFileData(file, &readSize);
|
||||
if (!fileData || !readSize) {
|
||||
return Err("Unable to read \"" + path.string() + "\"/" + file);
|
||||
} else {
|
||||
*target = sanitizeDetailsData(fileData, fileData + readSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(info);
|
||||
}
|
|
@ -34,7 +34,7 @@ Result<std::shared_ptr<Setting>> Setting::parse(
|
|||
if (rawJson.is_object()) {
|
||||
auto json = rawJson;
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("[setting \"" + key + "\"]");
|
||||
auto root = checker.root("[setting \"" + key + "\"]").obj();
|
||||
|
||||
auto res = Setting::parse(
|
||||
root.needs("type").get<std::string>(), key, root
|
||||
|
|
|
@ -37,9 +37,13 @@ Result<Mod*> Loader::loadModFromFile(std::string const& path) {
|
|||
// create and set up Mod instance
|
||||
auto mod = new Mod(res.value());
|
||||
mod->m_saveDirPath = Loader::get()->getGeodeSaveDirectory() / GEODE_MOD_DIRECTORY / res.value().m_id;
|
||||
mod->loadDataStore();
|
||||
ghc::filesystem::create_directories(mod->m_saveDirPath);
|
||||
|
||||
auto sett = mod->loadSettings();
|
||||
if (!sett) {
|
||||
mod->logInfo(sett.error(), Severity::Error);
|
||||
}
|
||||
|
||||
// enable mod if needed
|
||||
mod->m_enabled = Loader::get()->shouldLoadMod(mod->m_info.m_id);
|
||||
this->m_mods.insert({ res.value().m_id, mod });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue