From b86c0284ac23eede1905dac74578dc01a3f2947d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 06:42:38 +0200 Subject: [PATCH] Loaded all .d.ts files in assets/modId/api directory of ALL loaded mods --- .../interpreter/js_parser/DtsModScanner.java | 197 ++++++++++++++++++ .../interpreter/js_parser/JSTypeRegistry.java | 125 ++++++++--- 2 files changed, 298 insertions(+), 24 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java new file mode 100644 index 000000000..f8aa926c0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java @@ -0,0 +1,197 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.ModContainer; + +import java.io.*; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class DtsModScanner { + + private static final Pattern MOD_DTS_PATH_PATTERN = Pattern.compile("^assets/([^/]+)/api/(.+\\.d\\.ts)$"); + + private static final List DOMAIN_PRIORITY = Arrays.asList("customnpcs", "npcdbc"); + + private DtsModScanner() {} + + static List collectDtsFilesFromMods() { + List dtsFiles = new ArrayList<>(); + for (ModContainer mod : Loader.instance().getModList()) { + File source = mod.getSource(); + if (source == null || !source.exists()) { + continue; + } + if (source.isDirectory()) { + scanDirectoryForModDts(source, mod.getModId(), dtsFiles); + } else if (source.getName().endsWith(".jar") || source.getName().endsWith(".zip")) { + scanJarForModDts(source, mod.getModId(), dtsFiles); + } + } + return dtsFiles; + } + + static void sortDtsFiles(List dtsFiles) { + Collections.sort(dtsFiles, new DtsFileRefComparator()); + } + + static void logSummary(List dtsFiles) { + Map domainCounts = new LinkedHashMap<>(); + for (DtsFileRef ref : dtsFiles) { + domainCounts.put(ref.getDomain(), domainCounts.getOrDefault(ref.getDomain(), 0) + 1); + } + System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files across " + domainCounts.size() + " domains"); + for (Map.Entry entry : domainCounts.entrySet()) { + System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files"); + } + } + + private static void scanDirectoryForModDts(File baseDir, String modId, List dtsFiles) { + scanDirectoryForModDts(baseDir, baseDir, modId, dtsFiles); + } + + private static void scanDirectoryForModDts(File baseDir, File directory, String modId, List dtsFiles) { + File[] files = directory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + scanDirectoryForModDts(baseDir, file, modId, dtsFiles); + continue; + } + if (!file.getName().endsWith(".d.ts")) { + continue; + } + String relativePath = baseDir.toURI().relativize(file.toURI()).getPath(); + if (relativePath == null) { + continue; + } + String normalized = relativePath.replace('\\', '/'); + Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(normalized); + if (matcher.matches()) { + String domain = matcher.group(1); + String apiPath = matcher.group(2); + dtsFiles.add(DtsFileRef.forFile(modId, domain, apiPath, file)); + } + } + } + + private static void scanJarForModDts(File jarFile, String modId, List dtsFiles) { + try { + JarFile jar = new JarFile(jarFile); + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(entryName); + if (matcher.matches()) { + String domain = matcher.group(1); + String apiPath = matcher.group(2); + dtsFiles.add(DtsFileRef.forJar(modId, domain, apiPath, jarFile, entryName)); + } + } + jar.close(); + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Error scanning mod jar for .d.ts files: " + e.getMessage()); + } + } + + static final class DtsFileRef { + private final String modId; + private final String domain; + private final String relativePath; + private final File file; + private final File jarFile; + private final String jarEntryName; + + private DtsFileRef(String modId, String domain, String relativePath, File file, File jarFile, String jarEntryName) { + this.modId = modId; + this.domain = domain; + this.relativePath = relativePath; + this.file = file; + this.jarFile = jarFile; + this.jarEntryName = jarEntryName; + } + + static DtsFileRef forFile(String modId, String domain, String relativePath, File file) { + return new DtsFileRef(modId, domain, relativePath, file, null, null); + } + + static DtsFileRef forJar(String modId, String domain, String relativePath, File jarFile, String jarEntryName) { + return new DtsFileRef(modId, domain, relativePath, null, jarFile, jarEntryName); + } + + InputStream openStream() throws IOException { + if (file != null) { + return new FileInputStream(file); + } + if (jarFile != null && jarEntryName != null) { + final JarFile jar = new JarFile(jarFile); + JarEntry entry = jar.getJarEntry(jarEntryName); + if (entry == null) { + jar.close(); + return null; + } + InputStream is = jar.getInputStream(entry); + return new FilterInputStream(is) { + @Override + public void close() throws IOException { + super.close(); + jar.close(); + } + }; + } + return null; + } + + String getDomain() { + return domain; + } + + String getRelativePath() { + return relativePath; + } + + String getOrigin() { + return modId + ":" + domain + ":" + relativePath; + } + } + + private static class DtsFileRefComparator implements Comparator { + @Override + public int compare(DtsFileRef a, DtsFileRef b) { + int domainCompare = Integer.compare(getDomainRank(a.getDomain()), getDomainRank(b.getDomain())); + if (domainCompare != 0) { + return domainCompare; + } + int domainNameCompare = a.getDomain().compareTo(b.getDomain()); + if (domainNameCompare != 0) { + return domainNameCompare; + } + int filePriorityCompare = Integer.compare(getFilePriority(a.getRelativePath()), getFilePriority(b.getRelativePath())); + if (filePriorityCompare != 0) { + return filePriorityCompare; + } + return a.getRelativePath().compareTo(b.getRelativePath()); + } + + private int getDomainRank(String domain) { + int idx = DOMAIN_PRIORITY.indexOf(domain); + return idx >= 0 ? idx : DOMAIN_PRIORITY.size(); + } + + private int getFilePriority(String relativePath) { + if (relativePath.endsWith("hooks.d.ts")) { + return 0; + } + if (relativePath.endsWith("index.d.ts")) { + return 1; + } + return 2; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java index 7a4514a62..de7e92aa2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java @@ -6,6 +6,7 @@ import noppes.npcs.scripted.NpcAPI; import java.io.*; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -18,9 +19,6 @@ public class JSTypeRegistry { private static JSTypeRegistry INSTANCE; - // Resource location for the API definitions - private static final String API_RESOURCE_PATH = "customnpcs:api/"; - // All registered types by full name (e.g., "IPlayerEvent.InteractEvent") private final Map types = new LinkedHashMap<>(); @@ -29,6 +27,12 @@ public class JSTypeRegistry { // Type aliases (simple name -> full type name) private final Map typeAliases = new HashMap<>(); + + // Type full name -> origin string (modId:domain:relativePath) + private final Map typeOrigins = new HashMap<>(); + + // Alias name -> origin string (modId:domain:relativePath) + private final Map aliasOrigins = new HashMap<>(); // Context-aware hook function signatures: namespace -> functionName -> list of signatures // The namespace is the event interface name (e.g., "INpcEvent", "IPlayerEvent") @@ -53,6 +57,8 @@ public class JSTypeRegistry { private boolean initialized = false; private boolean initializationAttempted = false; + + private String currentSource = null; public static JSTypeRegistry getInstance() { if (INSTANCE == null) { @@ -73,25 +79,28 @@ public class JSTypeRegistry { try { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); - - // Recursively load all .d.ts files from the api directory - Set dtsFiles = findAllDtsFilesInResources("assets/customnpcs/api"); - - System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files in resources"); - - // Load hooks.d.ts and index.d.ts first if they exist (defines core types) - if (dtsFiles.contains("hooks.d.ts")) { - loadResourceFile(parser, "hooks.d.ts"); - } - if (dtsFiles.contains("index.d.ts")) { - loadResourceFile(parser, "index.d.ts"); - } - - // Load all other .d.ts files - for (String filePath : dtsFiles) { - if (!filePath.equals("hooks.d.ts") && !filePath.equals("index.d.ts")) { - loadResourceFile(parser, filePath); + + List dtsFiles = DtsModScanner.collectDtsFilesFromMods(); + if (dtsFiles.isEmpty()) { + Set fallbackFiles = findAllDtsFilesInResources("assets/customnpcs/api"); + + System.out.println("[JSTypeRegistry] Found " + fallbackFiles.size() + " .d.ts files in resources"); + + if (fallbackFiles.contains("hooks.d.ts")) { + loadResourceFile(parser, "hooks.d.ts"); } + if (fallbackFiles.contains("index.d.ts")) { + loadResourceFile(parser, "index.d.ts"); + } + + for (String filePath : fallbackFiles) { + if (!filePath.equals("hooks.d.ts") && !filePath.equals("index.d.ts")) { + loadResourceFile(parser, filePath); + } + } + } else { + DtsModScanner.logSummary(dtsFiles); + loadDtsFiles(parser, dtsFiles); } // Phase 2: Resolve all type parameters now that all types are loaded @@ -113,6 +122,33 @@ public class JSTypeRegistry { e.printStackTrace(); } } + + private void loadDtsFiles(TypeScriptDefinitionParser parser, List dtsFiles) { + DtsModScanner.sortDtsFiles(dtsFiles); + for (DtsModScanner.DtsFileRef ref : dtsFiles) { + try (InputStream is = ref.openStream()) { + if (is == null) { + continue; + } + String content = readFully(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))); + setCurrentSource(ref.getOrigin()); + parser.parseDefinitionFile(content, ref.getRelativePath()); + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Failed to load " + ref.getOrigin() + ": " + e.getMessage()); + } finally { + setCurrentSource(null); + } + } + } + + private String readFully(BufferedReader reader) throws IOException { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } /** * Recursively find all .d.ts files in the resources directory. @@ -210,7 +246,7 @@ public class JSTypeRegistry { System.err.println("[JSTypeRegistry] Error scanning JAR for .d.ts files: " + e.getMessage()); } } - + /** * Load a specific .d.ts file from resources. */ @@ -280,9 +316,20 @@ public class JSTypeRegistry { * Register a type. */ public void registerType(JSTypeInfo type) { - types.put(type.getFullName(), type); + String fullName = type.getFullName(); + if (types.containsKey(fullName)) { + logTypeCollision(fullName, getCurrentSource()); + return; + } + types.put(fullName, type); + typeOrigins.put(fullName, getCurrentSource()); if (type.getJavaFqn() != null && !type.getJavaFqn().isEmpty()) { - typesByJavaFqn.put(type.getJavaFqn(), type); + String javaFqn = type.getJavaFqn(); + if (!typesByJavaFqn.containsKey(javaFqn)) { + typesByJavaFqn.put(javaFqn, type); + } else { + System.out.println("[JSTypeRegistry] Duplicate javaFqn " + javaFqn + " from " + getCurrentSource()); + } } } @@ -298,7 +345,15 @@ public class JSTypeRegistry { * Register a type alias. */ public void registerTypeAlias(String alias, String fullType) { + if (typeAliases.containsKey(alias)) { + String existing = typeAliases.get(alias); + if (!Objects.equals(existing, fullType)) { + logAliasCollision(alias, existing, fullType, getCurrentSource()); + } + return; + } typeAliases.put(alias, fullType); + aliasOrigins.put(alias, getCurrentSource()); } /** @@ -755,13 +810,35 @@ public class JSTypeRegistry { */ public void clear() { types.clear(); + typesByJavaFqn.clear(); typeAliases.clear(); + typeOrigins.clear(); + aliasOrigins.clear(); hooks.clear(); contextHooks.clear(); globalEngineObjects.clear(); initialized = false; } + private void setCurrentSource(String source) { + currentSource = source; + } + + private String getCurrentSource() { + return currentSource == null ? "unknown" : currentSource; + } + + private void logTypeCollision(String fullName, String incomingSource) { + String existingSource = typeOrigins.getOrDefault(fullName, "unknown"); + System.out.println("[JSTypeRegistry] Duplicate type " + fullName + " from " + incomingSource + " (kept " + existingSource + ")"); + } + + private void logAliasCollision(String alias, String existingType, String incomingType, String incomingSource) { + String existingSource = aliasOrigins.getOrDefault(alias, "unknown"); + System.out.println("[JSTypeRegistry] Duplicate alias " + alias + " from " + incomingSource + " (kept " + existingSource + ")"); + System.out.println("[JSTypeRegistry] Alias " + alias + " existing=" + existingType + " incoming=" + incomingType); + } + /** * Represents a hook function signature with its namespace. */