diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dafe649 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# eclipse +eclipse +bin +*.launch +.settings +.metadata +.classpath +.project + +# idea +out +classes +*.ipr +*.iws +*.iml +.idea + +# gradle +build +.gradle + +#Netbeans +.nb-gradle +.nb-gradle-properties + +# other +run +.DS_Store +Thumbs.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c06eae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Dimensional Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0afd9dd --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# TooManyCrashes +[![Travis CI](https://travis-ci.org/DimensionalDevelopment/TooManyCrashes.svg?branch=master)](https://travis-ci.org/DimensionalDevelopment/TooManyCrashes) +[![Discord](https://img.shields.io/discord/214574167192764416.svg)](https://discord.gg/Z2EsuxJ) +[![CurseForge](http://cf.way2muchnoise.eu/toomanycrashes.svg)](https://minecraft.curseforge.com/projects/toomanycrashes) + +TooManyCrashes is a port of VanillaFix's crash improvements to Fabric. + +CurseForge: https://minecraft.curseforge.com/projects/toomanycrashes +Discord: https://discord.gg/Z2EsuxJ +Latest development build: https://github.com/DimensionalDevelopment/TooManyCrashes/releases/latest diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4519069 --- /dev/null +++ b/build.gradle @@ -0,0 +1,61 @@ +buildscript { + repositories { + jcenter() + maven { url = 'https://maven.fabricmc.net/' } + } + dependencies { + classpath "com.github.jengelman.gradle.plugins:shadow:2.0.4" + classpath "net.fabricmc:fabric-loom:0.2.0-SNAPSHOT" + } +} + +apply plugin: "java" +apply plugin: "com.github.johnrengelman.shadow" +apply plugin: net.fabricmc.loom.LoomGradlePlugin + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + minecraft "com.mojang:minecraft:19w02a" + mappings "net.fabricmc:yarn:19w02a.20" + modCompile "net.fabricmc:fabric-loader:0.3.2.93" + modCompile "net.fabricmc:fabric:0.1.4.71" +} + +def travisBuildNumber = System.getenv("TRAVIS_BUILD_NUMBER") +def versionSuffix = travisBuildNumber != null ? travisBuildNumber : "SNAPSHOT" + +version "1.0-$versionSuffix" +group "org.dimdev.toomanycrashes" +archivesBaseName = "TooManyCrashes" + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +jar { + classifier "thin" +} + +shadowJar { + classifier "" +} + +processResources { + filesMatching("fabric.mod.json") { + expand "version": project.version + } + + inputs.property "version", project.version +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier "sources" + from sourceSets.main.allSource +} + +artifacts { + archives jar + archives shadowJar + archives sourcesJar +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1948b90 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2c45a4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/org/dimdev/toomanycrashes/CrashScreenGui.java b/src/main/java/org/dimdev/toomanycrashes/CrashScreenGui.java new file mode 100644 index 0000000..f3c8c51 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/CrashScreenGui.java @@ -0,0 +1,61 @@ +package org.dimdev.toomanycrashes; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.MainMenuGui; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.util.crash.CrashReport; + +@Environment(EnvType.CLIENT) +public class CrashScreenGui extends ProblemScreenGui { + public CrashScreenGui(CrashReport report) { + super(report); + } + + @Override + public void onInitialized() { + super.onInitialized(); + ButtonWidget mainMenuButton = new ButtonWidget(0, width / 2 - 155, height / 4 + 120 + 12, 150, 20, I18n.translate("gui.toTitle")) { + @Override + public void onPressed(double x, double y) { + client.openGui(new MainMenuGui()); + } + }; + + if (ModConfig.instance().disableReturnToMainMenu) { + mainMenuButton.enabled = false; + mainMenuButton.text = I18n.translate("toomanycrashes.gui.disabledByConfig"); + } + + addButton(mainMenuButton); + } + + @Override + public void draw(int mouseX, int mouseY, float partialTicks) { // TODO: localize number of lines + drawBackground(); + drawStringCentered(fontRenderer, I18n.translate("toomanycrashes.crashscreen.title"), width / 2, height / 4 - 40, 0xFFFFFF); + + int textColor = 0xD0D0D0; + int x = width / 2 - 155; + int y = height / 4; + + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.summary"), x, y, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph1.line1"), x, y += 18, textColor); + + drawStringCentered(fontRenderer, getModListString(), width / 2, y += 11, 0xE0E000); + + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph2.line1"), x, y += 11, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph2.line2"), x, y += 9, textColor); + + String fileNameString = report.getFile() != null ? "\u00A7n" + report.getFile().getName() : I18n.translate("toomanycrashes.crashscreen.reportSaveFailed"); + drawStringCentered(fontRenderer, fileNameString, width / 2, y += 11, 0x00FF00); + + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph3.line1"), x, y += 12, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph3.line2"), x, y += 9, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph3.line3"), x, y += 9, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph3.line4"), x, y + 9, textColor); + + super.draw(mouseX, mouseY, partialTicks); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/CrashUtils.java b/src/main/java/org/dimdev/toomanycrashes/CrashUtils.java new file mode 100644 index 0000000..d829519 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/CrashUtils.java @@ -0,0 +1,44 @@ +package org.dimdev.toomanycrashes; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.crash.CrashReport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +public final class CrashUtils { + private static final Logger LOGGER = LogManager.getLogger("TMC"); + private static boolean isClient; + + static { + try { + isClient = MinecraftClient.getInstance() != null; + } catch (NoClassDefFoundError e) { + isClient = false; + } + } + + public static void outputReport(CrashReport report) { + try { + if (report.getFile() == null) { + String reportName = "crash-"; + reportName += new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date()); + reportName += MinecraftClient.getInstance().isMainThread() ? "-client" : "-server"; + reportName += ".txt"; + + File reportsDir = isClient ? new File(MinecraftClient.getInstance().runDirectory, "crash-reports") : new File("crash-reports"); + File reportFile = new File(reportsDir, reportName); + + report.writeToFile(reportFile); + } + } catch (Throwable e) { + LOGGER.fatal("Failed saving report", e); + } + + LOGGER.fatal("Minecraft ran into a problem! " + (report.getFile() != null ? "Report saved to: " + report.getFile() : "Crash report could not be saved.") + "\n" + + report.asString()); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/DeobfuscatingRewritePolicy.java b/src/main/java/org/dimdev/toomanycrashes/DeobfuscatingRewritePolicy.java new file mode 100644 index 0000000..12322b2 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/DeobfuscatingRewritePolicy.java @@ -0,0 +1,46 @@ +package org.dimdev.toomanycrashes; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.rewrite.RewriteAppender; +import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy; +import org.apache.logging.log4j.core.config.AppenderRef; +import org.apache.logging.log4j.core.config.LoggerConfig; + +import java.util.ArrayList; +import java.util.List; + +public class DeobfuscatingRewritePolicy implements RewritePolicy { + @Override + public LogEvent rewrite(LogEvent source) { + if (source.getThrown() != null) StacktraceDeobfuscator.deobfuscateThrowable(source.getThrown()); + return source; + } + + /** Modifies the log4j config to add the policy **/ + public static void install() { + Logger rootLogger = (Logger) LogManager.getRootLogger(); + LoggerConfig loggerConfig = rootLogger.get(); + + // Remove appender refs from config + List appenderRefs = new ArrayList<>(loggerConfig.getAppenderRefs()); + for (AppenderRef appenderRef : appenderRefs) { + loggerConfig.removeAppender(appenderRef.getRef()); + } + + // Create the RewriteAppender, which wraps the appenders + RewriteAppender rewriteAppender = RewriteAppender.createAppender( + "TooManyCrashesDeobfuscatingAppender", + "true", + appenderRefs.toArray(new AppenderRef[0]), + rootLogger.getContext().getConfiguration(), + new DeobfuscatingRewritePolicy(), + null + ); + rewriteAppender.start(); + + // Add the new appender + loggerConfig.addAppender(rewriteAppender, null, null); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/InitErrorScreenGui.java b/src/main/java/org/dimdev/toomanycrashes/InitErrorScreenGui.java new file mode 100644 index 0000000..5d919e2 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/InitErrorScreenGui.java @@ -0,0 +1,51 @@ +package org.dimdev.toomanycrashes; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.util.crash.CrashReport; + +@Environment(EnvType.CLIENT) +public class InitErrorScreenGui extends ProblemScreenGui { + + public InitErrorScreenGui(CrashReport report) { + super(report); + } + + @Override + public void onInitialized() { + super.onInitialized(); + ButtonWidget getLinkButton = buttons.get(0); + getLinkButton.x = width / 2 - 155; + getLinkButton.y = height / 4 + 120 + 12; + getLinkButton.setWidth(310); + } + + @Override + public void draw(int mouseX, int mouseY, float partialTicks) { // TODO: localize number of lines + drawBackground(); + drawStringCentered(fontRenderer, I18n.translate("toomanycrashes.initerrorscreen.title"), width / 2, height / 4 - 40, 0xFFFFFF); + + int textColor = 0xD0D0D0; + int x = width / 2 - 155; + int y = height / 4; + + drawString(fontRenderer, I18n.translate("toomanycrashes.initerrorscreen.summary"), x, y, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph1.line1"), x, y += 18, textColor); + + drawStringCentered(fontRenderer, getModListString(), width / 2, y += 11, 0xE0E000); + + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph2.line1"), x, y += 11, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.crashscreen.paragraph2.line2"), x, y += 9, textColor); + + drawStringCentered(fontRenderer, report.getFile() != null ? "\u00A7n" + report.getFile().getName() : I18n.translate("toomanycrashes.crashscreen.reportSaveFailed"), width / 2, y += 11, 0x00FF00); + + drawString(fontRenderer, I18n.translate("toomanycrashes.initerrorscreen.paragraph3.line1"), x, y += 12, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.initerrorscreen.paragraph3.line2"), x, y += 9, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.initerrorscreen.paragraph3.line3"), x, y += 9, textColor); + drawString(fontRenderer, I18n.translate("toomanycrashes.initerrorscreen.paragraph3.line4"), x, y + 9, textColor); + + super.draw(mouseX, mouseY, partialTicks); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/ModConfig.java b/src/main/java/org/dimdev/toomanycrashes/ModConfig.java new file mode 100644 index 0000000..cb98ced --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/ModConfig.java @@ -0,0 +1,39 @@ +package org.dimdev.toomanycrashes; + +import com.google.gson.Gson; +import net.fabricmc.loader.FabricLoader; + +import java.io.*; + +public class ModConfig { + public static final File CONFIG_FILE = new File(FabricLoader.INSTANCE.getConfigDirectory(), "toomanycrashes.json"); + public static final Gson GSON = new Gson(); + private static ModConfig instance = new ModConfig(); + + public String hasteURL = "https://paste.dimdev.org"; + public boolean disableReturnToMainMenu = false; + + public static ModConfig instance() { + if (instance != null) { + return instance; + } + + if (CONFIG_FILE.exists()) { + try { + return instance = new Gson().fromJson(new FileReader(CONFIG_FILE), ModConfig.class); + } catch (FileNotFoundException e) { + throw new IllegalStateException(e); + } + } + + instance = new ModConfig(); + + try { + GSON.toJson(instance, new FileWriter(CONFIG_FILE)); + } catch (IOException e) { + e.printStackTrace(); + } + + return instance; + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/PatchedCrashReport.java b/src/main/java/org/dimdev/toomanycrashes/PatchedCrashReport.java new file mode 100644 index 0000000..b466e0d --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/PatchedCrashReport.java @@ -0,0 +1,15 @@ +package org.dimdev.toomanycrashes; + +import net.fabricmc.loader.ModInfo; + +import java.util.Set; + +public interface PatchedCrashReport { + Set getSuspectedMods(); + + interface Element { + String invokeGetName(); + + String invokeGetDetail(); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/PatchedIntegratedServer.java b/src/main/java/org/dimdev/toomanycrashes/PatchedIntegratedServer.java new file mode 100644 index 0000000..e94a014 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/PatchedIntegratedServer.java @@ -0,0 +1,5 @@ +package org.dimdev.toomanycrashes; + +public interface PatchedIntegratedServer { + void setCrashNextTick(); +} diff --git a/src/main/java/org/dimdev/toomanycrashes/ProblemScreenGui.java b/src/main/java/org/dimdev/toomanycrashes/ProblemScreenGui.java new file mode 100644 index 0000000..103d2dd --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/ProblemScreenGui.java @@ -0,0 +1,85 @@ +package org.dimdev.toomanycrashes; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.loader.ModInfo; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.ingame.ConfirmChatLinkGui; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.util.crash.CrashReport; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dimdev.utils.HasteUpload; + +import java.lang.reflect.Field; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Environment(EnvType.CLIENT) +public abstract class ProblemScreenGui extends Gui { + private static final Logger LOGGER = LogManager.getLogger(); + + protected final CrashReport report; + private String hasteLink = null; + private String modListString = null; + + protected ProblemScreenGui(CrashReport report) { + this.report = report; + } + + @Override + public void onInitialized() { + addButton(new ButtonWidget(1, width / 2 - 155 + 160, height / 4 + 120 + 12, 150, 20, I18n.translate("toomanycrashes.gui.getLink", new Object[0])) { + @Override + public void onPressed(double x, double y) { + try { + if (hasteLink == null) { + hasteLink = HasteUpload.uploadToHaste(ModConfig.instance().hasteURL, "mccrash", report.asString()); + } + Field uriField; + try { + //noinspection JavaReflectionMemberAccess + uriField = Gui.class.getDeclaredField("field_2562"); + } catch (NoSuchFieldException e) { + uriField = Gui.class.getDeclaredField("uri"); + } + uriField.setAccessible(true); + uriField.set(ProblemScreenGui.this, new URI(hasteLink)); + client.openGui(new ConfirmChatLinkGui(ProblemScreenGui.this, hasteLink, 31102009, false)); + } catch (Throwable e) { + LOGGER.error("Exception when crash menu button clicked:", e); + text = I18n.translate("toomanycrashes.gui.failed"); + enabled = false; + } + } + }); + } + + @Override + public boolean doesEscapeKeyClose() { + return false; + } + + protected String getModListString() { + if (modListString == null) { + Set suspectedMods = ((PatchedCrashReport) report).getSuspectedMods(); + if (suspectedMods == null) { + return modListString = I18n.translate("toomanycrashes.crashscreen.identificationErrored"); + } + List modNames = new ArrayList<>(); + for (ModInfo mod : suspectedMods) { + modNames.add(mod.getName()); + } + if (modNames.isEmpty()) { + modListString = I18n.translate("toomanycrashes.crashscreen.unknownCause"); + } else { + modListString = StringUtils.join(modNames, ", "); + } + } + return modListString; + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/StacktraceDeobfuscator.java b/src/main/java/org/dimdev/toomanycrashes/StacktraceDeobfuscator.java new file mode 100644 index 0000000..3250cd9 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/StacktraceDeobfuscator.java @@ -0,0 +1,90 @@ +package org.dimdev.toomanycrashes; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.*; + +public final class StacktraceDeobfuscator { + private static final String MAPPINGS_URL = "https://gist.githubusercontent.com/Runemoro/cc6ad843f5403b870214ae34baaa4b60/raw/c7be9a0d5be49eaa365f915fd6326c1ddd419bbd/yarn-mappings.csv"; + private static final boolean DEBUG_IN_DEV = false; // Makes this deobf -> obf for testing in dev. Don't forget to set to false when done! + private static HashMap mappings = null; + + /** + * If the file does not exits, downloads latest method mappings and saves them to it. + * Initializes a HashMap between obfuscated and deobfuscated names from that file. + */ + public static void init(File mappingsFile) { + if (mappings != null) return; + + // Download the file if necessary + if (!mappingsFile.exists()) { + try { + try (InputStream is = new URL(MAPPINGS_URL).openStream()) { + Files.copy(is, mappingsFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // Read the mapping + HashMap mappings = new HashMap<>(); + try (Scanner scanner = new Scanner(mappingsFile)) { + scanner.nextLine(); // Skip CSV header + while (scanner.hasNext()) { + String[] mappingLine = scanner.nextLine().split(","); + String obfName = mappingLine[0]; + String deobfName = mappingLine[1]; + + if (!DEBUG_IN_DEV) { + mappings.put(obfName, deobfName); + } else { + mappings.put(deobfName, obfName); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + StacktraceDeobfuscator.mappings = mappings; + } + + public static void deobfuscateThrowable(Throwable t) { + Deque queue = new ArrayDeque<>(); + queue.add(t); + while (!queue.isEmpty()) { + t = queue.remove(); + t.setStackTrace(deobfuscateStacktrace(t.getStackTrace())); + if (t.getCause() != null) queue.add(t.getCause()); + Collections.addAll(queue, t.getSuppressed()); + } + } + + public static StackTraceElement[] deobfuscateStacktrace(StackTraceElement[] stackTrace) { + if (mappings == null) { + return stackTrace; + } + + int index = 0; + for (StackTraceElement el : stackTrace) { + stackTrace[index++] = new StackTraceElement( + mappings.getOrDefault(el.getClassName(), el.getClassName()), + mappings.getOrDefault(el.getMethodName(), el.getMethodName()), + el.getFileName(), + el.getLineNumber() + ); + } + return stackTrace; + } + + public static void main(String[] args) { + init(new File("mappings.csv")); + for (Map.Entry entry : mappings.entrySet()) { + System.out.println(entry.getKey() + " <=> " + entry.getValue()); + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/StateManager.java b/src/main/java/org/dimdev/toomanycrashes/StateManager.java new file mode 100644 index 0000000..3acd7f6 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/StateManager.java @@ -0,0 +1,36 @@ +package org.dimdev.toomanycrashes; + +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Allows registering objects to be reset after a crash. Objects registered + * use WeakReferences, so they will be garbage-collected despite still being + * registered here. + */ +public class StateManager { + public interface IResettable { + default void register() { + resettableRefs.add(new WeakReference<>(this)); + } + + void resetState(); + } + + // Use WeakReference to allow garbage collection, preventing memory leaks + private static Set> resettableRefs = new HashSet<>(); + + public static void resetStates() { + Iterator> iterator = resettableRefs.iterator(); + while (iterator.hasNext()) { + WeakReference ref = iterator.next(); + if (ref.get() != null) { + ref.get().resetState(); + } else { + iterator.remove(); + } + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/TooManyCrashes.java b/src/main/java/org/dimdev/toomanycrashes/TooManyCrashes.java new file mode 100644 index 0000000..3a49057 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/TooManyCrashes.java @@ -0,0 +1,58 @@ +package org.dimdev.toomanycrashes; + +import net.fabricmc.loader.FabricLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dimdev.utils.SSLUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class TooManyCrashes { + private static final Logger LOGGER = LogManager.getLogger("TooManyCrashes"); + private static final long MAPPINGS_CACHE_DURATION = 2 * 24 * 60 * 60 * 1000; + + public static void init() { + ModConfig.instance(); + trustIdenTrust(); + initStacktraceDeobfuscator(); + } + + private static void trustIdenTrust() { + // Trust the "IdenTrust DST Root CA X3" certificate (used by Let's Encrypt, which is used by paste.dimdev.org) + try (InputStream keyStoreInputStream = TooManyCrashes.class.getResourceAsStream("/dst_root_ca_x3.jks")) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(keyStoreInputStream, "password".toCharArray()); + SSLUtils.trustCertificates(keyStore); + } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) { + throw new RuntimeException(e); + } + } + + private static void initStacktraceDeobfuscator() { + File modDir = new File(FabricLoader.INSTANCE.getConfigDirectory(), "toomanycrashes"); + modDir.mkdirs(); + + LOGGER.info("Initializing StacktraceDeobfuscator"); + try { + File mappings = new File(modDir, "mappings-" + System.currentTimeMillis() / MAPPINGS_CACHE_DURATION + ".csv"); + if (mappings.exists()) { + LOGGER.info("Found mappings: " + mappings.getName()); + } else { + LOGGER.info("Downloading latest mappings to: " + mappings.getName()); + } + StacktraceDeobfuscator.init(mappings); + } catch (Exception e) { + LOGGER.error("Failed to get mappings!", e); + } + LOGGER.info("Done initializing StacktraceDeobfuscator"); + + // Install the log exception deobfuscation rewrite policy + DeobfuscatingRewritePolicy.install(); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/MixinCrashReport.java b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinCrashReport.java new file mode 100644 index 0000000..4a87081 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinCrashReport.java @@ -0,0 +1,135 @@ +package org.dimdev.toomanycrashes.mixins; + +import net.fabricmc.loader.ModInfo; +import net.minecraft.util.crash.CrashReport; +import net.minecraft.util.crash.CrashReportSection; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.dimdev.toomanycrashes.PatchedCrashReport; +import org.dimdev.toomanycrashes.StacktraceDeobfuscator; +import org.dimdev.utils.ModIdentifier; +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 org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; + +@Mixin(value = CrashReport.class, priority = 500) +public abstract class MixinCrashReport implements PatchedCrashReport { + @Shadow @Final private CrashReportSection systemDetailsSection; + @Shadow @Final private List otherSections; + @Shadow @Final private Throwable cause; + @Shadow @Final private String message; + @Shadow private static String generateWittyComment() { return null; } + + private static final boolean ANNOYING_EASTER_EGG_DISABLED = true; + private Set suspectedMods = null; + + @Override + public Set getSuspectedMods() { + return suspectedMods; + } + + /** + * @reason Deobfuscate the stacktrace + */ + @Inject(method = "fillSystemDetails", at = @At("HEAD")) + private void beforeFillSystemDetails(CallbackInfo ci) { + StacktraceDeobfuscator.deobfuscateThrowable(cause); + } + + /** + * @reason Adds a list of mods which may have caused the crash to the report. + */ + @Inject(method = "fillSystemDetails", at = @At("TAIL")) + private void afterFillSystemDetails(CallbackInfo ci) { + systemDetailsSection.add("Suspected Mods", () -> { + try { + suspectedMods = ModIdentifier.identifyFromStacktrace(cause); + + String modListString = "Unknown"; + List modNames = new ArrayList<>(); + for (ModInfo mod : suspectedMods) { + modNames.add(mod.getName() + " (" + mod.getId() + ")"); + } + + if (!modNames.isEmpty()) { + modListString = StringUtils.join(modNames, ", "); + } + return modListString; + } catch (Throwable e) { + return ExceptionUtils.getStackTrace(e).replace("\t", " "); + } + }); + } + + /** + * @reason Improve report formatting + */ + @Overwrite + public String asString() { + StringBuilder builder = new StringBuilder(); + + builder.append("---- Minecraft Crash Report ----\n") + .append("// ").append(ANNOYING_EASTER_EGG_DISABLED ? generateWittyComment() : generateEasterEggComment()) + .append("\n\n") + .append("Time: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").format(new Date())).append("\n") + .append("Description: ").append(message) + .append("\n\n") + .append(stacktraceToString(cause).replace("\t", " ")) // Vanilla's getCauseStackTraceOrString doesn't print causes and suppressed exceptions + .append("\n\nA detailed walkthrough of the error, its code path and all known details is as follows:\n"); + + for (int i = 0; i < 87; i++) { + builder.append("-"); + } + + builder.append("\n\n"); + addStackTrace(builder); + return builder.toString().replace("\t", " "); + } + + private static String stacktraceToString(Throwable cause) { + StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + return writer.toString(); + } + + /** + * @reason Improve report formatting + */ + @Overwrite + public void addStackTrace(StringBuilder builder) { + for (CrashReportSection section : otherSections) { + section.addStackTrace(builder); + builder.append("\n"); + } + + systemDetailsSection.addStackTrace(builder); + } + + private String generateEasterEggComment() { + try { + String comment = generateWittyComment(); + + if (comment.contains("Dinnerbone")) { + ModInfo mod = suspectedMods.iterator().next(); + String author = mod.getAuthors().get(0).getName(); + comment = comment.replace("Dinnerbone", author); + } + + return comment; + } catch (Throwable ignored) {} + + return generateWittyComment(); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/MixinCrashReportSection.java b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinCrashReportSection.java new file mode 100644 index 0000000..2b6dab8 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinCrashReportSection.java @@ -0,0 +1,84 @@ +package org.dimdev.toomanycrashes.mixins; + +import net.minecraft.util.crash.CrashReportSection; +import org.dimdev.toomanycrashes.PatchedCrashReport; +import org.dimdev.toomanycrashes.StacktraceDeobfuscator; +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 java.util.List; + +@Mixin(CrashReportSection.class) +public class MixinCrashReportSection { + @Shadow @Final private String title; + @Shadow @Final private List elements; + @Shadow private StackTraceElement[] stackTrace; + + @Mixin(targets = "net.minecraft.util.crash.CrashReportSection$Element") + public static abstract class MixinElement implements PatchedCrashReport.Element { + @Shadow + public abstract String getName(); + + @Shadow + public abstract String getDetail(); + + @Override + public String invokeGetName() { + return getName(); + } + + @Override + public String invokeGetDetail() { + return getDetail(); + } + } + + /** + * @reason Disable stack trace pruning + */ + @Overwrite + public void method_580(int size) {} + + /** + * @reason Disable stack trace pruning, deobfuscate stack trace + */ + @Overwrite + public int method_579(int prune) { + stackTrace = StacktraceDeobfuscator.deobfuscateStacktrace(Thread.currentThread().getStackTrace()); + return stackTrace.length; + } + + /** + * @reason Improve crash report formatting + **/ + @Overwrite + public void addStackTrace(StringBuilder builder) { + builder.append("-- ").append(title).append(" --\n"); + for (Object elementObject : elements) { + PatchedCrashReport.Element element = (PatchedCrashReport.Element) elementObject; + + String sectionIndent = " "; + + builder.append(sectionIndent) + .append(element.invokeGetName()) + .append(": "); + + StringBuilder indent = new StringBuilder(sectionIndent + " "); + for (char ignored : element.invokeGetName().toCharArray()) { + indent.append(" "); + } + + boolean first = true; + for (String line : element.invokeGetDetail().trim().split("\n")) { + if (!first) builder.append("\n").append(indent); + first = false; + if (line.startsWith("\t")) line = line.substring(1); + builder.append(line.replace("\t", "")); + } + + builder.append("\n"); + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/MixinEntity.java b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinEntity.java new file mode 100644 index 0000000..6914082 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinEntity.java @@ -0,0 +1,23 @@ +package org.dimdev.toomanycrashes.mixins; + +import net.minecraft.entity.Entity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.crash.CrashReportSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = Entity.class, priority = 10000) +public class MixinEntity { + private boolean noNBT = false; + + @Inject(method = "populateCrashReport", at = @At("TAIL")) + private void onPopulateCrashReport(CrashReportSection section, CallbackInfo ci) { + if (!noNBT) { + noNBT = true; + section.add("Entity NBT", () -> ((Entity) (Object) this).toTag(new CompoundTag()).toString()); + noNBT = false; + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/MixinIntegratedServer.java b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinIntegratedServer.java new file mode 100644 index 0000000..651c485 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinIntegratedServer.java @@ -0,0 +1,27 @@ +package org.dimdev.toomanycrashes.mixins; + +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.crash.CrashException; +import net.minecraft.util.crash.CrashReport; +import org.dimdev.toomanycrashes.PatchedIntegratedServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(IntegratedServer.class) +public class MixinIntegratedServer implements PatchedIntegratedServer { + private boolean crashNextTick = false; + + @Override + public void setCrashNextTick() { + crashNextTick = true; + } + + @Inject(method = "method_3748", at = @At("HEAD")) + private void beforeTick(CallbackInfo ci) { + if (crashNextTick) { + throw new CrashException(new CrashReport("Manually triggered server-side debug crash", new Throwable())); + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/MixinTileEntity.java b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinTileEntity.java new file mode 100644 index 0000000..ee5831a --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/MixinTileEntity.java @@ -0,0 +1,23 @@ +package org.dimdev.toomanycrashes.mixins; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.crash.CrashReportSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = BlockEntity.class, priority = 10000) +public class MixinTileEntity { + private boolean noNBT = false; + + @Inject(method = "populateCrashReport", at = @At("TAIL")) + private void onPopulateCrashReport(CrashReportSection section, CallbackInfo ci) { + if (!noNBT) { + noNBT = true; + section.add("Block Entity NBT", () -> ((BlockEntity) (Object) this).toTag(new CompoundTag()).toString()); + noNBT = false; + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinBufferBuilder.java b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinBufferBuilder.java new file mode 100644 index 0000000..721cc0f --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinBufferBuilder.java @@ -0,0 +1,27 @@ +package org.dimdev.toomanycrashes.mixins.client; + +import net.minecraft.client.render.BufferBuilder; +import org.dimdev.toomanycrashes.StateManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BufferBuilder.class) +public abstract class MixinBufferBuilder implements StateManager.IResettable { + @Shadow private boolean building; + @Shadow public abstract void end(); + + @Inject(method = "", at = @At("RETURN")) + public void onInit(int bufferSizeIn, CallbackInfo ci) { + register(); + } + + @Override + public void resetState() { + if (building) { + end(); + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinKeyboard.java b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinKeyboard.java new file mode 100644 index 0000000..37d7c9a --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinKeyboard.java @@ -0,0 +1,66 @@ +package org.dimdev.toomanycrashes.mixins.client; + +import com.google.common.util.concurrent.ListenableFutureTask; +import net.minecraft.client.Keyboard; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.util.GlfwUtil; +import net.minecraft.util.crash.CrashException; +import net.minecraft.util.crash.CrashReport; +import org.dimdev.toomanycrashes.PatchedIntegratedServer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(Keyboard.class) +public abstract class MixinKeyboard { + @Shadow @Final private MinecraftClient client; + @Shadow private long debugCrashStartTime; + + /** + * @reason Replaces the vanilla F3 + C logic to immediately crash rather than requiring + * that the buttons are pressed for 6 seconds and add more crash types: + * F3 + C - Client crash + * Ctrl + F3 + C - GL illegal access crash + * Alt + F3 + C - Integrated server crash + * Shift + F3 + C - Scheduled client task exception + * Alt + Shift + F3 + C - Scheduled server task exception + *

