Skip to content

Commit

Permalink
EventBusSubscriber improvements (#82)
Browse files Browse the repository at this point in the history
* make @EventBusSubscriber able to work for earlier init phases & safe condition method invocation

* add some docs

* no need to sort this anymore
  • Loading branch information
Lyfts authored Oct 12, 2024
1 parent 2da1d6f commit 28786c5
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 146 deletions.
10 changes: 8 additions & 2 deletions src/main/java/com/gtnewhorizon/gtnhlib/CommonProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
import com.gtnewhorizon.gtnhlib.eventbus.AutoEventBus;
import com.gtnewhorizon.gtnhlib.eventbus.Phase;
import com.gtnewhorizon.gtnhlib.network.NetworkHandler;
import com.gtnewhorizon.gtnhlib.network.PacketMessageAboveHotbar;

import cpw.mods.fml.common.event.FMLConstructionEvent;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
Expand All @@ -20,13 +22,17 @@

public class CommonProxy {

public void construct(FMLConstructionEvent event) {
AutoEventBus.executePhase(Phase.CONSTRUCT);
}

public void preInit(FMLPreInitializationEvent event) {
AutoEventBus.init(event.getAsmData());
AutoEventBus.executePhase(Phase.PRE);
GTNHLib.info("GTNHLib version " + Tags.VERSION + " loaded.");
}

public void init(FMLInitializationEvent event) {
AutoEventBus.registerSubscribers();
AutoEventBus.executePhase(Phase.INIT);
NetworkHandler.init();
ConfigurationManager.onInit();
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/gtnewhorizon/gtnhlib/GTNHLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public class GTNHLib {
@SidedProxy(clientSide = GTNHLib.GROUPNAME + ".ClientProxy", serverSide = GTNHLib.GROUPNAME + ".CommonProxy")
public static CommonProxy proxy;

@Mod.EventHandler
public void construct(FMLConstructionEvent event) {
proxy.construct(event);
}

@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
proxy.preInit(event);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/gtnewhorizon/gtnhlib/core/GTNHLibCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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.eventbus.EventBusUtil;
import com.gtnewhorizon.gtnhlib.mixins.Mixins;
import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader;

Expand Down Expand Up @@ -93,6 +93,6 @@ public boolean registerBus(EventBus bus, LoadController controller) {

@Subscribe
public void construct(FMLConstructionEvent event) {
EventBusSubTransformer.harvestData(event.getASMHarvestedData());
EventBusUtil.harvestData(event.getASMHarvestedData());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
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;
Expand All @@ -42,21 +41,14 @@ public class EventBusSubTransformer implements IClassTransformer {
private static final List<String> ANNOTATIONS = Arrays
.asList(OPTIONAL_DESC, SIDEONLY_DESC, SUBSCRIBE_DESC, CONDITION_DESC);
private static final String CURRENT_SIDE = FMLLaunchHandler.side().name();
private static ObjectSet<String> classesToVisit;

public static void harvestData(ASMDataTable table) {
classesToVisit = EventBusUtil.getClassesToVisit();
for (ASMDataTable.ASMData data : table.getAll(EventBusSubscriber.class.getName())) {
classesToVisit.add(data.getClassName());
}
}
private static final ObjectSet<String> classesToVisit = EventBusUtil.getClassesToVisit();

@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)) {
if (classesToVisit.isEmpty() || !classesToVisit.contains(transformedName)) {
return basicClass;
}

Expand Down
138 changes: 37 additions & 101 deletions src/main/java/com/gtnewhorizon/gtnhlib/eventbus/AutoEventBus.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

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.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;

import javax.annotation.Nonnull;
Expand All @@ -17,30 +19,24 @@
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;
Expand All @@ -50,12 +46,10 @@ public class AutoEventBus {

private static final Logger LOGGER = LogManager.getLogger("GTNHLib EventBus");
private static final DummyEvent INVALID_EVENT = new DummyEvent();
private static final Object2ObjectMap<ModContainer, ObjectSet<String>> subscribers = new Object2ObjectOpenHashMap<>();
private static final Object2ObjectMap<String, ModContainer> classPathToModLookup = new Object2ObjectOpenHashMap<>();
private static final Object2ObjectMap<String, Event> eventCache = new Object2ObjectOpenHashMap<>();
private static final Object2BooleanMap<String> optionalMods = new Object2BooleanOpenHashMap<>();

private static boolean hasRegistered;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodType CONDITION_TYPE = MethodType.methodType(boolean.class);

private enum EventBusType {

Expand Down Expand Up @@ -83,58 +77,16 @@ private boolean canRegister(Class<?> 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<ModContainer, ObjectSet<String>> entry : subscribers.object2ObjectEntrySet()) {
Set<ASMDataTable.ASMData> sideOnly = dataTable.getAnnotationsFor(entry.getKey())
.get(SideOnly.class.getName());

for (ASMDataTable.ASMData data : sideOnly) {
if (!data.getObjectName().equals(data.getClassName())) {
continue;
}
public static void executePhase(Phase phase) {
if (phase.hasExecuted) return;
phase.hasExecuted = true;
Object2ObjectMap<ModContainer, ObjectSet<String>> subscribers = phase.getModClassesForPhase();
if (subscribers.isEmpty()) return;

Map<String, Object> 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<ModContainer, ObjectSet<String>> 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) {
Expand All @@ -146,22 +98,23 @@ public static void registerSubscribers() {
ObjectSet<MethodInfo> methods = EventBusUtil.getMethodsToSubscribe().get(className);
if (methods == null || methods.isEmpty()) continue;
register(entry.getKey(), clazz, methods);
} catch (IllegalAccessException | ClassNotFoundException e) {
} catch (ClassNotFoundException e) {
if (DEBUG_EVENT_BUS) LOGGER.error("Failed to load class {}", className, e);
}
}
}

ObjectList<String> 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));
if (phase == Phase.INIT) {
ObjectList<String> 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" + i + "invalid methods. " + invalidMethods.get(i));
}
throw new IllegalArgumentException(
"Encountered" + invalidMethods.size() + "invalid methods. " + invalidMethods.get(i));
}
}

Expand All @@ -177,7 +130,7 @@ private static void register(ModContainer classOwner, Class<?> target, ObjectSet
Event event = getCachedEvent(EventBusUtil.getParameterClassName(method.desc));
if (INVALID_EVENT.equals(event)) continue;

StaticASMEventHandler listener = new StaticASMEventHandler(classOwner, method);
StaticASMEventHandler listener = new StaticASMEventHandler(method);
for (EventBusType bus : EventBusType.VALUES) {
if (!bus.canRegister(event.getClass())) {
continue;
Expand Down Expand Up @@ -213,39 +166,22 @@ private static void register(ModContainer classOwner, Class<?> target, ObjectSet
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);
MethodHandle handle = MethodHandles.publicLookup()
.findStatic(clazz, condition.substring(0, condition.indexOf("(")), CONDITION_TYPE);
CallSite call = LambdaMetafactory.metafactory(
LOOKUP,
"getAsBoolean",
MethodType.methodType(BooleanSupplier.class),
CONDITION_TYPE,
handle,
CONDITION_TYPE);
return ((BooleanSupplier) call.getTarget().invokeWithArguments()).getAsBoolean();
} catch (Throwable e) {
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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

/**
* 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.<br>
* listen for events. Registration will happen during the specified {@link EventBusSubscriber#phase()} or during
* {@link Phase#INIT} if not specified. <br>
* All methods annotated with {@link cpw.mods.fml.common.eventhandler.SubscribeEvent} are expected to be static.
*/
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -23,13 +24,17 @@
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. <br>
* 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.
* Which equivalent {@link cpw.mods.fml.common.LoaderState} this subscriber should be registered during.
*/
Phase phase() default Phase.INIT;

/**
* Can be applied to a boolean method in the annotated class that provides a condition for registering the
* subscriber. It is expected that the method is static, returns a boolean, and takes no parameters. <br>
* There is expected to be at most one condition for a class.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
@Target(ElementType.METHOD)
@interface Condition {}

}
Loading

0 comments on commit 28786c5

Please sign in to comment.