From 32f24a77fa04a3a98d15d452d5e3d9304808c241 Mon Sep 17 00:00:00 2001 From: Lyfts <127234178+Lyfts@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:45:32 +0200 Subject: [PATCH] actual completely safe event registration this time with less reflection --- .../com/gtnewhorizon/gtnhlib/CommonProxy.java | 2 +- .../gtnhlib/core/GTNHLibCore.java | 39 ++- .../transformer/EventBusSubTransformer.java | 148 +++++++++ .../gtnhlib/eventbus/AutoEventBus.java | 314 +++++------------- .../gtnhlib/eventbus/EventBusUtil.java | 31 ++ .../gtnhlib/eventbus/MethodInfo.java | 19 ++ .../eventbus/StaticASMEventHandler.java | 55 ++- .../gtnewhorizon/gtnhlib/mixins/Mixins.java | 2 +- .../mixins/early/fml/EnumHolderAccessor.java | 13 + 9 files changed, 361 insertions(+), 262 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/core/transformer/EventBusSubTransformer.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusUtil.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/eventbus/MethodInfo.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EnumHolderAccessor.java diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java b/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java index bc12a6d..a288fed 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java @@ -21,7 +21,7 @@ public class CommonProxy { public void preInit(FMLPreInitializationEvent event) { - AutoEventBus.setDataTable(event.getAsmData()); + AutoEventBus.init(event.getAsmData()); GTNHLib.info("GTNHLib version " + Tags.VERSION + " loaded."); } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/core/GTNHLibCore.java b/src/main/java/com/gtnewhorizon/gtnhlib/core/GTNHLibCore.java index fd5039c..1a0dc42 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/core/GTNHLibCore.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/core/GTNHLibCore.java @@ -9,9 +9,17 @@ import org.spongepowered.asm.launch.GlobalProperties; import org.spongepowered.asm.service.mojang.MixinServiceLaunchWrapper; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.gtnewhorizon.gtnhlib.Tags; +import com.gtnewhorizon.gtnhlib.core.transformer.EventBusSubTransformer; import com.gtnewhorizon.gtnhlib.mixins.Mixins; import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader; +import cpw.mods.fml.common.DummyModContainer; +import cpw.mods.fml.common.LoadController; +import cpw.mods.fml.common.ModMetadata; +import cpw.mods.fml.common.event.FMLConstructionEvent; import cpw.mods.fml.relauncher.FMLLaunchHandler; import cpw.mods.fml.relauncher.IFMLLoadingPlugin; @@ -20,26 +28,38 @@ "com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager", "com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator" }) @IFMLLoadingPlugin.SortingIndex(-1000) -public class GTNHLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader { +public class GTNHLibCore extends DummyModContainer implements IFMLLoadingPlugin, IEarlyMixinLoader { + + public static final String[] DEFAULT_TRANSFORMERS = new String[] { + "com.gtnewhorizon.gtnhlib.core.transformer.EventBusSubTransformer" }; + + public GTNHLibCore() { + super(new ModMetadata()); + ModMetadata md = getMetadata(); + md.autogenerated = true; + md.modId = md.name = "GTNHLib Core"; + md.parent = "gtnhlib"; + md.version = Tags.VERSION; + } @Override public String[] getASMTransformerClass() { if (!FMLLaunchHandler.side().isClient() || Launch.blackboard.getOrDefault("gtnhlib.rfbPluginLoaded", Boolean.FALSE) == Boolean.TRUE) { // Don't need any transformers if we're not on the client, or the RFB Plugin was loaded - return new String[0]; + return DEFAULT_TRANSFORMERS; } // Directly add this to the MixinServiceLaunchWrapper tweaker's list of Tweak Classes List mixinTweakClasses = GlobalProperties.get(MixinServiceLaunchWrapper.BLACKBOARD_KEY_TWEAKCLASSES); if (mixinTweakClasses != null) { mixinTweakClasses.add(MixinCompatHackTweaker.class.getName()); } - return new String[0]; + return DEFAULT_TRANSFORMERS; } @Override public String getModContainerClass() { - return null; + return "com.gtnewhorizon.gtnhlib.core.GTNHLibCore"; } @Override @@ -64,4 +84,15 @@ public String getMixinConfig() { public List getMixins(Set loadedCoreMods) { return Mixins.getEarlyMixins(loadedCoreMods); } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + bus.register(this); + return true; + } + + @Subscribe + public void construct(FMLConstructionEvent event) { + EventBusSubTransformer.harvestData(event.getASMHarvestedData()); + } } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/core/transformer/EventBusSubTransformer.java b/src/main/java/com/gtnewhorizon/gtnhlib/core/transformer/EventBusSubTransformer.java new file mode 100644 index 0000000..13815e9 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/core/transformer/EventBusSubTransformer.java @@ -0,0 +1,148 @@ +package com.gtnewhorizon.gtnhlib.core.transformer; + +import static com.gtnewhorizon.gtnhlib.eventbus.EventBusUtil.DEBUG_EVENT_BUS; + +import java.util.Arrays; +import java.util.List; + +import net.minecraft.launchwrapper.IClassTransformer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber; +import com.gtnewhorizon.gtnhlib.eventbus.EventBusUtil; +import com.gtnewhorizon.gtnhlib.eventbus.MethodInfo; + +import cpw.mods.fml.common.Optional; +import cpw.mods.fml.common.discovery.ASMDataTable; +import cpw.mods.fml.common.eventhandler.EventPriority; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.FMLLaunchHandler; +import cpw.mods.fml.relauncher.SideOnly; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSet; + +public class EventBusSubTransformer implements IClassTransformer { + + private static final Logger LOGGER = LogManager.getLogger("GTNHLib|EventBusSubTransformer"); + private static final String OPTIONAL_DESC = Type.getDescriptor(Optional.Method.class); + private static final String SIDEONLY_DESC = Type.getDescriptor(SideOnly.class); + private static final String SUBSCRIBE_DESC = Type.getDescriptor(SubscribeEvent.class); + private static final String CONDITION_DESC = Type.getDescriptor(EventBusSubscriber.Condition.class); + private static final List ANNOTATIONS = Arrays + .asList(OPTIONAL_DESC, SIDEONLY_DESC, SUBSCRIBE_DESC, CONDITION_DESC); + private static final String CURRENT_SIDE = FMLLaunchHandler.side().name(); + private static ObjectSet classesToVisit; + + public static void harvestData(ASMDataTable table) { + classesToVisit = EventBusUtil.getClassesToVisit(); + for (ASMDataTable.ASMData data : table.getAll(EventBusSubscriber.class.getName())) { + classesToVisit.add(data.getClassName()); + } + } + + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) { + if (basicClass == null) return null; + + // It's either too early or this class isn't an @EventBusSubscriber + if (classesToVisit == null || !classesToVisit.contains(transformedName)) { + return basicClass; + } + + final ClassReader cr = new ClassReader(basicClass); + final ClassNode cn = new ClassNode(); + cr.accept(cn, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE); + + // Processing all of this from the ASMDataTable is way too slow + for (MethodNode mn : cn.methods) { + Object2ObjectMap usableAnnotations = getUsableAnnotations(mn.visibleAnnotations); + if (usableAnnotations.isEmpty()) continue; + + if (!matchesSide(usableAnnotations.get(SIDEONLY_DESC))) { + if (DEBUG_EVENT_BUS) { + LOGGER.info("Skipping method {} due to side mismatch", transformedName); + } + continue; + } + + if (usableAnnotations.containsKey(CONDITION_DESC)) { + if (mn.desc.equals("()Z")) { + EventBusUtil.getConditionsToCheck().put(transformedName, mn.name + mn.desc); + } + continue; + } + + AnnotationNode subscribe = usableAnnotations.get(SUBSCRIBE_DESC); + if (subscribe == null) continue; + Object[] subscribeInfo = getSubscribeInfo(subscribe); + MethodInfo methodInfo = new MethodInfo( + transformedName, + mn.name, + mn.desc, + (Boolean) subscribeInfo[0], + (EventPriority) subscribeInfo[1]); + AnnotationNode optional = usableAnnotations.get(OPTIONAL_DESC); + if (optional != null) { + List values = optional.values; + methodInfo.setOptionalMod((String) values.get(1)); + } + + EventBusUtil.getMethodsToSubscribe().computeIfAbsent(transformedName, k -> new ObjectOpenHashSet<>()) + .add(methodInfo); + } + + return basicClass; + } + + private static Object2ObjectMap getUsableAnnotations(List annotations) { + if (annotations == null) return Object2ObjectMaps.emptyMap(); + Object2ObjectMap usable = new Object2ObjectOpenHashMap<>(); + for (AnnotationNode ann : annotations) { + if (ANNOTATIONS.contains(ann.desc)) { + usable.put(ann.desc, ann); + } + } + return usable; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean matchesSide(AnnotationNode side) { + if (side == null) return true; + for (int x = 0; x < side.values.size() - 1; x += 2) { + Object key = side.values.get(x); + Object value = side.values.get(x + 1); + if (!(key instanceof String) || !key.equals("value")) continue; + if (!(value instanceof String[]array)) continue; + if (!array[1].equals(CURRENT_SIDE)) { + return false; + } + } + return true; + } + + private static Object[] getSubscribeInfo(AnnotationNode annotation) { + Object[] info = { false, EventPriority.NORMAL }; + if (annotation.values == null) return info; + for (int i = 0; i < annotation.values.size() - 1; i += 2) { + Object key = annotation.values.get(i); + Object value = annotation.values.get(i + 1); + if (!(key instanceof String)) continue; + if (key.equals("receiveCanceled")) { + info[0] = value; + } else if (key.equals("priority") && value instanceof String[]array) { + info[1] = EventPriority.valueOf(array[1]); + } + } + return info; + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java index 894952f..faffee8 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java @@ -1,18 +1,15 @@ package com.gtnewhorizon.gtnhlib.eventbus; +import static com.gtnewhorizon.gtnhlib.eventbus.EventBusUtil.DEBUG_EVENT_BUS; + import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -26,48 +23,38 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; -import com.google.common.collect.SetMultimap; -import com.gtnewhorizon.gtnhlib.GTNHLib; +import com.gtnewhorizon.gtnhlib.mixins.early.fml.EnumHolderAccessor; import com.gtnewhorizon.gtnhlib.mixins.early.fml.EventBusAccessor; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.Loader; -import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.ModContainer; import cpw.mods.fml.common.discovery.ASMDataTable; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.common.eventhandler.EventBus; import cpw.mods.fml.common.eventhandler.IEventListener; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.relauncher.Side; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import cpw.mods.fml.relauncher.SideOnly; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectSet; -import it.unimi.dsi.fastutil.objects.ObjectSets; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class AutoEventBus { - private static final Boolean DEBUG = Boolean.getBoolean("gtnhlib.debug.eventbus"); private static final Logger LOGGER = LogManager.getLogger("GTNHLib EventBus"); private static final DummyEvent INVALID_EVENT = new DummyEvent(); - private static final ObjectSet registeredClasses = new ObjectOpenHashSet<>(); - private static final Object2ObjectMap classPathToModLookup; - private static final Object2ObjectMap, Event> eventCache = new Object2ObjectOpenHashMap<>(); - private static final Object2ObjectMap> eventClassCache = new Object2ObjectOpenHashMap<>(); - private static final Object2IntMap> eventsRegistered = new Object2IntOpenHashMap<>(); - private static boolean hasRegistered; - private static ASMDataTable asmDataTable; + private static final Object2ObjectMap> subscribers = new Object2ObjectOpenHashMap<>(); + private static final Object2ObjectMap classPathToModLookup = new Object2ObjectOpenHashMap<>(); + private static final Object2ObjectMap eventCache = new Object2ObjectOpenHashMap<>(); + private static final Object2BooleanMap optionalMods = new Object2BooleanOpenHashMap<>(); - static { - classPathToModLookup = getModContainerPackageMap(); - } + private static boolean hasRegistered; private enum EventBusType { @@ -93,46 +80,52 @@ private enum EventBusType { private boolean canRegister(Class clazz) { return canRegister.test(clazz); } + } + + public static void init(ASMDataTable dataTable) { + for (ModContainer container : Loader.instance().getActiveModList()) { + Object modObject = container.getMod(); + if (modObject == null) continue; + Package modPackage = modObject.getClass().getPackage(); + if (modPackage == null) continue; + classPathToModLookup.put(modPackage.getName(), container); + } - private boolean isRegistered(Class clazz) { - return listeners.containsKey(clazz); + for (String className : EventBusUtil.getClassesToVisit()) { + ModContainer mod = getOwningModContainer(className); + subscribers.computeIfAbsent(mod, k -> new ObjectOpenHashSet<>()).add(className); + } + + // Due to the way we are registering events, we need to filter invalid sides out manually. + // It's much faster to do it here than to load an invalid class and throw a couple exceptions. + Side currentSide = FMLCommonHandler.instance().getSide(); + for (Object2ObjectMap.Entry> entry : subscribers.object2ObjectEntrySet()) { + Set sideOnly = dataTable.getAnnotationsFor(entry.getKey()) + .get(SideOnly.class.getName()); + + for (ASMDataTable.ASMData data : sideOnly) { + if (!data.getObjectName().equals(data.getClassName())) { + continue; + } + + Map sideInfo = data.getAnnotationInfo(); + Side side = Side.valueOf(((EnumHolderAccessor) sideInfo.get("value")).getValue()); + if (side != currentSide) { + entry.getValue().remove(data.getClassName()); + } + } } } public static void registerSubscribers() { if (hasRegistered) return; hasRegistered = true; - for (ModContainer mod : Loader.instance().getModList()) { - SetMultimap annotations = asmDataTable.getAnnotationsFor(mod); - if (annotations == null) continue; - Set subscribers = getOwningModAnnotation(annotations, mod, EventBusSubscriber.class); - if (subscribers.isEmpty()) continue; - Set conditionAnnotations = getOwningModAnnotation( - annotations, - mod, - EventBusSubscriber.Condition.class); - Object2ObjectMap conditions = null; - if (!conditionAnnotations.isEmpty()) { - conditions = new Object2ObjectOpenHashMap<>(); - for (ASMDataTable.ASMData data : conditionAnnotations) { - conditions.put(data.getClassName(), data.getObjectName()); - } - } - - Object2ObjectMap> methods = getMethodsForMod(annotations, mod); - for (ASMDataTable.ASMData data : subscribers) { + for (Object2ObjectMap.Entry> entry : subscribers.object2ObjectEntrySet()) { + for (String className : entry.getValue()) { try { - Class clazz = Class.forName(data.getClassName(), false, Loader.instance().getModClassLoader()); - - if (registeredClasses.contains(clazz.getName())) { - if (DEBUG) { - LOGGER.info("Skipping registration for {}, already registered", clazz.getSimpleName()); - } - continue; - } - + Class clazz = Class.forName(className, false, Loader.instance().getModClassLoader()); if (!isValidSide(clazz)) { - if (DEBUG) { + if (DEBUG_EVENT_BUS) { LOGGER.info( "Skipping registration for {}, invalid side {}", clazz.getSimpleName(), @@ -141,91 +134,64 @@ public static void registerSubscribers() { continue; } - if (conditions != null) { - if (!isConditionMet(clazz, conditions.get(data.getClassName()))) { - if (DEBUG) { - LOGGER.info("Skipping registration for {}, condition not met", clazz.getSimpleName()); - } - continue; + String conditionToCheck = EventBusUtil.getConditionsToCheck().get(className); + if (conditionToCheck != null && !isConditionMet(clazz, conditionToCheck)) { + if (DEBUG_EVENT_BUS) { + LOGGER.info("Skipping registration for {}, condition not met", clazz.getSimpleName()); } + continue; } - ObjectSet methodsForClass = getMethodsToSubscribe(clazz, methods.get(data.getClassName())); - register(clazz, mod, methodsForClass); - } catch (ClassNotFoundException | IllegalAccessException e) { - if (DEBUG) LOGGER.error("Failed to load class for annotation", e); - } - } - } - if (DEBUG) { - printFailedEvents(); - printRegisteredEvents(); - } - } - private static void register(Class target, ModContainer classOwner, ObjectSet methods) { - Set> registeredEvents = new HashSet<>(); - for (Method method : methods) { - Class eventType = method.getParameterTypes()[0]; - - if (registerEvent(eventType, target, method, classOwner)) { - if (DEBUG) { - LOGGER.info( - "Registered event handler for {} in class {} with owner {}", - eventType.getName(), - target.getName(), - classOwner.getModId()); + ObjectSet methods = EventBusUtil.getMethodsToSubscribe().get(className); + if (methods == null || methods.isEmpty()) continue; + register(entry.getKey(), clazz, methods); + } catch (IllegalAccessException | ClassNotFoundException e) { + if (DEBUG_EVENT_BUS) LOGGER.error("Failed to load class {}", className, e); } - registeredEvents.add(eventType); } } - - registerOwner(target, classOwner, registeredEvents); } - private static boolean registerEvent(Class eventClass, Class target, Method method, ModContainer owner) { - try { - Event event = getCachedEvent(eventClass); - if (INVALID_EVENT.equals(event)) return false; - - StaticASMEventHandler listener = new StaticASMEventHandler(target, method, owner); - boolean registered = false; - for (EventBusType bus : EventBusType.VALUES) { - if (bus.isRegistered(eventClass) || !bus.canRegister(eventClass)) { - continue; + private static void register(ModContainer classOwner, Class target, ObjectSet methods) { + for (MethodInfo method : methods) { + try { + if (method.getOptionalMod() != null) { + if (!optionalMods.computeIfAbsent(method.getOptionalMod(), Loader::isModLoaded)) { + continue; + } } - event.getListenerList().register(bus.busID, listener.getPriority(), listener); - bus.listeners.computeIfAbsent(target, k -> new ArrayList<>()).add(listener); - registered = true; - if (DEBUG) { - eventsRegistered.merge(eventClass, 1, Integer::sum); - } - } + Event event = getCachedEvent(EventBusUtil.getParameterClassName(method.desc)); + if (INVALID_EVENT.equals(event)) continue; - return registered; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } + StaticASMEventHandler listener = new StaticASMEventHandler(classOwner, method); + for (EventBusType bus : EventBusType.VALUES) { + if (!bus.canRegister(event.getClass())) { + continue; + } + event.getListenerList().register(bus.busID, listener.getPriority(), listener); + bus.listenerOwners.putIfAbsent(target, classOwner); + bus.listeners.computeIfAbsent(target, k -> new ArrayList<>()).add(listener); - private static void registerOwner(Class target, ModContainer owner, Set> registeredEvents) { - for (EventBusType bus : EventBusType.VALUES) { - for (Class event : registeredEvents) { - if (bus.canRegister(event)) { - registeredClasses.add(target.getName()); - bus.listenerOwners.put(target, owner); - break; + if (DEBUG_EVENT_BUS) { + LOGGER.info("Registered event handler for {} on {}", event.getClass().getSimpleName(), bus); + } } + } catch (Exception e) { + if (DEBUG_EVENT_BUS) LOGGER.error("Failed to register event handler for {}", method.desc, e); } } } - private static @Nonnull Event getCachedEvent(Class eventClass) { + private static @Nonnull Event getCachedEvent(String eventClass) { return eventCache.computeIfAbsent(eventClass, e -> { try { - return (Event) ConstructorUtils.invokeConstructor(eventClass); + Class clazz = Class.forName(eventClass, false, Loader.instance().getModClassLoader()); + return (Event) ConstructorUtils.invokeConstructor(clazz); } catch (NoClassDefFoundError | ExceptionInInitializerError | Exception ex) { + // Event was likely for a mod that is not loaded or an invalid side. + // The subscribed method will never be invoked, so we can safely ignore it. + if (DEBUG_EVENT_BUS) LOGGER.error("Failed to create event instance for {}", eventClass, ex); return INVALID_EVENT; } }); @@ -244,100 +210,16 @@ private static boolean isConditionMet(@NotNull Class clazz, @Nullable String field.setAccessible(true); return field.getBoolean(null); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { - if (DEBUG) LOGGER.error("Failed to invoke condition {} for class {}", condition, clazz, e); + if (DEBUG_EVENT_BUS) LOGGER.error("Failed to invoke condition {} for class {}", condition, clazz, e); return false; } } - private static ObjectSet getMethodsToSubscribe(Class clazz, ObjectSet methodDescs) { - if (methodDescs.isEmpty()) return ObjectSets.emptySet(); - ObjectSet methodsToSub = new ObjectOpenHashSet<>(); - for (String methodDesc : methodDescs) { - Class eventClass = getCachedEventClass(methodDesc); - if (INVALID_EVENT.getClass().equals(eventClass)) continue; - String methodName = methodDesc.substring(0, methodDesc.indexOf("(")); - try { - Method method = clazz.getMethod(methodName, eventClass); - if (!Modifier.isStatic(method.getModifiers()) || !method.isAnnotationPresent(SubscribeEvent.class)) { - continue; - } - Class[] parameterTypes = method.getParameterTypes(); - if (parameterTypes.length != 1 || !Event.class.isAssignableFrom(parameterTypes[0])) { - throw new IllegalArgumentException( - "Method " + method - + " has @SubscribeEvent annotation, but requires invalid parameters. " - + "Event handler methods must take a single parameter of type Event."); - } - methodsToSub.add(method); - } catch (NoSuchMethodException ignored) { - if (DEBUG) { - LOGGER.error("Failed to register method {} for class {}", methodName, clazz); - } - } - return methodsToSub; - } - return ObjectSets.emptySet(); - } - - private static Object2ObjectMap> getMethodsForMod( - SetMultimap annotationTable, ModContainer mod) { - Set methodAnnotations = getOwningModAnnotation( - annotationTable, - mod, - SubscribeEvent.class); - if (methodAnnotations.isEmpty()) return Object2ObjectMaps.emptyMap(); - - Object2ObjectMap> methods = new Object2ObjectOpenHashMap<>(); - for (ASMDataTable.ASMData data : methodAnnotations) { - methods.computeIfAbsent(data.getClassName(), k -> new ObjectOpenHashSet<>()).add(data.getObjectName()); - } - return methods; - } - - private static @Nonnull Class getCachedEventClass(String methodDesc) { - String className = methodDesc.substring(methodDesc.indexOf("(L") + 2, methodDesc.indexOf(";)")) - .replaceAll("/", "."); - return eventClassCache.computeIfAbsent(className, a -> { - try { - return Class.forName(className); - } catch (NoClassDefFoundError | ClassNotFoundException e) { - if (DEBUG) { - LOGGER.error("Failed to register method {} for class {}", methodDesc, className, e); - } - return INVALID_EVENT.getClass(); - } - }); - } - - private static Object2ObjectMap getModContainerPackageMap() { - Object2ObjectMap classToModContainer = new Object2ObjectOpenHashMap<>(); - for (ModContainer container : Loader.instance().getActiveModList()) { - Object modObject = container.getMod(); - if (modObject == null) continue; - Package modPackage = modObject.getClass().getPackage(); - if (modPackage == null) continue; - classToModContainer.put(modPackage.getName(), container); - } - return classToModContainer; - } - private static @Nonnull ModContainer getOwningModContainer(String className) { return classPathToModLookup.object2ObjectEntrySet().stream().filter(e -> className.startsWith(e.getKey())) .map(Map.Entry::getValue).findFirst().orElse(Loader.instance().getMinecraftModContainer()); } - private static @Nonnull Set getOwningModAnnotation( - SetMultimap dataTable, ModContainer mod, Class annotationClass) { - Set annotationData = dataTable.get(annotationClass.getName()); - if (annotationData == null || annotationData.isEmpty()) return Collections.emptySet(); - if (dataTable.get(Mod.class.getName()).size() > 1) { - annotationData = annotationData.stream() - .filter(data -> Objects.equals(getOwningModContainer(data.getClassName()), mod)) - .collect(Collectors.toSet()); - } - return annotationData; - } - private static boolean isValidSide(Class subscribedClass) throws IllegalAccessException { Side currentSide = FMLCommonHandler.instance().getSide(); if (currentSide.isClient()) return true; @@ -351,28 +233,6 @@ private static boolean isValidSide(Class subscribedClass) throws IllegalAcces return !StringUtils.containsIgnoreCase(subscribedClass.getName(), "client"); } - public static void setDataTable(ASMDataTable dataTable) { - if (!Loader.instance().activeModContainer().getModId().equals(GTNHLib.MODID)) { - return; - } - asmDataTable = dataTable; - } - - private static void printFailedEvents() { - Side side = FMLCommonHandler.instance().getSide(); - for (Map.Entry, Event> entry : eventCache.object2ObjectEntrySet()) { - if (entry.getValue() == null) { - LOGGER.error("Failed to register event {} for side {}", entry.getKey(), side); - } - } - } - - private static void printRegisteredEvents() { - for (Object2IntMap.Entry> entry : eventsRegistered.object2IntEntrySet()) { - LOGGER.info("Event {} was registered {} times", entry.getKey().getSimpleName(), entry.getIntValue()); - } - } - private static boolean isFMLEvent(Class event) { return event.getName().startsWith("cpw.mods.fml"); } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusUtil.java b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusUtil.java new file mode 100644 index 0000000..0848085 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusUtil.java @@ -0,0 +1,31 @@ +package com.gtnewhorizon.gtnhlib.eventbus; + +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSet; +import lombok.Getter; + +public final class EventBusUtil { + + public static final Boolean DEBUG_EVENT_BUS = Boolean.getBoolean("gtnhlib.debug.eventbus"); + + @Getter + private static final ObjectSet classesToVisit = new ObjectOpenHashSet<>(); + @Getter + private static final Object2ObjectMap> methodsToSubscribe = new Object2ObjectOpenHashMap<>(); + @Getter + private static final Object2ObjectMap conditionsToCheck = new Object2ObjectOpenHashMap<>(); + + static String getParameterClassInternal(String desc) { + return desc.substring(desc.indexOf("(") + 2, desc.indexOf(";")); + } + + static String getParameterClassName(String desc) { + return getParameterClassInternal(desc).replace("/", "."); + } + + static String getSimpleClassName(String desc) { + return desc.substring(desc.lastIndexOf(".") + 1); + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/MethodInfo.java b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/MethodInfo.java new file mode 100644 index 0000000..dc3babe --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/MethodInfo.java @@ -0,0 +1,19 @@ +package com.gtnewhorizon.gtnhlib.eventbus; + +import cpw.mods.fml.common.eventhandler.EventPriority; +import lombok.Data; + +@Data +public final class MethodInfo { + + public final String declaringClass; + public final String name; + public final String desc; + public final boolean receiveCanceled; + public final EventPriority priority; + public String optionalMod; + + public String getKey() { + return declaringClass + " " + name + desc; + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/StaticASMEventHandler.java b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/StaticASMEventHandler.java index f8d8b3a..1c05609 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/StaticASMEventHandler.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/StaticASMEventHandler.java @@ -2,21 +2,18 @@ import static org.objectweb.asm.Opcodes.*; -import java.lang.reflect.Method; -import java.util.HashMap; - import org.apache.logging.log4j.ThreadContext; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; -import com.google.common.collect.Maps; - import cpw.mods.fml.common.ModContainer; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.common.eventhandler.EventPriority; import cpw.mods.fml.common.eventhandler.IEventListener; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; public class StaticASMEventHandler implements IEventListener { @@ -25,19 +22,22 @@ public class StaticASMEventHandler implements IEventListener { private static final String HANDLER_FUNC_DESC = Type .getMethodDescriptor(IEventListener.class.getDeclaredMethods()[0]); private static final ASMClassLoader LOADER = new ASMClassLoader(); - private static final HashMap> cache = Maps.newHashMap(); + private static final Object2ObjectMap> cache = new Object2ObjectOpenHashMap<>(); private static final boolean GETCONTEXT = Boolean.parseBoolean(System.getProperty("fml.LogContext", "false")); private final IEventListener handler; - private final SubscribeEvent subInfo; private final ModContainer owner; private final String readable; + private final boolean receiveCanceled; + @Getter + private final EventPriority priority; - StaticASMEventHandler(Object target, Method method, ModContainer owner) throws Exception { + StaticASMEventHandler(ModContainer owner, MethodInfo method) throws Exception { this.owner = owner; handler = (IEventListener) createWrapper(method).getDeclaredConstructor().newInstance(); - subInfo = method.getAnnotation(SubscribeEvent.class); - readable = "ASM: " + target + " " + method.getName() + Type.getMethodDescriptor(method); + readable = "ASM: " + method.getDeclaringClass() + " " + method.getName() + method.getDesc(); + receiveCanceled = method.receiveCanceled; + priority = method.getPriority(); } @Override @@ -48,29 +48,24 @@ public void invoke(Event event) { ThreadContext.put("mod", ""); } if (handler != null) { - if (!event.isCancelable() || !event.isCanceled() || subInfo.receiveCanceled()) { + if (!event.isCancelable() || !event.isCanceled() || receiveCanceled) { handler.invoke(event); } } if (GETCONTEXT) ThreadContext.remove("mod"); } - public EventPriority getPriority() { - return subInfo.priority(); - } - - public Class createWrapper(Method callback) { - if (cache.containsKey(callback)) { - return cache.get(callback); - } + public Class createWrapper(MethodInfo method) { + Class cached = cache.get(method.getKey()); + if (cached != null) return cached; ClassWriter cw = new ClassWriter(0); MethodVisitor mv; - String name = getUniqueName(callback); + String name = getUniqueName(method); String desc = name.replace('.', '/'); - String instType = Type.getInternalName(callback.getDeclaringClass()); - String eventType = Type.getInternalName(callback.getParameterTypes()[0]); + String instType = method.getDeclaringClass().replace('.', '/'); + String eventType = EventBusUtil.getParameterClassInternal(method.getDesc()); cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, desc, null, "java/lang/Object", new String[] { HANDLER_DESC }); @@ -90,25 +85,27 @@ public Class createWrapper(Method callback) { mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 1); mv.visitTypeInsn(CHECKCAST, eventType); - mv.visitMethodInsn(INVOKESTATIC, instType, callback.getName(), Type.getMethodDescriptor(callback), false); + mv.visitMethodInsn(INVOKESTATIC, instType, method.name, method.desc, false); mv.visitInsn(RETURN); mv.visitMaxs(2, 2); mv.visitEnd(); } cw.visitEnd(); Class ret = LOADER.define(name, cw.toByteArray()); - cache.put(callback, ret); + cache.put(method.getKey(), ret); return ret; } - private String getUniqueName(Method callback) { + private String getUniqueName(MethodInfo method) { + String param = EventBusUtil.getParameterClassName(method.getDesc()); + String declaring = method.getDeclaringClass(); return String.format( "%s_%d_%s_%s_%s", getClass().getName(), IDs++, - callback.getDeclaringClass().getSimpleName(), - callback.getName(), - callback.getParameterTypes()[0].getSimpleName()); + EventBusUtil.getSimpleClassName(declaring), + method.getName(), + EventBusUtil.getSimpleClassName(param)); } private static class ASMClassLoader extends ClassLoader { diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java index 9d9949a..8f9aa11 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java @@ -19,7 +19,7 @@ public enum Mixins { WAVEFRONT_VBO(new Builder("WavefrontObject").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) .setPhase(Phase.EARLY).setApplyIf(() -> true).addMixinClasses("MixinWavefrontObject")), EVENT_BUS_ACCESSOR(new Builder("EventBusAccessor").addTargetedMod(TargetedMod.VANILLA).setSide(Side.BOTH) - .setPhase(Phase.EARLY).addMixinClasses("fml.EventBusAccessor")),; + .setPhase(Phase.EARLY).addMixinClasses("fml.EventBusAccessor", "fml.EnumHolderAccessor")),; private final List mixinClasses; private final Supplier applyIf; diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EnumHolderAccessor.java b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EnumHolderAccessor.java new file mode 100644 index 0000000..9b6dd9b --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EnumHolderAccessor.java @@ -0,0 +1,13 @@ +package com.gtnewhorizon.gtnhlib.mixins.early.fml; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import cpw.mods.fml.common.discovery.asm.ModAnnotation; + +@Mixin(value = ModAnnotation.EnumHolder.class, remap = false) +public interface EnumHolderAccessor { + + @Accessor + String getValue(); +}