Copy eventhandler from Forge into FML. It is going to replace a lot of the runtime event systems.

This commit is contained in:
Christian 2013-12-16 11:48:09 -05:00
parent a0fcbdccb5
commit 26056619a9
9 changed files with 859 additions and 0 deletions

View File

@ -0,0 +1,192 @@
package cpw.mods.fml.common.asm.transformers;
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.IFNULL;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Type.VOID_TYPE;
import static org.objectweb.asm.Type.getMethodDescriptor;
import java.util.List;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import cpw.mods.fml.common.eventhandler.Event;
public class EventSubscriptionTransformer implements IClassTransformer
{
public EventSubscriptionTransformer()
{
}
@Override
public byte[] transform(String name, String transformedName, byte[] bytes)
{
if (bytes == null || name.equals("cpw.mods.fml.common.eventhandler.Event") || name.startsWith("net.minecraft.") || name.indexOf('.') == -1)
{
return bytes;
}
ClassReader cr = new ClassReader(bytes);
ClassNode classNode = new ClassNode();
cr.accept(classNode, 0);
try
{
if (buildEvents(classNode))
{
ClassWriter cw = new ClassWriter(COMPUTE_MAXS | COMPUTE_FRAMES);
classNode.accept(cw);
return cw.toByteArray();
}
return bytes;
}
catch (ClassNotFoundException ex)
{
// Discard silently- it's just noise
}
catch (Exception e)
{
e.printStackTrace();
}
return bytes;
}
private boolean buildEvents(ClassNode classNode) throws Exception
{
Class<?> parent = this.getClass().getClassLoader().loadClass(classNode.superName.replace('/', '.'));
if (!Event.class.isAssignableFrom(parent))
{
return false;
}
boolean hasSetup = false;
boolean hasGetListenerList = false;
boolean hasDefaultCtr = false;
Class<?> listenerListClazz = Class.forName("cpw.mods.fml.common.eventhandler.ListenerList", false, getClass().getClassLoader());
Type tList = Type.getType(listenerListClazz);
for (MethodNode method : (List<MethodNode>)classNode.methods)
{
if (method.name.equals("setup") &&
method.desc.equals(Type.getMethodDescriptor(VOID_TYPE)) &&
(method.access & ACC_PROTECTED) == ACC_PROTECTED)
{
hasSetup = true;
}
if (method.name.equals("getListenerList") &&
method.desc.equals(Type.getMethodDescriptor(tList)) &&
(method.access & ACC_PUBLIC) == ACC_PUBLIC)
{
hasGetListenerList = true;
}
if (method.name.equals("<init>") &&
method.desc.equals(Type.getMethodDescriptor(VOID_TYPE)))
{
hasDefaultCtr = true;
}
}
if (hasSetup)
{
if (!hasGetListenerList)
{
throw new RuntimeException("Event class defines setup() but does not define getListenerList! " + classNode.name);
}
else
{
return false;
}
}
Type tSuper = Type.getType(classNode.superName);
//Add private static ListenerList LISTENER_LIST
classNode.fields.add(new FieldNode(ACC_PRIVATE | ACC_STATIC, "LISTENER_LIST", tList.getDescriptor(), null, null));
/*Add:
* public <init>()
* {
* super();
* }
*/
MethodNode method = new MethodNode(ASM4, ACC_PUBLIC, "<init>", getMethodDescriptor(VOID_TYPE), null, null);
method.instructions.add(new VarInsnNode(ALOAD, 0));
method.instructions.add(new MethodInsnNode(INVOKESPECIAL, tSuper.getInternalName(), "<init>", getMethodDescriptor(VOID_TYPE)));
method.instructions.add(new InsnNode(RETURN));
if (!hasDefaultCtr)
{
classNode.methods.add(method);
}
/*Add:
* protected void setup()
* {
* super.setup();
* if (LISTENER_LIST != NULL)
* {
* return;
* }
* LISTENER_LIST = new ListenerList(super.getListenerList());
* }
*/
method = new MethodNode(ASM4, ACC_PROTECTED, "setup", getMethodDescriptor(VOID_TYPE), null, null);
method.instructions.add(new VarInsnNode(ALOAD, 0));
method.instructions.add(new MethodInsnNode(INVOKESPECIAL, tSuper.getInternalName(), "setup", getMethodDescriptor(VOID_TYPE)));
method.instructions.add(new FieldInsnNode(GETSTATIC, classNode.name, "LISTENER_LIST", tList.getDescriptor()));
LabelNode initLisitener = new LabelNode();
method.instructions.add(new JumpInsnNode(IFNULL, initLisitener));
method.instructions.add(new InsnNode(RETURN));
method.instructions.add(initLisitener);
method.instructions.add(new FrameNode(F_SAME, 0, null, 0, null));
method.instructions.add(new TypeInsnNode(NEW, tList.getInternalName()));
method.instructions.add(new InsnNode(DUP));
method.instructions.add(new VarInsnNode(ALOAD, 0));
method.instructions.add(new MethodInsnNode(INVOKESPECIAL, tSuper.getInternalName(), "getListenerList", getMethodDescriptor(tList)));
method.instructions.add(new MethodInsnNode(INVOKESPECIAL, tList.getInternalName(), "<init>", getMethodDescriptor(VOID_TYPE, tList)));
method.instructions.add(new FieldInsnNode(PUTSTATIC, classNode.name, "LISTENER_LIST", tList.getDescriptor()));
method.instructions.add(new InsnNode(RETURN));
classNode.methods.add(method);
/*Add:
* public ListenerList getListenerList()
* {
* return this.LISTENER_LIST;
* }
*/
method = new MethodNode(ASM4, ACC_PUBLIC, "getListenerList", getMethodDescriptor(tList), null, null);
method.instructions.add(new FieldInsnNode(GETSTATIC, classNode.name, "LISTENER_LIST", tList.getDescriptor()));
method.instructions.add(new InsnNode(ARETURN));
classNode.methods.add(method);
return true;
}
}

