mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-20 12:48:56 +07:00
Refactor: Consistent & correct usage of coroutines (#7077)
* Refactor: Consistent usage of coroutines * Add usage comments to the different threads * Refactor: Properly separate crash handling into its platform-specific parts * Fix autoSave never finishing * Correctly handle coroutines when the GL thread is not accepting runnables anymore Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
@ -7,7 +7,6 @@ import android.provider.DocumentsContract
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.GuardedBy
|
||||
import com.unciv.logic.CustomFileLocationHelper
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
@ -3,6 +3,7 @@ package com.unciv.app
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import com.unciv.ui.utils.GeneralPlatformSpecificHelpers
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/** See also interface [GeneralPlatformSpecificHelpers].
|
||||
*
|
||||
@ -35,4 +36,9 @@ Sources for Info about current orientation in case need:
|
||||
* External is probably on an SD-card or similar which is always accessible by the user.
|
||||
*/
|
||||
override fun shouldPreferExternalStorage(): Boolean = true
|
||||
|
||||
override fun handleUncaughtThrowable(ex: Throwable): Boolean {
|
||||
thread { throw ex } // this will kill the app but report the exception to the Google Play Console if the user allows it
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -17,13 +17,11 @@ import com.unciv.models.metadata.BaseRuleset
|
||||
import com.unciv.models.metadata.GameSetupInfo
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.ui.civilopedia.CivilopediaScreen
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.map.TileGroupMap
|
||||
import com.unciv.ui.mapeditor.EditorMapHolder
|
||||
import com.unciv.ui.mapeditor.MapEditorScreen
|
||||
import com.unciv.ui.multiplayer.MultiplayerScreen
|
||||
import com.unciv.ui.map.TileGroupMap
|
||||
import com.unciv.ui.newgamescreen.NewGameScreen
|
||||
import com.unciv.ui.pickerscreens.ModManagementScreen
|
||||
import com.unciv.ui.popup.ExitGamePopup
|
||||
@ -44,6 +42,8 @@ import com.unciv.ui.utils.extensions.setFontSize
|
||||
import com.unciv.ui.utils.extensions.surroundWithCircle
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.worldscreen.mainmenu.WorldScreenMenuPopup
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ class MainMenuScreen: BaseScreen() {
|
||||
// will not exist unless we reset the ruleset and images
|
||||
ImageGetter.ruleset = RulesetCache.getVanillaRuleset()
|
||||
|
||||
launchCrashHandling("ShowMapBackground") {
|
||||
Concurrency.run("ShowMapBackground") {
|
||||
var scale = 1f
|
||||
var mapWidth = stage.width / TileGroupMap.groupHorizontalAdvance
|
||||
var mapHeight = stage.height / TileGroupMap.groupSize
|
||||
@ -110,7 +110,7 @@ class MainMenuScreen: BaseScreen() {
|
||||
waterThreshold = -0.055f // Gives the same level as when waterThreshold was unused in MapType.default
|
||||
})
|
||||
|
||||
postCrashHandlingRunnable { // for GL context
|
||||
launchOnGLThread { // for GL context
|
||||
ImageGetter.setNewRuleset(mapRuleset)
|
||||
val mapHolder = EditorMapHolder(this@MainMenuScreen, newMap) {}
|
||||
mapHolder.setScale(scale)
|
||||
@ -210,18 +210,18 @@ class MainMenuScreen: BaseScreen() {
|
||||
private fun quickstartNewGame() {
|
||||
ToastPopup("Working...", this)
|
||||
val errorText = "Cannot start game with the default new game parameters!"
|
||||
launchCrashHandling("QuickStart") {
|
||||
Concurrency.run("QuickStart") {
|
||||
val newGame: GameInfo
|
||||
// Can fail when starting the game...
|
||||
try {
|
||||
newGame = GameStarter.startNewGame(GameSetupInfo.fromSettings("Chieftain"))
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable { ToastPopup(errorText, this@MainMenuScreen) }
|
||||
return@launchCrashHandling
|
||||
launchOnGLThread { ToastPopup(errorText, this@MainMenuScreen) }
|
||||
return@run
|
||||
}
|
||||
|
||||
// ...or when loading the game
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
try {
|
||||
game.loadGame(newGame)
|
||||
} catch (outOfMemory: OutOfMemoryError) {
|
||||
|
@ -20,9 +20,7 @@ import com.unciv.ui.audio.GameSounds
|
||||
import com.unciv.ui.audio.MusicController
|
||||
import com.unciv.ui.audio.MusicMood
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.crashhandling.closeExecutors
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.crashhandling.CrashScreen
|
||||
import com.unciv.ui.crashhandling.wrapCrashHandlingUnit
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.multiplayer.LoadDeepLinkScreen
|
||||
@ -32,8 +30,10 @@ import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.center
|
||||
import com.unciv.ui.worldscreen.PlayerReadyScreen
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import com.unciv.utils.debug
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
|
||||
class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
@ -123,7 +123,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
|
||||
Gdx.graphics.isContinuousRendering = settings.continuousRendering
|
||||
|
||||
launchCrashHandling("LoadJSON") {
|
||||
Concurrency.run("LoadJSON") {
|
||||
RulesetCache.loadRulesets()
|
||||
translations.tryReadTranslationForCurrentLanguage()
|
||||
translations.loadPercentageCompleteOfLanguages()
|
||||
@ -135,7 +135,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
}
|
||||
|
||||
// This stuff needs to run on the main thread because it needs the GL context
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
musicController.chooseTrack(suffix = MusicMood.Menu)
|
||||
|
||||
ImageGetter.ruleset = RulesetCache.getVanillaRuleset() // so that we can enter the map editor without having to load a game first
|
||||
@ -213,16 +213,16 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
setScreen(worldScreen!!)
|
||||
}
|
||||
|
||||
private fun tryLoadDeepLinkedGame() = launchCrashHandling("LoadDeepLinkedGame") {
|
||||
if (deepLinkedMultiplayerGame == null) return@launchCrashHandling
|
||||
private fun tryLoadDeepLinkedGame() = Concurrency.run("LoadDeepLinkedGame") {
|
||||
if (deepLinkedMultiplayerGame == null) return@run
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
setScreen(LoadDeepLinkScreen())
|
||||
}
|
||||
try {
|
||||
onlineMultiplayer.loadGame(deepLinkedMultiplayerGame!!)
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
val mainMenu = MainMenuScreen()
|
||||
setScreen(mainMenu)
|
||||
val popup = Popup(mainMenu)
|
||||
@ -250,7 +250,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
|
||||
override fun pause() {
|
||||
val curGameInfo = gameInfo
|
||||
if (curGameInfo != null) gameSaver.autoSave(curGameInfo)
|
||||
if (curGameInfo != null) gameSaver.requestAutoSave(curGameInfo)
|
||||
musicController.pause()
|
||||
super.pause()
|
||||
}
|
||||
@ -261,13 +261,12 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
|
||||
override fun render() = wrappedCrashHandlingRender()
|
||||
|
||||
override fun dispose() = runBlocking {
|
||||
override fun dispose() {
|
||||
Gdx.input.inputProcessor = null // don't allow ANRs when shutting down, that's silly
|
||||
|
||||
cancelDiscordEvent?.invoke()
|
||||
SoundPlayer.clearCache()
|
||||
if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out
|
||||
closeExecutors()
|
||||
|
||||
val curGameInfo = gameInfo
|
||||
if (curGameInfo != null) {
|
||||
@ -275,12 +274,15 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
if (autoSaveJob != null && autoSaveJob.isActive) {
|
||||
// auto save is already in progress (e.g. started by onPause() event)
|
||||
// let's allow it to finish and do not try to autosave second time
|
||||
autoSaveJob.join()
|
||||
Concurrency.runBlocking {
|
||||
autoSaveJob.join()
|
||||
}
|
||||
} else {
|
||||
gameSaver.autoSaveSingleThreaded(curGameInfo) // NO new thread
|
||||
gameSaver.autoSave(curGameInfo) // NO new thread
|
||||
}
|
||||
}
|
||||
settings.save()
|
||||
Concurrency.stopThreadPools()
|
||||
|
||||
// On desktop this should only be this one and "DestroyJavaVM"
|
||||
logRunningThreads()
|
||||
@ -295,6 +297,15 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles an uncaught exception or error. First attempts a platform-specific handler, and if that didn't handle the exception or error, brings the game to a [CrashScreen]. */
|
||||
fun handleUncaughtThrowable(ex: Throwable) {
|
||||
Log.error("Uncaught throwable", ex)
|
||||
if (platformSpecificHelper?.handleUncaughtThrowable(ex) == true) return
|
||||
Gdx.app.postRunnable {
|
||||
setScreen(CrashScreen(ex))
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the [worldScreen] if it is the currently active screen of the game */
|
||||
fun getWorldScreenIfActive(): WorldScreen? {
|
||||
return if (screen == worldScreen) worldScreen else null
|
||||
|
@ -2,7 +2,7 @@ package com.unciv.logic
|
||||
|
||||
import com.unciv.logic.GameSaver.CustomLoadResult
|
||||
import com.unciv.logic.GameSaver.CustomSaveResult
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@ -82,14 +82,14 @@ private fun callLoadCallback(loadCompleteCallback: (CustomLoadResult<String>) ->
|
||||
} else {
|
||||
CustomLoadResult(null, exception)
|
||||
}
|
||||
postCrashHandlingRunnable {
|
||||
Concurrency.runOnGLThread {
|
||||
loadCompleteCallback(result)
|
||||
}
|
||||
}
|
||||
private fun callSaveCallback(saveCompleteCallback: (CustomSaveResult) -> Unit,
|
||||
location: String? = null,
|
||||
exception: Exception? = null) {
|
||||
postCrashHandlingRunnable {
|
||||
Concurrency.runOnGLThread {
|
||||
saveCompleteCallback(CustomSaveResult(location, exception))
|
||||
}
|
||||
}
|
||||
|
@ -42,11 +42,8 @@ class GameInfo {
|
||||
// Set to false whenever the results still need te be processed
|
||||
var diplomaticVictoryVotesProcessed = false
|
||||
|
||||
/**Keep track of a custom location this game was saved to _or_ loaded from
|
||||
*
|
||||
* Note this was used as silent autosave destination, but it was decided (#3898) to
|
||||
* make the custom location feature a one-shot import/export kind of operation.
|
||||
* The tracking is left in place, however [GameSaver.autoSaveSingleThreaded] no longer uses it
|
||||
/**
|
||||
* Keep track of a custom location this game was saved to _or_ loaded from, using it as the default custom location for any further save/load attempts.
|
||||
*/
|
||||
@Volatile
|
||||
var customSaveLocation: String? = null
|
||||
|
@ -10,9 +10,8 @@ import com.unciv.json.json
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.metadata.doMigrations
|
||||
import com.unciv.models.metadata.isMigrationNecessary
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.saves.Gzip
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.debug
|
||||
import kotlinx.coroutines.Job
|
||||
@ -191,7 +190,7 @@ class GameSaver(
|
||||
val gameData = try {
|
||||
gameInfoToString(game)
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable { saveCompletionCallback(CustomSaveResult(exception = ex)) }
|
||||
Concurrency.runOnGLThread { saveCompletionCallback(CustomSaveResult(exception = ex)) }
|
||||
return
|
||||
}
|
||||
debug("Saving GameInfo %s to custom location %s", game.gameId, saveLocation)
|
||||
@ -351,25 +350,27 @@ class GameSaver(
|
||||
//region Autosave
|
||||
|
||||
/**
|
||||
* Runs autoSave
|
||||
* Auto-saves a snapshot of the [gameInfo] in a new thread.
|
||||
*/
|
||||
fun autoSave(gameInfo: GameInfo, postRunnable: () -> Unit = {}) {
|
||||
fun requestAutoSave(gameInfo: GameInfo): Job {
|
||||
// The save takes a long time (up to a few seconds on large games!) and we can do it while the player continues his game.
|
||||
// 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.
|
||||
autoSaveUnCloned(gameInfo.clone(), postRunnable)
|
||||
return requestAutoSaveUnCloned(gameInfo.clone())
|
||||
}
|
||||
|
||||
fun autoSaveUnCloned(gameInfo: GameInfo, postRunnable: () -> Unit = {}) {
|
||||
// This is used when returning from WorldScreen to MainMenuScreen - no clone since UI access to it should be gone
|
||||
autoSaveJob = launchCrashHandling(AUTOSAVE_FILE_NAME) {
|
||||
autoSaveSingleThreaded(gameInfo)
|
||||
// do this on main thread
|
||||
postCrashHandlingRunnable ( postRunnable )
|
||||
/**
|
||||
* In a new thread, auto-saves the [gameInfo] directly - only use this with [GameInfo] objects that are guaranteed not to be changed while the autosave is in progress!
|
||||
*/
|
||||
fun requestAutoSaveUnCloned(gameInfo: GameInfo): Job {
|
||||
val job = Concurrency.run("autoSaveUnCloned") {
|
||||
autoSave(gameInfo)
|
||||
}
|
||||
autoSaveJob = job
|
||||
return job
|
||||
}
|
||||
|
||||
fun autoSaveSingleThreaded(gameInfo: GameInfo) {
|
||||
fun autoSave(gameInfo: GameInfo) {
|
||||
try {
|
||||
saveGame(gameInfo, AUTOSAVE_FILE_NAME)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
|
@ -9,15 +9,16 @@ import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.event.EventBus
|
||||
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
|
||||
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.ui.crashhandling.CRASH_HANDLING_DAEMON_SCOPE
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.utils.extensions.isLargerThan
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.Dispatcher
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import com.unciv.utils.concurrency.launchOnThreadPool
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -63,7 +64,7 @@ class OnlineMultiplayer {
|
||||
val doNotUpdate = if (currentGame == null) listOf() else listOf(currentGame)
|
||||
throttle(lastAllGamesRefresh, multiplayerSettings.allGameRefreshDelay, {}) { requestUpdate(doNotUpdate = doNotUpdate) }
|
||||
}
|
||||
}.launchIn(CRASH_HANDLING_DAEMON_SCOPE)
|
||||
}.launchIn(CoroutineScope(Dispatcher.DAEMON))
|
||||
}
|
||||
|
||||
private fun getCurrentGame(): OnlineMultiplayerGame? {
|
||||
@ -83,14 +84,14 @@ class OnlineMultiplayer {
|
||||
* Fires: [MultiplayerGameUpdateStarted], [MultiplayerGameUpdated], [MultiplayerGameUpdateUnchanged], [MultiplayerGameUpdateFailed]
|
||||
*/
|
||||
fun requestUpdate(forceUpdate: Boolean = false, doNotUpdate: List<OnlineMultiplayerGame> = listOf()) {
|
||||
launchCrashHandling("Update all multiplayer games") {
|
||||
Concurrency.run("Update all multiplayer games") {
|
||||
val fileThrottleInterval = if (forceUpdate) Duration.ZERO else FILE_UPDATE_THROTTLE_PERIOD
|
||||
// An exception only happens here if the files can't be listed, should basically never happen
|
||||
throttle(lastFileUpdate, fileThrottleInterval, {}, action = ::updateSavesFromFiles)
|
||||
|
||||
for (game in savedGames.values) {
|
||||
if (game in doNotUpdate) continue
|
||||
launch {
|
||||
launchOnThreadPool {
|
||||
game.requestUpdate(forceUpdate)
|
||||
}
|
||||
}
|
||||
@ -155,7 +156,7 @@ class OnlineMultiplayer {
|
||||
private fun addGame(fileHandle: FileHandle, preview: GameInfoPreview = gameSaver.loadGamePreviewFromFile(fileHandle)) {
|
||||
val game = OnlineMultiplayerGame(fileHandle, preview, Instant.now())
|
||||
savedGames[fileHandle] = game
|
||||
postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(game.name)) }
|
||||
Concurrency.runOnGLThread { EventBus.send(MultiplayerGameAdded(game.name)) }
|
||||
}
|
||||
|
||||
fun getGameByName(name: String): OnlineMultiplayerGame? {
|
||||
@ -219,7 +220,7 @@ class OnlineMultiplayer {
|
||||
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
|
||||
* @throws FileNotFoundException if the file can't be found
|
||||
*/
|
||||
suspend fun loadGame(gameId: String) {
|
||||
suspend fun loadGame(gameId: String) = coroutineScope {
|
||||
val gameInfo = downloadGame(gameId)
|
||||
val preview = gameInfo.asPreview()
|
||||
val onlineGame = getGameByGameId(gameId)
|
||||
@ -229,18 +230,18 @@ class OnlineMultiplayer {
|
||||
} else if (onlinePreview != null && hasNewerGameState(preview, onlinePreview)){
|
||||
onlineGame.doManualUpdate(preview)
|
||||
}
|
||||
postCrashHandlingRunnable { UncivGame.Current.loadGame(gameInfo) }
|
||||
launchOnGLThread { UncivGame.Current.loadGame(gameInfo) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given game is current and loads it, otherwise loads the game from the server
|
||||
*/
|
||||
suspend fun loadGame(gameInfo: GameInfo) {
|
||||
suspend fun loadGame(gameInfo: GameInfo) = coroutineScope {
|
||||
val gameId = gameInfo.gameId
|
||||
val preview = onlineGameSaver.tryDownloadGamePreview(gameId)
|
||||
if (hasLatestGameState(gameInfo, preview)) {
|
||||
gameInfo.isUpToDate = true
|
||||
postCrashHandlingRunnable { UncivGame.Current.loadGame(gameInfo) }
|
||||
launchOnGLThread { UncivGame.Current.loadGame(gameInfo) }
|
||||
} else {
|
||||
loadGame(gameId)
|
||||
}
|
||||
@ -272,7 +273,7 @@ class OnlineMultiplayer {
|
||||
if (game == null) return
|
||||
|
||||
savedGames.remove(game.fileHandle)
|
||||
postCrashHandlingRunnable { EventBus.send(MultiplayerGameDeleted(game.name)) }
|
||||
Concurrency.runOnGLThread { EventBus.send(MultiplayerGameDeleted(game.name)) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,13 @@
|
||||
package com.unciv.logic.multiplayer
|
||||
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfoPreview
|
||||
import com.unciv.logic.event.EventBus
|
||||
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
|
||||
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.utils.extensions.isLargerThan
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import java.io.FileNotFoundException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -69,7 +68,9 @@ class OnlineMultiplayerGame(
|
||||
error = e
|
||||
GameUpdateResult.FAILURE
|
||||
}
|
||||
postCrashHandlingRunnable { EventBus.send(MultiplayerGameUpdateStarted(name)) }
|
||||
Concurrency.runOnGLThread {
|
||||
EventBus.send(MultiplayerGameUpdateStarted(name))
|
||||
}
|
||||
val throttleInterval = if (forceUpdate) Duration.ZERO else getUpdateThrottleInterval()
|
||||
val updateResult = if (forceUpdate || needsUpdate()) {
|
||||
attemptAction(lastOnlineUpdate, onUnchanged, onError, ::update)
|
||||
@ -85,7 +86,7 @@ class OnlineMultiplayerGame(
|
||||
GameUpdateResult.FAILURE -> MultiplayerGameUpdateFailed(name, error!!)
|
||||
GameUpdateResult.UNCHANGED -> MultiplayerGameUpdateUnchanged(name, preview!!)
|
||||
}
|
||||
postCrashHandlingRunnable { EventBus.send(updateEvent) }
|
||||
Concurrency.runOnGLThread { EventBus.send(updateEvent) }
|
||||
}
|
||||
|
||||
private suspend fun update(): GameUpdateResult {
|
||||
@ -101,7 +102,7 @@ class OnlineMultiplayerGame(
|
||||
lastOnlineUpdate.set(Instant.now())
|
||||
error = null
|
||||
preview = gameInfo
|
||||
postCrashHandlingRunnable { EventBus.send(MultiplayerGameUpdated(name, gameInfo)) }
|
||||
Concurrency.runOnGLThread { EventBus.send(MultiplayerGameUpdated(name, gameInfo)) }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is OnlineMultiplayerGame && fileHandle == other.fileHandle
|
||||
|
@ -5,11 +5,12 @@ import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.GameStarter
|
||||
import com.unciv.models.metadata.GameSetupInfo
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.time.Duration
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@ -48,7 +49,7 @@ class Simulation(
|
||||
startTime = System.currentTimeMillis()
|
||||
val jobs: ArrayList<Job> = ArrayList()
|
||||
for (threadId in 1..threadsNumber) {
|
||||
jobs.add(launchCrashHandling("simulation-${threadId}") {
|
||||
jobs.add(launch(CoroutineName("simulation-${threadId}")) {
|
||||
for (i in 1..simulationsPerThread) {
|
||||
val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo))
|
||||
gameInfo.simulateMaxTurns = maxTurns
|
||||
|
@ -8,7 +8,7 @@ import com.unciv.ui.crashhandling.wrapCrashHandling
|
||||
import com.unciv.ui.crashhandling.wrapCrashHandlingUnit
|
||||
|
||||
|
||||
/** Main stage for the game. Safely brings the game to a [CrashScreen] if any event handlers throw an exception or an error that doesn't get otherwise handled. */
|
||||
/** Main stage for the game. Catches all exceptions or errors thrown by event handlers, calling [com.unciv.UncivGame.handleUncaughtThrowable] with the thrown exception or error. */
|
||||
class UncivStage(viewport: Viewport, batch: Batch) : Stage(viewport, batch) {
|
||||
|
||||
/**
|
||||
@ -61,93 +61,3 @@ class UncivStage(viewport: Viewport, batch: Batch) : Stage(viewport, batch) {
|
||||
{ super.keyTyped(character) }.wrapCrashHandling()() ?: true
|
||||
|
||||
}
|
||||
|
||||
// Example Stack traces from unhandled exceptions after a button click on Desktop and on Android are below.
|
||||
|
||||
// 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 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, as in crashHandlingThread and postCrashHandlingRunnable.
|
||||
|
||||
|
||||
// Button click (event):
|
||||
|
||||
/*
|
||||
Exception in thread "main" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.Exception
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:122)
|
||||
at com.unciv.app.desktop.DesktopLauncher.main(DesktopLauncher.kt:61)
|
||||
Caused by: java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:107)
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:106)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClickEvent$1.clicked(ExtensionFunctions.kt:57)
|
||||
at com.badlogic.gdx.scenes.scene2d.utils.ClickListener.touchUp(ClickListener.java:88)
|
||||
at com.badlogic.gdx.scenes.scene2d.InputListener.handle(InputListener.java:71)
|
||||
at com.badlogic.gdx.scenes.scene2d.Stage.touchUp(Stage.java:355)
|
||||
at com.badlogic.gdx.InputEventQueue.drain(InputEventQueue.java:70)
|
||||
at com.badlogic.gdx.backends.lwjgl3.DefaultLwjgl3Input.update(DefaultLwjgl3Input.java:189)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Window.update(Lwjgl3Window.java:394)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.loop(Lwjgl3Application.java:143)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:116)
|
||||
... 1 more
|
||||
|
||||
E/AndroidRuntime: FATAL EXCEPTION: GLThread 299
|
||||
Process: com.unciv.app, PID: 5910
|
||||
java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:107)
|
||||
at com.unciv.MainMenuScreen$newGameButton$1.invoke(MainMenuScreen.kt:106)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClick$1.invoke(ExtensionFunctions.kt:64)
|
||||
at com.unciv.ui.utils.ExtensionFunctionsKt$onClickEvent$1.clicked(ExtensionFunctions.kt:57)
|
||||
at com.badlogic.gdx.scenes.scene2d.utils.ClickListener.touchUp(ClickListener.java:88)
|
||||
at com.badlogic.gdx.scenes.scene2d.InputListener.handle(InputListener.java:71)
|
||||
at com.badlogic.gdx.scenes.scene2d.Stage.touchUp(Stage.java:355)
|
||||
at com.badlogic.gdx.backends.android.DefaultAndroidInput.processEvents(DefaultAndroidInput.java:425)
|
||||
at com.badlogic.gdx.backends.android.AndroidGraphics.onDrawFrame(AndroidGraphics.java:469)
|
||||
at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1522)
|
||||
at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1239)
|
||||
*/
|
||||
|
||||
// Invalid Natural Wonder (rendering):
|
||||
|
||||
/*
|
||||
Exception in thread "main" java.lang.NullPointerException
|
||||
at com.unciv.logic.map.TileInfo.getNaturalWonder(TileInfo.kt:149)
|
||||
at com.unciv.logic.map.TileInfo.getTileStats(TileInfo.kt:255)
|
||||
at com.unciv.logic.map.TileInfo.getTileStats(TileInfo.kt:240)
|
||||
at com.unciv.ui.worldscreen.bottombar.TileInfoTable.getStatsTable(TileInfoTable.kt:43)
|
||||
at com.unciv.ui.worldscreen.bottombar.TileInfoTable.updateTileTable$core(TileInfoTable.kt:25)
|
||||
at com.unciv.ui.worldscreen.WorldScreen.update(WorldScreen.kt:383)
|
||||
at com.unciv.ui.worldscreen.WorldScreen.render(WorldScreen.kt:828)
|
||||
at com.badlogic.gdx.Game.render(Game.java:46)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Window.update(Lwjgl3Window.java:403)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.loop(Lwjgl3Application.java:143)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:116)
|
||||
at com.unciv.app.desktop.DesktopLauncher.main(DesktopLauncher.kt:61)
|
||||
*/
|
||||
|
||||
// Thread:
|
||||
|
||||
/*
|
||||
Exception in thread "Thread-5" java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$newGameButton$1$1.invoke(MainMenuScreen.kt:107)
|
||||
at com.unciv.MainMenuScreen$newGameButton$1$1.invoke(MainMenuScreen.kt:107)
|
||||
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
|
||||
*/
|
||||
|
||||
// Gdx.app.postRunnable:
|
||||
|
||||
/*
|
||||
Exception in thread "main" com.badlogic.gdx.utils.GdxRuntimeException: java.lang.Exception
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:122)
|
||||
at com.unciv.app.desktop.DesktopLauncher.main(DesktopLauncher.kt:61)
|
||||
Caused by: java.lang.Exception
|
||||
at com.unciv.MainMenuScreen$loadGameTable$1.invoke$lambda-0(MainMenuScreen.kt:112)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.loop(Lwjgl3Application.java:159)
|
||||
at com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application.<init>(Lwjgl3Application.java:116)
|
||||
... 1 more
|
||||
*/
|
||||
|
@ -6,7 +6,7 @@ import com.badlogic.gdx.audio.Sound
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.debug
|
||||
import kotlinx.coroutines.delay
|
||||
import java.io.File
|
||||
@ -171,7 +171,7 @@ object SoundPlayer {
|
||||
val initialDelay = if (isFresh && Gdx.app.type == Application.ApplicationType.Android) 40 else 0
|
||||
|
||||
if (initialDelay > 0 || resource.play(volume) == -1L) {
|
||||
launchCrashHandling("DelayedSound") {
|
||||
Concurrency.run("DelayedSound") {
|
||||
delay(initialDelay.toLong())
|
||||
while (resource.play(volume) == -1L) {
|
||||
delay(20L)
|
||||
|
@ -21,8 +21,6 @@ import com.unciv.models.ruleset.unit.BaseUnit
|
||||
import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.YesNoPopup
|
||||
@ -43,6 +41,8 @@ import com.unciv.ui.utils.extensions.packIfNeeded
|
||||
import com.unciv.ui.utils.extensions.surroundWithCircle
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
|
||||
@ -225,10 +225,10 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
|
||||
availableConstructionsTable.add(Constants.loading.toLabel()).pad(10f)
|
||||
}
|
||||
|
||||
launchCrashHandling("Construction info gathering - ${cityScreen.city.name}") {
|
||||
Concurrency.run("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()
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
val units = ArrayList<Table>()
|
||||
val buildableWonders = ArrayList<Table>()
|
||||
val buildableNationalWonders = ArrayList<Table>()
|
||||
|
@ -0,0 +1,34 @@
|
||||
package com.unciv.ui.crashhandling
|
||||
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
|
||||
|
||||
/**
|
||||
* Returns a wrapped version of a function that automatically handles an uncaught exception or error. In case of an uncaught exception or error, the return will be null.
|
||||
*
|
||||
* [com.unciv.ui.UncivStage], [UncivGame.render] and [Concurrency] 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.
|
||||
*/
|
||||
fun <R> (() -> R).wrapCrashHandling(
|
||||
): () -> R?
|
||||
= {
|
||||
try {
|
||||
this()
|
||||
} catch (e: Throwable) {
|
||||
UncivGame.Current.handleUncaughtThrowable(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapped version of a function that automatically handles an uncaught exception or error.
|
||||
*
|
||||
* [com.unciv.ui.UncivStage], [UncivGame.render] and [Concurrency] 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.
|
||||
*/
|
||||
fun (() -> Unit).wrapCrashHandlingUnit(): () -> Unit {
|
||||
val wrappedReturning = this.wrapCrashHandling()
|
||||
// Don't instantiate a new lambda every time the return get called.
|
||||
return { wrappedReturning() ?: Unit }
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
package com.unciv.ui.crashhandling
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.UncivGame
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
private val DAEMON_EXECUTOR = Executors.newCachedThreadPool(object : ThreadFactory {
|
||||
var n = 0
|
||||
override fun newThread(r: java.lang.Runnable): Thread =
|
||||
crashHandlingThread(name = "crash-handling-daemon-${n++}", start = false, isDaemon = true, block = r::run)
|
||||
}).asCoroutineDispatcher()
|
||||
/**
|
||||
* Coroutine Scope that runs coroutines in separate daemon threads.
|
||||
*
|
||||
* Brings the main game loop to a [com.unciv.CrashScreen] if an exception happens.
|
||||
*/
|
||||
val CRASH_HANDLING_DAEMON_SCOPE = CoroutineScope(DAEMON_EXECUTOR)
|
||||
|
||||
private val EXECUTOR = Executors.newCachedThreadPool(object : ThreadFactory {
|
||||
var n = 0
|
||||
override fun newThread(r: java.lang.Runnable): Thread =
|
||||
crashHandlingThread(name = "crash-handling-${n++}", start = false, isDaemon = false, block = r::run)
|
||||
}).asCoroutineDispatcher()
|
||||
/**
|
||||
* Coroutine Scope that runs coroutines in separate threads that are not started as daemons.
|
||||
*
|
||||
* Brings the main game loop to a [com.unciv.CrashScreen] if an exception happens.
|
||||
*/
|
||||
val CRASH_HANDLING_SCOPE = CoroutineScope(EXECUTOR)
|
||||
|
||||
/**
|
||||
* Must be called only in [com.unciv.UncivGame.dispose] to not have any threads running that prevent JVM shutdown.
|
||||
*/
|
||||
fun closeExecutors() {
|
||||
EXECUTOR.close()
|
||||
DAEMON_EXECUTOR.close()
|
||||
}
|
||||
|
||||
/** Wrapped version of [kotlin.concurrent.thread], that brings the main game loop to a [com.unciv.CrashScreen] if an exception happens. */
|
||||
private 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())
|
||||
}
|
||||
|
||||
/**
|
||||
* [launch]es a new coroutine that brings the game loop to a [com.unciv.CrashScreen] if an exception occurs.
|
||||
* @see crashHandlingThread
|
||||
*/
|
||||
fun launchCrashHandling(name: String, runAsDaemon: Boolean = true,
|
||||
flowBlock: suspend CoroutineScope.() -> Unit): Job {
|
||||
return getCoroutineContext(runAsDaemon).launch(CoroutineName(name)) { flowBlock(this) }
|
||||
}
|
||||
|
||||
private fun getCoroutineContext(runAsDaemon: Boolean): CoroutineScope {
|
||||
return if (runAsDaemon) CRASH_HANDLING_DAEMON_SCOPE else CRASH_HANDLING_SCOPE
|
||||
}
|
||||
/**
|
||||
* Returns a wrapped version of a function that safely crashes the game to [CrashScreen] if an exception or error is thrown.
|
||||
*
|
||||
* 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.
|
||||
* */
|
||||
fun <R> (() -> R).wrapCrashHandling(
|
||||
postToMainThread: Boolean = false
|
||||
): () -> R?
|
||||
= {
|
||||
try {
|
||||
this()
|
||||
} catch (e: Throwable) {
|
||||
if (postToMainThread) {
|
||||
Gdx.app.postRunnable {
|
||||
UncivGame.Current.setScreen(CrashScreen(e))
|
||||
}
|
||||
} else UncivGame.Current.setScreen(CrashScreen(e))
|
||||
null
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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 the return get called.
|
||||
return { wrappedReturning() ?: Unit }
|
||||
}
|
@ -24,16 +24,6 @@ import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/*
|
||||
Crashes are now handled from:
|
||||
- Event listeners, by [UncivStage].
|
||||
- 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() {
|
||||
|
||||
@ -113,8 +103,6 @@ class CrashScreen(val exception: Throwable): BaseScreen() {
|
||||
}
|
||||
|
||||
init {
|
||||
Log.error(text) // Also print to system terminal.
|
||||
thread { throw exception } // this is so the GPC logs catch the exception
|
||||
stage.addActor(makeLayoutTable())
|
||||
}
|
||||
|
||||
|
@ -5,8 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.logic.IdChecker
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
@ -14,6 +12,8 @@ import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import java.util.*
|
||||
|
||||
class AddMultiplayerGameScreen(backScreen: MultiplayerScreen) : PickerScreen() {
|
||||
@ -55,16 +55,16 @@ class AddMultiplayerGameScreen(backScreen: MultiplayerScreen) : PickerScreen() {
|
||||
popup.addGoodSizedLabel("Working...")
|
||||
popup.open()
|
||||
|
||||
launchCrashHandling("AddMultiplayerGame") {
|
||||
Concurrency.run("AddMultiplayerGame") {
|
||||
try {
|
||||
game.onlineMultiplayer.addGame(gameIDTextField.text.trim(), gameNameTextField.text.trim())
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.close()
|
||||
game.setScreen(backScreen)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
val message = MultiplayerHelpers.getLoadExceptionMessage(ex)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.reuseWith(message, true)
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
@ -15,6 +13,8 @@ import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
|
||||
/** Subscreen of MultiplayerScreen to edit and delete saves
|
||||
* backScreen is used for getting back to the MultiplayerScreen so it doesn't have to be created over and over again */
|
||||
@ -85,23 +85,23 @@ class EditMultiplayerGameInfoScreen(val multiplayerGame: OnlineMultiplayerGame,
|
||||
popup.addGoodSizedLabel("Working...").row()
|
||||
popup.open()
|
||||
|
||||
launchCrashHandling("Resign", runAsDaemon = false) {
|
||||
Concurrency.runOnNonDaemonThreadPool("Resign") {
|
||||
try {
|
||||
val resignSuccess = game.onlineMultiplayer.resign(multiplayerGame)
|
||||
if (resignSuccess) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.close()
|
||||
//go back to the MultiplayerScreen
|
||||
game.setScreen(backScreen)
|
||||
}
|
||||
} else {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.reuseWith("You can only resign if it's your turn", true)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
val message = MultiplayerHelpers.getLoadExceptionMessage(ex)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.reuseWith(message, true)
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,12 @@ import com.unciv.logic.multiplayer.OnlineMultiplayer
|
||||
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
|
||||
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.formatShort
|
||||
import com.unciv.ui.utils.extensions.toCheckBox
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import java.io.FileNotFoundException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -30,12 +30,12 @@ object MultiplayerHelpers {
|
||||
loadingGamePopup.addGoodSizedLabel("Loading latest game state...")
|
||||
loadingGamePopup.open()
|
||||
|
||||
launchCrashHandling("JoinMultiplayerGame") {
|
||||
Concurrency.run("JoinMultiplayerGame") {
|
||||
try {
|
||||
UncivGame.Current.onlineMultiplayer.loadGame(selectedGame)
|
||||
} catch (ex: Exception) {
|
||||
val message = getLoadExceptionMessage(ex)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingGamePopup.reuseWith(message, true)
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
|
||||
import com.unciv.models.metadata.GameSetupInfo
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.popup.Popup
|
||||
@ -37,6 +35,9 @@ import com.unciv.ui.utils.extensions.pad
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
|
||||
@ -155,13 +156,11 @@ class NewGameScreen(
|
||||
val mapSize = gameSetupInfo.mapParameters.mapSize
|
||||
val message = mapSize.fixUndesiredSizes(gameSetupInfo.mapParameters.worldWrap)
|
||||
if (message != null) {
|
||||
postCrashHandlingRunnable {
|
||||
ToastPopup( message, UncivGame.Current.screen!!, 4000 )
|
||||
with (mapOptionsTable.generatedMapOptionsTable) {
|
||||
customMapSizeRadius.text = mapSize.radius.toString()
|
||||
customMapWidth.text = mapSize.width.toString()
|
||||
customMapHeight.text = mapSize.height.toString()
|
||||
}
|
||||
ToastPopup( message, UncivGame.Current.screen!!, 4000 )
|
||||
with (mapOptionsTable.generatedMapOptionsTable) {
|
||||
customMapSizeRadius.text = mapSize.radius.toString()
|
||||
customMapWidth.text = mapSize.width.toString()
|
||||
customMapHeight.text = mapSize.height.toString()
|
||||
}
|
||||
game.setScreen(this) // to get the input back
|
||||
return@onClick
|
||||
@ -172,7 +171,7 @@ class NewGameScreen(
|
||||
rightSideButton.setText("Working...".tr())
|
||||
|
||||
// Creating a new game can take a while and we don't want ANRs
|
||||
launchCrashHandling("NewGame", runAsDaemon = false) {
|
||||
Concurrency.runOnNonDaemonThreadPool("NewGame") {
|
||||
startNewGame()
|
||||
}
|
||||
}
|
||||
@ -236,9 +235,9 @@ class NewGameScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startNewGame() {
|
||||
val popup = Popup(this)
|
||||
postCrashHandlingRunnable {
|
||||
private suspend fun startNewGame() = coroutineScope {
|
||||
val popup = Popup(this@NewGameScreen)
|
||||
launchOnGLThread {
|
||||
popup.addGoodSizedLabel("Working...").row()
|
||||
popup.open()
|
||||
}
|
||||
@ -248,7 +247,7 @@ class NewGameScreen(
|
||||
newGame = GameStarter.startNewGame(gameSetupInfo)
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.apply {
|
||||
reuseWith("It looks like we can't make a map with the parameters you requested!")
|
||||
row()
|
||||
@ -259,35 +258,35 @@ class NewGameScreen(
|
||||
rightSideButton.enable()
|
||||
rightSideButton.setText("Start game!".tr())
|
||||
}
|
||||
return
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
if (gameSetupInfo.gameParameters.isOnlineMultiplayer) {
|
||||
newGame.isUpToDate = true // So we don't try to download it from dropbox the second after we upload it - the file is not yet ready for loading!
|
||||
try {
|
||||
game.onlineMultiplayer.createGame(newGame)
|
||||
game.gameSaver.autoSave(newGame)
|
||||
game.gameSaver.requestAutoSave(newGame)
|
||||
} catch (ex: FileStorageRateLimitReached) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true)
|
||||
rightSideButton.enable()
|
||||
rightSideButton.setText("Start game!".tr())
|
||||
}
|
||||
Gdx.input.inputProcessor = stage
|
||||
rightSideButton.enable()
|
||||
rightSideButton.setText("Start game!".tr())
|
||||
return
|
||||
return@coroutineScope
|
||||
} catch (ex: Exception) {
|
||||
Log.error("Error while creating game", ex)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
popup.reuseWith("Could not upload game!", true)
|
||||
rightSideButton.enable()
|
||||
rightSideButton.setText("Start game!".tr())
|
||||
}
|
||||
Gdx.input.inputProcessor = stage
|
||||
rightSideButton.enable()
|
||||
rightSideButton.setText("Start game!".tr())
|
||||
return
|
||||
return@coroutineScope
|
||||
}
|
||||
}
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
val worldScreen = game.loadGame(newGame)
|
||||
if (newGame.gameParameters.isOnlineMultiplayer) {
|
||||
// Save gameId to clipboard because you have to do it anyway.
|
||||
|
@ -13,8 +13,6 @@ import com.unciv.UncivGame
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.translations.TranslationFileWriter
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.popup.YesNoPopup
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.FontFamilyData
|
||||
@ -27,6 +25,8 @@ import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.setFontColor
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import java.util.*
|
||||
|
||||
fun advancedTab(
|
||||
@ -115,14 +115,14 @@ fun addFontFamilySelect(table: Table, settings: GameSettings, selectBoxMinWidth:
|
||||
}
|
||||
}
|
||||
|
||||
launchCrashHandling("Add Font Select") {
|
||||
Concurrency.run("Add Font Select") {
|
||||
// This is a heavy operation and causes ANRs
|
||||
val fonts = Array<FontFamilyData>().apply {
|
||||
add(FontFamilyData.default)
|
||||
for (font in Fonts.getAvailableFontFamilyNames())
|
||||
add(font)
|
||||
}
|
||||
postCrashHandlingRunnable { loadFontSelect(fonts, selectCell) }
|
||||
launchOnGLThread { loadFontSelect(fonts, selectCell) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,9 +146,9 @@ private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) {
|
||||
val generateAction: () -> Unit = {
|
||||
optionsPopup.tabs.selectPage("Advanced")
|
||||
generateTranslationsButton.setText("Working...".tr())
|
||||
launchCrashHandling("WriteTranslations") {
|
||||
Concurrency.run("WriteTranslations") {
|
||||
val result = TranslationFileWriter.writeNewTranslationFiles()
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
// notify about completion
|
||||
generateTranslationsButton.setText(result.tr())
|
||||
generateTranslationsButton.disable()
|
||||
|
@ -10,8 +10,6 @@ import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.ruleset.unique.Unique
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.newgamescreen.TranslatedSelectBox
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
@ -24,6 +22,8 @@ import com.unciv.ui.utils.extensions.surroundWithCircle
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import com.unciv.utils.debug
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ class ModCheckTab(
|
||||
modCheckResultTable.add("Checking mods for errors...".toLabel()).row()
|
||||
modCheckBaseSelect!!.isDisabled = true
|
||||
|
||||
launchCrashHandling("ModChecker") {
|
||||
Concurrency.run("ModChecker") {
|
||||
for (mod in RulesetCache.values.sortedBy { it.name }) {
|
||||
if (base != MOD_CHECK_WITHOUT_BASE && mod.modOptions.isBaseRuleset) continue
|
||||
|
||||
@ -102,10 +102,10 @@ class ModCheckTab(
|
||||
if (modLinks.isNotEmpty()) modLinks += Ruleset.RulesetError("", Ruleset.RulesetErrorSeverity.OK)
|
||||
if (noProblem) modLinks += Ruleset.RulesetError("No problems found.".tr(), Ruleset.RulesetErrorSeverity.OK)
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
// When the options popup is already closed before this postRunnable is run,
|
||||
// Don't add the labels, as otherwise the game will crash
|
||||
if (stage == null) return@postCrashHandlingRunnable
|
||||
if (stage == null) return@launchOnGLThread
|
||||
// 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.
|
||||
@ -149,7 +149,7 @@ class ModCheckTab(
|
||||
}
|
||||
|
||||
// done with all mods!
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
modCheckResultTable.removeActor(modCheckResultTable.children.last())
|
||||
modCheckBaseSelect!!.isDisabled = false
|
||||
}
|
||||
|
@ -12,8 +12,6 @@ import com.unciv.models.metadata.GameSetting
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.format
|
||||
@ -23,6 +21,8 @@ import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toGdxArray
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import java.time.Duration
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@ -198,9 +198,9 @@ private fun addTurnCheckerOptions(
|
||||
}
|
||||
|
||||
private fun successfullyConnectedToServer(settings: GameSettings, action: (Boolean, String, Int?) -> Unit) {
|
||||
launchCrashHandling("TestIsAlive") {
|
||||
Concurrency.run("TestIsAlive") {
|
||||
SimpleHttp.sendGetRequest("${settings.multiplayer.server}/isalive") { success, result, code ->
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
action(success, result, code)
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,6 @@ import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.audio.MusicController
|
||||
import com.unciv.ui.audio.MusicTrackChooserFlags
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.UncivSlider
|
||||
import com.unciv.ui.utils.WrappableLabel
|
||||
@ -17,6 +15,8 @@ import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlin.math.floor
|
||||
|
||||
fun soundTab(
|
||||
@ -52,15 +52,15 @@ private fun addDownloadMusic(table: Table, optionsPopup: OptionsPopup) {
|
||||
errorTable.add("Downloading...".toLabel())
|
||||
|
||||
// So the whole game doesn't get stuck while downloading the file
|
||||
launchCrashHandling("MusicDownload") {
|
||||
Concurrency.run("MusicDownload") {
|
||||
try {
|
||||
UncivGame.Current.musicController.downloadDefaultFile()
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
optionsPopup.tabs.replacePage("Sound", soundTab(optionsPopup))
|
||||
UncivGame.Current.musicController.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
errorTable.clear()
|
||||
errorTable.add("Could not download music!".toLabel(Color.RED))
|
||||
}
|
||||
@ -144,7 +144,7 @@ private fun addMusicCurrentlyPlaying(table: Table, music: MusicController) {
|
||||
label.wrap = true
|
||||
table.add(label).padTop(20f).colspan(2).fillX().row()
|
||||
music.onChange {
|
||||
postCrashHandlingRunnable {
|
||||
Concurrency.runOnGLThread {
|
||||
label.setText("Currently playing: [$it]".tr())
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,12 @@ import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Button
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextArea
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.json.fromJsonFile
|
||||
@ -13,20 +18,29 @@ import com.unciv.models.ruleset.ModOptions
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.pickerscreens.ModManagementOptions.SortType
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.popup.YesNoPopup
|
||||
import com.unciv.ui.utils.*
|
||||
import com.unciv.ui.utils.extensions.*
|
||||
import com.unciv.ui.utils.AutoScrollPane
|
||||
import com.unciv.ui.utils.ExpanderTab
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.WrappableLabel
|
||||
import com.unciv.ui.utils.extensions.UncivDateFormat.formatDate
|
||||
import com.unciv.ui.utils.extensions.UncivDateFormat.parseDate
|
||||
import com.unciv.ui.utils.extensions.addSeparator
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.isEnabled
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toCheckBox
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
@ -190,23 +204,23 @@ class ModManagementScreen(
|
||||
* calls itself for the next page of search results
|
||||
*/
|
||||
private fun tryDownloadPage(pageNum: Int) {
|
||||
runningSearchJob = launchCrashHandling("GitHubSearch") {
|
||||
runningSearchJob = Concurrency.run("GitHubSearch") {
|
||||
val repoSearch: Github.RepoSearch
|
||||
try {
|
||||
repoSearch = Github.tryGetGithubReposWithTopic(amountPerPage, pageNum)!!
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
ToastPopup("Could not download mod list", this@ModManagementScreen)
|
||||
}
|
||||
runningSearchJob = null
|
||||
return@launchCrashHandling
|
||||
return@run
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
return@launchCrashHandling
|
||||
return@run
|
||||
}
|
||||
|
||||
postCrashHandlingRunnable { addModInfoFromRepoSearch(repoSearch, pageNum) }
|
||||
launchOnGLThread { addModInfoFromRepoSearch(repoSearch, pageNum) }
|
||||
runningSearchJob = null
|
||||
}
|
||||
}
|
||||
@ -394,13 +408,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 = {}) {
|
||||
launchCrashHandling("DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions
|
||||
Concurrency.run("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!
|
||||
Github.rewriteModOptions(repo, modFolder)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
ToastPopup("[${repo.name}] Downloaded!", this@ModManagementScreen)
|
||||
RulesetCache.loadRulesets()
|
||||
RulesetCache[repo.name]?.let {
|
||||
@ -412,7 +426,7 @@ class ModManagementScreen(
|
||||
postAction()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
ToastPopup("Could not download [${repo.name}]", this@ModManagementScreen)
|
||||
postAction()
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.civilopedia.CivilopediaCategories
|
||||
import com.unciv.ui.civilopedia.CivilopediaScreen
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.utils.Fonts
|
||||
@ -24,6 +23,7 @@ import com.unciv.ui.utils.extensions.darken
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
|
||||
|
||||
class TechPickerScreen(
|
||||
@ -301,7 +301,7 @@ class TechPickerScreen(
|
||||
}
|
||||
|
||||
private fun centerOnTechnology(tech: Technology) {
|
||||
postCrashHandlingRunnable {
|
||||
Concurrency.runOnGLThread {
|
||||
techNameToButton[tech.name]?.let {
|
||||
scrollPane.scrollTo(it.x, it.y, it.width, it.height, true, true)
|
||||
scrollPane.updateVisualScroll()
|
||||
|
@ -1,10 +1,10 @@
|
||||
package com.unciv.ui.popup
|
||||
|
||||
import com.badlogic.gdx.scenes.scene2d.Stage
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
@ -28,9 +28,9 @@ class ToastPopup (message: String, stage: Stage, val time: Long = 2000) : Popup(
|
||||
}
|
||||
|
||||
private fun startTimer(){
|
||||
launchCrashHandling("ResponsePopup") {
|
||||
Concurrency.run("ResponsePopup") {
|
||||
delay(time)
|
||||
postCrashHandlingRunnable { this@ToastPopup.close() }
|
||||
launchOnGLThread { this@ToastPopup.close() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,6 @@ import com.unciv.logic.MissingModsException
|
||||
import com.unciv.logic.UncivShowableException
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.pickerscreens.Github
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
@ -26,6 +24,8 @@ import com.unciv.ui.utils.extensions.isEnabled
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
@ -77,17 +77,17 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
val loadingPopup = Popup( this)
|
||||
loadingPopup.addGoodSizedLabel(Constants.loading)
|
||||
loadingPopup.open()
|
||||
launchCrashHandling(loadGame) {
|
||||
Concurrency.run(loadGame) {
|
||||
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 = game.gameSaver.loadGameByName(selectedSave)
|
||||
postCrashHandlingRunnable { game.loadGame(loadedGame) }
|
||||
launchOnGLThread { game.loadGame(loadedGame) }
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingPopup.close()
|
||||
if (ex is MissingModsException) {
|
||||
handleLoadGameException("Could not load game", ex)
|
||||
return@postCrashHandlingRunnable
|
||||
return@launchOnGLThread
|
||||
}
|
||||
val cantLoadGamePopup = Popup(this@LoadGameScreen)
|
||||
cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row()
|
||||
@ -114,13 +114,13 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
private fun getLoadFromClipboardButton(): TextButton {
|
||||
val pasteButton = loadFromClipboard.toTextButton()
|
||||
val pasteHandler: ()->Unit = {
|
||||
launchCrashHandling(loadFromClipboard) {
|
||||
Concurrency.run(loadFromClipboard) {
|
||||
try {
|
||||
val clipboardContentsString = Gdx.app.clipboard.contents.trim()
|
||||
val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString)
|
||||
postCrashHandlingRunnable { game.loadGame(loadedGame) }
|
||||
launchOnGLThread { game.loadGame(loadedGame) }
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable { handleLoadGameException("Could not load game from clipboard!", ex) }
|
||||
launchOnGLThread { handleLoadGameException("Could not load game from clipboard!", ex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,7 +138,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
errorLabel.isVisible = false
|
||||
loadFromCustomLocation.setText(Constants.loading.tr())
|
||||
loadFromCustomLocation.disable()
|
||||
launchCrashHandling(Companion.loadFromCustomLocation) {
|
||||
Concurrency.run(Companion.loadFromCustomLocation) {
|
||||
game.gameSaver.loadGameFromCustomLocation { result ->
|
||||
if (result.isError()) {
|
||||
handleLoadGameException("Could not load game from custom location!", result.exception)
|
||||
@ -154,7 +154,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
private fun getCopyExistingSaveToClipboardButton(): TextButton {
|
||||
val copyButton = copyExistingSaveToClipboard.toTextButton()
|
||||
val copyHandler: ()->Unit = {
|
||||
launchCrashHandling(copyExistingSaveToClipboard) {
|
||||
Concurrency.run(copyExistingSaveToClipboard) {
|
||||
try {
|
||||
val gameText = game.gameSaver.getSave(selectedSave).readString()
|
||||
Gdx.app.clipboard.contents = if (gameText[0] == '{') Gzip.zip(gameText) else gameText
|
||||
@ -185,7 +185,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
var errorText = primaryText.tr()
|
||||
if (ex is UncivShowableException) errorText += "\n${ex.localizedMessage}"
|
||||
ex?.printStackTrace()
|
||||
postCrashHandlingRunnable {
|
||||
Concurrency.runOnGLThread {
|
||||
errorLabel.setText(errorText)
|
||||
errorLabel.isVisible = true
|
||||
if (ex is MissingModsException) {
|
||||
@ -198,7 +198,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
private fun loadMissingMods() {
|
||||
loadMissingModsButton.isEnabled = false
|
||||
descriptionLabel.setText(Constants.loading.tr())
|
||||
launchCrashHandling(downloadMissingMods, runAsDaemon = false) {
|
||||
Concurrency.runOnNonDaemonThreadPool(downloadMissingMods) {
|
||||
try {
|
||||
val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-")
|
||||
for (modName in mods) {
|
||||
@ -215,9 +215,9 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
|
||||
val labelText = descriptionLabel.text // Surprise - a StringBuilder
|
||||
labelText.appendLine()
|
||||
labelText.append("[${repo.name}] Downloaded!".tr())
|
||||
postCrashHandlingRunnable { descriptionLabel.setText(labelText) }
|
||||
launchOnGLThread { descriptionLabel.setText(labelText) }
|
||||
}
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
RulesetCache.loadRulesets()
|
||||
missingModsToLoad = ""
|
||||
loadMissingModsButton.isVisible = false
|
||||
|
@ -6,8 +6,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.Constants
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
@ -20,7 +18,9 @@ import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.pad
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import java.util.Date
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import java.util.*
|
||||
|
||||
|
||||
abstract class LoadOrSaveScreen(
|
||||
@ -101,7 +101,7 @@ abstract class LoadOrSaveScreen(
|
||||
|
||||
private fun showSaveInfo(saveGameFile: FileHandle) {
|
||||
descriptionLabel.setText(Constants.loading.tr())
|
||||
launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones
|
||||
Concurrency.run("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones
|
||||
val textToSet = try {
|
||||
val savedAt = Date(saveGameFile.lastModified())
|
||||
val game = game.gameSaver.loadGamePreviewFromFile(saveGameFile)
|
||||
@ -118,7 +118,7 @@ abstract class LoadOrSaveScreen(
|
||||
"\n{Could not load game}!"
|
||||
}
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
descriptionLabel.setText(textToSet.tr())
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import com.unciv.Constants
|
||||
import com.unciv.MainMenuScreen
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.multiplayer.MultiplayerHelpers
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import com.unciv.utils.Log
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@ object QuickSave {
|
||||
fun save(gameInfo: GameInfo, screen: WorldScreen) {
|
||||
val gameSaver = UncivGame.Current.gameSaver
|
||||
val toast = ToastPopup("Quicksaving...", screen)
|
||||
launchCrashHandling("QuickSaveGame", runAsDaemon = false) {
|
||||
Concurrency.runOnNonDaemonThreadPool("QuickSaveGame") {
|
||||
gameSaver.saveGame(gameInfo, "QuickSave") {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
toast.close()
|
||||
if (it != null)
|
||||
ToastPopup("Could not save game!", screen)
|
||||
@ -35,16 +35,16 @@ object QuickSave {
|
||||
fun load(screen: WorldScreen) {
|
||||
val gameSaver = UncivGame.Current.gameSaver
|
||||
val toast = ToastPopup("Quickloading...", screen)
|
||||
launchCrashHandling("QuickLoadGame") {
|
||||
Concurrency.run("QuickLoadGame") {
|
||||
try {
|
||||
val loadedGame = gameSaver.loadGameByName("QuickSave")
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
toast.close()
|
||||
UncivGame.Current.loadGame(loadedGame)
|
||||
ToastPopup("Quickload successful.", screen)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
toast.close()
|
||||
ToastPopup("Could not load game!", screen)
|
||||
}
|
||||
@ -56,10 +56,10 @@ object QuickSave {
|
||||
val loadingPopup = Popup(screen)
|
||||
loadingPopup.addGoodSizedLabel(Constants.loading)
|
||||
loadingPopup.open()
|
||||
launchCrashHandling("autoLoadGame") {
|
||||
Concurrency.run("autoLoadGame") {
|
||||
// Load game from file to class on separate thread to avoid ANR...
|
||||
fun outOfMemory() {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingPopup.close()
|
||||
ToastPopup("Not enough memory on phone to load game!", screen)
|
||||
}
|
||||
@ -70,14 +70,14 @@ object QuickSave {
|
||||
savedGame = screen.game.gameSaver.loadLatestAutosave()
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
outOfMemory()
|
||||
return@launchCrashHandling
|
||||
return@run
|
||||
} catch (ex: Exception) {
|
||||
Log.error("Could not autoload game", ex)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingPopup.close()
|
||||
ToastPopup("Cannot resume game!", screen)
|
||||
}
|
||||
return@launchCrashHandling
|
||||
return@run
|
||||
}
|
||||
|
||||
if (savedGame.gameParameters.isOnlineMultiplayer) {
|
||||
@ -88,13 +88,13 @@ object QuickSave {
|
||||
} catch (ex: Exception) {
|
||||
val message = MultiplayerHelpers.getLoadExceptionMessage(ex)
|
||||
Log.error("Could not autoload game", ex)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingPopup.close()
|
||||
ToastPopup(message, screen)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context
|
||||
launchOnGLThread { /// ... and load it into the screen on main thread for GL context
|
||||
try {
|
||||
screen.game.loadGame(savedGame)
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
|
@ -9,8 +9,6 @@ import com.unciv.UncivGame
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.popup.ToastPopup
|
||||
import com.unciv.ui.popup.YesNoPopup
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
@ -20,6 +18,8 @@ import com.unciv.ui.utils.extensions.enable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.ui.utils.extensions.toTextButton
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
|
||||
|
||||
class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") {
|
||||
@ -70,13 +70,15 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves")
|
||||
}
|
||||
|
||||
private fun copyToClipboardHandler() {
|
||||
launchCrashHandling("Copy game to clipboard") {
|
||||
Concurrency.run("Copy game to clipboard") {
|
||||
// the Gzip rarely leads to ANRs
|
||||
try {
|
||||
Gdx.app.clipboard.contents = GameSaver.gameInfoToString(gameInfo, forceZip = true)
|
||||
} catch (ex: Throwable) {
|
||||
ex.printStackTrace()
|
||||
ToastPopup("Could not save game to clipboard!", this@SaveGameScreen)
|
||||
launchOnGLThread {
|
||||
ToastPopup("Could not save game to clipboard!", this@SaveGameScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,7 +91,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves")
|
||||
errorLabel.setText("")
|
||||
saveToCustomLocation.setText("Saving...".tr())
|
||||
saveToCustomLocation.disable()
|
||||
launchCrashHandling("Save to custom location", runAsDaemon = false) {
|
||||
Concurrency.runOnNonDaemonThreadPool("Save to custom location") {
|
||||
game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result ->
|
||||
if (result.isError()) {
|
||||
errorLabel.setText("Could not save game to custom location!".tr())
|
||||
@ -107,9 +109,9 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves")
|
||||
|
||||
private fun saveGame() {
|
||||
rightSideButton.setText("Saving...".tr())
|
||||
launchCrashHandling("SaveGame", runAsDaemon = false) {
|
||||
Concurrency.runOnNonDaemonThreadPool("SaveGame") {
|
||||
game.gameSaver.saveGame(gameInfo, gameNameTextField.text) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen)
|
||||
else UncivGame.Current.resetToWorldScreen()
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.AutoScrollPane
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.KeyPressDispatcher
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
|
||||
//todo key auto-repeat for navigation keys?
|
||||
|
||||
@ -67,11 +67,11 @@ class VerticalFileListScrollPane(
|
||||
|
||||
// Apparently, even just 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
|
||||
launchCrashHandling("GetSaves") {
|
||||
Concurrency.run("GetSaves") {
|
||||
// .toList() materializes the result of the sequence
|
||||
val saves = files.toList()
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadAnimation.reset()
|
||||
existingSavesTable.clear()
|
||||
for (saveGameFile in saves) {
|
||||
|
@ -1,6 +1,9 @@
|
||||
package com.unciv.ui.utils
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.ui.crashhandling.CrashScreen
|
||||
|
||||
/** Interface to support various platform-specific tools */
|
||||
interface GeneralPlatformSpecificHelpers {
|
||||
@ -23,4 +26,10 @@ interface GeneralPlatformSpecificHelpers {
|
||||
* otherwise uses [com.badlogic.gdx.Files.getLocalStoragePath]
|
||||
*/
|
||||
fun shouldPreferExternalStorage(): Boolean
|
||||
|
||||
/**
|
||||
* Handle an uncaught throwable.
|
||||
* @return true if the throwable was handled.
|
||||
*/
|
||||
fun handleUncaughtThrowable(ex: Throwable): Boolean = false
|
||||
}
|
||||
|
@ -21,11 +21,11 @@ import com.unciv.Constants
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.images.IconCircleGroup
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
|
||||
/**
|
||||
* Collection of extension functions mostly for libGdx widgets
|
||||
@ -74,7 +74,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) {
|
||||
launchCrashHandling("Sound") { SoundPlayer.play(sound) }
|
||||
Concurrency.run("Sound") { SoundPlayer.play(sound) }
|
||||
function(event, x, y)
|
||||
}
|
||||
})
|
||||
|
@ -3,11 +3,11 @@ package com.unciv.ui.worldscreen
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.Constants
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
|
||||
class PlayerReadyScreen(worldScreen: WorldScreen) : BaseScreen() {
|
||||
init {
|
||||
@ -19,7 +19,7 @@ class PlayerReadyScreen(worldScreen: WorldScreen) : BaseScreen() {
|
||||
table.add("[$curCiv] ready?".toLabel(curCiv.nation.getInnerColor(), Constants.headingFontSize))
|
||||
|
||||
table.onClick {
|
||||
postCrashHandlingRunnable { // To avoid ANRs on Android when the creation of the worldscreen takes more than 500ms
|
||||
Concurrency.runOnGLThread { // To avoid ANRs on Android when the creation of the worldscreen takes more than 500ms
|
||||
game.setScreen(worldScreen)
|
||||
}
|
||||
}
|
||||
|
@ -34,8 +34,6 @@ import com.unciv.models.helpers.MapArrowType
|
||||
import com.unciv.models.helpers.MiscArrowTypes
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.map.TileGroupMap
|
||||
import com.unciv.ui.tilegroups.TileGroup
|
||||
@ -50,6 +48,8 @@ import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.surroundWithCircle
|
||||
import com.unciv.ui.utils.extensions.toLabel
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
|
||||
|
||||
class WorldMapHolder(
|
||||
@ -153,7 +153,7 @@ class WorldMapHolder(
|
||||
override fun clicked(event: InputEvent?, x: Float, y: Float) {
|
||||
val unit = worldScreen.bottomUnitTable.selectedUnit
|
||||
?: return
|
||||
launchCrashHandling("WorldScreenClick") {
|
||||
Concurrency.run("WorldScreenClick") {
|
||||
onTileRightClicked(unit, tileGroup.tileInfo)
|
||||
}
|
||||
}
|
||||
@ -261,7 +261,7 @@ class WorldMapHolder(
|
||||
|
||||
val selectedUnit = selectedUnits.first()
|
||||
|
||||
launchCrashHandling("TileToMoveTo") {
|
||||
Concurrency.run("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
|
||||
@ -270,10 +270,10 @@ class WorldMapHolder(
|
||||
tileToMoveTo = selectedUnit.movement.getTileToMoveToThisTurn(targetTile)
|
||||
} catch (ex: Exception) {
|
||||
Log.error("Exception in getTileToMoveToThisTurn", ex)
|
||||
return@launchCrashHandling
|
||||
return@run
|
||||
} // can't move here
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
try {
|
||||
// Because this is darned concurrent (as it MUST be to avoid ANRs),
|
||||
// there are edge cases where the canReach is true,
|
||||
@ -315,7 +315,7 @@ class WorldMapHolder(
|
||||
}
|
||||
|
||||
private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tileInfo: TileInfo) {
|
||||
launchCrashHandling("TurnsToGetThere") {
|
||||
Concurrency.run("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,
|
||||
@ -346,12 +346,12 @@ class WorldMapHolder(
|
||||
unitToTurnsToTile[unit] = turnsToGetThere
|
||||
}
|
||||
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
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@postCrashHandlingRunnable
|
||||
return@launchOnGLThread
|
||||
}
|
||||
|
||||
val turnsToGetThere = unitsWhoCanMoveThere.values.maxOrNull()!!
|
||||
|
@ -32,8 +32,6 @@ import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.cityscreen.CityScreen
|
||||
import com.unciv.ui.civilopedia.CivilopediaScreen
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.multiplayer.MultiplayerHelpers
|
||||
import com.unciv.ui.overviewscreen.EmpireOverviewScreen
|
||||
@ -77,8 +75,12 @@ import com.unciv.ui.worldscreen.status.NextTurnButton
|
||||
import com.unciv.ui.worldscreen.status.StatusButtons
|
||||
import com.unciv.ui.worldscreen.unit.UnitActionsTable
|
||||
import com.unciv.ui.worldscreen.unit.UnitTable
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import com.unciv.utils.concurrency.launchOnThreadPool
|
||||
import com.unciv.utils.debug
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
/**
|
||||
* Unciv's world screen
|
||||
@ -218,7 +220,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
if (isNextTurnUpdateRunning() || game.onlineMultiplayer.hasLatestGameState(gameInfo, it.preview)) {
|
||||
return@receive
|
||||
}
|
||||
launchCrashHandling("Load latest multiplayer state") {
|
||||
Concurrency.run("Load latest multiplayer state") {
|
||||
loadLatestMultiplayerState()
|
||||
}
|
||||
}
|
||||
@ -327,9 +329,9 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
|
||||
}
|
||||
|
||||
private suspend fun loadLatestMultiplayerState() {
|
||||
val loadingGamePopup = Popup(this)
|
||||
postCrashHandlingRunnable {
|
||||
private suspend fun loadLatestMultiplayerState(): Unit = coroutineScope {
|
||||
val loadingGamePopup = Popup(this@WorldScreen)
|
||||
launchOnGLThread {
|
||||
loadingGamePopup.addGoodSizedLabel("Loading latest game state...")
|
||||
loadingGamePopup.open()
|
||||
}
|
||||
@ -343,20 +345,20 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
if (viewingCiv.civName == latestGame.currentPlayer || viewingCiv.civName == Constants.spectator) {
|
||||
game.platformSpecificHelper?.notifyTurnStarted()
|
||||
}
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingGamePopup.close()
|
||||
if (game.gameInfo!!.gameId == gameInfo.gameId) { // game could've been changed during download
|
||||
game.setScreen(createNewWorldScreen(latestGame))
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
val message = MultiplayerHelpers.getLoadExceptionMessage(ex)
|
||||
loadingGamePopup.innerTable.clear()
|
||||
loadingGamePopup.addGoodSizedLabel("Couldn't download the latest game state!").colspan(2).row()
|
||||
loadingGamePopup.addGoodSizedLabel(message).colspan(2).row()
|
||||
loadingGamePopup.addButtonInRow("Retry") {
|
||||
launchCrashHandling("Load latest multiplayer state after error") {
|
||||
launchOnThreadPool("Load latest multiplayer state after error") {
|
||||
loadLatestMultiplayerState()
|
||||
}
|
||||
}.right()
|
||||
@ -629,7 +631,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
shouldUpdate = true
|
||||
|
||||
// on a separate thread so the user can explore their world while we're passing the turn
|
||||
nextTurnUpdateJob = launchCrashHandling("NextTurn", runAsDaemon = false) {
|
||||
nextTurnUpdateJob = Concurrency.runOnNonDaemonThreadPool("NextTurn") {
|
||||
debug("Next turn starting")
|
||||
val startTime = System.currentTimeMillis()
|
||||
val originalGameInfo = gameInfo
|
||||
@ -646,7 +648,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds"
|
||||
else -> "Could not upload game!"
|
||||
}
|
||||
postCrashHandlingRunnable { // Since we're changing the UI, that should be done on the main thread
|
||||
launchOnGLThread { // Since we're changing the UI, that should be done on the main thread
|
||||
val cantUploadNewGamePopup = Popup(this@WorldScreen)
|
||||
cantUploadNewGamePopup.addGoodSizedLabel(message).row()
|
||||
cantUploadNewGamePopup.addCloseButton()
|
||||
@ -654,12 +656,12 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
}
|
||||
this@WorldScreen.isPlayersTurn = true // Since we couldn't push the new game clone, then it's like we never clicked the "next turn" button
|
||||
this@WorldScreen.shouldUpdate = true
|
||||
return@launchCrashHandling
|
||||
return@runOnNonDaemonThreadPool
|
||||
}
|
||||
}
|
||||
|
||||
if (game.gameInfo != originalGameInfo) // while this was turning we loaded another game
|
||||
return@launchCrashHandling
|
||||
return@runOnNonDaemonThreadPool
|
||||
|
||||
this@WorldScreen.game.gameInfo = gameInfoClone
|
||||
debug("Next turn took %sms", System.currentTimeMillis() - startTime)
|
||||
@ -668,7 +670,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
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
val newWorldScreen = createNewWorldScreen(gameInfoClone)
|
||||
if (gameInfoClone.currentPlayerCiv.civName != viewingCiv.civName
|
||||
&& !gameInfoClone.gameParameters.isOnlineMultiplayer) {
|
||||
@ -680,7 +682,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
if (shouldAutoSave) {
|
||||
newWorldScreen.waitingForAutosave = true
|
||||
newWorldScreen.shouldUpdate = true
|
||||
game.gameSaver.autoSave(gameInfoClone) {
|
||||
game.gameSaver.requestAutoSave(gameInfoClone).invokeOnCompletion {
|
||||
// only enable the user to next turn once we've saved the current one
|
||||
newWorldScreen.waitingForAutosave = false
|
||||
newWorldScreen.shouldUpdate = true
|
||||
@ -802,10 +804,10 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
viewingCiv.hasMovedAutomatedUnits = true
|
||||
isPlayersTurn = false // Disable state changes
|
||||
nextTurnButton.disable()
|
||||
launchCrashHandling("Move automated units") {
|
||||
Concurrency.run("Move automated units") {
|
||||
for (unit in viewingCiv.getCivUnits())
|
||||
unit.doAction()
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
shouldUpdate = true
|
||||
isPlayersTurn = true //Re-enable state changes
|
||||
nextTurnButton.enable()
|
||||
|
@ -16,7 +16,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
|
||||
defaults().fillX()
|
||||
|
||||
addButton("Main menu") {
|
||||
worldScreen.game.gameSaver.autoSaveUnCloned(worldScreen.gameInfo)
|
||||
worldScreen.game.gameSaver.requestAutoSaveUnCloned(worldScreen.gameInfo) // Can save gameInfo directly because the user can't modify it on the MainMenuScreen
|
||||
worldScreen.game.setScreen(MainMenuScreen())
|
||||
}
|
||||
addButton("Civilopedia") {
|
||||
|
@ -20,12 +20,12 @@ import com.unciv.logic.multiplayer.MultiplayerGameUpdateStarted
|
||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdated
|
||||
import com.unciv.logic.multiplayer.OnlineMultiplayerGame
|
||||
import com.unciv.logic.multiplayer.isUsersTurn
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.utils.extensions.setSize
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
import com.unciv.utils.concurrency.launchOnGLThread
|
||||
import kotlinx.coroutines.delay
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -55,7 +55,7 @@ class MultiplayerStatusButton(
|
||||
} else {
|
||||
gameNamesWithCurrentTurn.remove(it.name)
|
||||
}
|
||||
if (shouldUpdate) postCrashHandlingRunnable {
|
||||
if (shouldUpdate) Concurrency.runOnGLThread {
|
||||
updateTurnIndicator()
|
||||
}
|
||||
}
|
||||
@ -96,9 +96,9 @@ class MultiplayerStatusButton(
|
||||
} else {
|
||||
Duration.ZERO
|
||||
}
|
||||
launchCrashHandling("Hide loading indicator") {
|
||||
Concurrency.run("Hide loading indicator") {
|
||||
delay(waitFor.toMillis())
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
loadingImage.clearActions()
|
||||
loadingImage.isVisible = false
|
||||
multiplayerImage.color.a = 1f
|
||||
@ -175,9 +175,9 @@ private class TurnIndicator : HorizontalGroup() {
|
||||
if (alternations == 0) return
|
||||
gameAmount.color = nextColor
|
||||
image.color = nextColor
|
||||
launchCrashHandling("StatusButton color flash") {
|
||||
Concurrency.run("StatusButton color flash") {
|
||||
delay(500)
|
||||
postCrashHandlingRunnable {
|
||||
launchOnGLThread {
|
||||
flash(alternations - 1, nextColor, curColor)
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import com.unciv.UncivGame
|
||||
import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.models.UnitAction
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
import com.unciv.ui.crashhandling.launchCrashHandling
|
||||
import com.unciv.ui.images.IconTextButton
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable
|
||||
@ -15,6 +14,7 @@ import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||
import com.unciv.ui.utils.extensions.disable
|
||||
import com.unciv.ui.utils.extensions.onClick
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import com.unciv.utils.concurrency.Concurrency
|
||||
|
||||
class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
|
||||
|
||||
@ -46,7 +46,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
|
||||
actionButton.onClick(unitAction.uncivSound, action)
|
||||
if (key != KeyCharAndCode.UNKNOWN)
|
||||
worldScreen.keyPressDispatcher[key] = {
|
||||
launchCrashHandling("UnitSound") { SoundPlayer.play(unitAction.uncivSound) }
|
||||
Concurrency.run("UnitSound") { SoundPlayer.play(unitAction.uncivSound) }
|
||||
action()
|
||||
worldScreen.mapHolder.removeUnitActionOverlay()
|
||||
}
|
||||
|
158
core/src/com/unciv/utils/concurrency/Concurrency.kt
Normal file
158
core/src/com/unciv/utils/concurrency/Concurrency.kt
Normal file
@ -0,0 +1,158 @@
|
||||
package com.unciv.utils.concurrency
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.LifecycleListener
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.ui.crashhandling.wrapCrashHandlingUnit
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Created to make handling multiple threads as simple as possible. Everything is based upon [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-guide.html),
|
||||
* so fully understanding this code requires familiarity with that.
|
||||
*
|
||||
* However, the simple usage guide:
|
||||
* - Use the `run...` functions within code that does not use any concurrency yet.
|
||||
* - Then, use the `launch...` functions within `run...` code blocks.
|
||||
* - Within `suspend` functions, use [kotlinx.coroutines.coroutineScope] to gain access to the `launch...` functions.
|
||||
*
|
||||
* All methods in this file automatically wrap the given code blocks to catch all uncaught exceptions, calling [UncivGame.handleUncaughtThrowable].
|
||||
*/
|
||||
object Concurrency {
|
||||
|
||||
/**
|
||||
* See [kotlinx.coroutines.runBlocking]. Runs on a non-daemon thread pool by default.
|
||||
*
|
||||
* @return null if an uncaught exception occured
|
||||
*/
|
||||
fun <T> runBlocking(
|
||||
name: String? = null,
|
||||
context: CoroutineContext = Dispatcher.NON_DAEMON,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): T? {
|
||||
return kotlinx.coroutines.runBlocking(addName(context, name)) {
|
||||
try {
|
||||
block(this)
|
||||
} catch (ex: Throwable) {
|
||||
UncivGame.Current.handleUncaughtThrowable(ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-blocking version of [runBlocking]. Runs on a daemon thread pool by default. Use this for code that does not necessarily need to finish executing. */
|
||||
fun run(
|
||||
name: String? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatcher.DAEMON),
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job {
|
||||
return scope.launchCrashHandling(scope.coroutineContext, name, block)
|
||||
}
|
||||
|
||||
/** Non-blocking version of [runBlocking]. Runs on a non-daemon thread pool. Use this if you do something that should always finish if possible, like saving the game. */
|
||||
fun runOnNonDaemonThreadPool(name: String? = null, block: suspend CoroutineScope.() -> Unit) = run(name, CoroutineScope(Dispatcher.NON_DAEMON), block)
|
||||
|
||||
/** Non-blocking version of [runBlocking]. Runs on the GDX GL thread. Use this for all code that manipulates the GDX UI classes. */
|
||||
fun runOnGLThread(name: String? = null, block: suspend CoroutineScope.() -> Unit) = run(name, CoroutineScope(Dispatcher.GL), block)
|
||||
|
||||
/** Must only be called in [com.unciv.UncivGame.dispose] to not have any threads running that prevent JVM shutdown. */
|
||||
fun stopThreadPools() = EXECUTORS.forEach(ExecutorService::shutdown)
|
||||
}
|
||||
|
||||
/** See [launch] */
|
||||
// This method is not called `launch` (with a default DAEMON dispatcher) to prevent ambiguity between our `launch` methods and kotlin coroutine `launch` methods.
|
||||
fun CoroutineScope.launchCrashHandling(
|
||||
context: CoroutineContext,
|
||||
name: String? = null,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job {
|
||||
return launch(addName(context, name)) {
|
||||
try {
|
||||
block(this)
|
||||
} catch (ex: Throwable) {
|
||||
UncivGame.Current.handleUncaughtThrowable(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See [launch]. Runs on a daemon thread pool. Use this for code that does not necessarily need to finish executing. */
|
||||
fun CoroutineScope.launchOnThreadPool(name: String? = null, block: suspend CoroutineScope.() -> Unit) = launchCrashHandling(Dispatcher.DAEMON, name, block)
|
||||
/** See [launch]. Runs on a non-daemon thread pool. Use this if you do something that should always finish if possible, like saving the game. */
|
||||
fun CoroutineScope.launchOnNonDaemonThreadPool(name: String? = null, block: suspend CoroutineScope.() -> Unit) = launchCrashHandling(Dispatcher.NON_DAEMON, name, block)
|
||||
/** See [launch]. Runs on the GDX GL thread. Use this for all code that manipulates the GDX UI classes. */
|
||||
fun CoroutineScope.launchOnGLThread(name: String? = null, block: suspend CoroutineScope.() -> Unit) = launchCrashHandling(Dispatcher.GL, name, block)
|
||||
|
||||
|
||||
/**
|
||||
* All dispatchers here bring the main game loop to a [com.unciv.CrashScreen] if an exception happens.
|
||||
*/
|
||||
object Dispatcher {
|
||||
/** Runs coroutines on a daemon thread pool. */
|
||||
val DAEMON: CoroutineDispatcher = createThreadpoolDispatcher("threadpool-daemon-", isDaemon = true)
|
||||
|
||||
/** Runs coroutines on a non-daemon thread pool. */
|
||||
val NON_DAEMON: CoroutineDispatcher = createThreadpoolDispatcher("threadpool-nondaemon-", isDaemon = false)
|
||||
|
||||
/** Runs coroutines on the GDX GL thread. */
|
||||
val GL: CoroutineDispatcher = CrashHandlingDispatcher(GLDispatcher())
|
||||
}
|
||||
|
||||
private fun addName(context: CoroutineContext, name: String?) = if (name != null) context + CoroutineName(name) else context
|
||||
|
||||
private val EXECUTORS = mutableListOf<ExecutorService>()
|
||||
|
||||
private class GLDispatcher : CoroutineDispatcher(), LifecycleListener {
|
||||
var isDisposed = false
|
||||
|
||||
init {
|
||||
Gdx.app.addLifecycleListener(this)
|
||||
}
|
||||
|
||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||
if (isDisposed) {
|
||||
context.cancel(CancellationException("GDX GL thread is not handling runnables anymore"))
|
||||
Dispatcher.DAEMON.dispatch(context, block) // dispatch contract states that block has to be invoked
|
||||
return
|
||||
}
|
||||
Gdx.app.postRunnable(block)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
isDisposed = true
|
||||
}
|
||||
override fun pause() {}
|
||||
override fun resume() {}
|
||||
}
|
||||
|
||||
private fun createThreadpoolDispatcher(threadPrefix: String, isDaemon: Boolean): CrashHandlingDispatcher {
|
||||
val executor = Executors.newCachedThreadPool(object : ThreadFactory {
|
||||
var n = 0
|
||||
override fun newThread(r: Runnable): Thread {
|
||||
val thread = Thread(r, "${threadPrefix}${n++}")
|
||||
thread.isDaemon = isDaemon
|
||||
return thread
|
||||
}
|
||||
})
|
||||
EXECUTORS.add(executor)
|
||||
return CrashHandlingDispatcher(executor.asCoroutineDispatcher())
|
||||
}
|
||||
|
||||
class CrashHandlingDispatcher(
|
||||
private val decoratedDispatcher: CoroutineDispatcher
|
||||
) : CoroutineDispatcher() {
|
||||
|
||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||
decoratedDispatcher.dispatch(context, block::run.wrapCrashHandlingUnit())
|
||||
}
|
||||
}
|
@ -9,10 +9,10 @@ import com.badlogic.gdx.graphics.glutils.HdpiMode
|
||||
import com.sun.jna.Native
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.UncivGameParameters
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.debug
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.debug
|
||||
import java.util.*
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
|
Reference in New Issue
Block a user