diff --git a/core/src/io/anuke/mindustry/game/Content.java b/core/src/io/anuke/mindustry/game/Content.java index b0b4d03b1b..0c03f47eff 100644 --- a/core/src/io/anuke/mindustry/game/Content.java +++ b/core/src/io/anuke/mindustry/game/Content.java @@ -1,5 +1,6 @@ package io.anuke.mindustry.game; +import io.anuke.arc.files.*; import io.anuke.arc.util.ArcAnnotate.*; import io.anuke.mindustry.*; import io.anuke.mindustry.mod.Mods.*; @@ -11,6 +12,8 @@ public abstract class Content implements Comparable{ public final short id; /** The mod that loaded this piece of content. */ public @Nullable LoadedMod mod; + /** File that this content was loaded from. */ + public @Nullable FileHandle sourceFile; public Content(){ this.id = (short)Vars.content.getBy(getContentType()).size; diff --git a/core/src/io/anuke/mindustry/mod/ContentParser.java b/core/src/io/anuke/mindustry/mod/ContentParser.java index eaeb46b10b..a8b9a5ee91 100644 --- a/core/src/io/anuke/mindustry/mod/ContentParser.java +++ b/core/src/io/anuke/mindustry/mod/ContentParser.java @@ -4,6 +4,7 @@ import io.anuke.arc.*; import io.anuke.arc.audio.*; import io.anuke.arc.collection.Array; import io.anuke.arc.collection.*; +import io.anuke.arc.files.*; import io.anuke.arc.function.*; import io.anuke.arc.graphics.*; import io.anuke.arc.util.ArcAnnotate.*; @@ -229,9 +230,11 @@ public class ContentParser{ /** Call to read a content's extra info later.*/ private void read(Runnable run){ + Content cont = currentContent; LoadedMod mod = currentMod; reads.add(() -> { this.currentMod = mod; + this.currentContent = cont; run.run(); }); } @@ -255,7 +258,7 @@ public class ContentParser{ try{ reads.each(Runnable::run); }catch(Exception e){ - throw new RuntimeException("Error occurred parsing content: " + currentContent, e); + Vars.mods.handleError(new ModLoadException("Error occurred parsing content: " + currentContent, currentContent, e), currentMod); } reads.clear(); } @@ -265,9 +268,10 @@ public class ContentParser{ * @param name the name of the file without its extension * @param json the json to parse * @param type the type of content this is + * @param file file that this content is being parsed from * @return the content that was parsed */ - public Content parse(LoadedMod mod, String name, String json, ContentType type) throws Exception{ + public Content parse(LoadedMod mod, String name, String json, FileHandle file, ContentType type) throws Exception{ if(contentTypes.isEmpty()){ init(); } @@ -279,6 +283,7 @@ public class ContentParser{ currentMod = mod; Content c = parsers.get(type).parse(mod.name, name, value); + c.sourceFile = file; c.mod = mod; checkNulls(c); return c; diff --git a/core/src/io/anuke/mindustry/mod/Mods.java b/core/src/io/anuke/mindustry/mod/Mods.java index 181887e65e..24da6e0af1 100644 --- a/core/src/io/anuke/mindustry/mod/Mods.java +++ b/core/src/io/anuke/mindustry/mod/Mods.java @@ -268,25 +268,28 @@ public class Mods implements Loadable{ /** Creates all the content found in mod files. */ public void loadContent(){ for(LoadedMod mod : loaded){ - if(mod.root.child("content").exists()){ - FileHandle contentRoot = mod.root.child("content"); - for(ContentType type : ContentType.all){ - FileHandle folder = contentRoot.child(type.name().toLowerCase() + "s"); - if(folder.exists()){ - for(FileHandle file : folder.list()){ - if(file.extension().equals("json")){ - try{ - //this binds the content but does not load it entirely - Content loaded = parser.parse(mod, file.nameWithoutExtension(), file.readString("UTF-8"), type); - Log.info("[{0}] Loaded '{1}'.", mod.meta.name, (loaded instanceof UnlockableContent ? ((UnlockableContent)loaded).localizedName : loaded)); - }catch(Exception e){ - throw new RuntimeException("Failed to parse content file '" + file + "' for mod '" + mod.meta.name + "'.", e); + safeRun(mod, () -> { + if(mod.root.child("content").exists()){ + FileHandle contentRoot = mod.root.child("content"); + for(ContentType type : ContentType.all){ + FileHandle folder = contentRoot.child(type.name().toLowerCase() + "s"); + if(folder.exists()){ + for(FileHandle file : folder.list()){ + if(file.extension().equals("json")){ + try{ + //this binds the content but does not load it entirely + Content loaded = parser.parse(mod, file.nameWithoutExtension(), file.readString("UTF-8"), file, type); + Log.info("[{0}] Loaded '{1}'.", mod.meta.name, + (loaded instanceof UnlockableContent ? ((UnlockableContent)loaded).localizedName : loaded)); + }catch(Exception e){ + throw new RuntimeException("Failed to parse content file '" + file + "' for mod '" + mod.meta.name + "'.", e); + } } } } } } - } + }); } //this finishes parsing content fields @@ -345,7 +348,40 @@ public class Mods implements Loadable{ /** Iterates through each mod with a main class.*/ public void each(Consumer cons){ - loaded.each(p -> p.mod != null, p -> cons.accept(p.mod)); + loaded.each(p -> p.mod != null, p -> safeRun(p, () -> cons.accept(p.mod))); + } + + public void handleError(Throwable t, LoadedMod mod){ + Array causes = Strings.getCauses(t); + Content content = null; + for(Throwable e : causes){ + if(e instanceof ModLoadException && ((ModLoadException) e).content != null){ + content = ((ModLoadException) e).content; + } + } + + String realCause = ""; + for(int i = causes.size -1 ; i >= 0; i--){ + if(causes.get(i).getMessage() != null){ + realCause = causes.get(i).getMessage(); + break; + } + } + + if(content != null){ + throw new ModLoadException(Strings.format("Error loading '{0}' from mod '{1}' ({2}):\n{3}", + content, mod.meta.name, content.sourceFile.name(), realCause), content, t); + }else{ + throw new ModLoadException("Error loading mod " + mod.meta.name, t); + } + } + + public void safeRun(LoadedMod mod, Runnable run){ + try{ + run.run(); + }catch(Throwable t){ + handleError(t, mod); + } } /** Loads a mod file+meta, but does not add it to the list. @@ -439,4 +475,22 @@ public class Mods implements Loadable{ /** Hidden mods are only server-side or client-side, and do not support adding new content. */ public boolean hidden; } + + /** Thrown when an error occurs while loading a mod.*/ + public static class ModLoadException extends RuntimeException{ + public Content content; + public LoadedMod mod; + + public ModLoadException(String message, Throwable cause){ + super(message, cause); + } + + public ModLoadException(String message, @Nullable Content content, Throwable cause){ + super(message, cause); + this.content = content; + if(content != null){ + this.mod = content.mod; + } + } + } } diff --git a/core/src/io/anuke/mindustry/net/CrashSender.java b/core/src/io/anuke/mindustry/net/CrashSender.java index 332371ff58..a919b70538 100644 --- a/core/src/io/anuke/mindustry/net/CrashSender.java +++ b/core/src/io/anuke/mindustry/net/CrashSender.java @@ -26,7 +26,7 @@ public class CrashSender{ exception.printStackTrace(); //don't create crash logs for custom builds, as it's expected - if(Version.build == -1 || (System.getProperty("user.name").equals("anuke") && "release".equals(Version.modifier))) return; + //if(Version.build == -1 || (System.getProperty("user.name").equals("anuke") && "release".equals(Version.modifier))) return; //attempt to load version regardless if(Version.number == 0){ diff --git a/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java index a9a1049aac..92a69ddcc3 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java @@ -70,7 +70,7 @@ public class HostDialog extends FloatingDialog{ player.isAdmin = true; if(steam){ - Core.app.post(() -> Core.settings.getBoolOnce("steampublic", () -> { + Core.app.post(() -> Core.settings.getBoolOnce("steampublic2", () -> { ui.showCustomConfirm("$setting.publichost.name", "$public.confirm", "$yes", "$no", () -> { Core.settings.putSave("publichost", true); platform.updateLobby(); diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java index bff0615ca0..f8d2f873e2 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java @@ -23,6 +23,7 @@ import io.anuke.mindustry.desktop.steam.*; import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.game.Version; import io.anuke.mindustry.maps.Map; +import io.anuke.mindustry.mod.Mods.*; import io.anuke.mindustry.net.*; import io.anuke.mindustry.net.Net.*; import io.anuke.mindustry.ui.*; @@ -229,7 +230,10 @@ public class DesktopLauncher extends ClientLauncher{ boolean fbgp = badGPU; CrashSender.send(e, file -> { - Throwable cause = Strings.getFinalCause(e); + Array causes = Strings.getCauses(e); + Throwable fc = causes.find(t -> t instanceof ModLoadException); + if(fc == null) fc = Strings.getFinalCause(e); + Throwable cause = fc; if(!fbgp){ dialog.accept(() -> message("A crash has occured. It has been saved in:\n" + file.getAbsolutePath() + "\n" + cause.getClass().getSimpleName().replace("Exception", "") + (cause.getMessage() == null ? "" : ":\n" + cause.getMessage()))); } diff --git a/gradle.properties b/gradle.properties index 8bc8ccdec2..9806da2c03 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.daemon=true org.gradle.jvmargs=-Xms256m -Xmx1024m -archash=0db0c0d660952fb57cda0f83d78daa9acbcd5439 +archash=9e37a345d59b09e8677adbec29cb3981d26fe86b