View File

@ -0,0 +1,130 @@
package cpw.mods.fml.common.eventhandler;
import static org.objectweb.asm.Opcodes.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import com.google.common.collect.Maps;
public class ASMEventHandler 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 HashMap<Method, Class<?>> cache = Maps.newHashMap();
private final IEventListener handler;
private final SubscribeEvent subInfo;
public ASMEventHandler(Object target, Method method) throws Exception
{
handler = (IEventListener)createWrapper(method).getConstructor(Object.class).newInstance(target);
subInfo = method.getAnnotation(SubscribeEvent.class);
}
@Override
public void invoke(Event event)
{
if (handler != null)
{
if (!event.isCancelable() || !event.isCanceled() || subInfo.receiveCanceled())
{
handler.invoke(event);
}
}
}
public EventPriority getPriority()
{
return subInfo.priority();
}
public Class<?> createWrapper(Method callback)
{
if (cache.containsKey(callback))
{
return cache.get(callback);
}
ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;
String name = getUniqueName(callback);
String desc = name.replace('.', '/');
String instType = Type.getInternalName(callback.getDeclaringClass());
String eventType = Type.getInternalName(callback.getParameterTypes()[0]);
/*
System.out.println("Name: " + name);
System.out.println("Desc: " + desc);
System.out.println("InstType: " + instType);
System.out.println("Callback: " + callback.getName() + Type.getMethodDescriptor(callback));
System.out.println("Event: " + eventType);
*/
cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, desc, null, "java/lang/Object", new String[]{ HANDLER_DESC });
cw.visitSource(".dynamic", null);
{
cw.visitField(ACC_PUBLIC, "instance", "Ljava/lang/Object;", null, null).visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/Object;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, desc, "instance", "Ljava/lang/Object;");
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.visitFieldInsn(GETFIELD, desc, "instance", "Ljava/lang/Object;");
mv.visitTypeInsn(CHECKCAST, instType);
mv.visitVarInsn(ALOAD, 1);
mv.visitTypeInsn(CHECKCAST, eventType);
mv.visitMethodInsn(INVOKEVIRTUAL, instType, callback.getName(), Type.getMethodDescriptor(callback));
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
cw.visitEnd();
Class<?> ret = LOADER.define(name, cw.toByteArray());
cache.put(callback, ret);
return ret;
}
private String getUniqueName(Method callback)
{
return String.format("%s_%d_%s_%s_%s", getClass().getName(), IDs++,
callback.getDeclaringClass().getSimpleName(),
callback.getName(),
callback.getParameterTypes()[0].getSimpleName());
}
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);
}
}
}

