diff --git a/src/main/java/org/dimdev/ddutils/HasteUpload.java b/src/main/java/org/dimdev/ddutils/HasteUpload.java new file mode 100644 index 00000000..de67925f --- /dev/null +++ b/src/main/java/org/dimdev/ddutils/HasteUpload.java @@ -0,0 +1,40 @@ +package org.dimdev.ddutils; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public final class HasteUpload { + public static String uploadToHaste(String baseUrl, String extension, String str) throws IOException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + + URL uploadURL = new URL(baseUrl + "/documents"); + HttpURLConnection connection = (HttpURLConnection) uploadURL.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8"); + connection.setFixedLengthStreamingMode(bytes.length); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.connect(); + + try { + try (OutputStream os = connection.getOutputStream()) { + os.write(bytes); + } + + try (InputStream is = connection.getInputStream()) { + JsonObject json = new Gson().fromJson(new InputStreamReader(is), JsonObject.class); + return baseUrl + "/" + json.get("key").getAsString() + (extension == null || extension.equals("") ? "" : "." + extension); + } + } finally { + connection.disconnect(); + } + } +} diff --git a/src/main/java/org/dimdev/vanillafix/GuiCrashScreen.java b/src/main/java/org/dimdev/vanillafix/GuiCrashScreen.java new file mode 100644 index 00000000..aee6043c --- /dev/null +++ b/src/main/java/org/dimdev/vanillafix/GuiCrashScreen.java @@ -0,0 +1,72 @@ +package org.dimdev.vanillafix; + +import net.minecraft.client.gui.*; +import net.minecraft.client.resources.I18n; +import net.minecraft.crash.CrashReport; +import net.minecraftforge.fml.relauncher.ReflectionHelper; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dimdev.ddutils.HasteUpload; + +import java.io.File; +import java.net.URI; + +@SideOnly(Side.CLIENT) +public class GuiCrashScreen extends GuiScreen { + private static final String HASTE_BASE_URL = "https://paste.dimdev.org"; + private static final Logger log = LogManager.getLogger(); + + private File reportFile; + private final CrashReport report; + private String hasteLink = null; + + public GuiCrashScreen(File reportFile, CrashReport report) { + this.reportFile = reportFile; + this.report = report; + } + + @Override + public void initGui() { + buttonList.clear(); + buttonList.add(new GuiOptionButton(0, width / 2 - 155, height / 4 + 120 + 12, I18n.format("gui.toTitle"))); + buttonList.add(new GuiOptionButton(1, width / 2 - 155 + 160, height / 4 + 120 + 12, I18n.format("vanillafix.gui.getLink"))); + } + + @Override + protected void actionPerformed(GuiButton button) { + try { + if (button.id == 0) { + mc.displayGuiScreen(new GuiMainMenu()); + } else if (button.id == 1) { + if (hasteLink == null) { + hasteLink = HasteUpload.uploadToHaste(HASTE_BASE_URL, "txt", report.getCompleteReport()); + } + ReflectionHelper.findField(GuiScreen.class, "clickedLinkURI", "field_175286_t").set(this, new URI(hasteLink)); + mc.displayGuiScreen(new GuiConfirmOpenLink(this, hasteLink, 31102009, false)); + } + } catch (Throwable e) { + log.error(e); + } + } + + @Override + protected void keyTyped(char typedChar, int keyCode) { + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + drawDefaultBackground(); + drawCenteredString(fontRenderer, "Minecraft crashed!", width / 2, height / 4 - 60 + 20, 0xFFFFFF); + drawString(fontRenderer, "Minecraft ran into a problem and crashed.", width / 2 - 160, height / 4 - 60 + 60, 0xA0A0A0); + drawString(fontRenderer, "This is probably caused by a bug in one of your mods or vanilla", width / 2 - 160, height / 4 - 60 + 60 + 18, 0xA0A0A0); + drawString(fontRenderer, "Minecraft. Since you have VanillaFix installed, you can return to", width / 2 - 160, height / 4 - 60 + 60 + 27, 0xA0A0A0); + drawString(fontRenderer, "the main menu and keep playing despite the crash.", width / 2 - 160, height / 4 - 60 + 60 + 36, 0xA0A0A0); + drawString(fontRenderer, "A crash report has been generated, and can be found here (click):", width / 2 - 160, height / 4 - 60 + 60 + 54, 0xA0A0A0); + drawCenteredString(fontRenderer, reportFile != null ? "\u00A7n" + reportFile.getName() : "(report failed to save, see the log instead)", width / 2, height / 4 - 60 + 60 + 65, 0x00FF00); + drawString(fontRenderer, "You are encouraged to send it to the mod's author to fix this issue", width / 2 - 160, height / 4 - 60 + 60 + 78, 0xA0A0A0); + drawString(fontRenderer, "Click the \"Get Link\" button to upload the crash report.", width / 2 - 160, height / 4 - 60 + 60 + 87, 0xA0A0A0); + super.drawScreen(mouseX, mouseY, partialTicks); + } +} \ No newline at end of file diff --git a/src/main/java/org/dimdev/vanillafix/mixins/client/MixinMinecraft.java b/src/main/java/org/dimdev/vanillafix/mixins/client/MixinMinecraft.java new file mode 100644 index 00000000..4752dbcb --- /dev/null +++ b/src/main/java/org/dimdev/vanillafix/mixins/client/MixinMinecraft.java @@ -0,0 +1,129 @@ +package org.dimdev.vanillafix.mixins.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiMemoryErrorScreen; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.crash.CrashReport; +import net.minecraft.init.Bootstrap; +import net.minecraft.profiler.ISnooperInfo; +import net.minecraft.util.IThreadListener; +import net.minecraft.util.MinecraftError; +import net.minecraft.util.ReportedException; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import org.apache.logging.log4j.Logger; +import org.dimdev.vanillafix.GuiCrashScreen; +import org.lwjgl.LWJGLException; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +@SuppressWarnings({"unused", "NonConstantFieldWithUpperCaseName", "RedundantThrows"}) // Shadow +@SideOnly(Side.CLIENT) +@Mixin(Minecraft.class) +public abstract class MixinMinecraft implements IThreadListener, ISnooperInfo { + + @Shadow volatile boolean running; + @Shadow private boolean hasCrashed; + @Shadow private CrashReport crashReporter; + + @Shadow private void init() throws LWJGLException, IOException {} + + @Shadow private void runGameLoop() throws IOException {} + + @Shadow public void freeMemory() {} + + @Shadow public void displayGuiScreen(@Nullable GuiScreen guiScreenIn) {} + + @Shadow public CrashReport addGraphicsAndWorldToCrashReport(CrashReport theCrash) { return null; } + + @Shadow @Final private static Logger LOGGER; + + @Shadow public void shutdownMinecraftApplet() {} + + @Shadow public void displayCrashReport(CrashReport crashReportIn) {} + + @SuppressWarnings("CallToSystemGC") + @Overwrite + public void run() { + running = true; + + try { + init(); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Initializing game"); + crashreport.makeCategory("Initialization"); + displayCrashReport(addGraphicsAndWorldToCrashReport(crashreport)); // TODO: GUI for this too + return; + } + + try { + while (running) { + if (!hasCrashed || crashReporter == null) { + try { + runGameLoop(); + } catch (OutOfMemoryError e) { + freeMemory(); + displayGuiScreen(new GuiMemoryErrorScreen()); + System.gc(); + } catch (ReportedException e) { + addGraphicsAndWorldToCrashReport(e.getCrashReport()); + freeMemory(); + LOGGER.fatal("Reported exception thrown!", e); + displayCrashScreen(e.getCrashReport()); + } catch (Throwable e) { + CrashReport report = addGraphicsAndWorldToCrashReport(new CrashReport("Unexpected error", e)); + freeMemory(); + LOGGER.fatal("Unreported exception thrown!", e); + displayCrashScreen(report); + } + } else { + displayCrashReport(crashReporter); + } + } + } catch (MinecraftError ignored) { + } finally { + shutdownMinecraftApplet(); + } + } + + public void displayCrashScreen(CrashReport report) { + try { + File crashReportsDir = new File(Minecraft.getMinecraft().mcDataDir, "crash-reports"); + File crashReportSaveFile = new File(crashReportsDir, "crash-" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date()) + "-client.txt"); + + // Print the report in bootstrap + Bootstrap.printToSYSOUT(report.getCompleteReport()); + + // Save the report and print file in bootstrap + File reportFile = null; + if (report.getFile() != null) { + reportFile = report.getFile(); + } else if (report.saveToFile(crashReportSaveFile)) { + reportFile = crashReportSaveFile; + } + + if (reportFile != null) { + Bootstrap.printToSYSOUT("Recoverable game crash! Crash report saved to: " + reportFile); + } else { + Bootstrap.printToSYSOUT("Recoverable game crash! Crash report could not be saved."); + } + + // Display the crash screen + displayGuiScreen(new GuiCrashScreen(reportFile, report)); + + // Keep running + hasCrashed = false; + } catch (Throwable e) { + LOGGER.error("The crash screen threw an error, reverting to default crash report", e); + displayCrashReport(report); + } + } +} diff --git a/src/main/resources/assets/dimdoors/lang/en_US.lang b/src/main/resources/assets/dimdoors/lang/en_US.lang index d0538159..9bf90e56 100644 --- a/src/main/resources/assets/dimdoors/lang/en_US.lang +++ b/src/main/resources/assets/dimdoors/lang/en_US.lang @@ -201,3 +201,5 @@ dimdoors.graphics.riftSize=Rift Size dimdoors.graphics.riftSize.tooltip=Multiplier affecting how large rifts should be rendered, 1 being the default size. dimdoors.graphics.riftJitter=Rift Jitter dimdoors.graphics.riftJitter.tooltip=Multiplier affecting how much rifts should jitter, 1 being the default size. + +vanillafix.gui.getLink=Get Link diff --git a/src/main/resources/org.dimdev.vanillafix.mixins.json b/src/main/resources/org.dimdev.vanillafix.mixins.json index 341185f8..1b6fdc9a 100644 --- a/src/main/resources/org.dimdev.vanillafix.mixins.json +++ b/src/main/resources/org.dimdev.vanillafix.mixins.json @@ -7,7 +7,8 @@ "mixins": [ "MixinNetHandlerPlayServer" ], - "client": [], + "client": [ + "client.MixinMinecraft"], "server": [], "injectors": { "defaultRequire": 1