diff --git a/src/main/java/com/cleanroommc/cleanmix/service/FoundationTransformerProvider.java b/src/main/java/com/cleanroommc/cleanmix/service/FoundationTransformerProvider.java index a412384c9..004d872d3 100644 --- a/src/main/java/com/cleanroommc/cleanmix/service/FoundationTransformerProvider.java +++ b/src/main/java/com/cleanroommc/cleanmix/service/FoundationTransformerProvider.java @@ -25,7 +25,6 @@ final class FoundationTransformerProvider implements ITransformerProvider { * excluded automatically at runtime via the re-entrance lock. */ private static final Set REENTRANT_EXCLUSIONS = Sets.newHashSet( - "net.minecraftforge.fml.common.asm.transformers.EventSubscriptionTransformer", "net.minecraftforge.fml.common.asm.transformers.TerminalTransformer" ); diff --git a/src/main/java/net/minecraftforge/fml/common/asm/transformers/EventSubscriberTransformer.java b/src/main/java/net/minecraftforge/fml/common/asm/transformers/EventSubscriberTransformer.java deleted file mode 100644 index fbf702596..000000000 --- a/src/main/java/net/minecraftforge/fml/common/asm/transformers/EventSubscriberTransformer.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Minecraft Forge - * Copyright (c) 2016-2020. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -package net.minecraftforge.fml.common.asm.transformers; - -import java.lang.reflect.Modifier; -import java.util.List; - -import net.minecraft.launchwrapper.IClassTransformer; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AnnotationNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; - -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; - -public class EventSubscriberTransformer implements IClassTransformer -{ - @Override - public byte[] transform(String name, String transformedName, byte[] basicClass) - { - if (basicClass == null) return null; - - ClassNode classNode = new ClassNode(); - new ClassReader(basicClass).accept(classNode, 0); - - boolean isSubscriber = false; - - for (MethodNode methodNode : classNode.methods) - { - List anns = methodNode.visibleAnnotations; - - if (anns != null && Iterables.any(anns, SubscribeEventPredicate.INSTANCE)) - { - if (Modifier.isPrivate(methodNode.access)) - { - String msg = "Cannot apply @SubscribeEvent to private method %s/%s%s"; - throw new RuntimeException(String.format(msg, classNode.name, methodNode.name, methodNode.desc)); - } - - methodNode.access = toPublic(methodNode.access); - isSubscriber = true; - } - } - - if (isSubscriber) - { - classNode.access = toPublic(classNode.access); - - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); - classNode.accept(writer); - return writer.toByteArray(); - } - - return basicClass; - } - - private static int toPublic(int access) - { - return access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED) | Opcodes.ACC_PUBLIC; - } - - private static class SubscribeEventPredicate implements Predicate - { - static final SubscribeEventPredicate INSTANCE = new SubscribeEventPredicate(); - - @Override - public boolean apply(AnnotationNode input) - { - return input.desc.equals("Lnet/minecraftforge/fml/common/eventhandler/SubscribeEvent;"); - } - } -} diff --git a/src/main/java/net/minecraftforge/fml/common/asm/transformers/EventSubscriptionTransformer.java b/src/main/java/net/minecraftforge/fml/common/asm/transformers/EventSubscriptionTransformer.java deleted file mode 100644 index ed1fbdb09..000000000 --- a/src/main/java/net/minecraftforge/fml/common/asm/transformers/EventSubscriptionTransformer.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Minecraft Forge - * Copyright (c) 2016-2020. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -package net.minecraftforge.fml.common.asm.transformers; - -import static org.objectweb.asm.Type.VOID_TYPE; -import static org.objectweb.asm.Type.BOOLEAN_TYPE; -import static org.objectweb.asm.Type.getMethodDescriptor; - -import net.minecraft.launchwrapper.IClassTransformer; -import net.minecraftforge.fml.common.FMLLog; -import net.minecraftforge.fml.common.eventhandler.Event; - -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; - -public class EventSubscriptionTransformer implements IClassTransformer, Opcodes -{ - private static final Type LISTENER_LIST_TYPE = Type.getObjectType("net/minecraftforge/fml/common/eventhandler/ListenerList"); - private static final String LISTENER_LIST_INTERNAL_NAME = LISTENER_LIST_TYPE.getInternalName(); - private static final String LISTENER_LIST_DESC = LISTENER_LIST_TYPE.getDescriptor(); - private static final String LISTENER_LIST_METHOD_DESC = getMethodDescriptor(LISTENER_LIST_TYPE); - private static final String VOID_METHOD_DESC = getMethodDescriptor(VOID_TYPE); - private static final String BOOLEAN_METHOD_DESC = getMethodDescriptor(BOOLEAN_TYPE); - private static final String LISTENER_LIST_CTR_DESC = getMethodDescriptor(VOID_TYPE, LISTENER_LIST_TYPE); - private static final String CANCELABLE_ANNOTATION_DESC = "Lnet/minecraftforge/fml/common/eventhandler/Cancelable;"; - private static final String HAS_RESULT_ANNOTATION_DESC = "Lnet/minecraftforge/fml/common/eventhandler/Event$HasResult;"; - - public EventSubscriptionTransformer() - { - new Event(); // make sure the base event class loaded and initialized. - } - - @Override - public byte[] transform(String name, String transformedName, byte[] bytes) - { - if (bytes == null || name.equals("net.minecraftforge.fml.common.eventhandler.Event") || name.startsWith("net.minecraft.") || name.indexOf('.') == -1) - { - return bytes; - } - // ClassReader's constructor only indexes the constant pool, just enough for getSuperName() - ClassReader cr = new ClassReader(bytes); - String superName = cr.getSuperName(); - if (superName == null) - { - return bytes; - } - - try - { - // Yes, this recursively loads classes until we get this base class. THIS IS NOT A ISSUE. Coremods should handle re-entry just fine. - // If they do not this a COREMOD issue NOT a Forge/LaunchWrapper issue. - Class parent = this.getClass().getClassLoader().loadClass(superName.replace('/', '.')); - if (!Event.class.isAssignableFrom(parent)) - { - return bytes; - } - } - catch (ClassNotFoundException ex) - { - // Discard silently- it's just noise - return bytes; - } - - try - { - ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); - EventClassVisitor visitor = new EventClassVisitor(cw, superName); - cr.accept(visitor, 0); - return visitor.isEdited() ? cw.toByteArray() : bytes; - } - catch (Exception e) - { - FMLLog.log.error("Error building events.", e); - } - - return bytes; - } - - private static final class EventClassVisitor extends ClassVisitor - { - private final String superName; - - private String className; - private boolean edited; - - private boolean hasSetup; - private boolean hasGetListenerList; - private boolean hasDefaultCtr; - private boolean hasCancelable; - private boolean hasResult; - private boolean cancelableAnnotation; - private boolean hasResultAnnotation; - - EventClassVisitor(ClassVisitor classVisitor, String superName) - { - super(ASM9, classVisitor); - this.superName = superName; - } - - boolean isEdited() - { - return this.edited; - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) - { - this.className = name; - super.visit(version, access, name, signature, superName, interfaces); - } - - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) - { - if (visible) - { - if (HAS_RESULT_ANNOTATION_DESC.equals(descriptor)) this.hasResultAnnotation = true; - else if (CANCELABLE_ANNOTATION_DESC.equals(descriptor)) this.cancelableAnnotation = true; - } - return super.visitAnnotation(descriptor, visible); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) - { - if (name.equals("setup") && descriptor.equals(VOID_METHOD_DESC) && (access & ACC_PROTECTED) == ACC_PROTECTED) this.hasSetup = true; - if ((access & ACC_PUBLIC) == ACC_PUBLIC) - { - if (name.equals("getListenerList") && descriptor.equals(LISTENER_LIST_METHOD_DESC)) this.hasGetListenerList = true; - if (name.equals("isCancelable") && descriptor.equals(BOOLEAN_METHOD_DESC)) this.hasCancelable = true; - if (name.equals("hasResult") && descriptor.equals(BOOLEAN_METHOD_DESC)) this.hasResult = true; - } - if (name.equals("") && descriptor.equals(VOID_METHOD_DESC)) this.hasDefaultCtr = true; - - // Returning the writer's own MethodVisitor is what lets ASM copy this method verbatim. - return super.visitMethod(access, name, descriptor, signature, exceptions); - } - - @Override - public void visitEnd() - { - if (this.hasResultAnnotation && !this.hasResult) - { - /* Add: - * public boolean hasResult() - * { - * return true; - * } - */ - this.addConstantTrueMethod("hasResult"); - } - - if (this.cancelableAnnotation && !this.hasCancelable) - { - /* Add: - * public boolean isCancelable() - * { - * return true; - * } - */ - this.addConstantTrueMethod("isCancelable"); - } - - if (this.hasSetup) - { - if (!this.hasGetListenerList) - throw new RuntimeException("Event class defines setup() but does not define getListenerList! " + this.className); - - super.visitEnd(); - return; - } - - //Add private static ListenerList LISTENER_LIST - super.visitField(ACC_PRIVATE | ACC_STATIC, "LISTENER_LIST", LISTENER_LIST_DESC, null, null).visitEnd(); - - if (!this.hasDefaultCtr) - { - /*Add: - * public () - * { - * super(); - * } - */ - MethodVisitor mv = super.visitMethod(ACC_PUBLIC, "", VOID_METHOD_DESC, null, null); - mv.visitCode(); - mv.visitVarInsn(ALOAD, 0); - mv.visitMethodInsn(INVOKESPECIAL, this.superName, "", VOID_METHOD_DESC, false); - mv.visitInsn(RETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - } - - /*Add: - * protected void setup() - * { - * super.setup(); - * if (LISTENER_LIST != NULL) - * { - * return; - * } - * LISTENER_LIST = new ListenerList(super.getListenerList()); - * } - */ - MethodVisitor mv = super.visitMethod(ACC_PROTECTED, "setup", VOID_METHOD_DESC, null, null); - mv.visitCode(); - mv.visitVarInsn(ALOAD, 0); - mv.visitMethodInsn(INVOKESPECIAL, this.superName, "setup", VOID_METHOD_DESC, false); - mv.visitFieldInsn(GETSTATIC, this.className, "LISTENER_LIST", LISTENER_LIST_DESC); - Label initListener = new Label(); - mv.visitJumpInsn(IFNULL, initListener); - mv.visitInsn(RETURN); - mv.visitLabel(initListener); - mv.visitFrame(F_SAME, 0, null, 0, null); - mv.visitTypeInsn(NEW, LISTENER_LIST_INTERNAL_NAME); - mv.visitInsn(DUP); - mv.visitVarInsn(ALOAD, 0); - mv.visitMethodInsn(INVOKESPECIAL, this.superName, "getListenerList", LISTENER_LIST_METHOD_DESC, false); - mv.visitMethodInsn(INVOKESPECIAL, LISTENER_LIST_INTERNAL_NAME, "", LISTENER_LIST_CTR_DESC, false); - mv.visitFieldInsn(PUTSTATIC, this.className, "LISTENER_LIST", LISTENER_LIST_DESC); - mv.visitInsn(RETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - - /*Add: - * public ListenerList getListenerList() - * { - * return this.LISTENER_LIST; - * } - */ - mv = super.visitMethod(ACC_PUBLIC, "getListenerList", LISTENER_LIST_METHOD_DESC, null, null); - mv.visitCode(); - mv.visitFieldInsn(GETSTATIC, this.className, "LISTENER_LIST", LISTENER_LIST_DESC); - mv.visitInsn(ARETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - - this.edited = true; - super.visitEnd(); - } - - private void addConstantTrueMethod(String name) - { - MethodVisitor mv = super.visitMethod(ACC_PUBLIC, name, BOOLEAN_METHOD_DESC, null, null); - mv.visitCode(); - mv.visitInsn(ICONST_1); - mv.visitInsn(IRETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - this.edited = true; - } - } -} diff --git a/src/main/java/net/minecraftforge/fml/common/eventhandler/Event.java b/src/main/java/net/minecraftforge/fml/common/eventhandler/Event.java index 8b5ea455a..02a3f2820 100644 --- a/src/main/java/net/minecraftforge/fml/common/eventhandler/Event.java +++ b/src/main/java/net/minecraftforge/fml/common/eventhandler/Event.java @@ -49,7 +49,6 @@ public enum Result private boolean isCanceled = false; private Result result = Result.DEFAULT; - private static ListenerList listeners = new ListenerList(); private EventPriority phase = null; public Event() @@ -66,7 +65,7 @@ public Event() */ public boolean isCancelable() { - return false; + return EventProperties.CANCELLABLE.get(getClass()); } /** @@ -89,7 +88,7 @@ public boolean isCanceled() */ public void setCanceled(boolean cancel) { - if (!isCancelable()) + if (!EventProperties.CANCELLABLE.get(getClass())) { throw new UnsupportedOperationException( "Attempted to call Event#setCanceled() on a non-cancelable event of type: " @@ -107,7 +106,7 @@ public void setCanceled(boolean cancel) */ public boolean hasResult() { - return false; + return EventProperties.HAS_RESULT.get(getClass()); } /** @@ -147,7 +146,7 @@ protected void setup() */ public ListenerList getListenerList() { - return listeners; + return EventProperties.LISTENER_LIST.get(this.getClass()); } @Nullable diff --git a/src/main/java/net/minecraftforge/fml/common/eventhandler/EventBus.java b/src/main/java/net/minecraftforge/fml/common/eventhandler/EventBus.java index 0fa262bcd..c0bc004ba 100644 --- a/src/main/java/net/minecraftforge/fml/common/eventhandler/EventBus.java +++ b/src/main/java/net/minecraftforge/fml/common/eventhandler/EventBus.java @@ -19,14 +19,13 @@ package net.minecraftforge.fml.common.eventhandler; -import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -37,7 +36,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.MapMaker; -import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; import org.jspecify.annotations.NonNull; @@ -64,6 +62,52 @@ public EventBus(@Nonnull IEventExceptionHandler handler) exceptionHandler = handler; } + public void addListener(Class eventType, Consumer handler) + { + addListener(eventType, EventPriority.NORMAL, false, handler); + } + + public void addListener(Class eventType, EventPriority priority, Consumer handler) + { + addListener(eventType, priority, false, handler); + } + + public void addListener( + Class eventType, + EventPriority priority, + boolean receiveCanceled, + Consumer handler + ) + { + Objects.requireNonNull(eventType, "eventType"); + Objects.requireNonNull(priority, "priority"); + Objects.requireNonNull(handler, "handler"); + if (!Event.class.isAssignableFrom(eventType)) { + throw new IllegalArgumentException("Not an event type: " + eventType); + } + if (listeners.containsKey(handler)) { + return; + } + + @SuppressWarnings("unchecked") + IEventListener listener = receiveCanceled || !EventProperties.CANCELLABLE.get(eventType) + ? ((Consumer) handler)::accept + : event -> { + if (!event.isCanceled()) { + handler.accept((T) event); + } + }; + + ModContainer activeModContainer = Loader.instance().activeModContainer(); + if (activeModContainer == null) { + FMLLog.log.error("Unable to determine registrant mod for {}. This is a critical error and should be impossible", handler, new Throwable()); + activeModContainer = Loader.instance().getMinecraftModContainer(); + } + listenerOwners.put(handler, activeModContainer); + + register0(eventType, EventProperties.LISTENER_LIST.get(eventType), handler, listener, priority, activeModContainer); + } + public void register(Object target) { if (listeners.containsKey(target)) @@ -79,59 +123,43 @@ public void register(Object target) } listenerOwners.put(target, activeModContainer); - boolean isStatic; - Set> supers; - Class scanTarget; + Collection methods; if (target instanceof Class clazz) { - isStatic = true; - supers = Set.of(clazz); - scanTarget = clazz; + // static listener: subscribed methods must be declared by the class + methods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> !m.isSynthetic() + && Modifier.isStatic(m.getModifiers()) + && m.isAnnotationPresent(SubscribeEvent.class)) + .toList(); } else { - isStatic = false; - supers = TypeToken.of(target.getClass()).getTypes().rawTypes(); - scanTarget = target.getClass(); + // instance listener: methods overriding a subscribed method is also valid. + // In this case, we will register the subscribed parent method instead. JVM will + // handle it if a subclass overrides subscribed method + methods = TypeToken.of(target.getClass()) + .getTypes() + .rawTypes() + // get self & superclass & interface + .stream() + .map(Class::getDeclaredMethods) + .flatMap(Arrays::stream) + .filter(m -> !m.isSynthetic() + && !Modifier.isStatic(m.getModifiers()) + // private not allowed because it does not participate in inheritance + && !Modifier.isPrivate(m.getModifiers()) + && m.isAnnotationPresent(SubscribeEvent.class)) + // deduplicate by signature + .collect(Collectors.toMap( + m -> Map.entry(m.getName(), Arrays.asList(m.getParameterTypes())), + Function.identity(), + (a, b) -> a, + LinkedHashMap::new + )) + .values(); } - for (Method method : scanTarget.getMethods()) + for (Method method : methods) { - if (isStatic != Modifier.isStatic(method.getModifiers())) - continue; - - try { - // do `.getDeclaredMethod(...)` to force JVM to walk through declared methods and load their parameter - // types. This is for preventing shortcut below from skipping classloading - // - // mod developers should be responsible for not loading non-existent class, but :( - // related issue: https://github.com/CleanroomMC/Cleanroom/issues/349 - method.getDeclaringClass().getDeclaredMethod("forceClassLoadingForDeclaredMethods", Event.class); - } catch (NoSuchMethodException e) { - // swallow this specific exception, other exceptions, like ClassNotFoundException, will fall through - } - var parameterTypes = method.getParameterTypes(); - var matched = supers.stream() - .map(cls -> { - if (cls == method.getDeclaringClass()) { - // shortcut for most event handler classes with no explicit superclass - return method; - } - try { - return cls.getDeclaredMethod(method.getName(), parameterTypes); - } catch (NoSuchMethodException e) { - // Eat the error, this is not unexpected - return null; - } - }) - .filter(Objects::nonNull) - .filter(m -> m.isAnnotationPresent(SubscribeEvent.class)) - .findFirst() - .orElse(null); - - if (matched == null) - { - continue; - } - if (parameterTypes.length != 1) { throw new IllegalArgumentException( @@ -141,15 +169,12 @@ public void register(Object target) } Class eventType = parameterTypes[0]; - if (!Event.class.isAssignableFrom(eventType)) { throw new IllegalArgumentException("Method " + method + " has @SubscribeEvent annotation, but takes a argument that is not an Event " + eventType); } - // the method to be registered here is "matched", not "method", it should be a bug of - // the original event bus, since the exceptions above are all referencing "method" - register(eventType, target, matched, activeModContainer); + register(eventType, target, method, activeModContainer); } } @@ -158,21 +183,9 @@ private void register(Class eventType, Object target, Method method, final Mo { try { - Constructor ctr = eventType.getConstructor(); - ctr.setAccessible(true); - Event event = (Event)ctr.newInstance(); - final ASMEventHandler asm = new ASMEventHandler(target, method, owner, IGenericEvent.class.isAssignableFrom(eventType)); + ASMEventHandler asm = new ASMEventHandler(target, method, owner, IGenericEvent.class.isAssignableFrom(eventType)); - IEventListener listener = asm; - if (IContextSetter.class.isAssignableFrom(eventType)) - { - listener = new ContextSetterEventListener(owner, asm); - } - - event.getListenerList().register(busID, asm.getPriority(), listener); - - ArrayList others = listeners.computeIfAbsent(target, k -> new ArrayList<>()); - others.add(listener); + register0(eventType, EventProperties.LISTENER_LIST.get(eventType), target, asm, asm.getPriority(), owner); } catch (Exception e) { @@ -180,6 +193,24 @@ private void register(Class eventType, Object target, Method method, final Mo } } + private void register0( + Class eventType, + ListenerList listenerList, + Object key, + IEventListener listener, + EventPriority priority, + ModContainer owner + ) + { + if (IContextSetter.class.isAssignableFrom(eventType)) + { + listener = new ContextSetterEventListener(owner, listener); + } + + listenerList.register(busID, priority, listener); + listeners.computeIfAbsent(key, _ -> new ArrayList<>()).add(listener); + } + public void unregister(Object object) { ArrayList list = listeners.remove(object); @@ -195,7 +226,8 @@ public boolean post(Event event) { if (shutdown) return false; - IEventListener[] listeners = event.getListenerList().getListeners(busID); + var eventType = event.getClass(); + IEventListener[] listeners = EventProperties.LISTENER_LIST.get(eventType).getListeners(busID); int index = 0; try { @@ -210,7 +242,7 @@ public boolean post(Event event) Throwables.throwIfUnchecked(throwable); throw new RuntimeException(throwable); } - return event.isCancelable() && event.isCanceled(); + return EventProperties.CANCELLABLE.get(eventType) && event.isCanceled(); } public void shutdown() @@ -232,7 +264,7 @@ public void handleException(EventBus bus, Event event, IEventListener[] listener private record ContextSetterEventListener( ModContainer owner, - ASMEventHandler asm + IEventListener asm ) implements IEventListener { @Override diff --git a/src/main/java/net/minecraftforge/fml/common/eventhandler/EventListenerFactory.java b/src/main/java/net/minecraftforge/fml/common/eventhandler/EventListenerFactory.java index a7bfd2a43..c8900951f 100644 --- a/src/main/java/net/minecraftforge/fml/common/eventhandler/EventListenerFactory.java +++ b/src/main/java/net/minecraftforge/fml/common/eventhandler/EventListenerFactory.java @@ -39,7 +39,8 @@ private static MethodHandle createListenerFactory( Object instance ) { try { - var handle = LOOKUP.unreflect(callback); + var lookup = MethodHandles.privateLookupIn(callback.getDeclaringClass(), LOOKUP); + var handle = lookup.unreflect(callback); var factoryType = isStatic ? Constants.RETURNS_IT @@ -47,7 +48,7 @@ private static MethodHandle createListenerFactory( : Constants.RETURNS_IT.insertParameterTypes(0, instance.getClass()); var factoryHandle = LambdaMetafactory.metafactory( - LOOKUP, + lookup, Constants.METHOD_NAME, factoryType, Constants.METHOD_TYPE, diff --git a/src/main/java/net/minecraftforge/fml/common/eventhandler/EventProperties.java b/src/main/java/net/minecraftforge/fml/common/eventhandler/EventProperties.java new file mode 100644 index 000000000..a27ddb13f --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/common/eventhandler/EventProperties.java @@ -0,0 +1,90 @@ +package net.minecraftforge.fml.common.eventhandler; + +import net.lenni0451.reflect.accessor.UnsafeAccess; +import org.jspecify.annotations.NonNull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; + +abstract class EventProperties { + /// @see Event#getListenerList() + static final ClassValue LISTENER_LIST = new ClassValue<>() { + @Override + protected ListenerList computeValue(@NonNull Class type) { + if (!Event.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Not event class: " + type); + } + + if (type == Event.class) { + return new ListenerList(); + } + + // assignable to Event.class & not Event.class -> subclass of Event + @SuppressWarnings("unchecked") + Class eventType = (Class) type; + + ListenerList result = null; + try { + var method_getListenerList = eventType.getDeclaredMethod("getListenerList"); + // If this event implements its own .getListenerList() + var instance = UnsafeAccess.allocateInstance(eventType); + result = (ListenerList) method_getListenerList.invoke(instance); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // fall through + } + + if (result == null) { + ListenerList parent = get(type.getSuperclass()); + result = new ListenerList(parent); + } + + return result; + } + }; + + /// @see net.minecraftforge.fml.common.eventhandler.Event.HasResult + /// @see Event#hasResult() + static final ClassValue HAS_RESULT = checkChainedAnnotation(Event.HasResult.class, "hasResult"); + + /// @see Cancelable + /// @see Event#isCancelable() + static final ClassValue CANCELLABLE = checkChainedAnnotation(Cancelable.class, "isCancelable"); + + private static ClassValue checkChainedAnnotation(Class target, String methodName) { + return new ClassValue<>() { + @Override + protected Boolean computeValue(@NonNull Class type) { + if (!Event.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Not event class: " + type); + } + + try { + var method = type.getMethod(methodName); + + // walks superclass, until (exclusive) the first method implementation + Class checkEnd = method.getDeclaringClass(); + for (var c = type; c != checkEnd; c = c.getSuperclass()) { + if (type.isAnnotationPresent(target)) { + return true; + } + } + + // The first method implementation is not from Event -> custom impl + if (method.getDeclaringClass() != Event.class) { + // respect custom impl + var instance = UnsafeAccess.allocateInstance(type); + if ((boolean) method.invoke(instance)) { + return true; + } + } + + // fall through to default + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // fall through to default + } + + return false; + } + }; + } +} diff --git a/src/main/java/net/minecraftforge/fml/common/eventhandler/ListenerList.java b/src/main/java/net/minecraftforge/fml/common/eventhandler/ListenerList.java index 32cc6de12..1cce06a8e 100644 --- a/src/main/java/net/minecraftforge/fml/common/eventhandler/ListenerList.java +++ b/src/main/java/net/minecraftforge/fml/common/eventhandler/ListenerList.java @@ -21,7 +21,6 @@ import java.util.*; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import javax.annotation.Nullable; diff --git a/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java b/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java index 5fd23c16e..a3bfb2fcd 100644 --- a/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java +++ b/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java @@ -30,8 +30,6 @@ public String[] getASMTransformerClass() { return new String[] { "net.minecraftforge.fml.common.asm.transformers.SideTransformer", - "net.minecraftforge.fml.common.asm.transformers.EventSubscriptionTransformer", - "net.minecraftforge.fml.common.asm.transformers.EventSubscriberTransformer", "net.minecraftforge.fml.common.asm.transformers.SoundEngineFixTransformer", "net.minecraftforge.fml.common.asm.transformers.LWJGLTransformer", }; diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/EventBusTest.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/EventBusTest.java index ab87f10f8..27d40a048 100644 --- a/src/test/java/net/minecraftforge/fml/common/eventhandler/EventBusTest.java +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/EventBusTest.java @@ -3,14 +3,27 @@ import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.ModContainer; import net.minecraftforge.fml.common.eventhandler.impl.AbnormalListeners; +import net.minecraftforge.fml.common.eventhandler.impl.CancelableEvents; +import net.minecraftforge.fml.common.eventhandler.impl.CustomListEvent; import net.minecraftforge.fml.common.eventhandler.impl.ExampleEvent; +import net.minecraftforge.fml.common.eventhandler.impl.HandWrittenListParameterizedEvent; +import net.minecraftforge.fml.common.eventhandler.impl.HandWrittenSetupEvent; +import net.minecraftforge.fml.common.eventhandler.impl.HasResultEvents; +import net.minecraftforge.fml.common.eventhandler.impl.InheritedListeners; import net.minecraftforge.fml.common.eventhandler.impl.InstanceListeners; +import net.minecraftforge.fml.common.eventhandler.impl.NonPublicInstanceListeners; +import net.minecraftforge.fml.common.eventhandler.impl.NonPublicStaticListeners; +import net.minecraftforge.fml.common.eventhandler.impl.ParameterizedEvent; +import net.minecraftforge.fml.common.eventhandler.impl.PolymorphicEvents; import net.minecraftforge.fml.common.eventhandler.impl.StaticListeners; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; /** * @author ZZZank @@ -86,4 +99,269 @@ public void registerIllegalParamType() throws Exception { Assertions.assertEquals(event.id, listener.captured); } + + @Test + public void registerNonPublicInstance() { + var bus = new EventBus(); + + var listeners = new NonPublicInstanceListeners(); + bus.register(listeners); + + bus.post(new ExampleEvent()); + bus.post(new ExampleEvent()); + + Assertions.assertEquals(4, listeners.total(), "private/protected/package-private instance listeners must all fire"); + } + + @Test + public void registerNonPublicStatic() { + var bus = new EventBus(); + + bus.register(NonPublicStaticListeners.class); + + bus.post(new ExampleEvent()); + + Assertions.assertEquals(2, NonPublicStaticListeners.total(), "private/protected static listeners must all fire"); + } + + @Test + public void registerInheritedAnnotation() { + var bus = new EventBus(); + + var listeners = new InheritedListeners.Derived(); + bus.register(listeners); + + bus.post(new ExampleEvent()); + + Assertions.assertEquals(1, listeners.baseCalls, "override without @SubscribeEvent inherits the annotated supertype declaration"); + } + + @Test + public void cancelableEvent() { + // the @Cancelable annotation must make setCanceled legal without any class-load injection + var event = new CancelableEvents.CancelableEvent(); + event.setCanceled(true); + Assertions.assertTrue(event.isCanceled()); + + var bus = new EventBus(); + Assertions.assertTrue(bus.post(event), "post returns true when the event is cancelable and canceled"); + } + + @Test + public void nonCancelableEvent() { + var event = new CancelableEvents.NonCancelableEvent(); + Assertions.assertThrows( + UnsupportedOperationException.class, + () -> event.setCanceled(true), + "setCanceled on a non-cancelable event must throw" + ); + } + + @Test + public void hasResultEvent() { + Assertions.assertTrue(new HasResultEvents.Result().hasResult()); + Assertions.assertFalse(new HasResultEvents.NoResult().hasResult()); + } + + @Test + public void handWrittenOverrideRespected() { + // an explicit override without the annotation must win over the annotation probe + // (virtual dispatch never reaches the Event base implementation) + var event = new CancelableEvents.HandWritten(); + Assertions.assertTrue(event.isCancelable()); + event.setCanceled(true); + Assertions.assertTrue(event.isCanceled()); + } + + @Test + public void annotatedReceiveCanceledFalse() { + // @SubscribeEvent(receiveCanceled = false): the cancel check is kept for a cancelable + // event class (canceled events are skipped), but optimized away for a non-cancelable + // event class (the listener still receives the event) + var bus = new EventBus(); + + var skip = new CancelableEvents.SkipCanceled(); + bus.register(skip); + var canceled = new CancelableEvents.CancelableEvent(); + canceled.setCanceled(true); + bus.post(canceled); + Assertions.assertEquals(0, skip.calls, "canceled cancelable event must be skipped"); + + var nonCancelable = new CancelableEvents.NonCancelableListener(); + bus.register(nonCancelable); + bus.post(new CancelableEvents.NonCancelableEvent()); + Assertions.assertEquals(1, nonCancelable.calls, "non-cancelable event must still fire"); + } + + @Test + public void nonCancelableReceiveCanceledFalseLambda() { + // receiveCanceled=false on a non-cancelable event class: the per-invocation check is + // optimized away, so the listener still receives the event + var bus = new EventBus(); + AtomicInteger calls = new AtomicInteger(); + bus.addListener(CancelableEvents.NonCancelableEvent.class, EventPriority.NORMAL, false, e -> calls.incrementAndGet()); + bus.post(new CancelableEvents.NonCancelableEvent()); + Assertions.assertEquals(1, calls.get()); + } + + @Test + public void handWrittenReceiveCanceledFalseLambda() { + // a handwritten isCancelable() override forces the conservative path: the check is + // kept, so a canceled event still skips a receiveCanceled=false listener + var bus = new EventBus(); + AtomicInteger calls = new AtomicInteger(); + bus.addListener(CancelableEvents.HandWritten.class, EventPriority.NORMAL, false, e -> calls.incrementAndGet()); + var canceled = new CancelableEvents.HandWritten(); + canceled.setCanceled(true); + bus.post(canceled); + Assertions.assertEquals(0, calls.get()); + } + + @Test + public void polymorphicPost() { + var bus = new EventBus(); + var parent = new PolymorphicEvents.ParentListener(); + var child = new PolymorphicEvents.ChildListener(); + bus.register(parent); + bus.register(child); + + // posting a subclass event fires both subclass and superclass listeners + // (the subclass listener list is chained to the superclass list) + bus.post(new PolymorphicEvents.ChildEvent()); + Assertions.assertEquals(1, parent.calls, "superclass listeners fire for subclass events"); + Assertions.assertEquals(1, child.calls); + + // posting a superclass event fires only superclass listeners (the chain is one-way) + bus.post(new PolymorphicEvents.ParentEvent()); + Assertions.assertEquals(2, parent.calls); + Assertions.assertEquals(1, child.calls, "subclass listeners must not fire for superclass events"); + } + + @Test + public void registerHandWrittenListParameterizedEvent() { + // handwritten getListenerList() override + no no-arg constructor: registration must + // allocate an instance without a constructor call (constructor injection is gone) so + // it resolves the same handwritten list that posting resolves + var bus = new EventBus(); + var listener = new HandWrittenListParameterizedEvent.Listener(); + bus.register(listener); + + bus.post(new HandWrittenListParameterizedEvent(1)); + Assertions.assertEquals(1, listener.calls); + } + + @Test + public void registerLambda() { + var bus = new EventBus(); + List received = new ArrayList<>(); + bus.addListener(ExampleEvent.class, EventPriority.HIGHEST, received::add); + + var event = new ExampleEvent(); + bus.post(event); + + Assertions.assertEquals(List.of(event), received); + } + + @Test + public void lambdaPriorityOrder() { + var bus = new EventBus(); + List order = new ArrayList<>(); + bus.addListener(ExampleEvent.class, EventPriority.LOWEST, e -> order.add(EventPriority.LOWEST)); + bus.addListener(ExampleEvent.class, EventPriority.NORMAL, e -> order.add(EventPriority.NORMAL)); + bus.addListener(ExampleEvent.class, EventPriority.HIGHEST, e -> order.add(EventPriority.HIGHEST)); + + bus.post(new ExampleEvent()); + + Assertions.assertEquals( + List.of(EventPriority.HIGHEST, EventPriority.NORMAL, EventPriority.LOWEST), + order + ); + } + + @Test + public void lambdaReceiveCanceled() { + // default: canceled events are still dispatched, matching annotated listeners + var bus1 = new EventBus(); + AtomicInteger calls1 = new AtomicInteger(); + bus1.addListener(CancelableEvents.CancelableEvent.class, EventPriority.NORMAL, true, e -> calls1.incrementAndGet()); + var event1 = new CancelableEvents.CancelableEvent(); + event1.setCanceled(true); + bus1.post(event1); + Assertions.assertEquals(1, calls1.get()); + + // receiveCanceled = false: canceled events are skipped (a fresh instance: posting + // advances the event phase, so an already-posted instance cannot be reused) + var bus2 = new EventBus(); + AtomicInteger calls2 = new AtomicInteger(); + bus2.addListener(CancelableEvents.CancelableEvent.class, EventPriority.NORMAL, false, e -> calls2.incrementAndGet()); + // receiveCanceled default to false + bus2.addListener(CancelableEvents.CancelableEvent.class, e -> calls2.incrementAndGet()); + var canceled2 = new CancelableEvents.CancelableEvent(); + canceled2.setCanceled(true); + bus2.post(canceled2); + Assertions.assertEquals(0, calls2.get()); + } + + @Test + public void unregisterLambda() { + var bus = new EventBus(); + AtomicInteger calls = new AtomicInteger(); + Consumer handler = e -> calls.incrementAndGet(); + bus.addListener(ExampleEvent.class, handler); + + bus.post(new ExampleEvent()); + Assertions.assertEquals(1, calls.get()); + + bus.unregister(handler); + bus.post(new ExampleEvent()); + Assertions.assertEquals(1, calls.get(), "unregistered lambda must not be invoked"); + } + + @Test + public void registerHandWrittenSetupEvent() { + // the legacy full handwritten pattern: setup() + getListenerList(). The transformer + // skipped such classes (hasSetup branch), so the base constructor's setup() virtual + // call must still run and registration/posting must resolve the handwritten list + var bus = new EventBus(); + var listener = new HandWrittenSetupEvent.Listener(); + bus.register(listener); + + bus.post(new HandWrittenSetupEvent()); + Assertions.assertEquals(1, listener.calls); + } + + @Test + public void registerParameterizedEvent() { + // event classes without a no-arg constructor (e.g. TextureStitchEvent$Pre) can no longer + // rely on the transformer-injected constructor; registration must fall back to the probe + var bus = new EventBus(); + var listener = new ParameterizedEvent.Listener(); + bus.register(listener); + + bus.post(new ParameterizedEvent(42)); + Assertions.assertEquals(1, listener.calls); + } + + @Test + public void handWrittenListenerListRespected() { + var bus = new EventBus(); + var listener = new CustomListEvent.Listener(); + bus.register(listener); + + // registration and posting both go through the handwritten getListenerList() override + bus.post(new CustomListEvent()); + Assertions.assertEquals(1, listener.calls); + } + + @Test + public void customListenerListAtSuperclass() { + Assertions.assertEquals( + EventProperties.LISTENER_LIST.get(CustomListEvent.class), + new CustomListEvent().getListenerList() + ); + Assertions.assertNotEquals( + EventProperties.LISTENER_LIST.get(CustomListEvent.class), + EventProperties.LISTENER_LIST.get(CustomListEvent.Subclass.class) + ); + } } diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/AbnormalListeners.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/AbnormalListeners.java index 25faaff5b..649df4697 100644 --- a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/AbnormalListeners.java +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/AbnormalListeners.java @@ -12,12 +12,12 @@ public class AbnormalListeners { static class Parent { @SubscribeEvent - public static void listenerParentOnly(ExampleEvent event) { + public static void staticParentOnly(ExampleEvent event) { throw new IllegalStateException("listener method only present in super class should not be registered"); } @SubscribeEvent - public static void listenerParentAndSub(ExampleEvent event) { + public static void staticParentAndSub(ExampleEvent event) { throw new IllegalStateException( "listener method only registered in super class should not be registered in sub class"); } @@ -25,7 +25,7 @@ public static void listenerParentAndSub(ExampleEvent event) { public static class Actual extends Parent { - public static void listenerParentAndSub(ExampleEvent event) { + public static void staticParentAndSub(ExampleEvent event) { throw new IllegalStateException( "listener method only registered in super class should not be registered in sub class"); } diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/CancelableEvents.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/CancelableEvents.java new file mode 100644 index 000000000..d74ae03ab --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/CancelableEvents.java @@ -0,0 +1,54 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Cancelable; +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +public class CancelableEvents { + + @Cancelable + public static class CancelableEvent extends Event { + } + + public static class NonCancelableEvent extends Event { + } + + /** + * The legacy handwritten pattern: an explicit {@code isCancelable()} override without + * the {@code @Cancelable} annotation. Virtual dispatch must prefer this over the + * annotation probe in {@link net.minecraftforge.fml.common.eventhandler.EventCompatProbe}. + */ + public static class HandWritten extends Event { + + @Override + public boolean isCancelable() { + return true; + } + } + + /** + * {@code receiveCanceled = false} on a cancelable event class: the cancel check is kept + * and a canceled event must be skipped. + */ + public static class SkipCanceled { + public int calls; + + @SubscribeEvent(receiveCanceled = false) + public void onEvent(CancelableEvent e) { + calls++; + } + } + + /** + * {@code receiveCanceled = false} on a non-cancelable event class: the cancel check is + * optimized away, so the listener still receives the event. + */ + public static class NonCancelableListener { + public int calls; + + @SubscribeEvent(receiveCanceled = false) + public void onEvent(NonCancelableEvent e) { + calls++; + } + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/CustomListEvent.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/CustomListEvent.java new file mode 100644 index 000000000..af8134c69 --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/CustomListEvent.java @@ -0,0 +1,34 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.ListenerList; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * The legacy handwritten pattern: an event class managing its own listener list via a + * {@code getListenerList()} override. Virtual dispatch must prefer this over the + * {@link net.minecraftforge.fml.common.eventhandler.EventCompatProbe} cache. The list is + * static, mirroring the injected {@code LISTENER_LIST} field shared by all instances. + */ +public class CustomListEvent extends Event { + + private static final ListenerList custom = new ListenerList(); + + public static class Listener { + + public int calls = 0; + + @SubscribeEvent + public void onCustom(CustomListEvent event) { + calls++; + } + } + + @Override + public ListenerList getListenerList() { + return custom; + } + + public static class Subclass extends CustomListEvent { + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HandWrittenListParameterizedEvent.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HandWrittenListParameterizedEvent.java new file mode 100644 index 000000000..b7de6797f --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HandWrittenListParameterizedEvent.java @@ -0,0 +1,39 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.ListenerList; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * An event class with a handwritten {@code getListenerList()} override but no no-arg + * constructor. This is the hardest case for registration: the removed + * {@code EventSubscriptionTransformer} injected a no-arg constructor into such classes (so + * {@code EventBus.register} could instantiate them and reach the override via virtual + * dispatch), and without it registration must allocate an instance without a constructor + * call so that registration and posting resolve to the same handwritten list. + */ +public class HandWrittenListParameterizedEvent extends Event { + + private static final ListenerList MY_LIST = new ListenerList(); + + private final int seed; + + public HandWrittenListParameterizedEvent(int seed) { + this.seed = seed; + } + + @Override + public ListenerList getListenerList() { + return MY_LIST; + } + + public static class Listener { + + public int calls = 0; + + @SubscribeEvent + public void on(HandWrittenListParameterizedEvent event) { + calls++; + } + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HandWrittenSetupEvent.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HandWrittenSetupEvent.java new file mode 100644 index 000000000..8f125dddc --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HandWrittenSetupEvent.java @@ -0,0 +1,42 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.ListenerList; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * The legacy full handwritten pattern: an event class implementing both {@code setup()} and + * {@code getListenerList()} with its own lazily-initialized static list. The removed + * {@code EventSubscriptionTransformer} skipped such classes entirely (its {@code hasSetup} + * branch), so their behavior must be identical with and without the transformer: the base + * constructor's {@code setup()} virtual call runs the handwritten setup, and registration and + * posting both resolve the handwritten list. + */ +public class HandWrittenSetupEvent extends Event { + + private static ListenerList LISTENER_LIST; + + @Override + protected void setup() { + super.setup(); + if (LISTENER_LIST != null) { + return; + } + LISTENER_LIST = new ListenerList(super.getListenerList()); + } + + @Override + public ListenerList getListenerList() { + return LISTENER_LIST; + } + + public static class Listener { + + public int calls = 0; + + @SubscribeEvent + public void on(HandWrittenSetupEvent event) { + calls++; + } + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HasResultEvents.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HasResultEvents.java new file mode 100644 index 000000000..e679e145f --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/HasResultEvents.java @@ -0,0 +1,13 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Event; + +public class HasResultEvents { + + @Event.HasResult + public static class Result extends Event { + } + + public static class NoResult extends Event { + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/InheritedListeners.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/InheritedListeners.java new file mode 100644 index 000000000..0578f866b --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/InheritedListeners.java @@ -0,0 +1,25 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * Verifies the legacy inheritance semantics: a subclass override without the annotation still + * inherits the listener registration of the annotated supertype declaration. + */ +public class InheritedListeners { + public static class Base { + public int baseCalls = 0; + + @SubscribeEvent + public void onEvent(ExampleEvent event) { + baseCalls++; + } + } + + public static class Derived extends Base { + @Override + public void onEvent(ExampleEvent event) { + super.onEvent(event); + } + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/NonPublicInstanceListeners.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/NonPublicInstanceListeners.java new file mode 100644 index 000000000..e672c1b5c --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/NonPublicInstanceListeners.java @@ -0,0 +1,33 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * Instance listeners of every non-public visibility. Their registration must not depend on + * {@code EventSubscriberTransformer} publicising {@code @SubscribeEvent} methods at class load. + */ +public class NonPublicInstanceListeners { + private int privateCount = 0; + protected int protectedCount = 0; + int packageCount = 0; + + /// private event subscriber is invalid + @SubscribeEvent + private void onPrivate(ExampleEvent event) { + privateCount++; + } + + @SubscribeEvent + protected void onProtected(ExampleEvent event) { + protectedCount++; + } + + @SubscribeEvent + void onPackage(ExampleEvent event) { + packageCount++; + } + + public int total() { + return privateCount + protectedCount + packageCount; + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/NonPublicStaticListeners.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/NonPublicStaticListeners.java new file mode 100644 index 000000000..80af0f6eb --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/NonPublicStaticListeners.java @@ -0,0 +1,26 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * Static listeners of non-public visibility. Their registration must not depend on + * {@code EventSubscriberTransformer} publicising {@code @SubscribeEvent} methods at class load. + */ +public class NonPublicStaticListeners { + private static int privateCount = 0; + protected static int protectedCount = 0; + + @SubscribeEvent + private static void onPrivate(ExampleEvent event) { + privateCount++; + } + + @SubscribeEvent + protected static void onProtected(ExampleEvent event) { + protectedCount++; + } + + public static int total() { + return privateCount + protectedCount; + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/ParameterizedEvent.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/ParameterizedEvent.java new file mode 100644 index 000000000..5e89a57cf --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/ParameterizedEvent.java @@ -0,0 +1,34 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** + * An event class without a no-arg constructor, mirroring e.g. + * {@code net.minecraftforge.client.event.TextureStitchEvent$Pre}. The removed + * {@code EventSubscriptionTransformer} used to inject a no-arg constructor into such classes + * purely so {@code EventBus.register} could instantiate them; now registration must fall back + * to the {@link net.minecraftforge.fml.common.eventhandler.EventCompatProbe} cache. + */ +public class ParameterizedEvent extends Event { + + private final int seed; + + public ParameterizedEvent(int seed) { + this.seed = seed; + } + + public int getSeed() { + return seed; + } + + public static class Listener { + + public int calls = 0; + + @SubscribeEvent + public void onParameterized(ParameterizedEvent event) { + calls++; + } + } +} diff --git a/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/PolymorphicEvents.java b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/PolymorphicEvents.java new file mode 100644 index 000000000..94fa8abc5 --- /dev/null +++ b/src/test/java/net/minecraftforge/fml/common/eventhandler/impl/PolymorphicEvents.java @@ -0,0 +1,33 @@ +package net.minecraftforge.fml.common.eventhandler.impl; + +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +public class PolymorphicEvents { + + public static class ParentEvent extends Event { + } + + public static class ChildEvent extends ParentEvent { + } + + public static class ParentListener { + + public int calls = 0; + + @SubscribeEvent + public void onParent(ParentEvent event) { + calls++; + } + } + + public static class ChildListener { + + public int calls = 0; + + @SubscribeEvent + public void onChild(ChildEvent event) { + calls++; + } + } +}