Annotations for API events

This commit is contained in:
bigguy345 committed 2026-01-10 07:12:46 +02:00
1 parent 5b36cdac1b
commit 06aa0ce22e
2 files changed
+203 -43

No files matched your search

@@ -2,65 +2,49 @@ package noppes.npcs.constants;
import noppes.npcs.api.event.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Represents a script context that maps to one or more event interface namespaces.
*
* A script context can have multiple namespaces because script editors often
* support hooks from multiple event types. For example, Player scripts support:
* - IPlayerEvent (core player hooks)
* - IAnimationEvent (animation hooks)
* - IPartyEvent (party hooks)
* - ICustomGuiEvent (custom GUI hooks)
* - etc.
* Script contexts are automatically discovered by scanning for {@link ScriptContextMarker}
* annotations on event interfaces. For example:
*
* This is a registerable system - any mod can register their own contexts:
* <pre>
* {@literal @}ScriptContextMarker("NPC")
* public interface INpcEvent extends ICustomNPCsEvent { }
* </pre>
*
* // Register a new context with multiple namespaces:
* ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent");
*
* // Or add namespaces to an existing context:
* ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent");
* Multiple interfaces can share the same context ID - they will be grouped together.
* This is a registerable system - any mod can register their own contexts by annotating
* their event interfaces.
*/
public class ScriptContext {
// Registry of all script contexts
private static final Map<String, ScriptContext> REGISTRY = new ConcurrentHashMap<>();
// Static initializer to scan and register annotated classes
static {
scanAndRegisterAnnotatedClasses();
}
// ==================== BUILT-IN CONTEXTS ====================
// These are loaded dynamically from annotations, but we keep references for convenience
public static final ScriptContext NPC = register("NPC",
INpcEvent.class,
IProjectileEvent.class
);
public static final ScriptContext PLAYER = register("PLAYER",
IPlayerEvent.class,
IAnimationEvent.class,
IPartyEvent.class,
IDialogEvent.class,
IQuestEvent.class,
IFactionEvent.class,
ICustomGuiEvent.class
);
public static final ScriptContext BLOCK = register("BLOCK",
IBlockEvent.class
);
public static final ScriptContext ITEM = register("ITEM",
IItemEvent.class
);
public static final ScriptContext FORGE = register("FORGE",
IForgeEvent.class
);
public static final ScriptContext GLOBAL = register("GLOBAL",
"Global" // Special case: Global namespace doesn't have a corresponding event class
);
public static final ScriptContext NPC = byId("NPC");
public static final ScriptContext PLAYER = byId("PLAYER");
public static final ScriptContext BLOCK = byId("BLOCK");
public static final ScriptContext ITEM = byId("ITEM");
public static final ScriptContext FORGE = byId("FORGE");
public static final ScriptContext GLOBAL = byId("GLOBAL");
// ==================== INSTANCE FIELDS ====================
@@ -122,6 +106,155 @@ public class ScriptContext {
// ==================== REGISTRATION API ====================
/**
* Scan the classpath for classes annotated with {@link ScriptContextMarker}
* and automatically register them.
*/
private static void scanAndRegisterAnnotatedClasses() {
try {
// Scan the noppes.npcs.api.event package for annotated interfaces
List<Class<?>> annotatedClasses = findAnnotatedClasses("noppes.npcs.api.event", ScriptContextMarker.class);
// Group classes by their context ID
Map<String, List<Class<?>>> contextGroups = new HashMap<>();
for (Class<?> eventClass : annotatedClasses) {
ScriptContextMarker annotation = eventClass.getAnnotation(ScriptContextMarker.class);
if (annotation != null) {
String contextId = annotation.value();
contextGroups.computeIfAbsent(contextId, k -> new ArrayList<>()).add(eventClass);
}
}
// Register each context with its grouped classes
for (Map.Entry<String, List<Class<?>>> entry : contextGroups.entrySet()) {
String contextId = entry.getKey();
List<Class<?>> classes = entry.getValue();
register(contextId, classes.toArray(new Class<?>[0]));
}
// Special case: GLOBAL context (no annotation needed)
if (!REGISTRY.containsKey("GLOBAL")) {
register("GLOBAL", "Global");
}
} catch (Exception e) {
// Fallback: if scanning fails, at least register known core events
System.err.println("[ScriptContext] Failed to scan for annotated classes: " + e.getMessage());
e.printStackTrace();
fallbackRegistration();
}
}
/**
* Find all classes in a package that have a specific annotation.
*/
private static List<Class<?>> findAnnotatedClasses(String packageName, Class<? extends java.lang.annotation.Annotation> annotationClass) throws Exception {
List<Class<?>> annotatedClasses = new ArrayList<>();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String packagePath = packageName.replace('.', '/');
// Get resources for the package
Enumeration<URL> resources = classLoader.getResources(packagePath);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
String protocol = resource.getProtocol();
if ("file".equals(protocol)) {
// Scanning from file system (development environment)
String filePath = URLDecoder.decode(resource.getFile(), "UTF-8");
File directory = new File(filePath);
if (directory.exists()) {
findClassesInDirectory(directory, packageName, annotationClass, annotatedClasses);
}
} else if ("jar".equals(protocol)) {
// Scanning from JAR file (production)
String jarPath = resource.getPath().substring(5, resource.getPath().indexOf("!"));
jarPath = URLDecoder.decode(jarPath, "UTF-8");
findClassesInJar(jarPath, packageName, annotationClass, annotatedClasses);
}
}
return annotatedClasses;
}
/**
* Find classes with annotation in a directory (file system).
*/
private static void findClassesInDirectory(File directory, String packageName,
Class<? extends java.lang.annotation.Annotation> annotationClass,
List<Class<?>> result) {
File[] files = directory.listFiles();
if (files == null) return;
for (File file : files) {
String fileName = file.getName();
if (file.isDirectory()) {
findClassesInDirectory(file, packageName + "." + fileName, annotationClass, result);
} else if (fileName.endsWith(".class")) {
String className = packageName + '.' + fileName.substring(0, fileName.length() - 6);
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(annotationClass)) {
result.add(clazz);
}
} catch (Throwable e) {
// Skip classes that can't be loaded
}
}
}
}
/**
* Find classes with annotation in a JAR file.
*/
private static void findClassesInJar(String jarPath, String packageName,
Class<? extends java.lang.annotation.Annotation> annotationClass,
List<Class<?>> result) {
try {
JarFile jarFile = new JarFile(jarPath);
Enumeration<JarEntry> entries = jarFile.entries();
String packagePath = packageName.replace('.', '/');
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(packagePath) && entryName.endsWith(".class")) {
String className = entryName.replace('/', '.').substring(0, entryName.length() - 6);
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(annotationClass)) {
result.add(clazz);
}
} catch (Throwable e) {
// Skip classes that can't be loaded
}
}
}
jarFile.close();
} catch (IOException e) {
System.err.println("[ScriptContext] Failed to scan JAR: " + jarPath);
}
}
/**
* Fallback registration if classpath scanning fails.
*/
private static void fallbackRegistration() {
try {
register("NPC", INpcEvent.class, IProjectileEvent.class);
register("PLAYER", IPlayerEvent.class, IAnimationEvent.class, IPartyEvent.class,
IDialogEvent.class, IQuestEvent.class, IFactionEvent.class, ICustomGuiEvent.class);
register("BLOCK", IBlockEvent.class);
register("ITEM", IItemEvent.class);
register("FORGE", IForgeEvent.class);
register("GLOBAL", "Global");
} catch (Exception e) {
System.err.println("[ScriptContext] Fallback registration also failed: " + e.getMessage());
}
}
/**
* Register a new script context with one or more namespaces.
*
@@ -0,0 +1,27 @@
package noppes.npcs.constants;
import java.lang.annotation.*;
/**
* Marks an event interface as belonging to a specific script context.
*
* Usage:
* <pre>
* {@literal @}ScriptContextMarker("NPC")
* public interface INpcEvent extends ICustomNPCsEvent {
* // ...
* }
* </pre>
*
* Multiple interfaces can share the same context name - they will all be
* registered as namespaces for that context.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ScriptContextMarker {
/**
* The script context ID this event interface belongs to.
* Examples: "NPC", "PLAYER", "BLOCK", "ITEM", "FORGE", "GLOBAL"
*/
String value();
}