+ * Note: Left Shift + F3 + C doesn't work on most keyboards, see http://keyboardchecker.com/ + * Use the right shift instead. + */ + @Overwrite + public void pollDebugCrash() { + if (debugCrashStartTime > 0L && System.currentTimeMillis() - debugCrashStartTime >= 0) { + // TODO: if the client crashes between F3 + C is pressed and pollDebugCrash is called, + // debugCrashStartTime isn't reset and the client will crash again when joining + // a world. + debugCrashStartTime = -1; + + if (Gui.isControlPressed()) { + GlfwUtil.method_15973(); + } else if (Gui.isShiftPressed()) { + if (Gui.isAltPressed()) { + if (client.method_1496()) { + client.getServer().execute(() -> { + throw new CrashException(new CrashReport("Manually triggered server-side scheduled task exception", new Throwable())); + }); + } + } else { + client.execute(ListenableFutureTask.create(() -> { + throw new CrashException(new CrashReport("Manually triggered client-side scheduled task exception", new Throwable())); + })); + } + } else { + if (Gui.isAltPressed()) { + if (client.method_1496()) { + ((PatchedIntegratedServer) client.getServer()).setCrashNextTick(); + } + } else { + throw new CrashException(new CrashReport("Manually triggered client-side debug crash", new Throwable())); + } + } + } + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinMain.java b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinMain.java new file mode 100644 index 0000000..9695993 --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinMain.java @@ -0,0 +1,12 @@ +package org.dimdev.toomanycrashes.mixins.client; + +import net.minecraft.client.main.Main; +import org.dimdev.toomanycrashes.TooManyCrashes; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Main.class) +public class MixinMain { + static { + TooManyCrashes.init(); + } +} diff --git a/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinMinecraftClient.java b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinMinecraftClient.java new file mode 100644 index 0000000..45ea96d --- /dev/null +++ b/src/main/java/org/dimdev/toomanycrashes/mixins/client/MixinMinecraftClient.java @@ -0,0 +1,290 @@ +package org.dimdev.toomanycrashes.mixins.client; + +import com.mojang.blaze3d.platform.GLX; +import com.mojang.blaze3d.platform.GlStateManager; +import net.minecraft.client.Keyboard; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.Mouse; +import net.minecraft.client.audio.SoundLoader; +import net.minecraft.client.font.FontRenderer; +import net.minecraft.client.font.FontRendererManager; +import net.minecraft.client.gl.GlFramebuffer; +import net.minecraft.client.gui.CloseWorldGui; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.MainMenuGui; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.options.GameOptions; +import net.minecraft.client.resource.ClientResourcePackContainer; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.client.resource.language.LanguageManager; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.client.util.Window; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.resource.ReloadableResourceManager; +import net.minecraft.resource.ResourcePackContainerManager; +import net.minecraft.text.StringTextComponent; +import net.minecraft.util.Identifier; +import net.minecraft.util.ThreadTaskQueue; +import net.minecraft.util.crash.CrashException; +import net.minecraft.util.crash.CrashReport; +import org.apache.logging.log4j.Logger; +import org.dimdev.toomanycrashes.CrashScreenGui; +import org.dimdev.toomanycrashes.CrashUtils; +import org.dimdev.toomanycrashes.InitErrorScreenGui; +import org.dimdev.toomanycrashes.StateManager; +import org.dimdev.utils.GlUtil; +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 java.io.File; + +@Mixin(MinecraftClient.class) +@SuppressWarnings("StaticVariableMayNotBeInitialized") +public abstract class MixinMinecraftClient extends ThreadTaskQueue { + // @formatter:off + @Shadow @Final private static Logger LOGGER; + @Shadow volatile boolean isRunning; + @Shadow private boolean crashed; + @Shadow private CrashReport crashReport; + @Shadow public static byte[] memoryReservedForCrash; + @Shadow public GameOptions options; + @Shadow public InGameHud hudInGame; + @Shadow public Gui currentGui; + @Shadow public TextureManager textureManager; + @Shadow public FontRenderer fontRenderer; + @Shadow private int attackCooldown; + @Shadow private GlFramebuffer framebuffer; + @Shadow private ReloadableResourceManager resourceManager; + @Shadow private SoundLoader soundLoader; + @Shadow private LanguageManager languageManager; + @Shadow private void init() {} + @Shadow public void openGui(Gui gui) {} + @Shadow public CrashReport populateCrashReport(CrashReport report) { return null; } + @Shadow public void stop() {} + @Shadow public abstract ClientPlayNetworkHandler getNetworkHandler(); + @Shadow public abstract void updateDisplay(boolean respectFramerateLimit); + @Shadow protected abstract void render(boolean boolean_1); + @Shadow public abstract void method_1550(ClientWorld world, Gui loadingGui); + @Shadow public Window window; + @Shadow private FontRendererManager fontManager; + @Shadow public abstract void reloadResources(); + @Shadow public Mouse mouse; + @Shadow public abstract boolean forcesUnicodeFont(); + @Shadow @Final public static Identifier defaultFontRendererId; + @Shadow public abstract void stopThread(); + // @formatter:on + + @Shadow @Final public static boolean isSystemMac; + @Shadow @Final public File runDirectory; + @Shadow public Keyboard keyboard; + @Shadow @Final private ResourcePackContainerManager resourcePackContainerManager; + private int clientCrashCount = 0; + private int serverCrashCount = 0; + + /** + * @reason Allows the player to choose to return to the title screen after a crash, or get + * a pasteable link to the crash report on paste.dimdev.org. + */ + @Overwrite + public void start() { + isRunning = true; + + try { + init(); + } catch (Throwable throwable) { + // TODO: Error screen for crashes during Bootstrap.initialize() too + CrashReport report = CrashReport.create(throwable, "Initializing game"); + report.addElement("Initialization"); + displayInitErrorScreen(populateCrashReport(report)); + return; + } + + try { + while (isRunning) { + if (!crashed || crashReport == null) { + try { + render(true); + } catch (CrashException e) { + clientCrashCount++; + populateCrashReport(e.getReport()); + addInfoToCrash(e.getReport()); + resetGameState(); + LOGGER.fatal("Reported exception thrown!", e); + displayCrashScreen(e.getReport()); + } catch (Throwable e) { + clientCrashCount++; + CrashReport report = new CrashReport("Unexpected error", e); + + populateCrashReport(report); + addInfoToCrash(report); + resetGameState(); + LOGGER.fatal("Unreported exception thrown!", e); + displayCrashScreen(report); + } + } else { + serverCrashCount++; + addInfoToCrash(crashReport); + resetGameState(); + displayCrashScreen(crashReport); + crashed = false; + crashReport = null; + } + } + } finally { + stop(); + } + } + + public void addInfoToCrash(CrashReport report) { + report.getSystemDetailsSection().add("Client Crashes Since Restart", () -> String.valueOf(clientCrashCount)); + report.getSystemDetailsSection().add("Integrated Server Crashes Since Restart", () -> String.valueOf(serverCrashCount)); + } + + public void displayInitErrorScreen(CrashReport report) { + CrashUtils.outputReport(report); + + try { + GlUtil.resetState(); + isRunning = true; + runGUILoop(new InitErrorScreenGui(report)); + } catch (Throwable t) { + LOGGER.error("An uncaught exception occured while displaying the init error screen, making normal report instead", t); + printCrashReport(report); + System.exit(report.getFile() != null ? -1 : -2); + } + } + + private void runGUILoop(Gui screen) { + openGui(screen); + + while (isRunning && currentGui != null && !(currentGui instanceof MainMenuGui)) { + window.setPhase("TooManyCrashes GUI Loop"); + if (GLX.shouldClose(window)) { + stopThread(); + } + + attackCooldown = 10000; + currentGui.update(); + + mouse.updateMouse(); + GLX.pollEvents(); + + GlStateManager.pushMatrix(); + GlStateManager.clear(16640, isSystemMac); + framebuffer.beginWrite(true); + GlStateManager.enableTexture(); + + GlStateManager.viewport(0, 0, window.getWindowWidth(), window.getWindowHeight()); + GlStateManager.matrixMode(5889); + GlStateManager.loadIdentity(); + GlStateManager.matrixMode(5888); + GlStateManager.loadIdentity(); + window.method_4493(isSystemMac); + + GlStateManager.clear(256, isSystemMac); + currentGui.draw( + (int) (mouse.getX() * window.getScaledWidth() / window.method_4480()), + (int) (mouse.getY() * window.getScaledHeight() / window.method_4507()), + 0 + ); + + framebuffer.endWrite(); + GlStateManager.popMatrix(); + GlStateManager.pushMatrix(); + framebuffer.draw(window.getWindowWidth(), window.getWindowHeight()); + GlStateManager.popMatrix(); + GlStateManager.pushMatrix(); + window.method_4493(isSystemMac); + GlStateManager.popMatrix(); + updateDisplay(true); + Thread.yield(); + } + } + + public void displayCrashScreen(CrashReport report) { + try { + CrashUtils.outputReport(report); + + // Reset hasCrashed + crashed = false; + + // Vanilla does this when switching to main menu but not our custom crash screen + // nor the out of memory screen (see https://bugs.mojang.com/browse/MC-128953) + options.debugEnabled = false; + hudInGame.getHudChat().clear(true); + + // Display the crash screen + runGUILoop(new CrashScreenGui(report)); + } catch (Throwable t) { + // The crash screen has crashed. Report it normally instead. + LOGGER.error("An uncaught exception occured while displaying the crash screen, making normal report instead", t); + printCrashReport(report); + System.exit(report.getFile() != null ? -1 : -2); + } + } + + @Overwrite + public void printCrashReport(CrashReport report) { + CrashUtils.outputReport(report); + } + + public void resetGameState() { + try { + // Free up memory such that this works properly in case of an OutOfMemoryError + int originalReservedMemorySize = -1; + try { // In case another mod actually deletes the memoryReserve field + if (memoryReservedForCrash != null) { + originalReservedMemorySize = memoryReservedForCrash.length; + memoryReservedForCrash = new byte[0]; + } + } catch (Throwable ignored) {} + + // Reset registered resettables + StateManager.resetStates(); + + // Close the world + if (getNetworkHandler() != null) { + // Fix: Close the connection to avoid receiving packets from old server + // when playing in another world (MC-128953) + getNetworkHandler().getClientConnection().disconnect(new StringTextComponent("[TooManyCrashes] Client crashed")); + } + + method_1550(null, new CloseWorldGui(I18n.translate("menu.savingLevel"))); + taskQueue.clear(); // Fix: method_1550(null, ...) only clears when integrated server is running + + // Reset graphics + GlUtil.resetState(); + + // Re-create memory reserve so that future crashes work well too + if (originalReservedMemorySize != -1) { + try { + memoryReservedForCrash = new byte[originalReservedMemorySize]; + } catch (Throwable ignored) {} + } + + System.gc(); + } catch (Throwable t) { + LOGGER.error("Failed to reset state after a crash", t); + try { + StateManager.resetStates(); + GlUtil.resetState(); + } catch (Throwable ignored) {} + } + } + + /** + * @reason Disconnect from the current world and free memory, using a memory reserve + * to make sure that an OutOfMemory doesn't happen while doing this. + *

