Skip to content

Commit

Permalink
dependencies mod metadata changes
Browse files Browse the repository at this point in the history
`dependencies` and `incompatibilities` may now be objects instead of arrays
`dependencies` may now specify a `settings` key for arbitary dependency-specific data
mods can now listen for a `DependencyLoadedEvent` to know when mods that depend on them are loaded
  • Loading branch information
HJfod committed Jan 13, 2025
1 parent f1bf2b6 commit 1161e18
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 50 deletions.
10 changes: 10 additions & 0 deletions loader/include/Geode/loader/Mod.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ namespace geode {
*/
std::filesystem::path getResourcesDir() const;

/**
* Get the dependency settings for a specific dependency via its ID. For
* example, if this mod depends on Custom Keybinds, it can specify the
* keybinds it wants to add in `mod.json` under
* `dependencies."geode.custom-keybinds".settings.keybinds`
* @returns Null JSON value if there are no settings or if the mod
* doesn't depend on the given mod ID
*/
matjson::Value getDependencySettingsFor(std::string_view dependencyID) const;

#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void setMetadata(ModMetadata const& metadata);
std::vector<Mod*> getDependants() const;
Expand Down
36 changes: 35 additions & 1 deletion loader/include/Geode/loader/ModEvent.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once

#include "Event.hpp"

#include <matjson.hpp>
#include <optional>

namespace geode {
Expand Down Expand Up @@ -51,6 +51,40 @@ namespace geode {
ModStateFilter(Mod* mod, ModEventType type);
ModStateFilter(ModStateFilter const&) = default;
};

/**
* Event posted to a mod when another mod that depends on it is loaded
*/
class GEODE_DLL DependencyLoadedEvent final : public Event {
protected:
class Impl;
std::unique_ptr<Impl> m_impl;

public:
DependencyLoadedEvent(Mod* target, Mod* dependency);

Mod* getTarget() const;
Mod* getDependency() const;
matjson::Value getDependencySettings() const;
};

/**
* Listen for in a mod when a mod that depends on it is loaded
*/
class GEODE_DLL DependencyLoadedFilter final : public EventFilter<DependencyLoadedEvent> {
public:
using Callback = void(DependencyLoadedEvent*);

protected:
class Impl;
std::unique_ptr<Impl> m_impl;

public:
ListenerResult handle(std::function<Callback> fn, DependencyLoadedEvent* event);

DependencyLoadedFilter(Mod* target = geode::getMod());
DependencyLoadedFilter(DependencyLoadedFilter const&);
};
}

// clang-format off
Expand Down
3 changes: 3 additions & 0 deletions loader/include/Geode/loader/ModMetadata.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ namespace geode {
ModMetadata& operator=(ModMetadata&& other) noexcept;
~ModMetadata();

// todo in v5: pimpl this :sob:
struct GEODE_DLL Dependency {
enum class Importance : uint8_t { Required, Recommended, Suggested };
std::string id;
Expand All @@ -72,6 +73,7 @@ namespace geode {
[[nodiscard]] bool isResolved() const;
};

// todo in v5: pimpl this :sob:
struct GEODE_DLL Incompatibility {
enum class Importance : uint8_t {
Breaking,
Expand All @@ -85,6 +87,7 @@ namespace geode {
[[nodiscard]] bool isResolved() const;
};

// todo in v5: pimpl this :sob:
struct IssuesInfo {
std::string info;
std::optional<std::string> url;
Expand Down
7 changes: 7 additions & 0 deletions loader/src/loader/Mod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <Geode/loader/Dirs.hpp>
#include <Geode/loader/Mod.hpp>
#include <loader/ModMetadataImpl.hpp>
#include <optional>
#include <string_view>
#include <server/Server.hpp>
Expand Down Expand Up @@ -87,6 +88,12 @@ std::filesystem::path Mod::getResourcesDir() const {
return dirs::getModRuntimeDir() / this->getID() / "resources" / this->getID();
}

matjson::Value Mod::getDependencySettingsFor(std::string_view dependencyID) const {
auto id = std::string(dependencyID);
auto const& settings = ModMetadataImpl::getImpl(m_impl->m_metadata).m_dependencySettings;
return settings.contains(id) ? settings.at(id) : matjson::Value();
}

#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE)
void Mod::setMetadata(ModMetadata const& metadata) {
m_impl->setMetadata(metadata);
Expand Down
43 changes: 43 additions & 0 deletions loader/src/loader/ModEvent.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <Geode/loader/ModEvent.hpp>
#include <Geode/loader/Mod.hpp>

using namespace geode::prelude;

Expand All @@ -21,3 +22,45 @@ ListenerResult ModStateFilter::handle(std::function<Callback> fn, ModStateEvent*
}

ModStateFilter::ModStateFilter(Mod* mod, ModEventType type) : m_mod(mod), m_type(type) {}

class DependencyLoadedEvent::Impl final {
public:
Mod* target;
Mod* dependency;
};

DependencyLoadedEvent::DependencyLoadedEvent(Mod* target, Mod* dependency)
: m_impl(std::make_unique<Impl>())
{
m_impl->target = target;
m_impl->dependency = dependency;
}

Mod* DependencyLoadedEvent::getTarget() const {
return m_impl->target;
}
Mod* DependencyLoadedEvent::getDependency() const {
return m_impl->dependency;
}
matjson::Value DependencyLoadedEvent::getDependencySettings() const {
return m_impl->dependency->getDependencySettingsFor(m_impl->target->getID());
}

class DependencyLoadedFilter::Impl final {
public:
Mod* target;
};

ListenerResult DependencyLoadedFilter::handle(std::function<Callback> fn, DependencyLoadedEvent* event) {
if (event->getTarget() == m_impl->target) {
fn(event);
}
return ListenerResult::Propagate;
}

DependencyLoadedFilter::DependencyLoadedFilter(Mod* target = geode::getMod())
: m_impl(std::make_unique<Impl>())
{
m_impl->target = target;
}
DependencyLoadedFilter::DependencyLoadedFilter(DependencyLoadedFilter const&) = default;
10 changes: 9 additions & 1 deletion loader/src/loader/ModImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,18 @@ Result<> Mod::Impl::loadBinary() {

LoaderImpl::get()->releaseNextMod();


ModStateEvent(m_self, ModEventType::Loaded).post();
ModStateEvent(m_self, ModEventType::DataLoaded).post();

// do we not have a function for getting all the dependencies of a mod directly? ok then
// Anyway this lets all of this mod's dependencies know it has been loaded
// In case they're API mods and want to know those kinds of things
for (auto const& dep : ModMetadataImpl::getImpl(m_metadata).m_dependencies) {
if (auto depMod = Loader::get()->getLoadedMod(dep.id)) {
DependencyLoadedEvent(depMod, m_self).post();
}
}

m_isCurrentlyLoading = false;

return Ok();
Expand Down
135 changes: 87 additions & 48 deletions loader/src/loader/ModMetadataImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ static std::string sanitizeDetailsData(std::string const& str) {
return utils::string::replace(str, "\r", "");
}

// todo in v5: remove all support for old mod IDs and replace any calls to this with just validateID
bool ModMetadata::Impl::validateOldID(std::string const& id) {
// Old IDs may not be empty
if (id.empty()) return false;
Expand Down Expand Up @@ -129,8 +130,7 @@ Result<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs
root.needs("geode").into(impl->m_geodeVersion);

if (auto gd = root.needs("gd")) {
// In the future when we get rid of support for string format just
// change all of this to the gd.needs(...) stuff
// todo in v5: get rid of the string alternative and makes this always be an object
gd.assertIs({ matjson::Type::Object, matjson::Type::String });
if (gd.isObject()) {
if (gd.has(GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH)) {
Expand Down Expand Up @@ -194,62 +194,101 @@ Result<ModMetadata> ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs
});
}

for (auto& dep : root.has("dependencies").items()) {
bool onThisPlatform = !dep.has("platforms");
for (auto& plat : dep.has("platforms").items()) {
if (PlatformID::coveredBy(plat.get<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
if (auto deps = root.has("dependencies")) {
auto addDependency = [&impl](std::string const& id, JsonExpectedValue& dep) -> Result<> {
if (!ModMetadata::Impl::validateOldID(id)) {
return Err("[mod.json].dependencies.\"{}\" is not a valid Mod ID ({})", id, ID_REGEX);
}
}
if (!onThisPlatform) {
continue;
}

Dependency dependency;
dep.needs("id").mustBe<std::string>(ID_REGEX, &ModMetadata::Impl::validateOldID).into(dependency.id);
dep.needs("version").into(dependency.version);
dep.has("importance").into(dependency.importance);
dep.checkUnknownKeys();
bool onThisPlatform = !dep.has("platforms");
for (auto& plat : dep.has("platforms").items()) {
if (PlatformID::coveredBy(plat.get<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
}
}
if (!onThisPlatform) {
return Ok();
}

if (
dependency.version.getComparison() != VersionCompare::MoreEq &&
dependency.version.getComparison() != VersionCompare::Any
) {
return Err(
"[mod.json].dependencies.{}.version must be either a more-than "
"comparison for a specific version or a wildcard for any version",
dependency.id
);
}
matjson::Value dependencySettings;

Dependency dependency;
dependency.id = id;
dep.needs("id").mustBe<std::string>(ID_REGEX, &ModMetadata::Impl::validateOldID).into(dependency.id);
dep.needs("version").into(dependency.version);
dep.has("importance").into(dependency.importance);
dep.has("settings").into(dependencySettings);
dep.checkUnknownKeys();

if (
dependency.version.getComparison() != VersionCompare::MoreEq &&
dependency.version.getComparison() != VersionCompare::Any
) {
return Err(
"[mod.json].dependencies.\"{}\".version must be either a more-than "
"comparison for a specific version or a wildcard for any version",
dependency.id
);
}

// if (isDeprecatedIDForm(dependency.id)) {
// log::warn(
// "Dependency ID '{}' will be rejected in the future - "
// "IDs must match the regex `[a-z0-9\\-_]+\\.[a-z0-9\\-_]+`",
// impl->m_id
// );
// }
impl->m_dependencies.push_back(dependency);
// todo in v5: make Dependency pimpl and move this as a member there
// `dep.has("settings").into(dependency.settings);`
impl->m_dependencySettings.insert({ id, dependencySettings });

impl->m_dependencies.push_back(dependency);
}
return Ok();
};

for (auto& incompat : root.has("incompatibilities").items()) {
bool onThisPlatform = !incompat.has("platforms");
for (auto& plat : incompat.has("platforms").items()) {
if (PlatformID::coveredBy(plat.get<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
// todo in v5: make this always be an object
deps.assertIs({ matjson::Type::Object, matjson::Type::Array });
if (deps.isObject()) {
for (auto& [id, dep] : deps.properties()) {
GEODE_UNWRAP(addDependency(id, dep));
}
}
if (!onThisPlatform) {
continue;
else {
for (auto& dep : deps.items()) {
GEODE_UNWRAP(addDependency(dep.needs("id").template get<std::string>(), dep));
}
}
}

if (auto incompats = root.has("incompatibilities")) {
auto addIncompat = [&impl](std::string const& id, JsonExpectedValue& incompat) -> Result<> {
if (!ModMetadata::Impl::validateOldID(id)) {
return Err("[mod.json].incompatibilities.\"{}\" is not a valid Mod ID ({})", id, ID_REGEX);
}

Incompatibility incompatibility;
incompat.needs("id").mustBe<std::string>(ID_REGEX, &ModMetadata::Impl::validateOldID).into(incompatibility.id);
incompat.needs("version").into(incompatibility.version);
incompat.has("importance").into(incompatibility.importance);
incompat.checkUnknownKeys();
impl->m_incompatibilities.push_back(incompatibility);
bool onThisPlatform = !incompat.has("platforms");
for (auto& plat : incompat.has("platforms").items()) {
if (PlatformID::coveredBy(plat.get<std::string>(), GEODE_PLATFORM_TARGET)) {
onThisPlatform = true;
}
}
if (!onThisPlatform) {
return Ok();
}

Incompatibility incompatibility;
incompatibility.id = id;
incompat.needs("version").into(incompatibility.version);
incompat.has("importance").into(incompatibility.importance);
incompat.checkUnknownKeys();
impl->m_incompatibilities.push_back(incompatibility);
};

// todo in v5: make this always be an object
incompats.assertIs({ matjson::Type::Object, matjson::Type::Array });
if (incompats.isObject()) {
for (auto& [id, incompat] : incompats.properties()) {
GEODE_UNWRAP(addIncompat(id, incompat));
}
}
else {
for (auto& incompat : incompats.items()) {
GEODE_UNWRAP(addIncompat(incompat.needs("id").template get<std::string>(), incompat));
}
}
}

for (auto& [key, value] : root.has("settings").properties()) {
Expand Down
2 changes: 2 additions & 0 deletions loader/src/loader/ModMetadataImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ namespace geode {
ModMetadataLinks m_links;
std::optional<IssuesInfo> m_issues;
std::vector<Dependency> m_dependencies;
// todo in v5: make Dependency pimpl and move this as a member there (`matjson::Value settings;`)
std::unordered_map<std::string, matjson::Value> m_dependencySettings;
std::vector<Incompatibility> m_incompatibilities;
std::vector<std::string> m_spritesheets;
std::vector<std::pair<std::string, matjson::Value>> m_settings;
Expand Down

0 comments on commit 1161e18

Please sign in to comment.