From 2da1d6ff81542e5207de48fda961a6a565f6610e Mon Sep 17 00:00:00 2001 From: Lyft <127234178+Lyfts@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:23:20 +0200 Subject: [PATCH] Backport @EventBusSubscriber annotation from 1.12.2 (#80) * backport @EventBusSubscriber annotation from 1.12.2 * Cache events, safe method lookup, replace eventbus reflection with accessor * ily spotless * actual completely safe event registration this time with less reflection * throw exception when encountering an unexpected non-static method * oops that was too wide a net * more logging for the transformer --- .../com/gtnewhorizon/gtnhlib/ClientProxy.java | 8 +- .../com/gtnewhorizon/gtnhlib/CommonProxy.java | 3 + .../gtnhlib/core/GTNHLibCore.java | 39 ++- .../transformer/EventBusSubTransformer.java | 182 ++++++++++++ .../gtnhlib/eventbus/AutoEventBus.java | 267 ++++++++++++++++++ .../gtnhlib/eventbus/EventBusSubscriber.java | 35 +++ .../gtnhlib/eventbus/EventBusUtil.java | 35 +++ .../gtnhlib/eventbus/MethodInfo.java | 19 ++ .../eventbus/StaticASMEventHandler.java | 125 ++++++++ .../gtnewhorizon/gtnhlib/mixins/Mixins.java | 4 +- .../mixins/early/fml/EnumHolderAccessor.java | 13 + .../mixins/early/fml/EventBusAccessor.java | 25 ++ .../gtnhlib/util/AnimatedTooltipHandler.java | 30 +- 13 files changed, 763 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/core/transformer/EventBusSubTransformer.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusSubscriber.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/eventbus/StaticASMEventHandler.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EnumHolderAccessor.java create mode 100644 src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EventBusAccessor.java diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/ClientProxy.java b/src/main/java/com/gtnewhorizon/gtnhlib/ClientProxy.java index 02a190a..000f0f0 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/ClientProxy.java @@ -6,17 +6,18 @@ import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.IChatComponent; -import net.minecraftforge.common.MinecraftForge; import com.gtnewhorizon.gtnhlib.client.model.ModelLoader; +import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber; import com.gtnewhorizon.gtnhlib.util.AboveHotbarHUD; -import com.gtnewhorizon.gtnhlib.util.AnimatedTooltipHandler; import cpw.mods.fml.common.event.*; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent; +import cpw.mods.fml.relauncher.Side; @SuppressWarnings("unused") +@EventBusSubscriber(side = Side.CLIENT) public class ClientProxy extends CommonProxy { private static boolean modelsBaked = false; @@ -36,7 +37,6 @@ public void init(FMLInitializationEvent event) { @Override public void postInit(FMLPostInitializationEvent event) { super.postInit(event); - MinecraftForge.EVENT_BUS.register(new AnimatedTooltipHandler()); if (shouldLoadModels()) { Minecraft.getMinecraft().refreshResources(); @@ -131,7 +131,7 @@ public void printMessageAboveHotbar(String message, int displayDuration, boolean } @SubscribeEvent - public void onTick(TickEvent.ClientTickEvent event) { + public static void onTick(TickEvent.ClientTickEvent event) { if (!modelsBaked) { ModelLoader.bakeModels(); modelsBaked = true; diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java b/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java index c88a800..a288fed 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java @@ -5,6 +5,7 @@ import net.minecraftforge.common.util.FakePlayer; import com.gtnewhorizon.gtnhlib.config.ConfigurationManager; +import com.gtnewhorizon.gtnhlib.eventbus.AutoEventBus; import com.gtnewhorizon.gtnhlib.network.NetworkHandler; import com.gtnewhorizon.gtnhlib.network.PacketMessageAboveHotbar; @@ -20,10 +21,12 @@ public class CommonProxy { public void preInit(FMLPreInitializationEvent event) { + AutoEventBus.init(event.getAsmData()); GTNHLib.info("GTNHLib version " + Tags.VERSION + " loaded."); } public void init(FMLInitializationEvent event) { + AutoEventBus.registerSubscribers(); NetworkHandler.init(); ConfigurationManager.onInit(); } 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..86e7b6f --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/core/transformer/EventBusSubTransformer.java @@ -0,0 +1,182 @@ +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.Opcodes; +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; + } + + AnnotationNode subscribe = usableAnnotations.get(SUBSCRIBE_DESC); + boolean condition = usableAnnotations.containsKey(CONDITION_DESC); + if ((mn.access & Opcodes.ACC_STATIC) == 0) { + if (!condition && subscribe != null) { + EventBusUtil.getInvalidMethods().add( + "Encountered unexpected non-static method: " + transformedName + " " + mn.name + mn.desc); + } + continue; + } + + if (condition) { + if (mn.desc.equals("()Z")) { + EventBusUtil.getConditionsToCheck().put(transformedName, mn.name + mn.desc); + } else { + EventBusUtil.getInvalidMethods().add( + "Invalid condition method: " + transformedName + + " " + + mn.name + + mn.desc + + ". Condition method must have no parameters and return a boolean."); + } + continue; + } + + if (subscribe == null) { + if (DEBUG_EVENT_BUS) { + LOGGER.info( + "Skipping method {} with annotations {}. No @SubscribeEvent found.", + transformedName, + usableAnnotations.keySet()); + } + 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)); + if (DEBUG_EVENT_BUS) { + LOGGER.info( + "Found optional mod {} for method {}", + methodInfo.getOptionalMod(), + methodInfo.getKey()); + } + } + + EventBusUtil.getMethodsToSubscribe().computeIfAbsent(transformedName, k -> new ObjectOpenHashSet<>()) + .add(methodInfo); + if (DEBUG_EVENT_BUS) { + LOGGER.info("Found subscribed method {}", methodInfo.getKey()); + } + } + + 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 new file mode 100644 index 0000000..1b1e429 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java @@ -0,0 +1,267 @@ +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.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.terraingen.OreGenEvent; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.ConstructorUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +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.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.relauncher.Side; +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.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSet; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AutoEventBus { + + private static final Logger LOGGER = LogManager.getLogger("GTNHLib EventBus"); + private static final DummyEvent INVALID_EVENT = new DummyEvent(); + 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<>(); + + private static boolean hasRegistered; + + private enum EventBusType { + + FORGE(MinecraftForge.EVENT_BUS, AutoEventBus::isForgeEvent), + OREGEN(MinecraftForge.ORE_GEN_BUS, AutoEventBus::isOreGenEvent), + TERRAIN_GEN(MinecraftForge.TERRAIN_GEN_BUS, AutoEventBus::isTerrainEvent), + FML(FMLCommonHandler.instance().bus(), AutoEventBus::isFMLEvent); + + private static final EventBusType[] VALUES = values(); + private final ConcurrentHashMap> listeners; + private final Map listenerOwners; + private final Predicate> canRegister; + private final int busID; + + EventBusType(EventBus instance, Predicate> canRegister) { + this.canRegister = canRegister; + EventBusAccessor accessor = (EventBusAccessor) instance; + this.listeners = accessor.getListeners(); + this.listenerOwners = accessor.getListenerOwners(); + this.busID = accessor.getBusID(); + } + + 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); + } + + 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 (Object2ObjectMap.Entry> entry : subscribers.object2ObjectEntrySet()) { + for (String className : entry.getValue()) { + try { + Class clazz = Class.forName(className, false, Loader.instance().getModClassLoader()); + if (!isValidSide(clazz)) { + if (DEBUG_EVENT_BUS) { + LOGGER.info( + "Skipping registration for {}, invalid side {}", + clazz.getSimpleName(), + FMLCommonHandler.instance().getSide()); + } + 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 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); + } + } + } + + ObjectList invalidMethods = EventBusUtil.getInvalidMethods(); + if (invalidMethods.size() == 1) { + throw new IllegalArgumentException(invalidMethods.get(0)); + } else if (invalidMethods.size() > 1) { + int i; + for (i = 0; i < invalidMethods.size() - 1; i++) { + LOGGER.error(invalidMethods.get(i)); + } + throw new IllegalArgumentException( + "Encountered" + invalidMethods.size() + "invalid methods. " + invalidMethods.get(i)); + } + } + + 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 event = getCachedEvent(EventBusUtil.getParameterClassName(method.desc)); + if (INVALID_EVENT.equals(event)) continue; + + 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); + + 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(String eventClass) { + return eventCache.computeIfAbsent(eventClass, e -> { + try { + 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; + } + }); + } + + private static boolean isConditionMet(@NotNull Class clazz, @Nullable String condition) { + if (condition == null) return true; + try { + if (condition.contains("()Z")) { + Method method = clazz.getDeclaredMethod(condition.substring(0, condition.indexOf("("))); + method.setAccessible(true); + return (boolean) method.invoke(null); + } + + Field field = clazz.getDeclaredField(condition); + field.setAccessible(true); + return field.getBoolean(null); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { + if (DEBUG_EVENT_BUS) LOGGER.error("Failed to invoke condition {} for class {}", condition, clazz, e); + return false; + } + } + + 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 boolean isValidSide(Class subscribedClass) throws IllegalAccessException { + Side currentSide = FMLCommonHandler.instance().getSide(); + if (currentSide.isClient()) return true; + + EventBusSubscriber subscriber = subscribedClass.getAnnotation(EventBusSubscriber.class); + Side[] sides = subscriber.side(); + if (sides.length == 1) { + return currentSide == sides[0]; + } + + return !StringUtils.containsIgnoreCase(subscribedClass.getName(), "client"); + } + + private static boolean isFMLEvent(Class event) { + return event.getName().startsWith("cpw.mods.fml"); + } + + private static boolean isTerrainEvent(Class event) { + return event.getName().startsWith("net.minecraftforge.event.terraingen") && !isOreGenEvent(event); + } + + private static boolean isOreGenEvent(Class event) { + return OreGenEvent.class.isAssignableFrom(event); + } + + private static boolean isForgeEvent(Class event) { + return !isFMLEvent(event) && !isTerrainEvent(event) && !isOreGenEvent(event); + } + + private static class DummyEvent extends Event { + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusSubscriber.java b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusSubscriber.java new file mode 100644 index 0000000..6383ee1 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusSubscriber.java @@ -0,0 +1,35 @@ +package com.gtnewhorizon.gtnhlib.eventbus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import cpw.mods.fml.relauncher.Side; + +/** + * Annotation to mark a class as an EventBus subscriber. Classes annotated with this will automatically be registered to + * listen for events. Registration will happen during the init phase.
+ * All methods annotated with {@link cpw.mods.fml.common.eventhandler.SubscribeEvent} are expected to be static. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface EventBusSubscriber { + + /** + * The {@link cpw.mods.fml.relauncher.Side} that this subscriber should be registered on. Will default to both sides + * if not specified. + */ + Side[] side() default { Side.CLIENT, Side.SERVER }; + + /** + * Can be applied to a boolean field/method in the annotated class that provides a condition for registering the + * subscriber. It is expected that the field/method is static, returns a boolean, and takes no parameters.
+ * There is expected to be at most one condition for a class. Config values can be used as the return value since + * registration happens during init. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD }) + @interface Condition {} + +} 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..9a952d8 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/EventBusUtil.java @@ -0,0 +1,35 @@ +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.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +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<>(); + @Getter + private static final ObjectList invalidMethods = new ObjectArrayList<>(); + + 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 new file mode 100644 index 0000000..1c05609 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/eventbus/StaticASMEventHandler.java @@ -0,0 +1,125 @@ +package com.gtnewhorizon.gtnhlib.eventbus; + +import static org.objectweb.asm.Opcodes.*; + +import org.apache.logging.log4j.ThreadContext; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +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 it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; + +public class StaticASMEventHandler implements IEventListener { + + private static int IDs = 0; + private static final String HANDLER_DESC = Type.getInternalName(IEventListener.class); + private static final String HANDLER_FUNC_DESC = Type + .getMethodDescriptor(IEventListener.class.getDeclaredMethods()[0]); + private static final ASMClassLoader LOADER = new ASMClassLoader(); + 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 ModContainer owner; + private final String readable; + private final boolean receiveCanceled; + @Getter + private final EventPriority priority; + + StaticASMEventHandler(ModContainer owner, MethodInfo method) throws Exception { + this.owner = owner; + handler = (IEventListener) createWrapper(method).getDeclaredConstructor().newInstance(); + readable = "ASM: " + method.getDeclaringClass() + " " + method.getName() + method.getDesc(); + receiveCanceled = method.receiveCanceled; + priority = method.getPriority(); + } + + @Override + public void invoke(Event event) { + if (owner != null && GETCONTEXT) { + ThreadContext.put("mod", owner.getName()); + } else if (GETCONTEXT) { + ThreadContext.put("mod", ""); + } + if (handler != null) { + if (!event.isCancelable() || !event.isCanceled() || receiveCanceled) { + handler.invoke(event); + } + } + if (GETCONTEXT) ThreadContext.remove("mod"); + } + + 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(method); + String desc = name.replace('.', '/'); + 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 }); + + cw.visitSource(".dynamic", null); + { + mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(2, 2); + mv.visitEnd(); + } + { + mv = cw.visitMethod(ACC_PUBLIC, "invoke", HANDLER_FUNC_DESC, null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitTypeInsn(CHECKCAST, eventType); + 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(method.getKey(), ret); + return ret; + } + + 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++, + EventBusUtil.getSimpleClassName(declaring), + method.getName(), + EventBusUtil.getSimpleClassName(param)); + } + + private static class ASMClassLoader extends ClassLoader { + + private ASMClassLoader() { + super(ASMClassLoader.class.getClassLoader()); + } + + public Class define(String name, byte[] data) { + return defineClass(name, data, 0, data.length); + } + } + + public String toString() { + return readable; + } +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java index 29e5c17..8f9aa11 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/Mixins.java @@ -17,7 +17,9 @@ public enum Mixins { TESSELLATOR(new Builder("Sodium").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT).setPhase(Phase.EARLY) .setApplyIf(() -> true).addMixinClasses("MixinTessellator")), WAVEFRONT_VBO(new Builder("WavefrontObject").addTargetedMod(TargetedMod.VANILLA).setSide(Side.CLIENT) - .setPhase(Phase.EARLY).setApplyIf(() -> true).addMixinClasses("MixinWavefrontObject")),; + .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", "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(); +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EventBusAccessor.java b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EventBusAccessor.java new file mode 100644 index 0000000..8246f3e --- /dev/null +++ b/src/main/java/com/gtnewhorizon/gtnhlib/mixins/early/fml/EventBusAccessor.java @@ -0,0 +1,25 @@ +package com.gtnewhorizon.gtnhlib.mixins.early.fml; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import cpw.mods.fml.common.ModContainer; +import cpw.mods.fml.common.eventhandler.EventBus; +import cpw.mods.fml.common.eventhandler.IEventListener; + +@Mixin(value = EventBus.class, remap = false) +public interface EventBusAccessor { + + @Accessor + Map getListenerOwners(); + + @Accessor + ConcurrentHashMap> getListeners(); + + @Accessor + int getBusID(); +} diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/util/AnimatedTooltipHandler.java b/src/main/java/com/gtnewhorizon/gtnhlib/util/AnimatedTooltipHandler.java index 2d4d323..df0a4da 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/util/AnimatedTooltipHandler.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/util/AnimatedTooltipHandler.java @@ -12,11 +12,14 @@ import net.minecraftforge.event.entity.player.ItemTooltipEvent; import net.minecraftforge.oredict.OreDictionary; +import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber; import com.gtnewhorizon.gtnhlib.util.map.ItemStackMap; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.registry.GameRegistry; +import cpw.mods.fml.relauncher.Side; +@EventBusSubscriber(side = Side.CLIENT) public class AnimatedTooltipHandler { private static final Map> tooltipMap = new ItemStackMap<>(false); @@ -29,7 +32,7 @@ public class AnimatedTooltipHandler { /** * Helper method to concatenate multiple texts - * + * * @author glowredman */ @SafeVarargs @@ -45,7 +48,7 @@ public static Supplier chain(Supplier... parts) { /** * Helper method to create a static text - * + * * @author glowredman */ public static Supplier text(String text) { @@ -54,7 +57,7 @@ public static Supplier text(String text) { /** * Helper method to create a formatted and static text - * + * * @author glowredman */ public static Supplier text(String format, Object... args) { @@ -63,7 +66,7 @@ public static Supplier text(String format, Object... args) { /** * Helper method to create a translated and static text - * + * * @author glowredman */ public static Supplier translatedText(String translationKey) { @@ -72,7 +75,7 @@ public static Supplier translatedText(String translationKey) { /** * Helper method to create a translated, formatted and static text - * + * * @author glowredman */ public static Supplier translatedText(String translationKey, Object... args) { @@ -84,7 +87,7 @@ public static Supplier translatedText(String translationKey, Object... a *

* Taken and adapted from Avaritia - * + * * @param text The text to be animated * @param posstep How many steps {@code formattingArray} is shifted each {@code delay} * @param delay How many milliseconds are between each shift of {@code formattingArray} @@ -117,7 +120,7 @@ public static Supplier animatedText(String text, int posstep, int delay, *

* Taken and adapted from Avaritia - * + * * @param format The text to be formatted and animated * @param args The formatting arguments * @param posstep How many steps {@code formattingArray} is shifted each {@code delay} @@ -137,7 +140,7 @@ public static Supplier animatedText(String format, Object[] args, int po *

* Taken and adapted from Avaritia - * + * * @param translationKey The key used to look up the translation * @param posstep How many steps {@code formattingArray} is shifted each {@code delay} * @param delay How many milliseconds are between each shift of {@code formattingArray} @@ -156,7 +159,7 @@ public static Supplier translatedAnimatedText(String translationKey, int *

* Taken and adapted from Avaritia - * + * * @param translationKey The key used to look up the translation * @param args The formatting arguments * @param posstep How many steps {@code formattingArray} is shifted each {@code delay} @@ -179,7 +182,7 @@ public static Supplier translatedAnimatedText(String translationKey, Obj * Add {@code tooltip} to all items with {@code oredictName}.
* Note: The items must be registered to the {@link OreDictionary} when this method is called.
* Note: Items with equal registry name and meta but different NBT are considered equal. - * + * * @author glowredman */ public static void addOredictTooltip(String oredictName, Supplier tooltip) { @@ -193,7 +196,7 @@ public static void addOredictTooltip(String oredictName, Supplier toolti * Note: The item must be registered to the {@link GameRegistry} when this method is called.
* Note: Items with equal registry name and meta but different NBT are considered equal.
* Note: Using {@link OreDictionary#WILDCARD_VALUE} as {@code meta} is allowed. - * + * * @author glowredman */ public static void addItemTooltip(String modID, String registryName, int meta, Supplier tooltip) { @@ -206,7 +209,7 @@ public static void addItemTooltip(String modID, String registryName, int meta, S * Add {@code tooltip} to {@code item}.
* Note: Items with equal registry name and meta but different NBT are considered equal.
* Note: Using {@link OreDictionary#WILDCARD_VALUE} as meta is allowed. - * + * * @author glowredman */ public static void addItemTooltip(ItemStack item, Supplier tooltip) { @@ -215,7 +218,8 @@ public static void addItemTooltip(ItemStack item, Supplier tooltip) { } @SubscribeEvent - public void renderTooltip(ItemTooltipEvent event) { + @SuppressWarnings("unused") + public static void renderTooltip(ItemTooltipEvent event) { Supplier tooltip = tooltipMap.get(event.itemStack); if (tooltip == null) return;