+ * Bugs Fixed: + * - https://bugs.mojang.com/browse/MC-128953 + * - Memory reserve not recreated after out-of memory + */ + @Overwrite + public void cleanUpAfterCrash() { + resetGameState(); + } +} diff --git a/src/main/java/org/dimdev/utils/GlUtil.java b/src/main/java/org/dimdev/utils/GlUtil.java new file mode 100644 index 0000000..ee26395 --- /dev/null +++ b/src/main/java/org/dimdev/utils/GlUtil.java @@ -0,0 +1,157 @@ +package org.dimdev.utils; + +import com.mojang.blaze3d.platform.GLX; +import com.mojang.blaze3d.platform.GlStateManager; +import net.minecraft.client.render.GuiLighting; +import org.lwjgl.opengl.*; + +public class GlUtil { + public static void resetState() { + // Clear matrix stack + GlStateManager.matrixMode(GL11.GL_MODELVIEW); + GlStateManager.loadIdentity(); + GlStateManager.matrixMode(GL11.GL_PROJECTION); + GlStateManager.loadIdentity(); + GlStateManager.matrixMode(GL11.GL_TEXTURE); + GlStateManager.loadIdentity(); + GlStateManager.matrixMode(GL11.GL_COLOR); + GlStateManager.loadIdentity(); + + // Clear attribute stacks TODO: Broken, a stack underflow breaks LWJGL + // try { + // do GL11.glPopAttrib(); while (GlStateManager.glGetError() == 0); + // } catch (Throwable ignored) {} + // + // try { + // do GL11.glPopClientAttrib(); while (GlStateManager.glGetError() == 0); + // } catch (Throwable ignored) {} + + // Reset texture + GlStateManager.bindTexture(0); + GlStateManager.disableTexture(); + + // Reset GL lighting + GlStateManager.disableLighting(); + GlStateManager.lightModel(GL11.GL_LIGHT_MODEL_AMBIENT, GuiLighting.singletonBuffer(0.2F, 0.2F, 0.2F, 1.0F)); + for (int i = 0; i < 8; ++i) { + GlStateManager.disableLight(i); + GlStateManager.light(GL11.GL_LIGHT0 + i, GL11.GL_AMBIENT, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 1.0F)); + GlStateManager.light(GL11.GL_LIGHT0 + i, GL11.GL_POSITION, GuiLighting.singletonBuffer(0.0F, 0.0F, 1.0F, 0.0F)); + + if (i == 0) { + GlStateManager.light(GL11.GL_LIGHT0 + i, GL11.GL_DIFFUSE, GuiLighting.singletonBuffer(1.0F, 1.0F, 1.0F, 1.0F)); + GlStateManager.light(GL11.GL_LIGHT0 + i, GL11.GL_SPECULAR, GuiLighting.singletonBuffer(1.0F, 1.0F, 1.0F, 1.0F)); + } else { + GlStateManager.light(GL11.GL_LIGHT0 + i, GL11.GL_DIFFUSE, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 1.0F)); + GlStateManager.light(GL11.GL_LIGHT0 + i, GL11.GL_SPECULAR, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 1.0F)); + } + } + GlStateManager.disableColorMaterial(); + GlStateManager.colorMaterial(1032, 5634); + + // Reset depth + GlStateManager.disableDepthTest(); + GlStateManager.depthFunc(513); + GlStateManager.depthMask(true); + + // Reset blend mode + GlStateManager.disableBlend(); + GlStateManager.blendFunc(GlStateManager.SrcBlendFactor.ONE, GlStateManager.DstBlendFactor.ZERO); + GlStateManager.blendFuncSeparate(GlStateManager.SrcBlendFactor.ONE, GlStateManager.DstBlendFactor.ZERO, GlStateManager.SrcBlendFactor.ONE, GlStateManager.DstBlendFactor.ZERO); + GlStateManager.blendEquation(GL14.GL_FUNC_ADD); + + // Reset fog + GlStateManager.disableFog(); + GlStateManager.fogMode(GlStateManager.FogMode.LINEAR); + GlStateManager.fogDensity(1.0F); + GlStateManager.fogStart(0.0F); + GlStateManager.fogEnd(1.0F); + GlStateManager.fog(GL11.GL_FOG_COLOR, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 0.0F)); + if (GL.getCapabilities().GL_NV_fog_distance) { + GlStateManager.fog(GL11.GL_FOG_MODE, 34140); + } + + // Reset polygon offset + GlStateManager.polygonOffset(0.0F, 0.0F); + GlStateManager.disablePolygonOffset(); + + // Reset color logic + GlStateManager.disableColorLogicOp(); + GlStateManager.logicOp(5379); + + // Reset texgen + GlStateManager.disableTexGen(GlStateManager.TexCoord.S); + GlStateManager.disableTexGen(GlStateManager.TexCoord.T); + GlStateManager.disableTexGen(GlStateManager.TexCoord.R); + GlStateManager.disableTexGen(GlStateManager.TexCoord.Q); + GlStateManager.texGenMode(GlStateManager.TexCoord.S, 9216); + GlStateManager.texGenMode(GlStateManager.TexCoord.T, 9216); + GlStateManager.texGenMode(GlStateManager.TexCoord.R, 9216); + GlStateManager.texGenMode(GlStateManager.TexCoord.Q, 9216); + GlStateManager.texGenParam(GlStateManager.TexCoord.S, 9474, GuiLighting.singletonBuffer(1.0F, 0.0F, 0.0F, 0.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.T, 9474, GuiLighting.singletonBuffer(0.0F, 1.0F, 0.0F, 0.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.R, 9474, GuiLighting.singletonBuffer(0.0F, 0.0F, 1.0F, 0.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.Q, 9474, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 1.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.S, 9217, GuiLighting.singletonBuffer(1.0F, 0.0F, 0.0F, 0.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.T, 9217, GuiLighting.singletonBuffer(0.0F, 1.0F, 0.0F, 0.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.R, 9217, GuiLighting.singletonBuffer(0.0F, 0.0F, 1.0F, 0.0F)); + GlStateManager.texGenParam(GlStateManager.TexCoord.Q, 9217, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 1.0F)); + + // Disable lightmap + GlStateManager.activeTexture(GLX.GL_TEXTURE1); + GlStateManager.disableTexture(); + + GlStateManager.activeTexture(GLX.GL_TEXTURE0); + + // Reset texture parameters + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_LINEAR); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 1000); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LOD, 1000); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MIN_LOD, -1000); + GlStateManager.texParameter(GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_LOD_BIAS, 0.0F); + + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, GuiLighting.singletonBuffer(0.0F, 0.0F, 0.0F, 0.0F)); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL15.GL_SRC0_RGB, GL11.GL_TEXTURE); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL15.GL_SRC1_RGB, GL13.GL_PREVIOUS); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL15.GL_SRC2_RGB, GL13.GL_CONSTANT); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL15.GL_SRC0_ALPHA, GL11.GL_TEXTURE); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL15.GL_SRC1_ALPHA, GL13.GL_PREVIOUS); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL15.GL_SRC2_ALPHA, GL13.GL_CONSTANT); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL13.GL_RGB_SCALE, 1.0F); + GlStateManager.texEnv(GL11.GL_TEXTURE_ENV, GL11.GL_ALPHA_SCALE, 1.0F); + + GlStateManager.disableNormalize(); + GlStateManager.shadeModel(7425); + GlStateManager.disableRescaleNormal(); + GlStateManager.colorMask(true, true, true, true); + GlStateManager.clearDepth(1.0D); + GlStateManager.lineWidth(1.0F); + GlStateManager.normal3f(0.0F, 0.0F, 1.0F); + GlStateManager.polygonMode(GL11.GL_FRONT, GL11.GL_FILL); + GlStateManager.polygonMode(GL11.GL_BACK, GL11.GL_FILL); + + GlStateManager.enableTexture(); + GlStateManager.shadeModel(7425); + GlStateManager.clearDepth(1.0D); + GlStateManager.enableDepthTest(); + GlStateManager.depthFunc(515); + GlStateManager.enableAlphaTest(); + GlStateManager.alphaFunc(516, 0.1F); + GlStateManager.cullFace(GlStateManager.FaceSides.BACK); + GlStateManager.matrixMode(5889); + GlStateManager.loadIdentity(); + GlStateManager.matrixMode(5888); + } +} diff --git a/src/main/java/org/dimdev/utils/HasteUpload.java b/src/main/java/org/dimdev/utils/HasteUpload.java new file mode 100644 index 0000000..1a7d5cd --- /dev/null +++ b/src/main/java/org/dimdev/utils/HasteUpload.java @@ -0,0 +1,40 @@ +package org.dimdev.utils; + +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.isEmpty() ? "" : "." + extension); + } + } finally { + connection.disconnect(); + } + } +} diff --git a/src/main/java/org/dimdev/utils/ModIdentifier.java b/src/main/java/org/dimdev/utils/ModIdentifier.java new file mode 100644 index 0000000..68d0336 --- /dev/null +++ b/src/main/java/org/dimdev/utils/ModIdentifier.java @@ -0,0 +1,77 @@ +package org.dimdev.utils; + +import net.fabricmc.loader.FabricLoader; +import net.fabricmc.loader.ModContainer; +import net.fabricmc.loader.ModInfo; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.*; + +public final class ModIdentifier { + private static final Logger LOGGER = LogManager.getLogger(); + + public static Set identifyFromStacktrace(Throwable e) { + Map> modMap = makeModMap(); + + // Get the set of classes + Set classes = new LinkedHashSet<>(); + while (e != null) { + for (StackTraceElement element : e.getStackTrace()) { + classes.add(element.getClassName()); + } + e = e.getCause(); + } + + Set mods = new LinkedHashSet<>(); + for (String className : classes) { + Set classMods = identifyFromClass(className, modMap); + if (classMods != null) mods.addAll(classMods); + } + return mods; + } + + public static Set identifyFromClass(String className) { + return identifyFromClass(className, makeModMap()); + } + + // TODO: get a list of mixin transformers that affected the class and blame those too + private static Set identifyFromClass(String className, Map> modMap) { + // Skip identification for Mixin, one's mod copy of the library is shared with all other mods + if (className.startsWith("org.spongepowered.asm.mixin.")) return Collections.emptySet(); + + // Get the URL of the class + URL url = ModIdentifier.class.getResource(className); + if (url == null) { + LOGGER.warn("Failed to identify " + className); + return Collections.emptySet(); + } + + // Get the mod containing that class + try { + if (url.getProtocol().equals("jar")) url = new URL(url.getFile().substring(0, url.getFile().indexOf('!'))); + return modMap.get(new File(url.toURI()).getCanonicalFile()); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } + + private static Map> makeModMap() { + Map> modMap = new HashMap<>(); + for (ModContainer mod : FabricLoader.INSTANCE.getModContainers()) { + Set currentMods = modMap.getOrDefault(mod.getOriginFile(), new HashSet<>()); + currentMods.add(mod.getInfo()); + try { + modMap.put(mod.getOriginFile().getCanonicalFile(), currentMods); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return modMap; + } +} diff --git a/src/main/java/org/dimdev/utils/SSLUtils.java b/src/main/java/org/dimdev/utils/SSLUtils.java new file mode 100644 index 0000000..2f1acc7 --- /dev/null +++ b/src/main/java/org/dimdev/utils/SSLUtils.java @@ -0,0 +1,102 @@ +package org.dimdev.utils; + +import javax.net.ssl.*; +import java.io.*; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +public final class SSLUtils { + + /** Disables SSL certificate validation. */ + public static void trustAllCertificates() { + try { + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new X509TrustManager[]{new X509TrustManager() { + @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {} + @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {} + @Override public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }}, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** Trusts certificates in a key store on top of the ones currently trusted by wrapping the TrustManager */ + public static void trustCertificates(KeyStore keyStore) { + try { + // Init TFM with default trust store + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + final X509TrustManager defaultTrustManager = getX509TrustManager(trustManagerFactory); + + // Init TMF with new trust store + trustManagerFactory.init(keyStore); + final X509TrustManager customTrustManager = getX509TrustManager(trustManagerFactory); + + // Create a trust manager that wraps the default one + X509TrustManager wrappingTrustManager = new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return joinArrays(defaultTrustManager.getAcceptedIssuers(), customTrustManager.getAcceptedIssuers()); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + customTrustManager.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + defaultTrustManager.checkServerTrusted(chain, authType); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + customTrustManager.checkClientTrusted(chain, authType); + } catch (CertificateException e) { + defaultTrustManager.checkClientTrusted(chain, authType); + } + } + }; + + // Replace the default SSL context + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{wrappingTrustManager}, null); + SSLContext.setDefault(sslContext); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + private static X509TrustManager getX509TrustManager(TrustManagerFactory trustManagerFactory) { + for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + throw new RuntimeException("Failed to find X509TrustManager"); + } + + private static T[] joinArrays(T[] first, T[] second) { + T[] result = Arrays.copyOf(first, first.length + second.length); + System.arraycopy(second, 0, result, first.length, second.length); + return result; + } + + public static KeyStore loadKeyStoreFromFile(File keystoreFile, String password) { + try (InputStream keyStoreInputStream = new FileInputStream(keystoreFile)) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(keyStoreInputStream, password.toCharArray()); + return keyStore; + } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) { + throw new RuntimeException(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/toomanycrashes/lang/de_de.json b/src/main/resources/assets/toomanycrashes/lang/de_de.json new file mode 100644 index 0000000..094bc01 --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/de_de.json @@ -0,0 +1,14 @@ +{ + "toomanycrashes.gui.getLink": "Link erhalten", + "toomanycrashes.gui.failed": "[Fehlgeschlagen]", + "toomanycrashes.crashscreen.title": "Minecraft ist abgestürzt!", + "toomanycrashes.crashscreen.summary": "Minecraft ist ein Problem unterlaufen und ist abgestürzt.", + "toomanycrashes.crashscreen.paragraph1.line1": "Folgende Modifikation(en) wurde(n) als mögliche Ursache identifiziert:", + "toomanycrashes.crashscreen.paragraph2.line1": "Klicke auf einen beliebigen Mod in der Liste, um auf die Website zu gelangen.", + "toomanycrashes.crashscreen.paragraph2.line2": "Ein Bericht wurde erstellt und kann hier gefunden werden (Klick):", + "toomanycrashes.crashscreen.reportSaveFailed": "[FEHLER BEIM SPEICHERN, SIEHE LOG]", + "toomanycrashes.crashscreen.paragraph3.line1": "Klicke auf die Schaltfläche \"Link erhalten\", um ihn im Browser zu", + "toomanycrashes.crashscreen.paragraph3.line2": "öffnen. Du bist aufgefordert, diesen Bericht an den Modhersteller", + "toomanycrashes.crashscreen.paragraph3.line3": "zu senden, um ihm zu helfen, das Problem zu beheben. Da TooManyCrashes", + "toomanycrashes.crashscreen.paragraph3.line4": "installiert ist, kannst du trotz des Absturzes weiterspielen." +} diff --git a/src/main/resources/assets/toomanycrashes/lang/en_us.json b/src/main/resources/assets/toomanycrashes/lang/en_us.json new file mode 100644 index 0000000..fec18de --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/en_us.json @@ -0,0 +1,25 @@ +{ + "toomanycrashes.gui.getLink": "Get link", + "toomanycrashes.gui.failed": "[Failed]", + "toomanycrashes.gui.keepPlaying": "Keep playing", + "toomanycrashes.gui.restart": "Restart Minecraft", + "toomanycrashes.gui.disabledByConfig": "Disabled by config", + "toomanycrashes.crashscreen.title": "Minecraft crashed!", + "toomanycrashes.crashscreen.summary": "Minecraft ran into a problem and crashed.", + "toomanycrashes.crashscreen.paragraph1.line1": "The following mod(s) have been identified as potential causes:", + "toomanycrashes.crashscreen.unknownCause": "Unknown", + "toomanycrashes.crashscreen.identificationErrored": "[Error identifying, report to TooManyCrashes]", + "toomanycrashes.crashscreen.paragraph2.line1": "Click any mod in the list to go to their website for support. A", + "toomanycrashes.crashscreen.paragraph2.line2": "report has been generated, and can be found here (click):", + "toomanycrashes.crashscreen.reportSaveFailed": "[Error saving report, see log]", + "toomanycrashes.crashscreen.paragraph3.line1": "Click the \"Get link\" button to open it in your browser. You're", + "toomanycrashes.crashscreen.paragraph3.line2": "encouraged to send this report to the mod's author to help", + "toomanycrashes.crashscreen.paragraph3.line3": "them fix the issue. Since TooManyCrashes is installed, you can", + "toomanycrashes.crashscreen.paragraph3.line4": "keep playing despite the crash.", + "toomanycrashes.initerrorscreen.title": "Minecraft failed to start!", + "toomanycrashes.initerrorscreen.summary": "An error during startup prevented Minecraft from starting", + "toomanycrashes.initerrorscreen.paragraph3.line1": "Click the \"Get link\" button to open it in your browser. You're", + "toomanycrashes.initerrorscreen.paragraph3.line2": "encouraged to send this report to the mod's author to help", + "toomanycrashes.initerrorscreen.paragraph3.line3": "them fix the issue. Unfortunately, it is not possible to keep", + "toomanycrashes.initerrorscreen.paragraph3.line4": "loading Minecraft due to this error." +} diff --git a/src/main/resources/assets/toomanycrashes/lang/fr_fr.json b/src/main/resources/assets/toomanycrashes/lang/fr_fr.json new file mode 100644 index 0000000..0a7ba21 --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/fr_fr.json @@ -0,0 +1,17 @@ +{ + "toomanycrashes.gui.getLink": "Obtenir un lien", + "toomanycrashes.gui.failed": "[Échoué]", + "toomanycrashes.gui.keepPlaying": "Continuer à jouer", + "toomanycrashes.crashscreen.title": "Minecraft a crashé!", + "toomanycrashes.crashscreen.summary": "Minecraft a rencontré un problème et a crashé.", + "toomanycrashes.crashscreen.paragraph1.line1": "Ces mod(s) ont été identifiés comme des causes potentielles:", + "toomanycrashes.crashscreen.unknownCause": "Cause inconnue", + "toomanycrashes.crashscreen.identificationErrored": "[Problème dans l'identification, raportez à TooManyCrashes]", + "toomanycrashes.crashscreen.paragraph2.line1": "Cliquez n'importe quel mod dans la liste pour aller a leur page", + "toomanycrashes.crashscreen.paragraph2.line2": "pour du support. Un raport a été génére et se trouve ici (cliquez):", + "toomanycrashes.crashscreen.reportSaveFailed": "[Erreur durant la sauvegarde, voir le log]", + "toomanycrashes.crashscreen.paragraph3.line1": "Cliquez le bouton « Obtenir un lien » pour l'ouvrir dans votre", + "toomanycrashes.crashscreen.paragraph3.line2": "navigateur web. Vous êtez encouragé de l'envoyer à l'auteur du", + "toomanycrashes.crashscreen.paragraph3.line3": "mod pour l'aider à résoudre le problême. Vu que TooManyCrashes est", + "toomanycrashes.crashscreen.paragraph3.line4": "installé, vous pouvez continuer a jouer malgré le crash." +} diff --git a/src/main/resources/assets/toomanycrashes/lang/pt_br.json b/src/main/resources/assets/toomanycrashes/lang/pt_br.json new file mode 100644 index 0000000..a41e037 --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/pt_br.json @@ -0,0 +1,25 @@ +{ + "toomanycrashes.gui.getLink": "Acessar link", + "toomanycrashes.gui.failed": "[Falha]", + "toomanycrashes.gui.keepPlaying": "Continuar jogando", + "toomanycrashes.gui.restart": "Reiniciar o Minecraft", + "toomanycrashes.gui.disabledByConfig": "Desativado pela config.", + "toomanycrashes.crashscreen.title": "O Minecraft parou de funcionar!", + "toomanycrashes.crashscreen.summary": "O Minecraft parou em decorrência de um erro.", + "toomanycrashes.crashscreen.paragraph1.line1": "Os seguinte(s) mod(s) foram apontados como possíveis causas:", + "toomanycrashes.crashscreen.unknownCause": "Desconhecido", + "toomanycrashes.crashscreen.identificationErrored": "[Erro ao identificar, relatar ao TooManyCrashes]", + "toomanycrashes.crashscreen.paragraph2.line1": "Clique em qualquer mod da lista para acessar a página de assistência. Um", + "toomanycrashes.crashscreen.paragraph2.line2": "relatório foi criado e pode ser acessado aqui (clique):", + "toomanycrashes.crashscreen.reportSaveFailed": "[Erro ao salvar o relatório, ver o registro]", + "toomanycrashes.crashscreen.paragraph3.line1": "Clique em \"Acessar link\" para abri-lo em seu navegador. Se preferir,", + "toomanycrashes.crashscreen.paragraph3.line2": "envie este relatório para o desenvolvedor do mod a fim de ajudá-lo", + "toomanycrashes.crashscreen.paragraph3.line3": "a corrigir o erro. Já que o TooManyCrashes está instalado, você pode", + "toomanycrashes.crashscreen.paragraph3.line4": "continuar jogando.", + "toomanycrashes.initerrorscreen.title": "O Minecraft não pôde ser inicializado!", + "toomanycrashes.initerrorscreen.summary": "Um erro de inicialização impediu a execução do Minecraft", + "toomanycrashes.initerrorscreen.paragraph3.line1": "Clique em \"Acessar link\" para abri-lo em seu navegador. Se preferir,", + "toomanycrashes.initerrorscreen.paragraph3.line2": "envie este relatório ao desenvolvedor do mod para ajudá-lo a", + "toomanycrashes.initerrorscreen.paragraph3.line3": "corrigir o erro. Lamentamos, não será possível continuar jogando", + "toomanycrashes.initerrorscreen.paragraph3.line4": "o Minecraft devido ao erro." +} diff --git a/src/main/resources/assets/toomanycrashes/lang/ro_ro.json b/src/main/resources/assets/toomanycrashes/lang/ro_ro.json new file mode 100644 index 0000000..5ba9fdd --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/ro_ro.json @@ -0,0 +1,14 @@ +{ + "toomanycrashes.gui.getLink": "Obțineți un link", + "toomanycrashes.gui.failed": "[Eșuat]", + "toomanycrashes.crashscreen.title": "Minecraft a crash!", + "toomanycrashes.crashscreen.summary": "Minecraft întâmpinat o problemă și a crash", + "toomanycrashes.crashscreen.paragraph1.line1": "Următoarele mod(uri) au fost identificate ca cauze posibile:", + "toomanycrashes.crashscreen.paragraph2.line1": "Faceți click pe ori care mod din această lista pentru a merge", + "toomanycrashes.crashscreen.paragraph2.line2": "pe site-ul lor pentru suport. Un raport poate fi găsit aici:", + "toomanycrashes.crashscreen.reportSaveFailed": "[Eroare salvând raportul, vedeți log-ul]", + "toomanycrashes.crashscreen.paragraph3.line1": "Faceți click pe butonul \"Obțineți un link\" pentru a-l deschide", + "toomanycrashes.crashscreen.paragraph3.line2": "in browser. Sunteți encurajați să-l trimiteți la autorul modului", + "toomanycrashes.crashscreen.paragraph3.line3": "pentru a-l ajuta să rezolve problema. Fiind că TooManyCrashes e", + "toomanycrashes.crashscreen.paragraph3.line4": "instalat, puteți continua de a juca în ciuda crash-ului." +} diff --git a/src/main/resources/assets/toomanycrashes/lang/ru_ru.json b/src/main/resources/assets/toomanycrashes/lang/ru_ru.json new file mode 100644 index 0000000..bc7ccb7 --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/ru_ru.json @@ -0,0 +1,25 @@ +{ + "toomanycrashes.gui.getLink": "Получить ссылку", + "toomanycrashes.gui.failed": "[Ошибка]", + "toomanycrashes.gui.keepPlaying": "Продолжить играть", + "toomanycrashes.gui.restart": "Перезапустить Minecraft", + "toomanycrashes.gui.disabledByConfig": "Отключено в настройках", + "toomanycrashes.crashscreen.title": "Minecraft аварийно завершился!", + "toomanycrashes.crashscreen.summary": "Minecraft столкнулся с проблемой и аварийно завершился.", + "toomanycrashes.crashscreen.paragraph1.line1": "Следующие модификации были определены как возможные причины:", + "toomanycrashes.crashscreen.unknownCause": "Неизвестно", + "toomanycrashes.crashscreen.identificationErrored": "[Ошибка определения, сообщите разработчикам TooManyCrashes]", + "toomanycrashes.crashscreen.paragraph2.line1": "Нажмите на любую модификацию в списке, чтобы перейти на её сайт для получения", + "toomanycrashes.crashscreen.paragraph2.line2": "помощи. Был сгенерирован отчёт о падении, он может быть найдет тут (нажмите):", + "toomanycrashes.crashscreen.reportSaveFailed": "[Ошибка сохранения отчета, смотрите в журнале]", + "toomanycrashes.crashscreen.paragraph3.line1": "Нажмите кнопку \"Получить ссылку\", чтобы открыть его в вашем браузере.", + "toomanycrashes.crashscreen.paragraph3.line2": "Вы можете отправить этот отчёт автору модификации, чтобы помочь", + "toomanycrashes.crashscreen.paragraph3.line3": "ему исправить проблему. Так как установлен TooManyCrashes, вы можете", + "toomanycrashes.crashscreen.paragraph3.line4": "продолжать играть несмотря на падение.", + "toomanycrashes.initerrorscreen.title": "Minecraft не смог запуститься!", + "toomanycrashes.initerrorscreen.summary": "Ошибка во время запуска помешала игре запуститься.", + "toomanycrashes.initerrorscreen.paragraph3.line1": "Нажмите кнопку \"Получить ссылку\", чтобы открыть его в вашем браузере.", + "toomanycrashes.initerrorscreen.paragraph3.line2": "Вы можете отправить этот отчёт автору модификации, чтобы помочь", + "toomanycrashes.initerrorscreen.paragraph3.line3": "ему исправить проблему. К сожалению, из-за этой ошибки невозможно", + "toomanycrashes.initerrorscreen.paragraph3.line4": "продолжить загрузку Minecraft." +} diff --git a/src/main/resources/assets/toomanycrashes/lang/zh_cn.json b/src/main/resources/assets/toomanycrashes/lang/zh_cn.json new file mode 100644 index 0000000..173b990 --- /dev/null +++ b/src/main/resources/assets/toomanycrashes/lang/zh_cn.json @@ -0,0 +1,24 @@ +{ + "toomanycrashes.gui.getLink": "获取链接", + "toomanycrashes.gui.failed": "[失败]", + "toomanycrashes.gui.keepPlaying": "继续游戏", + "toomanycrashes.gui.restart": "重启Minecraft", + "toomanycrashes.crashscreen.title": "Minecraft崩溃啦!", + "toomanycrashes.crashscreen.summary": "Minecraft遇到一个问题崩溃了。", + "toomanycrashes.crashscreen.paragraph1.line1": "下列的mod被确定为崩溃的可能原因:", + "toomanycrashes.crashscreen.unknownCause": "未知", + "toomanycrashes.crashscreen.identificationErrored": "[鉴别错误,请报告给TooManyCrashes]", + "toomanycrashes.crashscreen.paragraph2.line1": "点击列表中的任意mod即可访问它们的网站获取技术支持。", + "toomanycrashes.crashscreen.paragraph2.line2": "一份报告已经生成,你可以在这里找到(点击):", + "toomanycrashes.crashscreen.reportSaveFailed": "[保存报告错误,请查看日志]", + "toomanycrashes.crashscreen.paragraph3.line1": "点击\"获取链接\"按钮在你的浏览器中打开它。", + "toomanycrashes.crashscreen.paragraph3.line2": "我们建议您将此报告发送给mod的作者以帮助它们修复问题。", + "toomanycrashes.crashscreen.paragraph3.line3": "由于安装了TooManyCrashes,尽管发生了崩溃但是你可以继续游戏。", + "toomanycrashes.crashscreen.paragraph3.line4": "", + "toomanycrashes.initerrorscreen.title": "Minecraft未能启动!", + "toomanycrashes.initerrorscreen.summary": "启动过程中一个错误阻止了Minecraft的启动", + "toomanycrashes.initerrorscreen.paragraph3.line1": "点击\"获取链接\"按钮在你的浏览器中打开它。", + "toomanycrashes.initerrorscreen.paragraph3.line2": "我们建议您将此报告发送给mod的作者以帮助它们修复问题。", + "toomanycrashes.initerrorscreen.paragraph3.line3": "不幸的是,在发生这个错误之后继续加载minecraft时不可能的。", + "toomanycrashes.initerrorscreen.paragraph3.line4": "" +} diff --git a/src/main/resources/dst_root_ca_x3.jks b/src/main/resources/dst_root_ca_x3.jks new file mode 100644 index 0000000..393e125 Binary files /dev/null and b/src/main/resources/dst_root_ca_x3.jks differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1b5b036 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,8 @@ +{ + "id": "toomanycrashes", + "name": "TooManyCrashes", + "version": "1.0", + "mixins": { + "common": "mixins.toomanycrashes.json" + } +} diff --git a/src/main/resources/mixins.toomanycrashes.json b/src/main/resources/mixins.toomanycrashes.json new file mode 100644 index 0000000..97a15e8 --- /dev/null +++ b/src/main/resources/mixins.toomanycrashes.json @@ -0,0 +1,22 @@ +{ + "package": "org.dimdev.toomanycrashes.mixins", + "required": true, + "refmap": "mixins.toomanycrashes.refmap.json", + "target": "@env(DEFAULT)", + "minVersion": "0.6", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "MixinCrashReport", + "MixinCrashReportSection", + "MixinCrashReportSection$MixinElement", + "MixinEntity", + "MixinIntegratedServer", + "MixinTileEntity" + ], + "client": [ + "client.MixinBufferBuilder", + "client.MixinKeyboard", + "client.MixinMain", + "client.MixinMinecraftClient" + ] +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..ed69f7b --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "TooManyCrashes", + "pack_format": 4 + } +}