diff --git a/android/src/mindustry/android/AndroidLauncher.java b/android/src/mindustry/android/AndroidLauncher.java index c8175aa4f0..1a3126234f 100644 --- a/android/src/mindustry/android/AndroidLauncher.java +++ b/android/src/mindustry/android/AndroidLauncher.java @@ -38,7 +38,7 @@ public class AndroidLauncher extends AndroidApplication{ UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler((thread, error) -> { - CrashSender.log(error); + CrashHandler.log(error); //try to forward exception to system handler if(handler != null){ diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index 9fa83c39ee..bfd27e6091 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -1252,6 +1252,7 @@ public class Mods implements Loadable{ if(name != null) name = Strings.stripColors(name); if(displayName != null) displayName = Strings.stripColors(displayName); if(displayName == null) displayName = name; + if(version == null) version = "0"; if(author != null) author = Strings.stripColors(author); if(description != null) description = Strings.stripColors(description); if(subtitle != null) subtitle = Strings.stripColors(subtitle).replace("\n", ""); diff --git a/core/src/mindustry/mod/Scripts.java b/core/src/mindustry/mod/Scripts.java index 3ba21c03b3..fdbc717c6d 100644 --- a/core/src/mindustry/mod/Scripts.java +++ b/core/src/mindustry/mod/Scripts.java @@ -10,7 +10,6 @@ import rhino.*; import rhino.module.*; import rhino.module.provider.*; -import java.io.*; import java.net.*; import java.util.*; import java.util.regex.*; @@ -85,7 +84,7 @@ public class Scripts implements Disposable{ public void run(LoadedMod mod, Fi file){ currentMod = mod; - run(file.readString(), file.name(), true); + run(file.readString(), mod.name + "/" + file.name(), true); currentMod = null; } @@ -95,15 +94,10 @@ public class Scripts implements Disposable{ //inject script info into file context.evaluateString(scope, "modName = \"" + currentMod.name + "\"\nscriptName = \"" + file + "\"", "initscript.js", 1); } - context.evaluateString(scope, - wrap ? "(function(){'use strict';\n" + script + "\n})();" : script, - file, 0); + context.evaluateString(scope, wrap ? "(function(){'use strict';\n" + script + "\n})();" : script, file, 0); return true; }catch(Throwable t){ - if(currentMod != null){ - file = currentMod.name + "/" + file; - } - log(LogLevel.err, file, "" + getError(t, true)); + log(LogLevel.err, file, getError(t, true)); return false; } } @@ -123,10 +117,10 @@ public class Scripts implements Disposable{ @Override public ModuleSource loadSource(String moduleId, Scriptable paths, Object validator) throws URISyntaxException{ if(currentMod == null) return null; - return loadSource(moduleId, currentMod.root.child("scripts"), validator); + return loadSource(currentMod, moduleId, currentMod.root.child("scripts"), validator); } - private ModuleSource loadSource(String moduleId, Fi root, Object validator) throws URISyntaxException{ + private ModuleSource loadSource(LoadedMod sourceMod, String moduleId, Fi root, Object validator) throws URISyntaxException{ Matcher matched = directory.matcher(moduleId); if(matched.find()){ LoadedMod required = Vars.mods.locateMod(matched.group(1)); @@ -134,18 +128,16 @@ public class Scripts implements Disposable{ if(required == null){ // Mod not found, treat it as a folder Fi dir = root.child(matched.group(1)); if(!dir.exists()) return null; // Mod and folder not found - return loadSource(script, dir, validator); + return loadSource(sourceMod, script, dir, validator); } currentMod = required; - return loadSource(script, required.root.child("scripts"), validator); + return loadSource(sourceMod, script, required.root.child("scripts"), validator); } Fi module = root.child(moduleId + ".js"); if(!module.exists() || module.isDirectory()) return null; - return new ModuleSource( - new InputStreamReader(new ByteArrayInputStream((module.readString()).getBytes())), - new URI(moduleId), root.file().toURI(), validator); + return new ModuleSource(module.reader(Vars.bufferSize), new URI(sourceMod.name + "/" + moduleId + ".js"), root.file().toURI(), validator); } } } diff --git a/core/src/mindustry/net/CrashSender.java b/core/src/mindustry/net/CrashHandler.java similarity index 64% rename from core/src/mindustry/net/CrashSender.java rename to core/src/mindustry/net/CrashHandler.java index 26f90007a8..b559d654e8 100644 --- a/core/src/mindustry/net/CrashSender.java +++ b/core/src/mindustry/net/CrashHandler.java @@ -17,13 +17,17 @@ import java.util.*; import static arc.Core.*; import static mindustry.Vars.*; -public class CrashSender{ +public class CrashHandler{ - public static String createReport(String error){ - String report = "Mindustry has crashed. How unfortunate.\n"; + public static String createReport(Throwable exception){ + String error = writeException(exception); + LoadedMod cause = getModCause(exception); + + String report = cause == null ? "Mindustry has crashed. How unfortunate.\n" : "The mod '" + cause.meta.displayName + "' (" + cause.name + ")" + " has caused Mindustry to crash.\n"; if(mods != null && mods.list().size == 0 && Version.build != -1){ report += "Report this at " + Vars.reportIssueURL + "\n\n"; } + return report + "Version: " + Version.combined() + (Vars.headless ? " (Server)" : "") + "\n" + "OS: " + OS.osName + " x" + (OS.osArchBits) + " (" + OS.osArch + ")\n" @@ -31,6 +35,7 @@ public class CrashSender{ + "Java Version: " + OS.javaVersion + "\n" + "Runtime Available Memory: " + (Runtime.getRuntime().maxMemory() / 1024 / 1024) + "mb\n" + "Cores: " + Runtime.getRuntime().availableProcessors() + "\n" + + (cause == null ? "" : "Likely Cause: " + cause.meta.displayName + " (" + cause.name + " v" + cause.meta.version + ")\n") + (mods == null ? "" : "Mods: " + (!mods.list().contains(LoadedMod::shouldBeEnabled) ? "none (vanilla)" : mods.list().select(LoadedMod::shouldBeEnabled).toString(", ", mod -> mod.name + ":" + mod.meta.version))) + "\n\n" + error; } @@ -38,12 +43,12 @@ public class CrashSender{ public static void log(Throwable exception){ try{ Core.settings.getDataDirectory().child("crashes").child("crash_" + System.currentTimeMillis() + ".txt") - .writeString(createReport(Strings.neatError(exception))); + .writeString(createReport(exception)); }catch(Throwable ignored){ } } - public static void send(Throwable exception, Cons writeListener){ + public static void handle(Throwable exception, Cons writeListener){ try{ try{ //log to file @@ -59,14 +64,14 @@ public class CrashSender{ //don't create crash logs for custom builds, as it's expected if(OS.username.equals("anuke") && !"steam".equals(Version.modifier)){ - ret(); + // System.exit(1); } //attempt to load version regardless if(Version.number == 0){ try{ ObjectMap map = new ObjectMap<>(); - PropertiesUtils.load(map, new InputStreamReader(CrashSender.class.getResourceAsStream("/version.properties"))); + PropertiesUtils.load(map, new InputStreamReader(CrashHandler.class.getResourceAsStream("/version.properties"))); Version.type = map.get("type"); Version.number = Integer.parseInt(map.get("number")); @@ -87,7 +92,7 @@ public class CrashSender{ try{ File file = new File(OS.getAppDataDirectoryString(Vars.appName), "crashes/crash-report-" + new SimpleDateFormat("MM_dd_yyyy_HH_mm_ss").format(new Date()) + ".txt"); new Fi(OS.getAppDataDirectoryString(Vars.appName)).child("crashes").mkdirs(); - new Fi(file).writeString(createReport(writeException(exception))); + new Fi(file).writeString(createReport(exception)); writeListener.get(file); }catch(Throwable e){ Log.err("Failed to save local crash report.", e); @@ -103,11 +108,41 @@ public class CrashSender{ death.printStackTrace(); } - ret(); + System.exit(1); } - private static void ret(){ - System.exit(1); + /** @return the mod that is likely to have caused the supplied crash */ + public static @Nullable LoadedMod getModCause(Throwable e){ + if(Vars.mods == null) return null; + try{ + for(var element : e.getStackTrace()){ + String name = element.getClassName(); + if(!name.matches("(mindustry|arc|java|javax|sun|jdk)\\..*")){ + for(var mod : mods.list()){ + if(mod.meta.main != null && getMatches(mod.meta.main, name) > 0){ + return mod; + }else if(element.getFileName() != null && element.getFileName().endsWith(".js") && element.getFileName().startsWith(mod.name + "/")){ + return mod; + } + } + } + } + }catch(Throwable ignored){} + return null; + } + + private static int getMatches(String name1, String name2){ + String[] arr1 = name1.split("\\."), arr2 = name2.split("\\."); + int matches = 0; + for(int i = 0; i < Math.min(arr1.length, arr2.length); i++){ + + if(!arr1[i].equals(arr2[i])){ + return i; + }else if(!arr1[i].matches("net|org|com|io")){ //ignore common domain prefixes, as that's usually not enough to call something a "match" + matches ++; + } + } + return matches; } private static String writeException(Throwable e){ diff --git a/desktop/src/mindustry/desktop/DesktopLauncher.java b/desktop/src/mindustry/desktop/DesktopLauncher.java index 44f3b5b03f..2500d18e59 100644 --- a/desktop/src/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/mindustry/desktop/DesktopLauncher.java @@ -7,7 +7,6 @@ import arc.backend.sdl.jni.*; import arc.discord.*; import arc.discord.DiscordRPC.*; import arc.files.*; -import arc.func.*; import arc.math.*; import arc.struct.*; import arc.util.*; @@ -19,6 +18,7 @@ import mindustry.core.*; import mindustry.desktop.steam.*; import mindustry.game.EventType.*; import mindustry.gen.*; +import mindustry.mod.Mods.*; import mindustry.net.*; import mindustry.net.Net.*; import mindustry.service.*; @@ -223,27 +223,30 @@ public class DesktopLauncher extends ClientLauncher{ } static void handleCrash(Throwable e){ - Cons dialog = Runnable::run; boolean badGPU = false; String finalMessage = Strings.getFinalMessage(e); String total = Strings.getCauses(e).toString(); if(total.contains("Couldn't create window") || total.contains("OpenGL 2.0 or higher") || total.toLowerCase().contains("pixel format") || total.contains("GLEW")|| total.contains("unsupported combination of formats")){ - dialog.get(() -> message( + message( total.contains("Couldn't create window") ? "A graphics initialization error has occured! Try to update your graphics drivers:\n" + finalMessage : "Your graphics card does not support the right OpenGL features.\n" + "Try to update your graphics drivers. If this doesn't work, your computer may not support Mindustry.\n\n" + - "Full message: " + finalMessage)); + "Full message: " + finalMessage); badGPU = true; } boolean fbgp = badGPU; - CrashSender.send(e, file -> { + LoadedMod cause = CrashHandler.getModCause(e); + String causeString = cause == null ? (Structs.contains(e.getStackTrace(), st -> st.getClassName().contains("rhino.gen.")) ? "A mod or script has caused Mindustry to crash.\nConsider disabling your mods if the issue persists.\n" : "Mindustry has crashed.") : + "'" + cause.meta.displayName + "' (" + cause.name + ") has caused Mindustry to crash.\nConsider disabling this mod if issues persist.\n"; + + CrashHandler.handle(e, file -> { Throwable fc = Strings.getFinalCause(e); if(!fbgp){ - dialog.get(() -> message("A crash has occured. It has been saved in:\n" + file.getAbsolutePath() + "\n" + fc.getClass().getSimpleName().replace("Exception", "") + (fc.getMessage() == null ? "" : ":\n" + fc.getMessage()))); + message(causeString + "\nThe logs have been saved in:\n" + file.getAbsolutePath() + "\n" + fc.getClass().getSimpleName().replace("Exception", "") + (fc.getMessage() == null ? "" : ":\n" + fc.getMessage())); } }); } diff --git a/ios/src/mindustry/ios/IOSLauncher.java b/ios/src/mindustry/ios/IOSLauncher.java index cf1b6b8208..776a2ec3e0 100644 --- a/ios/src/mindustry/ios/IOSLauncher.java +++ b/ios/src/mindustry/ios/IOSLauncher.java @@ -250,7 +250,7 @@ public class IOSLauncher extends IOSApplication.Delegate{ UIApplication.main(argv, null, IOSLauncher.class); }catch(Throwable t){ //attempt to log the exception - CrashSender.log(t); + CrashHandler.log(t); Log.err(t); //rethrow the exception so it actually crashes throw t; diff --git a/server/src/mindustry/server/ServerLauncher.java b/server/src/mindustry/server/ServerLauncher.java index 1aea11b2cc..d8737fc5e1 100644 --- a/server/src/mindustry/server/ServerLauncher.java +++ b/server/src/mindustry/server/ServerLauncher.java @@ -32,9 +32,9 @@ public class ServerLauncher implements ApplicationListener{ String result = "[" + dateTime.format(LocalDateTime.now()) + "] " + format(tags[level1.ordinal()] + " " + text + "&fr"); System.out.println(result); }; - new HeadlessApplication(new ServerLauncher(), throwable -> CrashSender.send(throwable, f -> {})); + new HeadlessApplication(new ServerLauncher(), throwable -> CrashHandler.handle(throwable, f -> {})); }catch(Throwable t){ - CrashSender.send(t, f -> {}); + CrashHandler.handle(t, f -> {}); } } @@ -46,15 +46,15 @@ public class ServerLauncher implements ApplicationListener{ Vars.loadSettings(); Vars.init(); - + UI.loadColors(); Fonts.loadContentIconsHeadless(); - + content.createBaseContent(); mods.loadScripts(); content.createModContent(); content.init(); - + if(mods.hasContentErrors()){ err("Error occurred loading mod content:"); for(LoadedMod mod : mods.list()){