/*
* Copyright 2016 Nathan Howard
*
* This file is part of OpenGrave
*
* OpenGrave is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenGrave is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenGrave. If not, see <http://www.gnu.org/licenses/>.
*/
package com.opengrave.common.event;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import javax.script.*;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaFunction;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
import org.luaj.vm2.script.LuaScriptEngineFactory;
import com.opengrave.api.hgcore;
import com.opengrave.common.DebugExceptionHandler;
import com.opengrave.common.ModSession;
/**
* Runs as a thread to dispatch events to handlers. Handlers must register on
* construction but need never notify of their destruction or garbage
* collection. Uses WeakReference to hold onto handlers and dead ones are
* dropped. Doubles as Mod handler, any mod loading, calling etc must be done
* from this class via Events. No events handling functions should attempt to
* use OpenGL commands as this will simply not work (This thread does not hold
* an OpenGL context)
*
* @author triggerhapp
*
*/
public class EventDispatcher implements Runnable, EventListener {
private static Object lock = new Object();
private ArrayList<WeakReference<EventListener>> javaListeners = new ArrayList<WeakReference<EventListener>>();
private ArrayList<Mod> modsLoaded = new ArrayList<Mod>();
private HashMap<String, HashMap<LUAEventPriority, ArrayList<LuaValue>>> luaListeners = new HashMap<String, HashMap<LUAEventPriority, ArrayList<LuaValue>>>();
private HashMap<Class<? extends EventListener>, HashMap<JavaEventPriority, ArrayList<Method>>> classInfo = new HashMap<Class<? extends EventListener>, HashMap<JavaEventPriority, ArrayList<Method>>>();
public static EventDispatcher events = new EventDispatcher();
public static ModSession loadingSession;
private ArrayList<Event> eventsRemaining = new ArrayList<Event>();
private boolean running;
Thread t;
ScriptEngine engine;
private SimpleBindings sb;
private hgcore hgcore1;
/**
* Adds an event handler.
*/
public static void addHandler(EventListener handler) {
synchronized (events.classInfo) {
events.prepareClass(handler.getClass());
}
synchronized (events.javaListeners) {
events.javaListeners.add(new WeakReference<EventListener>(handler));
}
}
public static void removeHandler(EventListener state) {
synchronized (events.javaListeners) {
@SuppressWarnings("unused")
int dud = 0;
for (int i = 0; i < events.javaListeners.size(); dud++) {
WeakReference<EventListener> handlerRef = events.javaListeners.get(i);
EventListener handler = handlerRef.get();
if (handler == null || handler == state) {
events.javaListeners.remove(i);
continue;
}
i++;
}
}
}
public static void addHandler(LuaValue modLib, String eventType, String priority, LuaValue handler) {
// if (loadingMod == null) {
// System.out.println("Cannot register LUA events outside of main!");
// return;
// }
Mod thisMod = null;
for (Mod mod : Mod.getAll()) {
if (modLib.equals(mod.getLibrary())) {
thisMod = mod;
}
}
synchronized (events.luaListeners) {
if (!events.luaListeners.containsKey(thisMod.getId())) {
events.luaListeners.put(thisMod.getId(), new HashMap<LUAEventPriority, ArrayList<LuaValue>>());
}
LUAEventPriority lep = new LUAEventPriority(eventType, priority);
if (!events.luaListeners.get(thisMod.getId()).containsKey(lep)) {
events.luaListeners.get(thisMod.getId()).put(lep, new ArrayList<LuaValue>());
}
if (!events.luaListeners.get(thisMod.getId()).get(lep).contains(handler)) {
events.luaListeners.get(thisMod.getId()).get(lep).add(handler);
}
}
}
/***
* Adds Methods and parameters to a hashmap inside of a hashmap. Not thread
* safe, be sure to synchronize this.classInfo first!
*
* @param class1
*/
@SuppressWarnings("unchecked")
private void prepareClass(Class<? extends EventListener> klass) {
if (classInfo.containsKey(klass)) {
return;
}
HashMap<JavaEventPriority, ArrayList<Method>> methodHash = new HashMap<JavaEventPriority, ArrayList<Method>>();
classInfo.put(klass, methodHash);
Method[] methods = klass.getMethods();
for (Method method : methods) {
Class<?>[] list = method.getParameterTypes();
Annotation[] annotations = method.getDeclaredAnnotations();
EventHandlerPriority priority = null;
for (Annotation annotation : annotations) {
if (annotation instanceof EventHandler) {
priority = ((EventHandler) annotation).priority();
}
}
if (priority == null) {
continue;
}
if (list.length == 1) {
if (Event.class.isAssignableFrom(list[0])) {
Class<? extends Event> event = (Class<? extends Event>) list[0];
JavaEventPriority jep = new JavaEventPriority(event, priority);
if (methodHash.containsKey(jep)) {
methodHash.get(jep).add(method);
} else {
ArrayList<Method> methodList = new ArrayList<Method>();
methodList.add(method);
methodHash.put(jep, methodList);
}
}
}
}
}
/**
* Dispatch an event to the registered handlers. If the event was thrown
* during handling an event, do it immediately
*/
public static void dispatchEvent(Event event) {
if (Thread.currentThread() == events.t) {
// We're being called from within another Event
// Dispatch immediatly
events.sendEvent(event);
} else {
synchronized (lock) {
events.eventsRemaining.add(event);
lock.notifyAll();
}
}
}
private void dispatchEventTo(Event event, EventListener handler, EventHandlerPriority priority) {
JavaEventPriority jep = new JavaEventPriority(event.getClass(), priority);
synchronized (this.classInfo) {
if (!classInfo.get(handler.getClass()).containsKey(jep)) {
// Silently skip classes which don't handle this event type
return;
}
for (Method m : classInfo.get(handler.getClass()).get(jep)) {
try {
m.invoke(handler, event);
} catch (IllegalAccessException e) {
new DebugExceptionHandler(e, m.getName(), m.getClass());
} catch (IllegalArgumentException e) {
new DebugExceptionHandler(e, m.getName(), m.getClass());
} catch (InvocationTargetException e) {
new DebugExceptionHandler(e.getCause());
}
}
}
}
@Override
public void run() {
running = true;
sb = new SimpleBindings();
hgcore1 = new hgcore();
hgcore1.bind(sb);
addHandler(this);
engine = new LuaScriptEngineFactory().getScriptEngine();
if (engine == null) {
System.out.println("LUA Engine null");
System.exit(1);
}
hgcore1.bind(engine);
while (running) {
try {
synchronized (lock) {
lock.wait();
}
} catch (InterruptedException e1) {
new DebugExceptionHandler(e1);
}
// Oddly formed while allows us to accept changes and not lock
// threads while working on each separate event
int size = 0;
synchronized (lock) {
size = eventsRemaining.size();
}
while (size > 0) {
Event e = null;
synchronized (lock) {
e = eventsRemaining.remove(0);
}
if (e == null) {
continue;
}
sendEvent(e);
synchronized (lock) {
size = eventsRemaining.size();
}
}
}
}
private void sendEvent(Event e) {
// DebugBenchmark time = new DebugBenchmark("Event " +
// e.getEventName());
sendLuaEvent(e, "first");
if (e instanceof ConsumableEvent && ((ConsumableEvent) e).isConsumed()) {
// time.done();
return;
}
sendJavaEvent(e, EventHandlerPriority.EARLY);
if (e instanceof ConsumableEvent && ((ConsumableEvent) e).isConsumed()) {
// time.done();
return;
}
sendLuaEvent(e, "third");
if (e instanceof ConsumableEvent && ((ConsumableEvent) e).isConsumed()) {
// time.done();
return;
}
sendJavaEvent(e, EventHandlerPriority.LATE);
if (e instanceof ConsumableEvent && ((ConsumableEvent) e).isConsumed()) {
// time.done();
return;
}
sendLuaEvent(e, "fifth");
// time.done();
}
private void sendLuaEvent(Event e, String priority) {
if (e.getEventName() == null || e.getEventName().equals("")) {
System.out.println(e.getClass() + " has no event name");
return;
}
LUAEventPriority lep = new LUAEventPriority(e.getEventName(), priority);
synchronized (e) {
for (String key : luaListeners.keySet()) {
HashMap<LUAEventPriority, ArrayList<LuaValue>> valueSets = luaListeners.get(key);
Mod mod = Mod.getMod(key);
if (!valueSets.containsKey(lep)) {
continue;
}
ArrayList<LuaValue> list = valueSets.get(lep);
for (LuaValue function : list) {
try {
EventDispatcher.loadingSession = mod.getSession();
function.call(mod.getLibrary(), CoerceJavaToLua.coerce(e));
} catch (LuaError err) {
System.out.println(err.getMessage());
err.printStackTrace();
}
EventDispatcher.loadingSession = null;
if (e instanceof ConsumableEvent && ((ConsumableEvent) e).isConsumed()) {
return;
}
}
}
}
}
private void sendJavaEvent(Event e, EventHandlerPriority priority) {
synchronized (e) {
@SuppressWarnings("unused")
int dud = 0;
synchronized (this.javaListeners) {
for (int i = 0; i < javaListeners.size(); dud++) {
WeakReference<EventListener> handlerRef = javaListeners.get(i);
EventListener handler = handlerRef.get();
if (handler != null) {
dispatchEventTo(e, handler, priority);
} else {
javaListeners.remove(i);
continue;
}
i++;
}
e.isDispatchCompleted = true;
}
}
}
public void beginEventThread() {
t = new Thread(this, "Event Thread");
t.start();
}
@EventHandler(priority = EventHandlerPriority.LATE)
public void onModLoad(ModLoadEvent event) {
EventDispatcher.loadingSession = event.getModSession();
event.getMod().setSession(event.getModSession());
if (event.getMod() == null) {
return;
}
System.out.println("Attempting to load mod " + event.getMod().getId());
File main = event.getMod().getMainFile();
if (main != null) {
try (FileReader fr = new FileReader(main)) {
CompiledScript script = ((Compilable) engine).compile(fr);
script.eval(sb); // Put the Lua functions into the sb
// environment
LuaValue library = new LuaTable();
event.getMod().setLibrary(library);
LuaFunction mainFunc = (LuaFunction) sb.get("main");
event.getMod().setLibrary(library);
mainFunc.call(library);
synchronized (modsLoaded) {
modsLoaded.add(event.getMod());
}
} catch (FileNotFoundException e) {
new DebugExceptionHandler(e, main.getAbsolutePath());
} catch (ScriptException e) {
new DebugExceptionHandler(e, main.getAbsolutePath());
} catch (IOException e) {
new DebugExceptionHandler(e, main.getAbsolutePath());
}
}
EventDispatcher.loadingSession = null;
}
@EventHandler(priority = EventHandlerPriority.LATE)
public void onModUnload(ModUnloadEvent event) {
luaListeners.remove(event.getMod().getId());
System.out.println("Unlinking mod " + event.getMod().getId());
}
@EventHandler(priority = EventHandlerPriority.EARLY)
public void onModUnloadAll(ModUnloadAllEvent event) {
synchronized (modsLoaded) {
for (Mod mod : modsLoaded) {
ModUnloadEvent event2 = new ModUnloadEvent(mod);
dispatchEvent(event2);
}
}
}
@EventHandler(priority = EventHandlerPriority.LATE)
public void onLuaDebug(DebugWindowTextInputEvent event) {
String command = event.getText();
try {
engine.eval(command, engine.getContext());
} catch (LuaError e) {
// System.err.println(e.getMessage());
new DebugExceptionHandler(e);
} catch (ScriptException e) {
// System.err.println(e.getMessage());
new DebugExceptionHandler(e);
}
}
/**
* Return a copy of the list of loaded mods. Alterations to the list are
* ignored, but alterations to individual mods will persist
*
* @return
*/
public static ArrayList<Mod> getModsLoaded() {
ArrayList<Mod> modCopy = new ArrayList<Mod>();
synchronized (events.modsLoaded) {
for (Mod mod : events.modsLoaded) {
modCopy.add(mod);
}
}
return modCopy;
}
}