diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java
index 59d02d98d..942c406e6 100644
--- a/src/main/java/noppes/npcs/constants/ScriptContext.java
+++ b/src/main/java/noppes/npcs/constants/ScriptContext.java
@@ -2,49 +2,65 @@ 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.
*
- * Script contexts are automatically discovered by scanning for {@link ScriptContextMarker}
- * annotations on event interfaces. For example:
+ * 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.
*
- *
- * {@literal @}ScriptContextMarker("NPC")
- * public interface INpcEvent extends ICustomNPCsEvent { }
- *
+ * This is a registerable system - any mod can register their own contexts:
*
- * 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.
+ * // Register a new context with multiple namespaces:
+ * ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent");
+ *
+ * // Or add namespaces to an existing context:
+ * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent");
*/
public class ScriptContext {
// Registry of all script contexts
private static final Map 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 = 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");
+ 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
+ );
// ==================== INSTANCE FIELDS ====================
@@ -106,155 +122,6 @@ 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> annotatedClasses = findAnnotatedClasses("noppes.npcs.api.event", ScriptContextMarker.class);
-
- // Group classes by their context ID
- Map>> 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>> entry : contextGroups.entrySet()) {
- String contextId = entry.getKey();
- List> 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> findAnnotatedClasses(String packageName, Class extends java.lang.annotation.Annotation> annotationClass) throws Exception {
- List> annotatedClasses = new ArrayList<>();
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
- String packagePath = packageName.replace('.', '/');
-
- // Get resources for the package
- Enumeration 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> 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> result) {
- try {
- JarFile jarFile = new JarFile(jarPath);
- Enumeration 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.
*
diff --git a/src/main/java/noppes/npcs/constants/ScriptContextMarker.java b/src/main/java/noppes/npcs/constants/ScriptContextMarker.java
deleted file mode 100644
index 6da6e23ae..000000000
--- a/src/main/java/noppes/npcs/constants/ScriptContextMarker.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package noppes.npcs.constants;
-
-import java.lang.annotation.*;
-
-/**
- * Marks an event interface as belonging to a specific script context.
- *
- * Usage:
- *
- * {@literal @}ScriptContextMarker("NPC")
- * public interface INpcEvent extends ICustomNPCsEvent {
- * // ...
- * }
- *
- *
- * 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();
-}