Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement auto bus detection in EBS #152

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions loader/src/main/java/net/neoforged/fml/Bindings.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.event.IModBusEvent;

// @formatter:off - spotless doesn't like @
/**
* Annotate a class which will be subscribed to an Event Bus at mod construction time. Defaults to subscribing the current modid to the {@code NeoForge#EVENT_BUS} on both sides.
* Annotate a class which will be subscribed to an Event Bus at mod construction time. Defaults to selecting the event bus
* based on the listeners you have in the class ({@link Bus#MOD} if all listeners listen to an event of type {@link IModBusEvent}).
*
* <p>Annotated classes will be scanned for <b>static</b> methods that have the {@link SubscribeEvent} annotation.
* For example:
Expand Down Expand Up @@ -60,7 +62,7 @@
*
* @return the bus you wish to listen to
*/
Bus bus() default Bus.GAME;
Bus bus() default Bus.AUTOMATIC;

enum Bus {
/**
Expand All @@ -75,5 +77,13 @@ enum Bus {
* @see ModContainer#getEventBus()
*/
MOD,
/**
* Detect the bus to use automatically based on the listeners in the class.
* <p>
* If all listeners listen to an event of type {@link IModBusEvent}, the bus will be the {@linkplain #MOD mod bus},
* otherwise it will be the {@linkplain #GAME game bus}.
* <p><strong>A class must not mix game and mod bus listeners</strong>.
*/
AUTOMATIC
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import java.util.function.Function;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.Bindings;
import net.neoforged.fml.loading.FMLLoader;
import org.jetbrains.annotations.Nullable;

public interface IConfigEvent {
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading, @Nullable Function<ModConfig, IConfigEvent> unloading) {}

ConfigConfig CONFIGCONFIG = Bindings.getConfigConfiguration();
ConfigConfig CONFIGCONFIG = FMLLoader.getBindings().getConfigConfiguration();

static IConfigEvent reloading(ModConfig modConfig) {
return CONFIGCONFIG.reloading().apply(modConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@

import static net.neoforged.fml.Logging.LOADING;

import java.lang.reflect.Modifier;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.Bindings;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingIssue;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.fml.loading.FMLEnvironment;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.loading.modscan.ModAnnotation;
import net.neoforged.neoforgespi.language.ModFileScanData;
import org.apache.logging.log4j.LogManager;
Expand All @@ -42,22 +48,47 @@ public static void inject(final ModContainer mod, final ModFileScanData scanData
Map<String, String> modids = scanData.getAnnotations().stream().filter(annotationData -> MOD_TYPE.equals(annotationData.annotationType())).collect(Collectors.toMap(a -> a.clazz().getClassName(), a -> (String) a.annotationData().get("value")));

ebsTargets.forEach(ad -> {
@SuppressWarnings("unchecked")
final EnumSet<Dist> sides = getSides(ad.annotationData().get("value"));
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 ModAnnotation.EnumHolder busTargetHolder = (ModAnnotation.EnumHolder) ad.annotationData().getOrDefault("bus", new ModAnnotation.EnumHolder(null, EventBusSubscriber.Bus.AUTOMATIC.name()));
final EventBusSubscriber.Bus busTarget = EventBusSubscriber.Bus.valueOf(busTargetHolder.value());
if (Objects.equals(mod.getModId(), modId) && sides.contains(FMLEnvironment.dist)) {
try {
var clazz = Class.forName(ad.clazz().getClassName(), true, layer.getClassLoader());

IEventBus bus = switch (busTarget) {
case GAME -> Bindings.getGameBus();
case GAME -> FMLLoader.getBindings().getGameBus();
case MOD -> mod.getEventBus();
case AUTOMATIC -> {
boolean hasGame = false, hasMod = false;
for (var method : clazz.getDeclaredMethods()) {
if (!Modifier.isStatic(method.getModifiers()) || !method.isAnnotationPresent(SubscribeEvent.class) || method.getParameterTypes().length != 1) {
continue;
}

var type = method.getParameterTypes()[0];
if (IModBusEvent.class.isAssignableFrom(type)) {
hasMod = true;
} else if (Event.class.isAssignableFrom(type)) {
hasGame = true;
}
}

if (hasGame && hasMod) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I understand the idea, but why do we limit this to one bus per listener?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a "limitation" imposed by event bus' class, where all listeners must be of the correct marker interface. And given that game and mod events are fired during different parts of the runtime, I think the limitation makes sense

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oke perfect, was just curious. Thanks for the explanation. Nice work!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach does not provide meaningful separation (there is no reason that these must be separated at the class level, aside from the current EBS implementation), and unifying these events in the same class can readily be achieved via IEventBus#addListener.

Despite the explained "limitation", leaving a class level separation here does not fix #140's main expectations.

Moving further to change the EBS implementation would be better than an incomplete solution imo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no raw variant of addListener that would support that use case, and reflection based consumers on the neo side is not acceptable, both because of performance concerns and because it will make unregistering those listeners impossible. In any case, I've decided to not proceed with this PR further because more magic will increase confusion.

Instead, we should make the bus parameter required and the mod id default to first in the file (instead of all).

ModLoader.addLoadingIssue(ModLoadingIssue.error("fml.modloading.javafml.ebs.mixed_buses", clazz.getName()).withAffectedMod(mod.getModInfo()));
yield null;
}

// We'll let the bus do the rest of the checking (instance methods, or inheritance) to catch other errors
// if we haven't found any methods
yield hasMod ? mod.getEventBus() : FMLLoader.getBindings().getGameBus();
}
};

if (bus != null) {
LOGGER.debug(LOADING, "Auto-subscribing {} to {}", ad.clazz().getClassName(), busTarget);
LOGGER.debug(LOADING, "Auto-subscribing {} to the {} bus", ad.clazz().getClassName(), bus == mod.getEventBus() ? "mod" : "game");

bus.register(Class.forName(ad.clazz().getClassName(), true, layer.getClassLoader()));
bus.register(clazz);
}
} catch (ClassNotFoundException e) {
LOGGER.fatal(LOADING, "Failed to load mod class {} for @EventBusSubscriber annotation", ad.clazz(), e);
Expand Down
19 changes: 19 additions & 0 deletions loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import net.neoforged.accesstransformer.api.AccessTransformerEngine;
import net.neoforged.accesstransformer.ml.AccessTransformerService;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.coremod.CoreModScriptingEngine;
import net.neoforged.fml.IBindingsProvider;
import net.neoforged.fml.common.asm.RuntimeDistCleaner;
import net.neoforged.fml.loading.mixin.DeferredMixinConfigRegistration;
import net.neoforged.fml.loading.moddiscovery.ModDiscoverer;
Expand All @@ -33,6 +35,7 @@
import net.neoforged.neoforgespi.ILaunchContext;
import net.neoforged.neoforgespi.locating.IModFileCandidateLocator;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;

public class FMLLoader {
Expand All @@ -53,6 +56,8 @@ public class FMLLoader {
private static boolean production;
@Nullable
private static ModuleLayer gameLayer;
@VisibleForTesting
static IBindingsProvider bindings;

static void onInitialLoad(IEnvironment environment) throws IncompatibleEnvironmentException {
final String version = LauncherVersion.getVersion();
Expand Down Expand Up @@ -93,6 +98,8 @@ static void onInitialLoad(IEnvironment environment) throws IncompatibleEnvironme
coreModEngine = new CoreModScriptingEngine();
LOGGER.debug(LogMarkers.CORE, "FML found CoreMods version : {}", coreModEngine.getClass().getPackage().getImplementationVersion());

bindings = null;

try {
Class.forName("com.electronwill.nightconfig.core.Config", false, environment.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.toml.TomlFormat", false, environment.getClass().getClassLoader());
Expand Down Expand Up @@ -209,4 +216,16 @@ public static ModuleLayer getGameLayer() {
public static VersionInfo versionInfo() {
return versionInfo;
}

public static IBindingsProvider getBindings() {
if (bindings == null) {
var providers = ServiceLoader.load(getGameLayer(), IBindingsProvider.class)
.stream().toList();
if (providers.size() != 1) {
throw new IllegalStateException("Could not find bindings provider");
}
bindings = providers.get(0).get();
}
return bindings;
}
}
1 change: 1 addition & 0 deletions loader/src/main/resources/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"fml.modloading.failedtoloadforge": "Failed to load NeoForge",

"fml.modloading.javafml.dangling_entrypoint": "File {5} contains mod entrypoint class \u00a76{4}\u00a7r for mod with id \u00a7e{3}\u00a7r, which does not exist or is not in the same file.\nDid you forget to update the mod id in the entrypoint?",
"fml.modloading.javafml.ebs.mixed_buses": "Class {3} annotated with @EventBusSubscriber has listeners for both mod and game bus events, which cannot be mixed",

"fml.modloading.missingdependency": "Mod \u00a7e{4}\u00a7r requires \u00a76{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a76{3}\u00a7r\u00a77 is \u00a7o{6,i18n,fml.messages.artifactversion.ornotinstalled}§r\n{7,optional,§7Reason for the dependency: §r}",
"fml.modloading.missingdependency.optional": "Mod \u00a7e{4}\u00a7r only supports \u00a73{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a73{3}\u00a7r\u00a77 is \u00a7o{6}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package net.neoforged.fml.javafmlmod;

import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
Expand Down Expand Up @@ -170,4 +171,74 @@ public EntryPoint(net.neoforged.bus.api.IEventBus modEventBus) {
assertThat(getTranslatedIssues(e.getIssues())).containsOnly("ERROR: testmod (testmod) encountered an error while dispatching the net.neoforged.fml.event.lifecycle.FMLClientSetupEvent event"
+ "\njava.lang.RuntimeException: null");
}

@Test
void testEventBusSubscriberAutomaticDetection() throws Exception {
installation.setupProductionClient();
installation.buildModJar("test.jar")
.withModsToml(builder -> {
builder.unlicensedJavaMod().addMod("testmod", "1.0");
})
Comment on lines +179 to +181
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a shorthand for the testmod mods toml .withTestmodModsToml()

.addClass("testmod.TestEvent", """
public class TestEvent extends net.neoforged.bus.api.Event {
}""")
.addClass("testmod.EBSListener", """
@net.neoforged.fml.common.EventBusSubscriber(bus = net.neoforged.fml.common.EventBusSubscriber.Bus.AUTOMATIC)
public class EBSListener {
@net.neoforged.bus.api.SubscribeEvent
public static void onClientSetup(net.neoforged.fml.event.lifecycle.FMLClientSetupEvent event) {
net.neoforged.fml.javafmlmod.FMLJavaModLanguageProviderTest.EVENTS.add(event);
net.neoforged.fml.loading.FMLLoader.getBindings().getGameBus().post(new TestEvent());
}
}
""")
.addClass("testmod.EBSListenerGame", """
@net.neoforged.fml.common.EventBusSubscriber(bus = net.neoforged.fml.common.EventBusSubscriber.Bus.AUTOMATIC)
public class EBSListenerGame {
@net.neoforged.bus.api.SubscribeEvent
public static void onTest(TestEvent event) {
net.neoforged.fml.javafmlmod.FMLJavaModLanguageProviderTest.MESSAGES.add("game listener invoked");
}
}
""")
.build();

var result = launch("forgeclient");
loadMods(result);

ModLoader.dispatchParallelEvent("test", Runnable::run, Runnable::run, () -> {}, FMLClientSetupEvent::new);

assertThat(EVENTS).hasSize(1);
assertThat(MESSAGES).containsExactly("game listener invoked");
}

@Test
void testEventBusSubscriberMixedBuses() throws Exception {
installation.setupProductionClient();
installation.buildModJar("test.jar")
.withModsToml(builder -> {
builder.unlicensedJavaMod().addMod("testmod", "1.0");
})
.addClass("testmod.TestEvent", """
public class TestEvent extends net.neoforged.bus.api.Event {
}""")
.addClass("testmod.EBSListener", """
@net.neoforged.fml.common.EventBusSubscriber(bus = net.neoforged.fml.common.EventBusSubscriber.Bus.AUTOMATIC)
public class EBSListener {
@net.neoforged.bus.api.SubscribeEvent
public static void onClientSetup(net.neoforged.fml.event.lifecycle.FMLClientSetupEvent event) {
}

@net.neoforged.bus.api.SubscribeEvent
public static void onGame(TestEvent event) {
}
}
""")
.build();

var result = launch("forgeclient");
loadMods(result);

assertThat(getTranslatedIssues(ModLoader.getLoadingIssues())).containsExactly("ERROR: Class testmod.EBSListener annotated with @EventBusSubscriber has listeners for both mod and game bus events, which cannot be mixed");
}
}
14 changes: 14 additions & 0 deletions loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.IBindingsProvider;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingIssue;
import net.neoforged.fml.config.IConfigEvent;
import net.neoforged.fml.i18n.FMLTranslations;
import net.neoforged.neoforgespi.Environment;
import org.junit.jupiter.api.AfterEach;
Expand Down Expand Up @@ -96,6 +99,17 @@ protected LaunchResult launch(String launchTarget) throws Exception {

FMLPaths.loadAbsolutePaths(installation.getGameDir());

FMLLoader.bindings = new IBindingsProvider() {
@Override
public IEventBus getGameBus() {
return installation.getGameBus();
}

@Override
public IConfigEvent.ConfigConfig getConfigConfiguration() {
return null;
}
};
FMLLoader.onInitialLoad(environment);
FMLPaths.setup(environment);
FMLConfig.load();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.google.common.base.Suppliers;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
import cpw.mods.jarhandling.SecureJar;
Expand All @@ -25,13 +26,16 @@
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.neoforged.bus.api.BusBuilder;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.jarjar.metadata.ContainedJarMetadata;
import net.neoforged.jarjar.metadata.Metadata;
import net.neoforged.jarjar.metadata.MetadataIOHandler;
Expand Down Expand Up @@ -81,6 +85,8 @@ public class SimulatedInstallation implements AutoCloseable {
private final Path librariesDir;
// Used for testing running out of a Gradle project. Is the simulated Gradle project root directory.
private final Path projectRoot;
// Simulates NeoForge's game event bus
private final Supplier<IEventBus> gameBus = Suppliers.memoize(() -> BusBuilder.builder().build());
Comment on lines +88 to +89
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, you are putting this here because it's usually in the NeoForge jar?


private static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = { SHARED_ASSETS };
private static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = { CLIENT_ASSETS, SHARED_ASSETS };
Expand All @@ -96,6 +102,10 @@ public SimulatedInstallation() throws IOException {
projectRoot = Files.createTempDirectory("projectRoot");
}

public IEventBus getGameBus() {
return gameBus.get();
}

public Path getModsFolder() throws IOException {
var modsFolder = gameDir.resolve("mods");
Files.createDirectories(modsFolder);
Expand Down
Loading