From 06b3ef05becc62534ad2ffd330361b4ff58d6c24 Mon Sep 17 00:00:00 2001 From: wired-tomato Date: Mon, 11 Nov 2024 23:35:45 -0500 Subject: [PATCH] Conditional mods & event bus subscribers --- .../neoforged/fml/common/ChainDependency.java | 16 +++++++ .../net/neoforged/fml/common/Dependency.java | 23 ++++++++++ .../fml/common/EventBusSubscriber.java | 5 +++ .../java/net/neoforged/fml/common/Mod.java | 5 +++ .../javafmlmod/AutomaticEventSubscriber.java | 6 ++- .../fml/javafmlmod/DependencyUtil.java | 44 +++++++++++++++++++ .../fml/javafmlmod/FMLModContainer.java | 16 +++++-- 7 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 loader/src/main/java/net/neoforged/fml/common/ChainDependency.java create mode 100644 loader/src/main/java/net/neoforged/fml/common/Dependency.java create mode 100644 loader/src/main/java/net/neoforged/fml/javafmlmod/DependencyUtil.java diff --git a/loader/src/main/java/net/neoforged/fml/common/ChainDependency.java b/loader/src/main/java/net/neoforged/fml/common/ChainDependency.java new file mode 100644 index 000000000..e5b0b85eb --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/common/ChainDependency.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.common; + +public @interface ChainDependency { + Dependency value(); + + Operator operator() default Operator.AND; + + enum Operator { + AND, OR + } +} diff --git a/loader/src/main/java/net/neoforged/fml/common/Dependency.java b/loader/src/main/java/net/neoforged/fml/common/Dependency.java new file mode 100644 index 000000000..da43b8853 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/common/Dependency.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.common; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Dependency { + /** + * @return the array of mod ids to be checked against the {@link #condition() condition} + */ + String[] value(); + + Condition condition() default Condition.ALL_PRESENT; + + enum Condition { + ALL_PRESENT, AT_LEAST_ONE_PRESENT, NONE_PRESENT, AT_LEAST_ONE_IS_NOT_PRESENT + } +} diff --git a/loader/src/main/java/net/neoforged/fml/common/EventBusSubscriber.java b/loader/src/main/java/net/neoforged/fml/common/EventBusSubscriber.java index 49499969c..933596f20 100644 --- a/loader/src/main/java/net/neoforged/fml/common/EventBusSubscriber.java +++ b/loader/src/main/java/net/neoforged/fml/common/EventBusSubscriber.java @@ -62,6 +62,11 @@ */ Bus bus() default Bus.GAME; + /** + * @return the array of dependencies required for contained events to be auto-subscribed (all dependencies must match) + */ + ChainDependency[] dependencies() default {}; + enum Bus { /** * The main NeoForge Event Bus, used after the game has started up. diff --git a/loader/src/main/java/net/neoforged/fml/common/Mod.java b/loader/src/main/java/net/neoforged/fml/common/Mod.java index 7fabff9bb..f9ae345cb 100644 --- a/loader/src/main/java/net/neoforged/fml/common/Mod.java +++ b/loader/src/main/java/net/neoforged/fml/common/Mod.java @@ -34,4 +34,9 @@ * {@return the side to load this mod entrypoint on} */ Dist[] dist() default { Dist.CLIENT, Dist.DEDICATED_SERVER }; + + /** + * {@return the array of dependencies required for this class to be loaded (all dependencies must match for this class to be loaded)} + */ + ChainDependency[] dependencies() default {}; } diff --git a/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java b/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java index ee4ed9a83..0cb746779 100644 --- a/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java +++ b/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java @@ -7,6 +7,7 @@ import static net.neoforged.fml.Logging.LOADING; +import java.util.Arrays; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -16,6 +17,7 @@ import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.Bindings; import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.ChainDependency; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLEnvironment; @@ -47,7 +49,9 @@ public static void inject(final ModContainer mod, final ModFileScanData scanData final String modId = (String) ad.annotationData().getOrDefault("modid", modids.getOrDefault(ad.clazz().getClassName(), mod.getModId())); final ModAnnotation.EnumHolder busTargetHolder = (ModAnnotation.EnumHolder) ad.annotationData().getOrDefault("bus", new ModAnnotation.EnumHolder(null, EventBusSubscriber.Bus.GAME.name())); final EventBusSubscriber.Bus busTarget = EventBusSubscriber.Bus.valueOf(busTargetHolder.value()); - if (Objects.equals(mod.getModId(), modId) && sides.contains(FMLEnvironment.dist)) { + final List chain = Arrays.stream(((ChainDependency[]) ad.annotationData().getOrDefault("dependencies", new ChainDependency[] {}))).toList(); + var allDepsMatch = DependencyUtil.evaluateChain(chain, modids.values()); + if (Objects.equals(mod.getModId(), modId) && sides.contains(FMLEnvironment.dist) && allDepsMatch) { try { IEventBus bus = switch (busTarget) { case GAME -> Bindings.getGameBus(); diff --git a/loader/src/main/java/net/neoforged/fml/javafmlmod/DependencyUtil.java b/loader/src/main/java/net/neoforged/fml/javafmlmod/DependencyUtil.java new file mode 100644 index 000000000..08d118b26 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/javafmlmod/DependencyUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.javafmlmod; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import net.neoforged.fml.common.ChainDependency; +import net.neoforged.fml.common.Dependency; + +public final class DependencyUtil { + public static Boolean evaluateChain(List chain, Collection loadedMods) { + var matches = true; + ChainDependency.Operator previousOperator = null; + for (ChainDependency chainDep : chain) { + if (previousOperator == null) { + matches = evaluateDependency(chainDep.value(), loadedMods); + previousOperator = chainDep.operator(); + continue; + } + + switch (previousOperator) { + case AND -> matches = matches && evaluateDependency(chainDep.value(), loadedMods); + case OR -> matches = matches || evaluateDependency(chainDep.value(), loadedMods); + } + + previousOperator = chainDep.operator(); + } + + return matches; + } + + public static Boolean evaluateDependency(Dependency dep, Collection loadedMods) { + return switch (dep.condition()) { + case ALL_PRESENT -> Arrays.stream(dep.value()).allMatch(loadedMods::contains); + case AT_LEAST_ONE_PRESENT -> Arrays.stream(dep.value()).anyMatch(loadedMods::contains); + case NONE_PRESENT -> Arrays.stream(dep.value()).noneMatch(loadedMods::contains); + case AT_LEAST_ONE_IS_NOT_PRESENT -> Arrays.stream(dep.value()).anyMatch((modId) -> !loadedMods.contains(modId)); + }; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java b/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java index 7efe961dc..a418dafe7 100644 --- a/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java +++ b/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java @@ -5,8 +5,10 @@ package net.neoforged.fml.javafmlmod; +import java.lang.annotation.ElementType; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -22,6 +24,7 @@ import net.neoforged.fml.ModLoadingContext; import net.neoforged.fml.ModLoadingException; import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.common.Mod; import net.neoforged.fml.event.IModBusEvent; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.neoforgespi.language.IModInfo; @@ -112,9 +115,16 @@ protected void constructMod() { } // All arguments are found - constructor.newInstance(constructorArgs); - - LOGGER.trace(LOADING, "Loaded mod instance {} of type {}", getModId(), modClass.getName()); + var chain = Arrays.stream(modClass.getAnnotation(Mod.class).dependencies()).toList(); + var loadedMods = scanResults.getAnnotatedBy(Mod.class, ElementType.TYPE) + .map(data -> (String) data.annotationData().get("value")) + .toList(); + + var dependenciesMatch = DependencyUtil.evaluateChain(chain, loadedMods); + if (dependenciesMatch) { + constructor.newInstance(constructorArgs); + LOGGER.trace(LOADING, "Loaded mod instance {} of type {}", getModId(), modClass.getName()); + } else LOGGER.trace(LOADING, "Didn't load mod instance {} of type {} because of non-matching dependencies", modClass, getModId()); } catch (Throwable e) { if (e instanceof InvocationTargetException) e = e.getCause(); // exceptions thrown when a reflected method call throws are wrapped in an InvocationTargetException. However, this isn't useful for the end user who has to dig through the logs to find the actual cause. LOGGER.error(LOADING, "Failed to create mod instance. ModID: {}, class {}", getModId(), modClass.getName(), e);