View File

@ -0,0 +1,11 @@
package cpw.mods.fml.common.eventhandler;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(value = RUNTIME)
@Target(value = TYPE)
public @interface Cancelable{}

View File

@ -0,0 +1,156 @@
package cpw.mods.fml.common.eventhandler;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Base Event class that all other events are derived from
*/
public class Event
{
@Retention(value = RUNTIME)
@Target(value = TYPE)
public @interface HasResult{}
public enum Result
{
DENY,
DEFAULT,
ALLOW
}
private boolean isCanceled = false;
private final boolean isCancelable;
private Result result = Result.DEFAULT;
private final boolean hasResult;
private static ListenerList listeners = new ListenerList();
private static final Map<Class<?>, Map<Class<?>, Boolean>> annotationMap = new ConcurrentHashMap<Class<?>, Map<Class<?>, Boolean>>();
public Event()
{
setup();
isCancelable = hasAnnotation(Cancelable.class);
hasResult = hasAnnotation(HasResult.class);
}
private boolean hasAnnotation(Class<? extends Annotation> annotation)
{
Class<?> me = this.getClass();
Map<Class<?>, Boolean> list = annotationMap.get(me);
if (list == null)
{
list = new ConcurrentHashMap<Class<?>, Boolean>();
annotationMap.put(me, list);
}
Boolean cached = list.get(annotation);
if (cached != null)
{
return cached;
}
Class<?> cls = me;
while (cls != Event.class)
{
if (cls.isAnnotationPresent(annotation))
{
list.put(annotation, true);
return true;
}
cls = cls.getSuperclass();
}
list.put(annotation, false);
return false;
}
/**
* Determine if this function is cancelable at all.
* @return If access to setCanceled should be allowed
*/
public boolean isCancelable()
{
return isCancelable;
}
/**
* Determine if this event is canceled and should stop executing.
* @return The current canceled state
*/
public boolean isCanceled()
{
return isCanceled;
}
/**
* Sets the state of this event, not all events are cancelable, and any attempt to
* cancel a event that can't be will result in a IllegalArgumentException.
*
* The functionality of setting the canceled state is defined on a per-event bases.
*
* @param cancel The new canceled value
*/
public void setCanceled(boolean cancel)
{
if (!isCancelable())
{
throw new IllegalArgumentException("Attempted to cancel a uncancelable event");
}
isCanceled = cancel;
}
/**
* Determines if this event expects a significant result value.
*/
public boolean hasResult()
{
return hasResult;
}
/**
* Returns the value set as the result of this event
*/
public Result getResult()
{
return result;
}
/**
* Sets the result value for this event, not all events can have a result set, and any attempt to
* set a result for a event that isn't expecting it will result in a IllegalArgumentException.
*
* The functionality of setting the result is defined on a per-event bases.
*
* @param value The new result
*/
public void setResult(Result value)
{
result = value;
}
/**
* Called by the base constructor, this is used by ASM generated
* event classes to setup various functionality such as the listenerlist.
*/
protected void setup()
{
}
/**
* Returns a ListenerList object that contains all listeners
* that are registered to this event.
*
* @return Listener List
*/
public ListenerList getListenerList()
{
return listeners;
}
}

