mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 23:39:40 +07:00
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:
@ -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: =
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
13
android/src/com/unciv/app/CrashReportSysInfoAndroid.kt
Normal file
13
android/src/com/unciv/app/CrashReportSysInfoAndroid.kt
Normal 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()
|
||||
}
|
@ -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):
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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 )
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
package com.unciv.models
|
||||
|
||||
data class CrashReport(
|
||||
val gameInfo: String,
|
||||
val mods: LinkedHashSet<String>,
|
||||
val version: String
|
||||
)
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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>()
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
@ -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() +
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
core/src/com/unciv/ui/utils/CrashHandlingThread.kt
Normal file
26
core/src/com/unciv/ui/utils/CrashHandlingThread.kt
Normal 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())
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.unciv.models.CrashReport
|
||||
|
||||
interface CrashReportSender {
|
||||
|
||||
fun sendReport(report: CrashReport)
|
||||
}
|
5
core/src/com/unciv/ui/utils/CrashReportSysInfo.kt
Normal file
5
core/src/com/unciv/ui/utils/CrashReportSysInfo.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
interface CrashReportSysInfo {
|
||||
fun getInfo(): String
|
||||
}
|
@ -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 }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()!!
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
Reference in New Issue
Block a user