New crash handler screen Part 2: Threads, runnables, more informative reports. (#5810)

* Add crashHandlingThread.

* Replace all uses of `thread` with `crashHandlingThread`.

* Add `postCrashHandlingRunnable`.

* Replace all uses of `Gdx.app.postRunnable` with `postCrashHandlingRunnable`.

* Remove CrashController and CrashReport; Strip down CrashReportSender to CrashReportSysInfo; Fold their functionality into CrashScreen.

* Typo in comments, rename `SafeCrashStage` to `CrashHandlingStage`.

* Tweak docs.

* Tweak docs, comments, text. Undo an accidentally recursive Replace All change.

* Remove replaced translations.

* More readable indentation handling in report template.
This commit is contained in:
will-ca
2022-01-09 01:33:45 -08:00
committed by GitHub
parent 5931853c37
commit 24dfad696c
37 changed files with 278 additions and 298 deletions

View File

@ -31,9 +31,6 @@ Conquer a city!\nBring an enemy city down to low health > \nEnter the city with
Move an air unit!\nSelect an air unit > select another city within range > \nMove the unit to the other city =
See your stats breakdown!\nEnter the Overview screen (top right corner) >\nClick on 'Stats' =
Oh no! It looks like something went DISASTROUSLY wrong! This is ABSOLUTELY not supposed to happen! Please send me (yairm210@hotmail.com) an email with the game information (menu -> save game -> copy game info -> paste into email) and I'll try to fix it as fast as I can! =
Oh no! It looks like something went DISASTROUSLY wrong! This is ABSOLUTELY not supposed to happen! Please send us an report and we'll try to fix it as fast as we can! =
# Crash screen
An unrecoverable error has occurred in Unciv: =

View File

@ -39,7 +39,7 @@ open class AndroidLauncher : AndroidApplication() {
}
val androidParameters = UncivGameParameters(
version = BuildConfig.VERSION_NAME,
crashReportSender = CrashReportSenderAndroid(this),
crashReportSysInfo = CrashReportSysInfoAndroid,
fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt()),
customSaveLocationHelper = customSaveLocationHelper,
limitOrientationsHelper = limitOrientationsHelper

View File

@ -1,46 +0,0 @@
package com.unciv.app
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Build
import android.widget.Toast
import com.unciv.models.CrashReport
import com.unciv.ui.utils.CrashReportSender
class CrashReportSenderAndroid(private val activity: Activity) : CrashReportSender {
companion object {
private const val EMAIL_TO = "yairm210@hotmail.com"
private const val CHOOSER_TITLE = "Send mail"
private const val CANNOT_SEND_EMAIL = "There are no email clients installed."
private const val EMAIL_TITLE = "Crash report"
private const val EMAIL_BODY = "\n--------------------------------\n" +
"Game version: %s\n" +
"OS version: %s\n" +
"Device model: %s\n" +
"Mods: %s\n" +
"Game data: \n%s\n"
}
override fun sendReport(report: CrashReport) {
activity.runOnUiThread {
try {
activity.startActivity(Intent.createChooser(prepareIntent(report), CHOOSER_TITLE))
} catch (ex: ActivityNotFoundException) {
Toast.makeText(activity, CANNOT_SEND_EMAIL, Toast.LENGTH_SHORT).show()
}
}
}
private fun prepareIntent(report: CrashReport) = Intent(Intent.ACTION_SEND).apply {
type = "message/rfc822"
putExtra(Intent.EXTRA_EMAIL, arrayOf(EMAIL_TO))
putExtra(Intent.EXTRA_SUBJECT, "$EMAIL_TITLE - ${report.version}")
putExtra(Intent.EXTRA_TEXT, buildEmailBody(report))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
private fun buildEmailBody(report: CrashReport): String =
EMAIL_BODY.format(report.version, Build.VERSION.SDK_INT, Build.MODEL, report.mods.joinToString(), report.gameInfo)
}

View File

@ -0,0 +1,13 @@
package com.unciv.app
import android.os.Build
import com.unciv.ui.utils.CrashReportSysInfo
object CrashReportSysInfoAndroid: CrashReportSysInfo {
override fun getInfo(): String =
"""
Device Model: ${Build.MODEL}
API Level: ${Build.VERSION.SDK_INT}
""".trimIndent()
}

View File

@ -5,6 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.utils.viewport.Viewport
import com.unciv.ui.utils.*
/** Stage that safely brings the game to a [CrashScreen] if any event handlers throw an exception or an error that doesn't get otherwise handled. */
class CrashHandlingStage: Stage {
constructor(): super()
@ -38,11 +39,11 @@ class CrashHandlingStage: Stage {
// Another stack trace from an exception after setting TileInfo.naturalWonder to an invalid value is below that.
// Below that are another two exceptions from a lambda given to Gdx.app.postRunnable{} and another to thread{}.
// Below that are another two exceptions, from a lambda given to thread{} and another given to Gdx.app.postRunnable{}.
// Stage()'s event handlers seem to be the most universal place to intercept exceptions from events.
// Events and the render loop are the main ways that code gets run with GDX, right? So if we wrap both of those in exception handling, it should hopefully gracefully catch most unhandled exceptions… Threads may be the exception, hence why I put the wrapping as extension functions that can be invoked on the lambdas passed to threads.
// Events and the render loop are the main ways that code gets run with GDX, right? So if we wrap both of those in exception handling, it should hopefully gracefully catch most unhandled exceptions… Threads may be the exception, hence why I put the wrapping as extension functions that can be invoked on the lambdas passed to threads, as in crashHandlingThread and postCrashHandlingRunnable.
// Button click (event):

View File

@ -6,12 +6,24 @@ 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() {
@ -23,17 +35,79 @@ class CrashScreen(val exception: Throwable): BaseScreen() {
}
}
val text = generateReportHeader() + exception.stringify()
/** 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
fun generateReportHeader(): String {
/** @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}
Version: ${UncivGame.Current.version}
Rulesets: ${RulesetCache.keys}
**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:**
<details><summary>Show Saved Game</summary>
```
${tryGetSaveGame().prependIndentToOnlyNewLines(baseIndent)}
```
</details>
""".trimIndent()
}

View File

@ -68,10 +68,10 @@ class MainMenuScreen: BaseScreen() {
// will not exist unless we reset the ruleset and images
ImageGetter.ruleset = RulesetCache.getBaseRuleset()
thread(name = "ShowMapBackground") {
crashHandlingThread(name = "ShowMapBackground") {
val newMap = MapGenerator(RulesetCache.getBaseRuleset())
.generateMap(MapParameters().apply { mapSize = MapSizeNew(MapSize.Small); type = MapType.default })
Gdx.app.postRunnable { // for GL context
postCrashHandlingRunnable { // for GL context
ImageGetter.setNewRuleset(RulesetCache.getBaseRuleset())
val mapHolder = EditorMapHolder(MapEditorScreen(), newMap)
backgroundTable.addAction(Actions.sequence(
@ -197,10 +197,10 @@ class MainMenuScreen: BaseScreen() {
val loadingPopup = Popup(this)
loadingPopup.addGoodSizedLabel("Loading...")
loadingPopup.open()
thread {
crashHandlingThread {
// Load game from file to class on separate thread to avoid ANR...
fun outOfMemory() {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
loadingPopup.close()
ToastPopup("Not enough memory on phone to load game!", this)
}
@ -211,7 +211,7 @@ class MainMenuScreen: BaseScreen() {
savedGame = GameSaver.loadGameByName(autosave)
} catch (oom: OutOfMemoryError) {
outOfMemory()
return@thread
return@crashHandlingThread
} catch (ex: Exception) { // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first
// This can help for situations when the autosave is corrupted
try {
@ -221,17 +221,17 @@ class MainMenuScreen: BaseScreen() {
GameSaver.loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!)
} catch (oom: OutOfMemoryError) { // The autosave could have oom problems as well... smh
outOfMemory()
return@thread
return@crashHandlingThread
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
loadingPopup.close()
ToastPopup("Cannot resume game!", this)
}
return@thread
return@crashHandlingThread
}
}
Gdx.app.postRunnable { /// ... and load it into the screen on main thread for GL context
postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context
try {
game.loadGame(savedGame)
dispose()
@ -245,18 +245,18 @@ class MainMenuScreen: BaseScreen() {
private fun quickstartNewGame() {
ToastPopup("Working...", this)
val errorText = "Cannot start game with the default new game parameters!"
thread {
crashHandlingThread {
val newGame: GameInfo
// Can fail when starting the game...
try {
newGame = GameStarter.startNewGame(GameSetupInfo.fromSettings("Chieftain"))
} catch (ex: Exception) {
Gdx.app.postRunnable { ToastPopup(errorText, this) }
return@thread
postCrashHandlingRunnable { ToastPopup(errorText, this) }
return@crashHandlingThread
}
// ...or when loading the game
Gdx.app.postRunnable {
postCrashHandlingRunnable {
try {
game.loadGame(newGame)
} catch (outOfMemory: OutOfMemoryError) {

View File

@ -20,8 +20,6 @@ import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.PlayerReadyScreen
import com.unciv.ui.worldscreen.WorldScreen
import java.util.*
import kotlin.concurrent.thread
class UncivGame(parameters: UncivGameParameters) : Game() {
@ -29,7 +27,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
constructor(version: String) : this(UncivGameParameters(version, null))
val version = parameters.version
private val crashReportSender = parameters.crashReportSender
val crashReportSysInfo = parameters.crashReportSysInfo
val cancelDiscordEvent = parameters.cancelDiscordEvent
val fontImplementation = parameters.fontImplementation
val consoleMode = parameters.consoleMode
@ -39,7 +37,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
lateinit var gameInfo: GameInfo
fun isGameInfoInitialized() = this::gameInfo.isInitialized
lateinit var settings: GameSettings
lateinit var crashController: CrashController
lateinit var musicController: MusicController
/**
@ -105,7 +102,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
Gdx.graphics.isContinuousRendering = settings.continuousRendering
thread(name = "LoadJSON") {
crashHandlingThread(name = "LoadJSON") {
RulesetCache.loadRulesets(printOutput = true)
translations.tryReadTranslationForCurrentLanguage()
translations.loadPercentageCompleteOfLanguages()
@ -117,7 +114,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
}
// This stuff needs to run on the main thread because it needs the GL context
Gdx.app.postRunnable {
postCrashHandlingRunnable {
musicController.chooseTrack(suffix = MusicMood.Menu)
ImageGetter.ruleset = RulesetCache.getBaseRuleset() // so that we can enter the map editor without having to load a game first
@ -128,7 +125,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
isInitialized = true
}
}
crashController = CrashController.Impl(crashReportSender)
}
fun loadGame(gameInfo: GameInfo) {

View File

@ -1,12 +1,12 @@
package com.unciv
import com.unciv.logic.CustomSaveLocationHelper
import com.unciv.ui.utils.CrashReportSender
import com.unciv.ui.utils.CrashReportSysInfo
import com.unciv.ui.utils.LimitOrientationsHelper
import com.unciv.ui.utils.NativeFontImplementation
class UncivGameParameters(val version: String,
val crashReportSender: CrashReportSender? = null,
val crashReportSysInfo: CrashReportSysInfo? = null,
val cancelDiscordEvent: (() -> Unit)? = null,
val fontImplementation: NativeFontImplementation? = null,
val consoleMode: Boolean = false,

View File

@ -5,6 +5,8 @@ import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.utils.Json
import com.unciv.UncivGame
import com.unciv.models.metadata.GameSettings
import com.unciv.ui.utils.crashHandlingThread
import com.unciv.ui.utils.postCrashHandlingRunnable
import java.io.File
import kotlin.concurrent.thread
@ -144,10 +146,10 @@ object GameSaver {
// On the other hand if we alter the game data while it's being serialized we could get a concurrent modification exception.
// So what we do is we clone all the game data and serialize the clone.
val gameInfoClone = gameInfo.clone()
thread(name = "Autosave") {
crashHandlingThread(name = "Autosave") {
autoSaveSingleThreaded(gameInfoClone)
// do this on main thread
Gdx.app.postRunnable ( postRunnable )
postCrashHandlingRunnable ( postRunnable )
}
}

View File

@ -1,7 +0,0 @@
package com.unciv.models
data class CrashReport(
val gameInfo: String,
val mods: LinkedHashSet<String>,
val version: String
)

View File

@ -4,8 +4,8 @@ import com.unciv.logic.GameInfo
import com.unciv.logic.GameStarter
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.ui.utils.crashHandlingThread
import kotlin.time.Duration
import kotlin.concurrent.thread
import kotlin.math.max
import kotlin.time.ExperimentalTime
@ -44,7 +44,7 @@ class Simulation(
startTime = System.currentTimeMillis()
val threads: ArrayList<Thread> = ArrayList()
for (threadId in 1..threadsNumber) {
threads.add(thread {
threads.add(crashHandlingThread {
for (i in 1..simulationsPerThread) {
val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo))
gameInfo.simulateMaxTurns = maxTurns

View File

@ -3,6 +3,7 @@ package com.unciv.ui.audio
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.audio.Music
import com.badlogic.gdx.files.FileHandle
import com.unciv.ui.utils.crashHandlingThread
import kotlin.concurrent.thread
/** Wraps one Gdx Music instance and manages threaded loading, playback, fading and cleanup */
@ -57,7 +58,7 @@ class MusicTrackController(private var volume: Float) {
) {
if (state != State.None || loaderThread != null || music != null)
throw IllegalStateException("MusicTrackController.load should only be called once")
loaderThread = thread(name = "MusicLoader") {
loaderThread = crashHandlingThread(name = "MusicLoader") {
state = State.Loading
try {
music = Gdx.audio.newMusic(file)

View File

@ -203,10 +203,10 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
availableConstructionsTable.add("Loading...".toLabel()).pad(10f)
}
thread(name = "Construction info gathering - ${cityScreen.city.name}") {
crashHandlingThread(name = "Construction info gathering - ${cityScreen.city.name}") {
// Since this can be a heavy operation and leads to many ANRs on older phones we put the metadata-gathering in another thread.
val constructionButtonDTOList = getConstructionButtonDTOs()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val units = ArrayList<Table>()
val buildableWonders = ArrayList<Table>()
val buildableNationalWonders = ArrayList<Table>()

View File

@ -147,7 +147,7 @@ class MapEditorScreen(): BaseScreen() {
tileMap.mapParameters.run {
val areaFromSize = getArea()
if (areaFromSize == areaFromTiles) return
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val message = ("Invalid map: Area ([$areaFromTiles]) does not match saved dimensions ([" +
displayMapDimensions() + "]).").tr() +
"\n" + "The dimensions have now been fixed for you.".tr()

View File

@ -83,7 +83,7 @@ class NewMapScreen(val mapParameters: MapParameters = getDefaultParameters()) :
rightSideButton.onClick {
val message = mapParameters.mapSize.fixUndesiredSizes(mapParameters.worldWrap)
if (message != null) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup( message, UncivGame.Current.screen as BaseScreen, 4000 )
with (mapParameters.mapSize) {
mapParametersTable.customMapSizeRadius.text = radius.toString()
@ -96,12 +96,12 @@ class NewMapScreen(val mapParameters: MapParameters = getDefaultParameters()) :
Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked!
rightButtonSetEnabled(false)
thread(name = "MapGenerator") {
crashHandlingThread(name = "MapGenerator") {
try {
// Map generation can take a while and we don't want ANRs
generatedMap = MapGenerator(ruleset).generateMap(mapParameters)
Gdx.app.postRunnable {
postCrashHandlingRunnable {
saveDefaultParameters(mapParameters)
val mapEditorScreen = MapEditorScreen(generatedMap!!)
mapEditorScreen.ruleset = ruleset
@ -110,7 +110,7 @@ class NewMapScreen(val mapParameters: MapParameters = getDefaultParameters()) :
} catch (exception: Exception) {
println("Map generator exception: ${exception.message}")
Gdx.app.postRunnable {
postCrashHandlingRunnable {
rightButtonSetEnabled(true)
val cantMakeThatMapPopup = Popup(this)
cantMakeThatMapPopup.addGoodSizedLabel("It looks like we can't make a map with the parameters you requested!".tr())

View File

@ -32,17 +32,17 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
rightSideButtonAction = {
mapToSave!!.mapParameters.name = mapNameTextField.text
mapToSave.mapParameters.type = MapType.custom
thread(name = "SaveMap") {
crashHandlingThread(name = "SaveMap") {
try {
MapSaver.saveMap(mapNameTextField.text, getMapCloneForSave(mapToSave))
Gdx.app.postRunnable {
postCrashHandlingRunnable {
Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up.
game.setScreen(MapEditorScreen(mapToSave))
dispose()
}
} catch (ex: Exception) {
ex.printStackTrace()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val cantLoadGamePopup = Popup(this)
cantLoadGamePopup.addGoodSizedLabel("It looks like your map can't be saved!").row()
cantLoadGamePopup.addCloseButton()
@ -54,11 +54,11 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
} else {
rightSideButton.setText("Load map".tr())
rightSideButtonAction = {
thread(name = "MapLoader") {
crashHandlingThread(name = "MapLoader") {
var popup: Popup? = null
var needPopup = true // loadMap can fail faster than postRunnable runs
Gdx.app.postRunnable {
if (!needPopup) return@postRunnable
postCrashHandlingRunnable {
if (!needPopup) return@postCrashHandlingRunnable
popup = Popup(this).apply {
addGoodSizedLabel("Loading...")
open()
@ -76,12 +76,12 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
if (map.mapParameters.baseRuleset !in RulesetCache) missingMods += map.mapParameters.baseRuleset
if (missingMods.isNotEmpty()) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
needPopup = false
popup?.close()
ToastPopup("Missing mods: [${missingMods.joinToString()}]", this)
}
} else Gdx.app.postRunnable {
} else postCrashHandlingRunnable {
Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up.
try {
// For deprecated maps, set the base ruleset field if it's still saved in the mods field
@ -105,7 +105,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
} catch (ex: Throwable) {
needPopup = false
ex.printStackTrace()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
popup?.close()
println("Error loading map \"$chosenMap\": ${ex.localizedMessage}")
ToastPopup("Error loading map!".tr() +

View File

@ -80,7 +80,7 @@ class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: St
popup.addGoodSizedLabel("Working...").row()
popup.open()
thread {
crashHandlingThread {
try {
//download to work with newest game state
val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId)
@ -107,14 +107,14 @@ class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: St
GameSaver.saveGame(updatedSave, gameName)
OnlineMultiplayer().tryUploadGame(gameInfo, withPreview = true)
Gdx.app.postRunnable {
postCrashHandlingRunnable {
popup.close()
//go back to the MultiplayerScreen
backScreen.game.setScreen(backScreen)
backScreen.reloadGameListUI()
}
} else {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
//change popup text
popup.innerTable.clear()
popup.addGoodSizedLabel("You can only resign if it's your turn").row()
@ -122,7 +122,7 @@ class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: St
}
}
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
//change popup text
popup.innerTable.clear()
popup.addGoodSizedLabel("Could not upload game!").row()

View File

@ -141,7 +141,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
addGameButton.setText("Working...".tr())
addGameButton.disable()
thread(name = "MultiplayerDownload") {
crashHandlingThread(name = "MultiplayerDownload") {
try {
// The tryDownload can take more than 500ms. Therefore, to avoid ANRs,
// we need to run it in a different thread.
@ -151,7 +151,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
else
GameSaver.saveGame(gamePreview, gameName)
Gdx.app.postRunnable { reloadGameListUI() }
postCrashHandlingRunnable { reloadGameListUI() }
} catch (ex: FileNotFoundException) {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
try {
@ -161,9 +161,9 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
else
GameSaver.saveGame(gamePreview, gameName)
Gdx.app.postRunnable { reloadGameListUI() }
postCrashHandlingRunnable { reloadGameListUI() }
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val errorPopup = Popup(this)
errorPopup.addGoodSizedLabel("Could not download game!")
errorPopup.row()
@ -172,7 +172,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
}
}
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val errorPopup = Popup(this)
errorPopup.addGoodSizedLabel("Could not download game!")
errorPopup.row()
@ -180,7 +180,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
errorPopup.open()
}
}
Gdx.app.postRunnable {
postCrashHandlingRunnable {
addGameButton.setText(addGameText)
addGameButton.enable()
}
@ -197,7 +197,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
// For whatever reason, the only way to show the popup before the ANRs started was to
// call the loadGame explicitly with a runnable on the main thread.
// Maybe this adds just enough lag for the popup to show up
Gdx.app.postRunnable {
postCrashHandlingRunnable {
game.loadGame(OnlineMultiplayer().tryDownloadGame((multiplayerGames[selectedGameFile]!!.gameId)))
}
} catch (ex: Exception) {
@ -279,7 +279,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
continue
}
thread(name = "loadGameFile") {
crashHandlingThread(name = "loadGameFile") {
try {
val game = gameSaver.loadGamePreviewFromFile(gameSaveFile)
@ -288,7 +288,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
multiplayerGames[gameSaveFile] = game
}
Gdx.app.postRunnable {
postCrashHandlingRunnable {
turnIndicator.clear()
if (isUsersTurn(game)) {
turnIndicator.add(ImageGetter.getImage("OtherIcons/ExclamationMark")).size(50f)
@ -299,7 +299,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
}
} catch (usx: UncivShowableException) {
//Gets thrown when mods are not installed
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val popup = Popup(this)
popup.addGoodSizedLabel(usx.message!! + " in ${gameSaveFile.name()}").row()
popup.addCloseButton()
@ -309,7 +309,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
turnIndicator.add(ImageGetter.getImage("StatIcons/Malcontent")).size(50f)
}
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("Could not refresh!", this)
turnIndicator.clear()
turnIndicator.add(ImageGetter.getImage("StatIcons/Malcontent")).size(50f)
@ -330,7 +330,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
refreshButton.disable()
//One thread for all downloads
thread(name = "multiplayerGameDownload") {
crashHandlingThread(name = "multiplayerGameDownload") {
for ((fileHandle, gameInfo) in multiplayerGames) {
try {
// Update game without overriding multiplayer settings
@ -347,21 +347,21 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
multiplayerGames[fileHandle] = game
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("Could not download game!" + " ${fileHandle.name()}", this)
}
}
} catch (ex: Exception) {
//skipping one is not fatal
//Trying to use as many prev. used strings as possible
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("Could not download game!" + " ${fileHandle.name()}", this)
}
}
}
//Reset UI
Gdx.app.postRunnable {
postCrashHandlingRunnable {
addGameButton.enable()
refreshButton.setText(refreshText)
refreshButton.enable()

View File

@ -109,7 +109,7 @@ class NewGameScreen(
val mapSize = gameSetupInfo.mapParameters.mapSize
val message = mapSize.fixUndesiredSizes(gameSetupInfo.mapParameters.worldWrap)
if (message != null) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup( message, UncivGame.Current.screen as BaseScreen, 4000 )
with (mapOptionsTable.generatedMapOptionsTable) {
customMapSizeRadius.text = mapSize.radius.toString()
@ -125,7 +125,7 @@ class NewGameScreen(
rightSideButton.disable()
rightSideButton.setText("Working...".tr())
thread(name = "NewGame") {
crashHandlingThread(name = "NewGame") {
// Creating a new game can take a while and we don't want ANRs
newGameThread()
}
@ -181,7 +181,7 @@ class NewGameScreen(
newGame = GameStarter.startNewGame(gameSetupInfo)
} catch (exception: Exception) {
exception.printStackTrace()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
Popup(this).apply {
addGoodSizedLabel("It looks like we can't make a map with the parameters you requested!".tr()).row()
addGoodSizedLabel("Maybe you put too many players into too small a map?".tr()).row()
@ -202,7 +202,7 @@ class NewGameScreen(
// Save gameId to clipboard because you have to do it anyway.
Gdx.app.clipboard.contents = newGame!!.gameId
// Popup to notify the User that the gameID got copied to the clipboard
Gdx.app.postRunnable { ToastPopup("gameID copied to clipboard".tr(), game.worldScreen, 2500) }
postCrashHandlingRunnable { ToastPopup("gameID copied to clipboard".tr(), game.worldScreen, 2500) }
GameSaver.autoSave(newGame!!) {}
@ -210,7 +210,7 @@ class NewGameScreen(
val newGamePreview = newGame!!.asPreview()
GameSaver.saveGame(newGamePreview, newGamePreview.gameId)
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
Popup(this).apply {
addGoodSizedLabel("Could not upload game!")
addCloseButton()

View File

@ -194,19 +194,19 @@ class ModManagementScreen(
* calls itself for the next page of search results
*/
private fun tryDownloadPage(pageNum: Int) {
runningSearchThread = thread(name="GitHubSearch") {
runningSearchThread = crashHandlingThread(name="GitHubSearch") {
val repoSearch: Github.RepoSearch
try {
repoSearch = Github.tryGetGithubReposWithTopic(amountPerPage, pageNum)!!
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("Could not download mod list", this)
}
runningSearchThread = null
return@thread
return@crashHandlingThread
}
Gdx.app.postRunnable {
postCrashHandlingRunnable {
// clear and remove last cell if it is the "..." indicator
val lastCell = downloadTable.cells.lastOrNull()
if (lastCell != null && lastCell.actor is Label && (lastCell.actor as Label).text.toString() == "...") {
@ -215,7 +215,7 @@ class ModManagementScreen(
}
for (repo in repoSearch.items) {
if (stopBackgroundTasks) return@postRunnable
if (stopBackgroundTasks) return@postCrashHandlingRunnable
repo.name = repo.name.replace('-', ' ')
// Mods we have manually decided to remove for instability are removed here
@ -404,13 +404,13 @@ class ModManagementScreen(
/** Download and install a mod in the background, called both from the right-bottom button and the URL entry popup */
private fun downloadMod(repo: Github.Repo, postAction: () -> Unit = {}) {
thread(name="DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions
crashHandlingThread(name="DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions
try {
val modFolder = Github.downloadAndExtract(repo.html_url, repo.default_branch,
Gdx.files.local("mods"))
?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something!
rewriteModOptions(repo, modFolder)
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("[${repo.name}] Downloaded!", this)
RulesetCache.loadRulesets()
RulesetCache[repo.name]?.let {
@ -421,7 +421,7 @@ class ModManagementScreen(
unMarkUpdatedMod(repo.name)
}
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("Could not download [${repo.name}]", this)
}
} finally {

View File

@ -288,7 +288,7 @@ class TechPickerScreen(
}
private fun centerOnTechnology(tech: Technology) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
techNameToButton[tech.name]?.let {
scrollPane.scrollTo(it.x, it.y, it.width, it.height, true, true)
scrollPane.updateVisualScroll()

View File

@ -41,13 +41,13 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
val loadingPopup = Popup( this)
loadingPopup.addGoodSizedLabel("Loading...")
loadingPopup.open()
thread {
crashHandlingThread {
try {
// This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread
val loadedGame = GameSaver.loadGameByName(selectedSave)
Gdx.app.postRunnable { UncivGame.Current.loadGame(loadedGame) }
postCrashHandlingRunnable { UncivGame.Current.loadGame(loadedGame) }
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
loadingPopup.close()
val cantLoadGamePopup = Popup(this)
cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row()
@ -99,7 +99,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
loadFromCustomLocation.onClick {
GameSaver.loadGameFromCustomLocation { gameInfo, exception ->
if (gameInfo != null) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
game.loadGame(gameInfo)
}
} else if (exception !is CancellationException) {
@ -153,12 +153,12 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
loadImage.addAction(Actions.rotateBy(360f, 2f))
saveTable.add(loadImage).size(50f)
thread { // Apparently, even jut getting the list of saves can cause ANRs -
crashHandlingThread { // Apparently, even jut getting the list of saves can cause ANRs -
// not sure how many saves these guys had but Google Play reports this to have happened hundreds of times
// .toList() because otherwise the lastModified will only be checked inside the postRunnable
val saves = GameSaver.getSaves().sortedByDescending { it.lastModified() }.toList()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
saveTable.clear()
for (save in saves) {
if (save.name().startsWith("Autosave") && !showAutosaves) continue
@ -183,7 +183,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
val savedAt = Date(save.lastModified())
var textToSet = save.name() + "\n${"Saved at".tr()}: " + savedAt.formatDate()
thread { // Even loading the game to get its metadata can take a long time on older phones
crashHandlingThread { // Even loading the game to get its metadata can take a long time on older phones
try {
val game = GameSaver.loadGamePreviewFromFile(save)
val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() }
@ -196,7 +196,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
textToSet += "\n${"Could not load game".tr()}!"
}
Gdx.app.postRunnable {
postCrashHandlingRunnable {
descriptionLabel.setText(textToSet)
}
}

View File

@ -56,10 +56,10 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
errorLabel.setText("")
saveToCustomLocation.setText("Saving...".tr())
saveToCustomLocation.disable()
thread(name = "SaveGame") {
crashHandlingThread(name = "SaveGame") {
GameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e ->
if (e == null) {
Gdx.app.postRunnable { game.setWorldScreen() }
postCrashHandlingRunnable { game.setWorldScreen() }
} else if (e !is CancellationException) {
errorLabel.setText("Could not save game to custom location!".tr())
e.printStackTrace()
@ -94,9 +94,9 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
private fun saveGame() {
rightSideButton.setText("Saving...".tr())
thread(name = "SaveGame") {
crashHandlingThread(name = "SaveGame") {
GameSaver.saveGame(gameInfo, gameNameTextField.text) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
if (it != null) ToastPopup("Could not save game!", this)
else UncivGame.Current.setWorldScreen()
}

View File

@ -17,7 +17,6 @@ import com.unciv.models.Tutorial
import com.unciv.ui.tutorials.TutorialController
import com.unciv.ui.worldscreen.WorldScreen
import com.unciv.ui.worldscreen.mainmenu.OptionsPopup
import kotlin.concurrent.thread
open class BaseScreen : Screen {
@ -127,12 +126,12 @@ open class BaseScreen : Screen {
throw IllegalArgumentException("openOptionsPopup called on wrong derivative class")
}
limitOrientationsHelper.allowPortrait(false)
thread(name="WaitForRotation") {
crashHandlingThread(name="WaitForRotation") {
var waited = 0
while (true) {
val newScreen = (UncivGame.Current.screen as? BaseScreen)
if (waited >= 10000 || newScreen!=null && !newScreen.isPortrait() ) {
Gdx.app.postRunnable { OptionsPopup(newScreen ?: this).open(true) }
postCrashHandlingRunnable { OptionsPopup(newScreen ?: this).open(true) }
break
}
Thread.sleep(200)

View File

@ -1,71 +0,0 @@
package com.unciv.ui.utils
import com.badlogic.gdx.utils.Json
import com.unciv.UncivGame
import com.unciv.models.CrashReport
import com.unciv.ui.saves.Gzip
interface CrashController {
fun crashOccurred()
fun showDialogIfNeeded()
class Impl(private val crashReportSender: CrashReportSender?) : CrashController {
companion object {
private const val MESSAGE = "Oh no! It looks like something went DISASTROUSLY wrong!" +
" This is ABSOLUTELY not supposed to happen! Please send us an report" +
" and we'll try to fix it as fast as we can!"
private const val MESSAGE_FALLBACK = "Oh no! It looks like something went DISASTROUSLY wrong!" +
" This is ABSOLUTELY not supposed to happen! Please send me (yairm210@hotmail.com)" +
" an email with the game information (menu -> save game -> copy game info -> paste into email)" +
" and I'll try to fix it as fast as I can!"
}
override fun crashOccurred() {
UncivGame.Current.settings.run {
hasCrashedRecently = true
save()
}
}
override fun showDialogIfNeeded() {
UncivGame.Current.settings.run {
if (hasCrashedRecently) {
prepareDialog().open()
hasCrashedRecently = false
save()
}
}
}
private fun prepareDialog(): Popup {
return if (crashReportSender == null) {
Popup(UncivGame.Current.worldScreen).apply {
addGoodSizedLabel(MESSAGE_FALLBACK).row()
addCloseButton()
}
} else {
Popup(UncivGame.Current.worldScreen).apply {
addGoodSizedLabel(MESSAGE).row()
addButton("Send report") {
crashReportSender.sendReport(buildReport())
close()
}
addCloseButton()
}
}
}
private fun buildReport(): CrashReport {
return UncivGame.Current.run {
val zippedGameInfo = Json().toJson(gameInfo).let { Gzip.zip(it) }
CrashReport(
zippedGameInfo,
LinkedHashSet(gameInfo.gameParameters.getModsAndBaseRuleset()),
version
)
}
}
}
}

View File

@ -0,0 +1,26 @@
package com.unciv.ui.utils
import com.badlogic.gdx.Gdx
import kotlin.concurrent.thread
/** Wrapped version of [kotlin.concurrent.thread], that brings the main game loop to a [com.unciv.CrashScreen] if an exception happens. */
fun crashHandlingThread(
start: Boolean = true,
isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null,
name: String? = null,
priority: Int = -1,
block: () -> Unit
) = thread(
start = start,
isDaemon = isDaemon,
contextClassLoader = contextClassLoader,
name = name,
priority = priority,
block = block.wrapCrashHandlingUnit(true)
)
/** Wrapped version of Gdx.app.postRunnable ([com.badlogic.gdx.Application.postRunnable]), that brings the game loop to a [com.unciv.CrashScreen] if an exception occurs. */
fun postCrashHandlingRunnable(runnable: () -> Unit) {
Gdx.app.postRunnable(runnable.wrapCrashHandlingUnit())
}

View File

@ -1,8 +0,0 @@
package com.unciv.ui.utils
import com.unciv.models.CrashReport
interface CrashReportSender {
fun sendReport(report: CrashReport)
}

View File

@ -0,0 +1,5 @@
package com.unciv.ui.utils
interface CrashReportSysInfo {
fun getInfo(): String
}

View File

@ -12,7 +12,6 @@ import com.unciv.models.UncivSound
import com.unciv.models.translations.tr
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
import kotlin.random.Random
/**
@ -53,7 +52,7 @@ fun Actor.center(parent: Stage){ centerX(parent); centerY(parent)}
fun Actor.onClickEvent(sound: UncivSound = UncivSound.Click, function: (event: InputEvent?, x: Float, y: Float) -> Unit) {
this.addListener(object : ClickListener() {
override fun clicked(event: InputEvent?, x: Float, y: Float) {
thread(name = "Sound") { Sounds.play(sound) }
crashHandlingThread(name = "Sound") { Sounds.play(sound) }
function(event, x, y)
}
})
@ -319,6 +318,10 @@ object UncivDateFormat {
*
* In case an exception or error is thrown, the return will be null. Therefore the return type is always nullable.
*
* The game loop, threading, and event systems already use this to wrap nearly everything that can happen during the lifespan of the Unciv application.
*
* Therefore, it usually shouldn't be necessary to manually use this. See the note at the top of [CrashScreen].kt for details.
*
* @param postToMainThread Whether the [CrashScreen] should be opened by posting a runnable to the main thread, instead of directly. Set this to true if the function is going to run on any thread other than the main loop.
* @return Result from the function, or null if an exception is thrown.
* */
@ -343,12 +346,16 @@ fun <R> (() -> R).wrapCrashHandling(
/**
* Returns a wrapped a version of a Unit-returning function which safely crashes the game to [CrashScreen] if an exception or error is thrown.
*
* The game loop, threading, and event systems already use this to wrap nearly everything that can happen during the lifespan of the Unciv application.
*
* Therefore, it usually shouldn't be necessary to manually use this. See the note at the top of [CrashScreen].kt for details.
*
* @param postToMainThread Whether the [CrashScreen] should be opened by posting a runnable to the main thread, instead of directly. Set this to true if the function is going to run on any thread other than the main loop.
* */
fun (() -> Unit).wrapCrashHandlingUnit(
postToMainThread: Boolean = false
): () -> Unit {
val wrappedReturning = this.wrapCrashHandling(postToMainThread)
// Don't instantiate a new lambda every time.
// Don't instantiate a new lambda every time the return get called.
return { wrappedReturning() ?: Unit }
}

View File

@ -164,7 +164,7 @@ object Sounds {
val initialDelay = if (isFresh && Gdx.app.type == Application.ApplicationType.Android) 40 else 0
if (initialDelay > 0 || resource.play(volume) == -1L) {
thread (name = "DelayedSound") {
crashHandlingThread(name = "DelayedSound") {
Thread.sleep(initialDelay.toLong())
while (resource.play(volume) == -1L) {
Thread.sleep(20L)

View File

@ -21,9 +21,9 @@ class ToastPopup (message: String, screen: BaseScreen, val time: Long = 2000) :
}
private fun startTimer(){
thread (name = "ResponsePopup") {
crashHandlingThread(name = "ResponsePopup") {
Thread.sleep(time)
Gdx.app.postRunnable { this.close() }
postCrashHandlingRunnable { this.close() }
}
}

View File

@ -5,10 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.GameInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.ImageGetter
import com.unciv.ui.utils.onClick
import com.unciv.ui.utils.toLabel
import com.unciv.ui.utils.*
class PlayerReadyScreen(gameInfo: GameInfo, currentPlayerCiv: CivilizationInfo) : BaseScreen(){
init {
@ -19,7 +16,7 @@ class PlayerReadyScreen(gameInfo: GameInfo, currentPlayerCiv: CivilizationInfo)
table.add("[$currentPlayerCiv] ready?".toLabel(currentPlayerCiv.nation.getInnerColor(),24))
table.onClick {
Gdx.app.postRunnable { // To avoid ANRs on Android when the creation of the worldscreen takes more than 500ms
postCrashHandlingRunnable { // To avoid ANRs on Android when the creation of the worldscreen takes more than 500ms
game.worldScreen = WorldScreen(gameInfo, currentPlayerCiv)
game.setWorldScreen()
}

View File

@ -99,7 +99,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
override fun clicked(event: InputEvent?, x: Float, y: Float) {
val unit = worldScreen.bottomUnitTable.selectedUnit
?: return
thread {
crashHandlingThread {
val tile = tileGroup.tileInfo
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
@ -107,7 +107,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
swapMoveUnitToTargetTile(unit, tile)
}
// If we are in unit-swapping mode, we don't want to move or attack
return@thread
return@crashHandlingThread
}
val attackableTile = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
@ -115,13 +115,13 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
if (unit.canAttack() && attackableTile != null) {
Battle.moveAndAttack(MapUnitCombatant(unit), attackableTile)
worldScreen.shouldUpdate = true
return@thread
return@crashHandlingThread
}
val canUnitReachTile = unit.movement.canReach(tile)
if (canUnitReachTile) {
moveUnitToTargetTile(listOf(unit), tile)
return@thread
return@crashHandlingThread
}
}
}
@ -198,7 +198,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
val selectedUnit = selectedUnits.first()
thread(name = "TileToMoveTo") {
crashHandlingThread(name = "TileToMoveTo") {
// these are the heavy parts, finding where we want to go
// Since this runs in a different thread, even if we check movement.canReach()
// then it might change until we get to the getTileToMoveTo, so we just try/catch it
@ -208,10 +208,10 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
} catch (ex: Exception) {
println("Exception in getTileToMoveToThisTurn: ${ex.message}")
ex.printStackTrace()
return@thread
return@crashHandlingThread
} // can't move here
Gdx.app.postRunnable {
postCrashHandlingRunnable {
try {
// Because this is darned concurrent (as it MUST be to avoid ANRs),
// there are edge cases where the canReach is true,
@ -254,7 +254,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tileInfo: TileInfo) {
thread(name = "TurnsToGetThere") {
crashHandlingThread(name = "TurnsToGetThere") {
/** LibGdx sometimes has these weird errors when you try to edit the UI layout from 2 separate threads.
* And so, all UI editing will be done on the main thread.
* The only "heavy lifting" that needs to be done is getting the turns to get there,
@ -279,12 +279,12 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
unitToTurnsToTile[unit] = turnsToGetThere
}
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val unitsWhoCanMoveThere = HashMap(unitToTurnsToTile.filter { it.value != 0 })
if (unitsWhoCanMoveThere.isEmpty()) { // give the regular tile overlays with no unit movement
addTileOverlays(tileInfo)
worldScreen.shouldUpdate = true
return@postRunnable
return@postCrashHandlingRunnable
}
val turnsToGetThere = unitsWhoCanMoveThere.values.maxOrNull()!!

View File

@ -205,9 +205,9 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// GameSaver.autoSave, SaveGameScreen.saveGame, LoadGameScreen.rightSideButton.onClick,...
val quickSave = {
val toast = ToastPopup("Quicksaving...", this)
thread(name = "SaveGame") {
crashHandlingThread(name = "SaveGame") {
GameSaver.saveGame(gameInfo, "QuickSave") {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
toast.close()
if (it != null)
ToastPopup("Could not save game!", this)
@ -221,16 +221,16 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
}
val quickLoad = {
val toast = ToastPopup("Quickloading...", this)
thread(name = "SaveGame") {
crashHandlingThread(name = "SaveGame") {
try {
val loadedGame = GameSaver.loadGameByName("QuickSave")
Gdx.app.postRunnable {
postCrashHandlingRunnable {
toast.close()
UncivGame.Current.loadGame(loadedGame)
ToastPopup("Quickload successful.", this)
}
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
ToastPopup("Could not load game!", this)
}
}
@ -328,7 +328,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// Since we're on a background thread, all the UI calls in this func need to run from the
// main thread which has a GL context
val loadingGamePopup = Popup(this)
Gdx.app.postRunnable {
postCrashHandlingRunnable {
loadingGamePopup.add("Loading latest game state...".tr())
loadingGamePopup.open()
}
@ -343,18 +343,18 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
&& gameInfo.turns == latestGame.turns
&& latestGame.currentPlayer != gameInfo.getPlayerToViewAs().civName
) {
Gdx.app.postRunnable { loadingGamePopup.close() }
postCrashHandlingRunnable { loadingGamePopup.close() }
shouldUpdate = true
return
} else { // if the game updated, even if it's not our turn, reload the world -
// stuff has changed and the "waiting for X" will now show the correct civ
stopMultiPlayerRefresher()
latestGame.isUpToDate = true
Gdx.app.postRunnable { createNewWorldScreen(latestGame) }
postCrashHandlingRunnable { createNewWorldScreen(latestGame) }
}
} catch (ex: Throwable) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
val couldntDownloadLatestGame = Popup(this)
couldntDownloadLatestGame.addGoodSizedLabel("Couldn't download the latest game state!").row()
couldntDownloadLatestGame.addCloseButton()
@ -504,7 +504,6 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
}
private fun displayTutorialsOnUpdate() {
game.crashController.showDialogIfNeeded()
displayTutorial(Tutorial.Introduction)
@ -632,21 +631,20 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
shouldUpdate = true
thread(name = "NextTurn") { // on a separate thread so the user can explore their world while we're passing the turn
crashHandlingThread(name = "NextTurn") { // on a separate thread so the user can explore their world while we're passing the turn
if (consoleLog)
println("\nNext turn starting " + Date().formatDate())
val startTime = System.currentTimeMillis()
val gameInfoClone = gameInfo.clone()
gameInfoClone.setTransients() // this can get expensive on large games, not the clone itself
try {
gameInfoClone.nextTurn()
if (gameInfo.gameParameters.isOnlineMultiplayer) {
try {
OnlineMultiplayer().tryUploadGame(gameInfoClone, withPreview = true)
} catch (ex: Exception) {
Gdx.app.postRunnable { // Since we're changing the UI, that should be done on the main thread
postCrashHandlingRunnable { // Since we're changing the UI, that should be done on the main thread
val cantUploadNewGamePopup = Popup(this)
cantUploadNewGamePopup.addGoodSizedLabel("Could not upload game!").row()
cantUploadNewGamePopup.addCloseButton()
@ -654,13 +652,9 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
}
isPlayersTurn = true // Since we couldn't push the new game clone, then it's like we never clicked the "next turn" button
shouldUpdate = true
return@thread
return@crashHandlingThread
}
}
} catch (ex: Exception) {
Gdx.app.postRunnable { game.crashController.crashOccurred() }
throw ex
}
game.gameInfo = gameInfoClone
if (consoleLog)
@ -670,7 +664,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// create a new WorldScreen to show the new stuff we've changed, and switch out the current screen.
// do this on main thread - it's the only one that has a GL context to create images from
Gdx.app.postRunnable {
postCrashHandlingRunnable {
if (gameInfoClone.currentPlayerCiv.civName != viewingCiv.civName
@ -798,10 +792,10 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
viewingCiv.hasMovedAutomatedUnits = true
isPlayersTurn = false // Disable state changes
nextTurnButton.disable()
thread(name="Move automated units") {
crashHandlingThread(name="Move automated units") {
for (unit in viewingCiv.getCivUnits())
unit.doAction()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
shouldUpdate = true
isPlayersTurn = true //Re-enable state changes
nextTurnButton.enable()

View File

@ -273,7 +273,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
if (modCheckCheckBox == null) return
modCheckCheckBox!!.disable()
if (modCheckResultCell == null) return
thread(name="ModChecker") {
crashHandlingThread(name="ModChecker") {
val lines = ArrayList<FormattedLine>()
var noProblem = true
for (mod in RulesetCache.values.sortedBy { it.name }) {
@ -297,7 +297,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
}
if (noProblem) lines += FormattedLine("{No problems found}.",)
Gdx.app.postRunnable {
postCrashHandlingRunnable {
// Don't just render text, since that will make all the conditionals in the mod replacement messages move to the end, which makes it unreadable
// Don't use .toLabel() either, since that activates translations as well, which is what we're trying to avoid,
// Instead, some manual work needs to be put in.
@ -504,7 +504,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
label.wrap = true
add(label).padTop(20f).colspan(2).fillX().row()
previousScreen.game.musicController.onChange {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
label.setText("Currently playing: [$it]".tr())
}
}
@ -523,15 +523,15 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
errorTable.add("Downloading...".toLabel())
// So the whole game doesn't get stuck while downloading the file
thread(name = "Music") {
crashHandlingThread(name = "Music") {
try {
previousScreen.game.musicController.downloadDefaultFile()
Gdx.app.postRunnable {
postCrashHandlingRunnable {
tabs.replacePage("Sound", getSoundTab())
previousScreen.game.musicController.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
}
} catch (ex: Exception) {
Gdx.app.postRunnable {
postCrashHandlingRunnable {
errorTable.clear()
errorTable.add("Could not download music!".toLabel(Color.RED))
}

View File

@ -44,7 +44,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
actionButton.onClick(unitAction.uncivSound, action)
if (key != KeyCharAndCode.UNKNOWN)
worldScreen.keyPressDispatcher[key] = {
thread(name = "Sound") { Sounds.play(unitAction.uncivSound) }
crashHandlingThread(name = "Sound") { Sounds.play(unitAction.uncivSound) }
action()
worldScreen.mapHolder.removeUnitActionOverlay()
}