package com.unciv import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Json import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.saves.Gzip import com.unciv.ui.utils.* import java.io.PrintWriter import java.io.StringWriter import kotlin.concurrent.thread /* Crashes are now handled from: - Event listeners, by [CrashHandlingStage]. - The main rendering loop, by [UncivGame.render]. - Threads, by [crashHandlingThread]. - Main loop runnables, by [postCrashHandlingRunnable]. Altogether, I *think* that should cover 90%-99% of all potential crashes. */ /** Screen to crash to when an otherwise unhandled exception or error is thrown. */ class CrashScreen(val exception: Throwable): BaseScreen() { private companion object { fun Throwable.stringify(): String { val out = StringWriter() this.printStackTrace(PrintWriter(out)) return out.toString() } } /** Qualified class name of the game screen that was active at the construction of this instance, or an error note. */ val lastScreenType = try { UncivGame.Current.screen::class.qualifiedName.toString() } catch (e: Throwable) { "Could not get screen type: $e" } val text = formatReport(exception.stringify()) var copied = false private set /** @return The last active save game serialized as a compressed string if any, or an informational note otherwise. */ private fun tryGetSaveGame() = try { UncivGame.Current.gameInfo.let { gameInfo -> Json().toJson(gameInfo).let { jsonString -> Gzip.zip(jsonString) } } // Taken from old CrashController().buildReport(). } catch (e: Throwable) { "No save data: $e" // In theory .toString() could still error here. } /** @return Mods from the last active save game if any, or an informational note otherwise. */ private fun tryGetSaveMods() = try { // Also from old CrashController().buildReport(), also could still error at .toString(). LinkedHashSet(UncivGame.Current.gameInfo.gameParameters.getModsAndBaseRuleset()).toString() } catch (e: Throwable) { "No mod data: $e" } /** * @param message Error text. Probably exception traceback. * @return Message with application, platform, and game state metadata. * */ private fun formatReport(message: String): String { val indent = " ".repeat(4) val baseIndent = indent.repeat(3) // To be even with the template string. val subIndent = baseIndent + indent // TO be one level more than the template string. /** We only need the indent after any new lines in each substitution itself. So this prepends to all lines, and then removes from the start. */ fun String.prependIndentToOnlyNewLines(indent: String) = this.prependIndent(indent).removePrefix(indent) /// The $lastScreenType substitution is the only one completely under the control of this class— Everything else can, in theory, have new lines in it due to containing strings or custom .toString behaviour with new lines (which… I think Table.toString or something actually does). So normalize indentation for basically everything. return """ **Platform:** ${Gdx.app.type.toString().prependIndentToOnlyNewLines(subIndent)} **Version:** ${UncivGame.Current.version.prependIndentToOnlyNewLines(subIndent)} **Rulesets:** ${RulesetCache.keys.toString().prependIndentToOnlyNewLines(subIndent)} **Last Screen:** `$lastScreenType` -------------------------------- ${UncivGame.Current.crashReportSysInfo?.getInfo().toString().prependIndentToOnlyNewLines(baseIndent)} -------------------------------- **Message:** ``` ${message.prependIndentToOnlyNewLines(baseIndent)} ``` **Save Mods:** ``` ${tryGetSaveMods().prependIndentToOnlyNewLines(baseIndent)} ``` **Save Data:**
Show Saved Game ``` ${tryGetSaveGame().prependIndentToOnlyNewLines(baseIndent)} ```
""".trimIndent() } init { println(text) // Also print to system terminal. thread { throw exception } // this is so the GPC logs catch the exception stage.addActor(makeLayoutTable()) } /** @return A Table containing the layout of the whole screen. */ private fun makeLayoutTable(): Table { val layoutTable = Table().also { it.width = stage.width it.height = stage.height } layoutTable.add(makeTitleLabel()) .padBottom(15f) .width(stage.width) .row() layoutTable.add(makeErrorScroll()) .maxWidth(stage.width * 0.7f) .maxHeight(stage.height * 0.5f) .minHeight(stage.height * 0.2f) .row() layoutTable.add(makeInstructionLabel()) .padTop(15f) .width(stage.width) .row() layoutTable.add(makeActionButtonsTable()) .padTop(10f) return layoutTable } /** @return Label for title at top of screen. */ private fun makeTitleLabel() = "An unrecoverable error has occurred in Unciv:".toLabel(fontSize = Constants.headingFontSize) .apply { wrap = true setAlignment(Align.center) } /** @return Actor that displays a scrollable view of the error report text. */ private fun makeErrorScroll(): Actor { val errorLabel = Label(text, skin).apply { setFontSize(15) } val errorTable = Table() errorTable.add(errorLabel) .pad(10f) return AutoScrollPane(errorTable) .addBorder(4f, Color.DARK_GRAY) } /** @return Label to give the user more information and context below the error report. */ private fun makeInstructionLabel() = "{If this keeps happening, you can try disabling mods.}\n{You can also report this on the issue tracker.}".toLabel() .apply { wrap = true setAlignment(Align.center) } /** @return Table that displays decision buttons for the bottom of the screen. */ private fun makeActionButtonsTable(): Table { val copyButton = IconTextButton("Copy", fontSize = Constants.headingFontSize) .onClick { Gdx.app.clipboard.contents = text copied = true ToastPopup( "Error report copied.", this@CrashScreen ) } val reportButton = IconTextButton("Open Issue Tracker", ImageGetter.getImage("OtherIcons/Link"), Constants.headingFontSize) .onClick { if (copied) { Gdx.net.openURI("https://github.com/yairm210/Unciv/issues") } else { ToastPopup( "Please copy the error report first.", this@CrashScreen ) } } val closeButton = IconTextButton("Close Unciv", fontSize = Constants.headingFontSize) .onClick { Gdx.app.exit() } val buttonsTable = Table() buttonsTable.add(copyButton) .pad(10f) buttonsTable.add(reportButton) .pad(10f) .also { if (isCrampedPortrait()) { it.row() buttonsTable.add() } } buttonsTable.add(closeButton) .pad(10f) return buttonsTable } }