diff --git a/eventbus-test-jar/src/main/java/net/minecraftforge/eventbus/testjar/events/PassthroughEvent.java b/eventbus-test-jar/src/main/java/net/minecraftforge/eventbus/testjar/events/PassthroughEvent.java
new file mode 100644
index 0000000..83ba153
--- /dev/null
+++ b/eventbus-test-jar/src/main/java/net/minecraftforge/eventbus/testjar/events/PassthroughEvent.java
@@ -0,0 +1,24 @@
+package net.minecraftforge.eventbus.testjar.events;
+
+import net.minecraftforge.eventbus.api.bus.EventBus;
+import net.minecraftforge.eventbus.api.event.InheritableEvent;
+
+/**
+ * This event tests the passthrough optimisation.
+ *
An event is considered passthrough when all the following conditions are met:
+ *
+ * - The parent is sealed with a single child
+ * - The child is final or a record
+ * - They share the same event characteristics
+ *
+ * When these conditions are met, the {@code EventBus.create(Impl.class)} call returns the EventBus instance of its
+ * parent, instead of creating a new EventBus for the child. All calls to the child go directly to the parent, saving
+ * memory.
+ */
+public sealed interface PassthroughEvent extends InheritableEvent {
+ EventBus BUS = EventBus.create(PassthroughEvent.class);
+
+ record Impl() implements PassthroughEvent {
+ public static final EventBus BUS = EventBus.create(Impl.class);
+ }
+}
diff --git a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java
index 7dd418a..eab87af 100644
--- a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java
+++ b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/EventBusCreationTests.java
@@ -12,6 +12,7 @@
import net.minecraftforge.eventbus.api.event.RecordEvent;
import net.minecraftforge.eventbus.api.event.characteristic.Cancellable;
import net.minecraftforge.eventbus.api.event.characteristic.MonitorAware;
+import net.minecraftforge.eventbus.testjar.events.PassthroughEvent;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -91,6 +92,17 @@ interface Child extends InheritableTestEvent {}
);
}
+ /**
+ * Tests that the references of the {@code BUS} fields of {@link PassthroughEvent} and its child refer to
+ * the same instance.
+ * @see PassthroughEvent
+ */
+ @Test
+ public void testPassthroughEventCreation() {
+ Assertions.assertSame(PassthroughEvent.BUS, PassthroughEvent.Impl.BUS);
+ Assertions.assertEquals(PassthroughEvent.BUS.hashCode(), PassthroughEvent.Impl.BUS.hashCode());
+ }
+
/**
* Tests that only cancellable events can be created with a {@link CancellableEventBus}.
*/
diff --git a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java
index 7493308..d22baf5 100644
--- a/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java
+++ b/eventbus-test/src/test/java/net/minecraftforge/eventbus/test/InheritanceTests.java
@@ -9,6 +9,7 @@
import net.minecraftforge.eventbus.api.event.MutableEvent;
import net.minecraftforge.eventbus.api.event.characteristic.Cancellable;
import net.minecraftforge.eventbus.internal.EventBusImpl;
+import net.minecraftforge.eventbus.testjar.events.PassthroughEvent;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -231,4 +232,31 @@ final class SubEvent extends SuperEvent implements Cancellable {
SuperEvent.BUS.removeListener(listener);
}
+
+ /**
+ * Tests that listener inheritance works when posting in a passthrough context.
+ * @see PassthroughEvent
+ */
+ @Test
+ public void testListenerCallInheritanceWithPassthrough() {
+ var handled = new AtomicBoolean();
+ var listener = PassthroughEvent.BUS.addListener(event -> handled.set(true));
+
+ Assertions.assertFalse(handled.get(), "PassthroughEvent should not be handled yet");
+
+ Assertions.assertDoesNotThrow(() -> PassthroughEvent.Impl.BUS.post(new PassthroughEvent.Impl()));
+
+ Assertions.assertTrue(handled.get(), "PassthroughEvent should be handled");
+
+ PassthroughEvent.BUS.removeListener(listener);
+ handled.set(false);
+
+ listener = PassthroughEvent.Impl.BUS.addListener(event -> handled.set(true));
+
+ Assertions.assertDoesNotThrow(() -> PassthroughEvent.Impl.BUS.post(new PassthroughEvent.Impl()));
+
+ Assertions.assertTrue(handled.get(), "PassthroughEvent should be handled");
+
+ PassthroughEvent.BUS.removeListener(listener);
+ }
}
diff --git a/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java b/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java
index faf5f93..cd7f91d 100644
--- a/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java
+++ b/src/main/java/net/minecraftforge/eventbus/internal/AbstractEventBusImpl.java
@@ -20,6 +20,7 @@
public sealed interface AbstractEventBusImpl extends EventBus
permits CancellableEventBusImpl, EventBusImpl {
//region Record component accessors
+ Class eventType();
ArrayList backingList();
ArrayList monitorBackingList();
List> children();
diff --git a/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java b/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java
index f79bc9a..fe63478 100644
--- a/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java
+++ b/src/main/java/net/minecraftforge/eventbus/internal/BusGroupImpl.java
@@ -94,8 +94,8 @@ private EventBus createEventBus(Class eventType) {
int characteristics = AbstractEventBusImpl.computeEventCharacteristics(eventType);
+ boolean isRecord = (Constants.STRICT_BUS_CREATION_CHECKS || Constants.isInheritable(characteristics)) && eventType.isRecord();
if (Constants.STRICT_BUS_CREATION_CHECKS) {
- boolean isRecord = eventType.isRecord();
if (!isRecord && RecordEvent.class.isAssignableFrom(eventType))
throw new IllegalArgumentException("Event type " + eventType + " implements RecordEvent but is not a record class");
@@ -113,13 +113,29 @@ private EventBus createEventBus(Class eventType) {
}
}
- var backingList = new ArrayList();
+ ArrayList backingList;
List> parents = Collections.emptyList();
if (Constants.isInheritable(characteristics)) {
parents = getParentEvents(eventType);
+
+ // Direct pass-through of sealed inheritable events that only have one subclass to save memory (EventBus#97)
+ if ((isRecord || Modifier.isFinal(eventType.getModifiers())) // if this event is effectively final
+ && parents.size() == 1 // only has one parent EventBus
+ && parents.getFirst() instanceof AbstractEventBusImpl, ?> parent) {
+ var permittedSubclasses = parent.eventType().getPermittedSubclasses(); // that parent is sealed
+ if (permittedSubclasses != null && permittedSubclasses.length == 1 // only permits this event
+ && characteristics == parent.eventCharacteristics()) { // has the same characteristics
+ assert permittedSubclasses[0] == eventType;
+ return (EventBus) parents.getFirst(); // then we can reuse the parent directly
+ }
+ }
+
+ backingList = new ArrayList<>();
for (var parent : parents) {
backingList.addAll(((AbstractEventBusImpl, ?>) parent).backingList());
}
+ } else {
+ backingList = new ArrayList<>();
}
@SuppressWarnings("rawtypes")