View File

@ -0,0 +1,112 @@
package cpw.mods.fml.common.eventhandler;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.google.common.reflect.TypeToken;
public class EventBus
{
private static int maxID = 0;
private ConcurrentHashMap<Object, ArrayList<IEventListener>> listeners = new ConcurrentHashMap<Object, ArrayList<IEventListener>>();
private final int busID = maxID++;
public EventBus()
{
ListenerList.resize(busID + 1);
}
public void register(Object target)
{
if (listeners.containsKey(target))
{
return;
}
Set<? extends Class<?>> supers = TypeToken.of(target.getClass()).getTypes().rawTypes();
for (Method method : target.getClass().getMethods())
{
for (Class<?> cls : supers)
{
try
{
Method real = cls.getDeclaredMethod(method.getName(), method.getParameterTypes());
if (real.isAnnotationPresent(SubscribeEvent.class))
{
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1)
{
throw new IllegalArgumentException(
"Method " + method + " has @SubscribeEvent annotation, but requires " + parameterTypes.length +
" arguments. Event handler methods must require a single argument."
);
}
Class<?> eventType = parameterTypes[0];
if (!Event.class.isAssignableFrom(eventType))
{
throw new IllegalArgumentException("Method " + method + " has @SubscribeEvent annotation, but takes a argument that is not a Event " + eventType);
}
register(eventType, target, method);
break;
}
}
catch (NoSuchMethodException e)
{
;
}
}
}
}
private void register(Class<?> eventType, Object target, Method method)
{
try
{
Constructor<?> ctr = eventType.getConstructor();
ctr.setAccessible(true);
Event event = (Event)ctr.newInstance();
ASMEventHandler listener = new ASMEventHandler(target, method);
event.getListenerList().register(busID, listener.getPriority(), listener);
ArrayList<IEventListener> others = listeners.get(target);
if (others == null)
{
others = new ArrayList<IEventListener>();
listeners.put(target, others);
}
others.add(listener);
}
catch (Exception e)
{
e.printStackTrace();
}
}
public void unregister(Object object)
{
ArrayList<IEventListener> list = listeners.remove(object);
for (IEventListener listener : list)
{
ListenerList.unregisterAll(busID, listener);
}
}
public boolean post(Event event)
{
IEventListener[] listeners = event.getListenerList().getListeners(busID);
for (IEventListener listener : listeners)
{
listener.invoke(event);
}
return (event.isCancelable() ? event.isCanceled() : false);
}
}

View File

@ -0,0 +1,16 @@
package cpw.mods.fml.common.eventhandler;
public enum EventPriority
{
/*Priority of event listeners, listeners will be sorted with respect to this priority level.
*
* Note:
* Due to using a ArrayList in the ListenerList,
* these need to stay in a contiguous index starting at 0. {Default ordinal}
*/
HIGHEST, //First to execute
HIGH,
NORMAL,
LOW,
LOWEST //Last to execute
}

View File

@ -0,0 +1,8 @@
package cpw.mods.fml.common.eventhandler;
public interface IEventListener
{
public void invoke(Event event);
}

View File

@ -0,0 +1,219 @@
package cpw.mods.fml.common.eventhandler;
import java.util.*;
public class ListenerList
{
private static ArrayList<ListenerList> allLists = new ArrayList<ListenerList>();
private static int maxSize = 0;
private ListenerList parent;
private ListenerListInst[] lists = new ListenerListInst[0];
public ListenerList()
{
allLists.add(this);
resizeLists(maxSize);
}
public ListenerList(ListenerList parent)
{
allLists.add(this);
this.parent = parent;
resizeLists(maxSize);
}
public static void resize(int max)
{
if (max <= maxSize)
{
return;
}
for (ListenerList list : allLists)
{
list.resizeLists(max);
}
maxSize = max;
}
public void resizeLists(int max)
{
if (parent != null)
{
parent.resizeLists(max);
}
if (lists.length >= max)
{
return;
}
ListenerListInst[] newList = new ListenerListInst[max];
int x = 0;
for (; x < lists.length; x++)
{
newList[x] = lists[x];
}
for(; x < max; x++)
{
if (parent != null)
{
newList[x] = new ListenerListInst(parent.getInstance(x));
}
else
{
newList[x] = new ListenerListInst();
}
}
lists = newList;
}
public static void clearBusID(int id)
{
for (ListenerList list : allLists)
{
list.lists[id].dispose();
}
}
protected ListenerListInst getInstance(int id)
{
return lists[id];
}
public IEventListener[] getListeners(int id)
{
return lists[id].getListeners();
}
public void register(int id, EventPriority priority, IEventListener listener)
{
lists[id].register(priority, listener);
}
public void unregister(int id, IEventListener listener)
{
lists[id].unregister(listener);
}
public static void unregisterAll(int id, IEventListener listener)
{
for (ListenerList list : allLists)
{
list.unregister(id, listener);
}
}
private class ListenerListInst
{
private boolean rebuild = true;
private IEventListener[] listeners;
private ArrayList<ArrayList<IEventListener>> priorities;
private ListenerListInst parent;
private ListenerListInst()
{
int count = EventPriority.values().length;
priorities = new ArrayList<ArrayList<IEventListener>>(count);
for (int x = 0; x < count; x++)
{
priorities.add(new ArrayList<IEventListener>());
}
}
public void dispose()
{
for (ArrayList<IEventListener> listeners : priorities)
{
listeners.clear();
}
priorities.clear();
parent = null;
listeners = null;
}
private ListenerListInst(ListenerListInst parent)
{
this();
this.parent = parent;
}
/**
* Returns a ArrayList containing all listeners for this event,
* and all parent events for the specified priority.
*
* The list is returned with the listeners for the children events first.
*
* @param priority The Priority to get
* @return ArrayList containing listeners
*/
public ArrayList<IEventListener> getListeners(EventPriority priority)
{
ArrayList<IEventListener> ret = new ArrayList<IEventListener>(priorities.get(priority.ordinal()));
if (parent != null)
{
ret.addAll(parent.getListeners(priority));
}
return ret;
}
/**
* Returns a full list of all listeners for all priority levels.
* Including all parent listeners.
*
* List is returned in proper priority order.
*
* Automatically rebuilds the internal Array cache if its information is out of date.
*
* @return Array containing listeners
*/
public IEventListener[] getListeners()
{
if (shouldRebuild()) buildCache();
return listeners;
}
protected boolean shouldRebuild()
{
return rebuild || (parent != null && parent.shouldRebuild());
}
/**
* Rebuild the local Array of listeners, returns early if there is no work to do.
*/
private void buildCache()
{
if(parent != null && parent.shouldRebuild())
{
parent.buildCache();
}
ArrayList<IEventListener> ret = new ArrayList<IEventListener>();
for (EventPriority value : EventPriority.values())
{
ret.addAll(getListeners(value));
}
listeners = ret.toArray(new IEventListener[ret.size()]);
rebuild = false;
}
public void register(EventPriority priority, IEventListener listener)
{
priorities.get(priority.ordinal()).add(listener);
rebuild = true;
}
public void unregister(IEventListener listener)
{
for(ArrayList<IEventListener> list : priorities)
{
if (list.remove(listener))
{
rebuild = true;
}
}
}
}
}

View File

@ -0,0 +1,15 @@
package cpw.mods.fml.common.eventhandler;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.*;
import static java.lang.annotation.ElementType.*;
@Retention(value = RUNTIME)
@Target(value = METHOD)
public @interface SubscribeEvent
{
public EventPriority priority() default EventPriority.NORMAL;
public boolean receiveCanceled() default false;
}