Fix coroutines not being added to desktop:dist (#6822)

* Use kotlin coroutines instead of raw threads (+ refactorings)

Equal to f8e0f572

* Fix coroutine class files not being added to desktop:dist
This commit is contained in:
Timo T
2022-05-17 17:37:12 +02:00
committed by GitHub
parent 7b3a4c741f
commit df9b62ff6f
35 changed files with 539 additions and 380 deletions

View File

@ -20,7 +20,9 @@ android {
} }
} }
packagingOptions { packagingOptions {
resources.excludes.add("META-INF/robovm/ios/robovm.xml") resources.excludes += "META-INF/robovm/ios/robovm.xml"
// part of kotlinx-coroutines-android, should not go into the apk
resources.excludes += "DebugProbesKt.bin"
} }
defaultConfig { defaultConfig {
applicationId = "com.unciv.app" applicationId = "com.unciv.app"

View File

@ -15,9 +15,10 @@ import androidx.work.*
import com.badlogic.gdx.backends.android.AndroidApplication import com.badlogic.gdx.backends.android.AndroidApplication
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import kotlinx.coroutines.runBlocking
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
@ -234,7 +235,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
} }
} }
override fun doWork(): Result { override fun doWork(): Result = runBlocking {
val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true) val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true)
val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5) val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5)
val fileStorage = inputData.getString(FILE_STORAGE) val fileStorage = inputData.getString(FILE_STORAGE)
@ -253,7 +254,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
continue continue
try { try {
val gamePreview = OnlineMultiplayer(fileStorage).tryDownloadGamePreview(gameId) val gamePreview = OnlineMultiplayerGameSaver(fileStorage).tryDownloadGamePreview(gameId)
val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer) val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer)
//Save game so MultiplayerScreen gets updated //Save game so MultiplayerScreen gets updated
@ -302,7 +303,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
with(NotificationManagerCompat.from(applicationContext)) { with(NotificationManagerCompat.from(applicationContext)) {
cancel(NOTIFICATION_ID_SERVICE) cancel(NOTIFICATION_ID_SERVICE)
} }
return Result.failure() return@runBlocking Result.failure()
} else { } else {
if (showPersistNotific) { showPersistentNotification(applicationContext, if (showPersistNotific) { showPersistentNotification(applicationContext,
applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) } applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) }
@ -313,9 +314,9 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
enqueue(applicationContext, 1, inputDataFailIncrease) enqueue(applicationContext, 1, inputDataFailIncrease)
} }
} catch (outOfMemory: OutOfMemoryError){ // no point in trying multiple times if this was an oom error } catch (outOfMemory: OutOfMemoryError){ // no point in trying multiple times if this was an oom error
return Result.failure() return@runBlocking Result.failure()
} }
return Result.success() return@runBlocking Result.success()
} }
private fun getStackTraceString(ex: Exception): String { private fun getStackTraceString(ex: Exception): String {

View File

@ -93,6 +93,7 @@ project(":android") {
dependencies { dependencies {
"implementation"(project(":core")) "implementation"(project(":core"))
"implementation"("com.badlogicgames.gdx:gdx-backend-android:$gdxVersion") "implementation"("com.badlogicgames.gdx:gdx-backend-android:$gdxVersion")
"implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1")
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a") natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a")
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a") natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a")
natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86") natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86")
@ -119,6 +120,7 @@ project(":core") {
dependencies { dependencies {
"implementation"("com.badlogicgames.gdx:gdx:$gdxVersion") "implementation"("com.badlogicgames.gdx:gdx:$gdxVersion")
"implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
} }

View File

@ -20,7 +20,7 @@ import com.unciv.ui.MultiplayerScreen
import com.unciv.ui.mapeditor.* import com.unciv.ui.mapeditor.*
import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.GameSetupInfo
import com.unciv.ui.civilopedia.CivilopediaScreen import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.newgamescreen.NewGameScreen import com.unciv.ui.newgamescreen.NewGameScreen
@ -73,7 +73,7 @@ class MainMenuScreen: BaseScreen() {
// will not exist unless we reset the ruleset and images // will not exist unless we reset the ruleset and images
ImageGetter.ruleset = RulesetCache.getVanillaRuleset() ImageGetter.ruleset = RulesetCache.getVanillaRuleset()
crashHandlingThread(name = "ShowMapBackground") { launchCrashHandling("ShowMapBackground") {
val newMap = MapGenerator(RulesetCache.getVanillaRuleset()) val newMap = MapGenerator(RulesetCache.getVanillaRuleset())
.generateMap(MapParameters().apply { .generateMap(MapParameters().apply {
mapSize = MapSizeNew(MapSize.Small) mapSize = MapSizeNew(MapSize.Small)
@ -82,7 +82,7 @@ class MainMenuScreen: BaseScreen() {
}) })
postCrashHandlingRunnable { // for GL context postCrashHandlingRunnable { // for GL context
ImageGetter.setNewRuleset(RulesetCache.getVanillaRuleset()) ImageGetter.setNewRuleset(RulesetCache.getVanillaRuleset())
val mapHolder = EditorMapHolder(this, newMap) {} val mapHolder = EditorMapHolder(this@MainMenuScreen, newMap) {}
backgroundTable.addAction(Actions.sequence( backgroundTable.addAction(Actions.sequence(
Actions.fadeOut(0f), Actions.fadeOut(0f),
Actions.run { Actions.run {
@ -171,12 +171,12 @@ class MainMenuScreen: BaseScreen() {
val loadingPopup = Popup(this) val loadingPopup = Popup(this)
loadingPopup.addGoodSizedLabel("Loading...") loadingPopup.addGoodSizedLabel("Loading...")
loadingPopup.open() loadingPopup.open()
crashHandlingThread { launchCrashHandling("autoLoadGame") {
// Load game from file to class on separate thread to avoid ANR... // Load game from file to class on separate thread to avoid ANR...
fun outOfMemory() { fun outOfMemory() {
postCrashHandlingRunnable { postCrashHandlingRunnable {
loadingPopup.close() loadingPopup.close()
ToastPopup("Not enough memory on phone to load game!", this) ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen)
} }
} }
@ -185,7 +185,7 @@ class MainMenuScreen: BaseScreen() {
savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName) savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName)
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
outOfMemory() outOfMemory()
return@crashHandlingThread return@launchCrashHandling
} catch (ex: Exception) { // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first } catch (ex: Exception) { // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first
// This can help for situations when the autosave is corrupted // This can help for situations when the autosave is corrupted
try { try {
@ -195,13 +195,13 @@ class MainMenuScreen: BaseScreen() {
GameSaver.loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!) GameSaver.loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!)
} catch (oom: OutOfMemoryError) { // The autosave could have oom problems as well... smh } catch (oom: OutOfMemoryError) { // The autosave could have oom problems as well... smh
outOfMemory() outOfMemory()
return@crashHandlingThread return@launchCrashHandling
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
loadingPopup.close() loadingPopup.close()
ToastPopup("Cannot resume game!", this) ToastPopup("Cannot resume game!", this@MainMenuScreen)
} }
return@crashHandlingThread return@launchCrashHandling
} }
} }
@ -219,14 +219,14 @@ class MainMenuScreen: BaseScreen() {
private fun quickstartNewGame() { private fun quickstartNewGame() {
ToastPopup("Working...", this) ToastPopup("Working...", this)
val errorText = "Cannot start game with the default new game parameters!" val errorText = "Cannot start game with the default new game parameters!"
crashHandlingThread { launchCrashHandling("QuickStart") {
val newGame: GameInfo val newGame: GameInfo
// Can fail when starting the game... // Can fail when starting the game...
try { try {
newGame = GameStarter.startNewGame(GameSetupInfo.fromSettings("Chieftain")) newGame = GameStarter.startNewGame(GameSetupInfo.fromSettings("Chieftain"))
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { ToastPopup(errorText, this) } postCrashHandlingRunnable { ToastPopup(errorText, this@MainMenuScreen) }
return@crashHandlingThread return@launchCrashHandling
} }
// ...or when loading the game // ...or when loading the game
@ -234,9 +234,9 @@ class MainMenuScreen: BaseScreen() {
try { try {
game.loadGame(newGame) game.loadGame(newGame)
} catch (outOfMemory: OutOfMemoryError) { } catch (outOfMemory: OutOfMemoryError) {
ToastPopup("Not enough memory on phone to load game!", this) ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen)
} catch (ex: Exception) { } catch (ex: Exception) {
ToastPopup(errorText, this) ToastPopup(errorText, this@MainMenuScreen)
} }
} }
} }

View File

@ -19,11 +19,14 @@ import com.unciv.ui.audio.MusicMood
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.PlayerReadyScreen import com.unciv.ui.worldscreen.PlayerReadyScreen
import com.unciv.ui.worldscreen.WorldScreen import com.unciv.ui.worldscreen.WorldScreen
import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import com.unciv.ui.audio.Sounds import com.unciv.ui.audio.Sounds
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.closeExecutors
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.multiplayer.LoadDeepLinkScreen
import com.unciv.ui.popup.Popup
import java.util.* import java.util.*
class UncivGame(parameters: UncivGameParameters) : Game() { class UncivGame(parameters: UncivGameParameters) : Game() {
@ -114,7 +117,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
Gdx.graphics.isContinuousRendering = settings.continuousRendering Gdx.graphics.isContinuousRendering = settings.continuousRendering
crashHandlingThread(name = "LoadJSON") { launchCrashHandling("LoadJSON") {
RulesetCache.loadRulesets(printOutput = true) RulesetCache.loadRulesets(printOutput = true)
translations.tryReadTranslationForCurrentLanguage() translations.tryReadTranslationForCurrentLanguage()
translations.loadPercentageCompleteOfLanguages() translations.loadPercentageCompleteOfLanguages()
@ -169,13 +172,26 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
Gdx.graphics.requestRendering() Gdx.graphics.requestRendering()
} }
fun tryLoadDeepLinkedGame() { fun tryLoadDeepLinkedGame() = launchCrashHandling("LoadDeepLinkedGame") {
if (deepLinkedMultiplayerGame != null) { if (deepLinkedMultiplayerGame != null) {
postCrashHandlingRunnable {
setScreen(LoadDeepLinkScreen())
}
try { try {
val onlineGame = OnlineMultiplayer().tryDownloadGame(deepLinkedMultiplayerGame!!) val onlineGame = OnlineMultiplayerGameSaver().tryDownloadGame(deepLinkedMultiplayerGame!!)
postCrashHandlingRunnable {
loadGame(onlineGame) loadGame(onlineGame)
}
} catch (ex: Exception) { } catch (ex: Exception) {
setScreen(MainMenuScreen()) postCrashHandlingRunnable {
val mainMenu = MainMenuScreen()
setScreen(mainMenu)
val popup = Popup(mainMenu)
popup.addGoodSizedLabel("Failed to load multiplayer game: ${ex.message ?: ex::class.simpleName}")
popup.row()
popup.addCloseButton()
popup.open()
}
} }
} }
} }
@ -210,6 +226,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
cancelDiscordEvent?.invoke() cancelDiscordEvent?.invoke()
Sounds.clearCache() Sounds.clearCache()
if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out
closeExecutors()
// Log still running threads (on desktop that should be only this one and "DestroyJavaVM") // Log still running threads (on desktop that should be only this one and "DestroyJavaVM")
val numThreads = Thread.activeCount() val numThreads = Thread.activeCount()

View File

@ -4,9 +4,8 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.json.json import com.unciv.json.json
import com.unciv.logic.multiplayer.OnlineMultiplayer
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.saves.Gzip import com.unciv.ui.saves.Gzip
import java.io.File import java.io.File
@ -31,20 +30,20 @@ object GameSaver {
//endregion //endregion
//region Helpers //region Helpers
private fun getSubfolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder private fun getSavefolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder
fun getSave(GameName: String, multiplayer: Boolean = false): FileHandle { fun getSave(GameName: String, multiplayer: Boolean = false): FileHandle {
val localFile = Gdx.files.local("${getSubfolder(multiplayer)}/$GameName") val localFile = Gdx.files.local("${getSavefolder(multiplayer)}/$GameName")
if (externalFilesDirForAndroid == "" || !Gdx.files.isExternalStorageAvailable) return localFile if (externalFilesDirForAndroid == "" || !Gdx.files.isExternalStorageAvailable) return localFile
val externalFile = Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}/$GameName") val externalFile = Gdx.files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}/$GameName")
if (localFile.exists() && !externalFile.exists()) return localFile if (localFile.exists() && !externalFile.exists()) return localFile
return externalFile return externalFile
} }
fun getSaves(multiplayer: Boolean = false): Sequence<FileHandle> { fun getSaves(multiplayer: Boolean = false): Sequence<FileHandle> {
val localSaves = Gdx.files.local(getSubfolder(multiplayer)).list().asSequence() val localSaves = Gdx.files.local(getSavefolder(multiplayer)).list().asSequence()
if (externalFilesDirForAndroid == "" || !Gdx.files.isExternalStorageAvailable) return localSaves if (externalFilesDirForAndroid == "" || !Gdx.files.isExternalStorageAvailable) return localSaves
return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}").list().asSequence() return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}").list().asSequence()
} }
fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null
@ -56,12 +55,21 @@ object GameSaver {
//endregion //endregion
//region Saving //region Saving
fun saveGame(game: GameInfo, GameName: String, saveCompletionCallback: ((Exception?) -> Unit)? = null) { fun saveGame(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle {
val file = getSave(GameName)
saveGame(game, file, saveCompletionCallback)
return file
}
/**
* Only use this with a [FileHandle] obtained by [getSaves]!
*/
fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
getSave(GameName).writeString(gameInfoToString(game), false) file.writeString(gameInfoToString(game), false)
saveCompletionCallback?.invoke(null) saveCompletionCallback(null)
} catch (ex: Exception) { } catch (ex: Exception) {
saveCompletionCallback?.invoke(ex) saveCompletionCallback(ex)
} }
} }
@ -71,7 +79,7 @@ object GameSaver {
return if (forceZip ?: saveZipped) Gzip.zip(plainJson) else plainJson return if (forceZip ?: saveZipped) Gzip.zip(plainJson) else plainJson
} }
/** Returns gzipped serialization of preview [game] - only called from [OnlineMultiplayer] */ /** Returns gzipped serialization of preview [game] - only called from [OnlineMultiplayerGameSaver] */
fun gameInfoToString(game: GameInfoPreview): String { fun gameInfoToString(game: GameInfoPreview): String {
return Gzip.zip(json().toJson(game)) return Gzip.zip(json().toJson(game))
} }
@ -79,12 +87,21 @@ object GameSaver {
/** /**
* Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder * Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder
*/ */
fun saveGame(game: GameInfoPreview, GameName: String, saveCompletionCallback: ((Exception?) -> Unit)? = null) { fun saveGame(game: GameInfoPreview, GameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle {
val file = getSave(GameName, true)
saveGame(game, file, saveCompletionCallback)
return file
}
/**
* Only use this with a [FileHandle] obtained by [getSaves]!
*/
fun saveGame(game: GameInfoPreview, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
json().toJson(game, getSave(GameName, true)) json().toJson(game, file)
saveCompletionCallback?.invoke(null) saveCompletionCallback(null)
} catch (ex: Exception) { } catch (ex: Exception) {
saveCompletionCallback?.invoke(ex) saveCompletionCallback(ex)
} }
} }
@ -121,7 +138,7 @@ object GameSaver {
} }
} }
/** Parses [gameData] as gzipped serialization of a [GameInfoPreview] - only called from [OnlineMultiplayer] */ /** Parses [gameData] as gzipped serialization of a [GameInfoPreview] - only called from [OnlineMultiplayerGameSaver] */
fun gameInfoPreviewFromString(gameData: String): GameInfoPreview { fun gameInfoPreviewFromString(gameData: String): GameInfoPreview {
return json().fromJson(GameInfoPreview::class.java, Gzip.unzip(gameData)) return json().fromJson(GameInfoPreview::class.java, Gzip.unzip(gameData))
} }
@ -184,7 +201,7 @@ object GameSaver {
fun autoSaveUnCloned(gameInfo: GameInfo, postRunnable: () -> Unit = {}) { 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 // This is used when returning from WorldScreen to MainMenuScreen - no clone since UI access to it should be gone
crashHandlingThread(name = autoSaveFileName) { launchCrashHandling(autoSaveFileName, runAsDaemon = false) {
autoSaveSingleThreaded(gameInfo) autoSaveSingleThreaded(gameInfo)
// do this on main thread // do this on main thread
postCrashHandlingRunnable ( postRunnable ) postCrashHandlingRunnable ( postRunnable )

View File

@ -1,130 +0,0 @@
package com.unciv.logic.multiplayer
import com.badlogic.gdx.Net
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.GameInfoPreview
import com.unciv.logic.GameSaver
import java.io.FileNotFoundException
import java.util.*
interface IFileStorage {
/**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws FileStorageConflictException if the file already exists and [overwrite] is false
*/
fun saveFileData(fileName: String, data: String, overwrite: Boolean)
/**
* @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
*/
fun loadFileData(fileName: String): String
/**
* @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
*/
fun getFileMetaData(fileName: String): IFileMetaData
/**
* @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
*/
fun deleteFile(fileName: String)
}
interface IFileMetaData {
fun getLastModified(): Date?
}
class UncivServerFileStorage(val serverUrl:String):IFileStorage {
override fun saveFileData(fileName: String, data: String, overwrite: Boolean) {
SimpleHttp.sendRequest(Net.HttpMethods.PUT, "$serverUrl/files/$fileName", data){
success: Boolean, result: String ->
if (!success) {
println(result)
throw java.lang.Exception(result)
}
}
}
override fun loadFileData(fileName: String): String {
var fileData = ""
SimpleHttp.sendGetRequest("$serverUrl/files/$fileName"){
success: Boolean, result: String ->
if (!success) {
println(result)
throw java.lang.Exception(result)
}
else fileData = result
}
return fileData
}
override fun getFileMetaData(fileName: String): IFileMetaData {
TODO("Not yet implemented")
}
override fun deleteFile(fileName: String) {
SimpleHttp.sendRequest(Net.HttpMethods.DELETE, "$serverUrl/files/$fileName", ""){
success: Boolean, result: String ->
if (!success) throw java.lang.Exception(result)
}
}
}
class FileStorageConflictException: Exception()
class FileStorageRateLimitReached(val limitRemainingSeconds: Int): Exception()
/**
* Allows access to games stored on a server for multiplayer purposes.
* Defaults to using UncivGame.Current.settings.multiplayerServer if fileStorageIdentifier is not given.
*
* @param fileStorageIdentifier must be given if UncivGame.Current might not be initialized
* @see IFileStorage
* @see UncivGame.Current.settings.multiplayerServer
*/
class OnlineMultiplayer(var fileStorageIdentifier: String? = null) {
val fileStorage: IFileStorage
init {
if (fileStorageIdentifier == null)
fileStorageIdentifier = UncivGame.Current.settings.multiplayerServer
fileStorage = if (fileStorageIdentifier == Constants.dropboxMultiplayerServer)
DropBox
else UncivServerFileStorage(fileStorageIdentifier!!)
}
fun tryUploadGame(gameInfo: GameInfo, withPreview: Boolean) {
// We upload the gamePreview before we upload the game as this
// seems to be necessary for the kick functionality
if (withPreview) {
tryUploadGamePreview(gameInfo.asPreview())
}
val zippedGameInfo = GameSaver.gameInfoToString(gameInfo, forceZip = true)
fileStorage.saveFileData(gameInfo.gameId, zippedGameInfo, true)
}
/**
* Used to upload only the preview of a game. If the preview is uploaded together with (before/after)
* the gameInfo, it is recommended to use tryUploadGame(gameInfo, withPreview = true)
* @see tryUploadGame
* @see GameInfo.asPreview
*/
fun tryUploadGamePreview(gameInfo: GameInfoPreview) {
val zippedGameInfo = GameSaver.gameInfoToString(gameInfo)
fileStorage.saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo, true)
}
fun tryDownloadGame(gameId: String): GameInfo {
val zippedGameInfo = fileStorage.loadFileData(gameId)
return GameSaver.gameInfoFromString(zippedGameInfo)
}
fun tryDownloadGamePreview(gameId: String): GameInfoPreview {
val zippedGameInfo = fileStorage.loadFileData("${gameId}_Preview")
return GameSaver.gameInfoPreviewFromString(zippedGameInfo)
}
}

View File

@ -1,4 +1,4 @@
package com.unciv.logic.multiplayer package com.unciv.logic.multiplayer.storage
import com.unciv.json.json import com.unciv.json.json
import com.unciv.ui.utils.UncivDateFormat.parseDate import com.unciv.ui.utils.UncivDateFormat.parseDate
@ -11,7 +11,7 @@ import kotlin.collections.ArrayList
import kotlin.concurrent.timer import kotlin.concurrent.timer
object DropBox: IFileStorage { object DropBox: FileStorage {
private var remainingRateLimitSeconds = 0 private var remainingRateLimitSeconds = 0
private var rateLimitTimer: Timer? = null private var rateLimitTimer: Timer? = null
@ -76,7 +76,7 @@ object DropBox: IFileStorage {
) )
} }
override fun getFileMetaData(fileName: String): IFileMetaData { override fun getFileMetaData(fileName: String): FileMetaData {
val stream = dropboxApi( val stream = dropboxApi(
url="https://api.dropboxapi.com/2/files/get_metadata", url="https://api.dropboxapi.com/2/files/get_metadata",
data="{\"path\":\"${getLocalGameLocation(fileName)}\"}", data="{\"path\":\"${getLocalGameLocation(fileName)}\"}",
@ -124,8 +124,8 @@ object DropBox: IFileStorage {
throw FileStorageRateLimitReached(remainingRateLimitSeconds) throw FileStorageRateLimitReached(remainingRateLimitSeconds)
} }
fun getFolderList(folder: String): ArrayList<IFileMetaData> { fun getFolderList(folder: String): ArrayList<FileMetaData> {
val folderList = ArrayList<IFileMetaData>() val folderList = ArrayList<FileMetaData>()
// The DropBox API returns only partial file listings from one request. list_folder and // The DropBox API returns only partial file listings from one request. list_folder and
// list_folder/continue return similar responses, but list_folder/continue requires a cursor // list_folder/continue return similar responses, but list_folder/continue requires a cursor
// instead of the path. // instead of the path.
@ -168,7 +168,7 @@ object DropBox: IFileStorage {
} }
@Suppress("PropertyName") @Suppress("PropertyName")
private class MetaData: IFileMetaData { private class MetaData: FileMetaData {
var name = "" var name = ""
private var server_modified = "" private var server_modified = ""

View File

@ -0,0 +1,34 @@
package com.unciv.logic.multiplayer.storage
import java.io.FileNotFoundException
import java.util.*
class FileStorageConflictException : Exception()
class FileStorageRateLimitReached(val limitRemainingSeconds: Int) : Exception()
interface FileMetaData {
fun getLastModified(): Date?
}
interface FileStorage {
/**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws FileStorageConflictException if the file already exists and [overwrite] is false
*/
fun saveFileData(fileName: String, data: String, overwrite: Boolean)
/**
* @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
*/
fun loadFileData(fileName: String): String
/**
* @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
*/
fun getFileMetaData(fileName: String): FileMetaData
/**
* @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
*/
fun deleteFile(fileName: String)
}

View File

@ -0,0 +1,58 @@
package com.unciv.logic.multiplayer.storage
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.GameInfoPreview
import com.unciv.logic.GameSaver
/**
* Allows access to games stored on a server for multiplayer purposes.
* Defaults to using UncivGame.Current.settings.multiplayerServer if fileStorageIdentifier is not given.
*
* @param fileStorageIdentifier must be given if UncivGame.Current might not be initialized
* @see FileStorage
* @see UncivGame.Current.settings.multiplayerServer
*/
@Suppress("RedundantSuspendModifier") // Methods can take a long time, so force users to use them in a coroutine to not get ANRs on Android
class OnlineMultiplayerGameSaver(
private var fileStorageIdentifier: String? = null
) {
fun fileStorage(): FileStorage {
val identifier = if (fileStorageIdentifier == null) UncivGame.Current.settings.multiplayerServer else fileStorageIdentifier
return if (identifier == Constants.dropboxMultiplayerServer) DropBox else UncivServerFileStorage(identifier!!)
}
suspend fun tryUploadGame(gameInfo: GameInfo, withPreview: Boolean) {
// We upload the gamePreview before we upload the game as this
// seems to be necessary for the kick functionality
if (withPreview) {
tryUploadGamePreview(gameInfo.asPreview())
}
val zippedGameInfo = GameSaver.gameInfoToString(gameInfo, forceZip = true)
fileStorage().saveFileData(gameInfo.gameId, zippedGameInfo, true)
}
/**
* Used to upload only the preview of a game. If the preview is uploaded together with (before/after)
* the gameInfo, it is recommended to use tryUploadGame(gameInfo, withPreview = true)
* @see tryUploadGame
* @see GameInfo.asPreview
*/
suspend fun tryUploadGamePreview(gameInfo: GameInfoPreview) {
val zippedGameInfo = GameSaver.gameInfoToString(gameInfo)
fileStorage().saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo, true)
}
suspend fun tryDownloadGame(gameId: String): GameInfo {
val zippedGameInfo = fileStorage().loadFileData(gameId)
return GameSaver.gameInfoFromString(zippedGameInfo)
}
suspend fun tryDownloadGamePreview(gameId: String): GameInfoPreview {
val zippedGameInfo = fileStorage().loadFileData("${gameId}_Preview")
return GameSaver.gameInfoPreviewFromString(zippedGameInfo)
}
}

View File

@ -1,4 +1,4 @@
package com.unciv.logic.multiplayer package com.unciv.logic.multiplayer.storage
import com.unciv.json.json import com.unciv.json.json
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
@ -49,7 +49,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) {
// We have to check if the lock file already exists before we try to upload a new // We have to check if the lock file already exists before we try to upload a new
// lock file to not overuse the dropbox file upload limit else it will return an error // lock file to not overuse the dropbox file upload limit else it will return an error
try { try {
val metaData = OnlineMultiplayer().fileStorage.getFileMetaData(fileName) val metaData = OnlineMultiplayerGameSaver().fileStorage().getFileMetaData(fileName)
val date = metaData.getLastModified() val date = metaData.getLastModified()
// 30 seconds should be more than sufficient for everything lock related // 30 seconds should be more than sufficient for everything lock related
@ -57,7 +57,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) {
if (date != null && System.currentTimeMillis() - date.time < 30000) { if (date != null && System.currentTimeMillis() - date.time < 30000) {
return locked return locked
} else { } else {
OnlineMultiplayer().fileStorage.deleteFile(fileName) OnlineMultiplayerGameSaver().fileStorage().deleteFile(fileName)
} }
} catch (ex: FileNotFoundException) { } catch (ex: FileNotFoundException) {
// Catching this exception means no lock file is present // Catching this exception means no lock file is present
@ -65,7 +65,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) {
} }
try { try {
OnlineMultiplayer().fileStorage.saveFileData(fileName, Gzip.zip(json().toJson(LockFile())), false) OnlineMultiplayerGameSaver().fileStorage().saveFileData(fileName, Gzip.zip(json().toJson(LockFile())), false)
} catch (ex: FileStorageConflictException) { } catch (ex: FileStorageConflictException) {
return locked return locked
} }
@ -116,7 +116,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) {
if (!locked) if (!locked)
return return
OnlineMultiplayer().fileStorage.deleteFile("${gameInfo.gameId}_Lock") OnlineMultiplayerGameSaver().fileStorage().deleteFile("${gameInfo.gameId}_Lock")
locked = false locked = false
} }

View File

@ -1,4 +1,4 @@
package com.unciv.logic.multiplayer package com.unciv.logic.multiplayer.storage
import com.badlogic.gdx.Net import com.badlogic.gdx.Net
import com.unciv.UncivGame import com.unciv.UncivGame
@ -9,11 +9,11 @@ import java.net.*
import java.nio.charset.Charset import java.nio.charset.Charset
object SimpleHttp { object SimpleHttp {
fun sendGetRequest(url: String, action: (success: Boolean, result: String)->Unit) { fun sendGetRequest(url: String, action: (success: Boolean, result: String, code: Int?)->Unit) {
sendRequest(Net.HttpMethods.GET, url, "", action) sendRequest(Net.HttpMethods.GET, url, "", action)
} }
fun sendRequest(method: String, url: String, content: String, action: (success: Boolean, result: String)->Unit) { fun sendRequest(method: String, url: String, content: String, action: (success: Boolean, result: String, code: Int?)->Unit) {
var uri = URI(url) var uri = URI(url)
if (uri.host == null) uri = URI("http://$url") if (uri.host == null) uri = URI("http://$url")
@ -21,7 +21,7 @@ object SimpleHttp {
try { try {
urlObj = uri.toURL() urlObj = uri.toURL()
} catch (t:Throwable){ } catch (t:Throwable){
action(false, "Bad URL") action(false, "Bad URL", null)
return return
} }
@ -43,14 +43,14 @@ object SimpleHttp {
} }
val text = BufferedReader(InputStreamReader(inputStream)).readText() val text = BufferedReader(InputStreamReader(inputStream)).readText()
action(true, text) action(true, text, responseCode)
} catch (t: Throwable) { } catch (t: Throwable) {
println(t.message) println(t.message)
val errorMessageToReturn = val errorMessageToReturn =
if (errorStream != null) BufferedReader(InputStreamReader(errorStream)).readText() if (errorStream != null) BufferedReader(InputStreamReader(errorStream)).readText()
else t.message!! else t.message!!
println(errorMessageToReturn) println(errorMessageToReturn)
action(false, errorMessageToReturn) action(false, errorMessageToReturn, if (errorStream != null) responseCode else null)
} }
} }
} }

View File

@ -0,0 +1,51 @@
package com.unciv.logic.multiplayer.storage
import com.badlogic.gdx.Net
import java.io.FileNotFoundException
import java.lang.Exception
class UncivServerFileStorage(val serverUrl:String): FileStorage {
override fun saveFileData(fileName: String, data: String, overwrite: Boolean) {
SimpleHttp.sendRequest(Net.HttpMethods.PUT, "$serverUrl/files/$fileName", data) {
success, result, code ->
if (!success) {
println(result)
throw Exception(result)
}
}
}
override fun loadFileData(fileName: String): String {
var fileData = ""
SimpleHttp.sendGetRequest("$serverUrl/files/$fileName"){
success, result, code ->
if (!success) {
println(result)
when (code) {
404 -> throw FileNotFoundException(result)
else -> throw Exception(result)
}
}
else fileData = result
}
return fileData
}
override fun getFileMetaData(fileName: String): FileMetaData {
TODO("Not yet implemented")
}
override fun deleteFile(fileName: String) {
SimpleHttp.sendRequest(Net.HttpMethods.DELETE, "$serverUrl/files/$fileName", "") {
success, result, code ->
if (!success) {
when (code) {
404 -> throw FileNotFoundException(result)
else -> throw Exception(result)
}
}
}
}
}

View File

@ -5,7 +5,9 @@ import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.GameSetupInfo
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.math.max import kotlin.math.max
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@ -40,12 +42,12 @@ class Simulation(
} }
} }
fun start() { fun start() = runBlocking {
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
val threads: ArrayList<Thread> = ArrayList() val jobs: ArrayList<Job> = ArrayList()
for (threadId in 1..threadsNumber) { for (threadId in 1..threadsNumber) {
threads.add(crashHandlingThread { jobs.add(launchCrashHandling("simulation-${threadId}") {
for (i in 1..simulationsPerThread) { for (i in 1..simulationsPerThread) {
val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo)) val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo))
gameInfo.simulateMaxTurns = maxTurns gameInfo.simulateMaxTurns = maxTurns
@ -66,8 +68,8 @@ class Simulation(
} }
}) })
} }
// wait for all threads to finish // wait for all to finish
for (thread in threads) thread.join() for (job in jobs) job.join()
endTime = System.currentTimeMillis() endTime = System.currentTimeMillis()
} }

View File

@ -6,7 +6,7 @@ import com.badlogic.gdx.audio.Music
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.logic.multiplayer.DropBox import com.unciv.logic.multiplayer.storage.DropBox
import java.util.* import java.util.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.concurrent.timer import kotlin.concurrent.timer

View File

@ -6,7 +6,8 @@ import com.badlogic.gdx.audio.Sound
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.models.UncivSound import com.unciv.models.UncivSound
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import kotlinx.coroutines.delay
import java.io.File import java.io.File
/* /*
@ -164,10 +165,10 @@ object Sounds {
val initialDelay = if (isFresh && Gdx.app.type == Application.ApplicationType.Android) 40 else 0 val initialDelay = if (isFresh && Gdx.app.type == Application.ApplicationType.Android) 40 else 0
if (initialDelay > 0 || resource.play(volume) == -1L) { if (initialDelay > 0 || resource.play(volume) == -1L) {
crashHandlingThread(name = "DelayedSound") { launchCrashHandling("DelayedSound") {
Thread.sleep(initialDelay.toLong()) delay(initialDelay.toLong())
while (resource.play(volume) == -1L) { while (resource.play(volume) == -1L) {
Thread.sleep(20L) delay(20L)
} }
} }
} }

View File

@ -17,7 +17,7 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.audio.Sounds import com.unciv.ui.audio.Sounds
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.Popup import com.unciv.ui.popup.Popup
@ -207,7 +207,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
availableConstructionsTable.add("Loading...".toLabel()).pad(10f) availableConstructionsTable.add("Loading...".toLabel()).pad(10f)
} }
crashHandlingThread(name = "Construction info gathering - ${cityScreen.city.name}") { launchCrashHandling("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. // 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() val constructionButtonDTOList = getConstructionButtonDTOs()
postCrashHandlingRunnable { postCrashHandlingRunnable {

View File

@ -2,10 +2,45 @@ package com.unciv.ui.crashhandling
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.unciv.ui.utils.wrapCrashHandlingUnit import com.unciv.ui.utils.wrapCrashHandlingUnit
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import kotlin.concurrent.thread 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. */ /** Wrapped version of [kotlin.concurrent.thread], that brings the main game loop to a [com.unciv.CrashScreen] if an exception happens. */
fun crashHandlingThread( private fun crashHandlingThread(
start: Boolean = true, start: Boolean = true,
isDaemon: Boolean = false, isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null, contextClassLoader: ClassLoader? = null,
@ -25,3 +60,24 @@ fun crashHandlingThread(
fun postCrashHandlingRunnable(runnable: () -> Unit) { fun postCrashHandlingRunnable(runnable: () -> Unit) {
Gdx.app.postRunnable(runnable.wrapCrashHandlingUnit()) 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) }
}
/**
* Uses [async] to return a result from a new coroutine that brings the game loop to a [com.unciv.CrashScreen] if an exception occurs.
* @see crashHandlingThread
*/
fun <T> asyncCrashHandling(name: String, runAsDaemon: Boolean = true,
flowBlock: suspend CoroutineScope.() -> T): Deferred<T> {
return getCoroutineContext(runAsDaemon).async(CoroutineName(name)) { flowBlock(this) }
}
private fun getCoroutineContext(runAsDaemon: Boolean): CoroutineScope {
return if (runAsDaemon) CRASH_HANDLING_DAEMON_SCOPE else CRASH_HANDLING_SCOPE
}

View File

@ -5,12 +5,12 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameInfoPreview
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.crashHandlingThread
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.popup.Popup import com.unciv.ui.popup.Popup
import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.popup.YesNoPopup
@ -83,10 +83,10 @@ class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: St
popup.addGoodSizedLabel("Working...").row() popup.addGoodSizedLabel("Working...").row()
popup.open() popup.open()
crashHandlingThread { launchCrashHandling("Resign", runAsDaemon = false) {
try { try {
//download to work with newest game state //download to work with newest game state
val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId) val gameInfo = OnlineMultiplayerGameSaver().tryDownloadGame(gameId)
val playerCiv = gameInfo.currentPlayerCiv val playerCiv = gameInfo.currentPlayerCiv
//only give up if it's the users turn //only give up if it's the users turn
@ -106,9 +106,9 @@ class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: St
} }
//save game so multiplayer list stays up to date but do not override multiplayer settings //save game so multiplayer list stays up to date but do not override multiplayer settings
val updatedSave = this.gameInfo!!.updateCurrentTurn(gameInfo) val updatedSave = this@EditMultiplayerGameInfoScreen.gameInfo!!.updateCurrentTurn(gameInfo)
GameSaver.saveGame(updatedSave, gameName) GameSaver.saveGame(updatedSave, gameName)
OnlineMultiplayer().tryUploadGame(gameInfo, withPreview = true) OnlineMultiplayerGameSaver().tryUploadGame(gameInfo, withPreview = true)
postCrashHandlingRunnable { postCrashHandlingRunnable {
popup.close() popup.close()

View File

@ -0,0 +1,14 @@
package com.unciv.ui.multiplayer
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.center
import com.unciv.ui.utils.toLabel
class LoadDeepLinkScreen : BaseScreen() {
init {
val loadingLabel = "Loading...".toLabel()
stage.addActor(loadingLabel)
loadingLabel.center(stage)
}
}

View File

@ -4,12 +4,12 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.scenes.scene2d.ui.* import com.badlogic.gdx.scenes.scene2d.ui.*
import com.unciv.logic.* import com.unciv.logic.*
import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.crashHandlingThread
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.Popup import com.unciv.ui.popup.Popup
@ -146,11 +146,10 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
addGameButton.setText("Working...".tr()) addGameButton.setText("Working...".tr())
addGameButton.disable() addGameButton.disable()
crashHandlingThread(name = "MultiplayerDownload") {
launchCrashHandling("MultiplayerDownload", runAsDaemon = false) {
try { try {
// The tryDownload can take more than 500ms. Therefore, to avoid ANRs, val gamePreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameId.trim())
// we need to run it in a different thread.
val gamePreview = OnlineMultiplayer().tryDownloadGamePreview(gameId.trim())
if (gameName == "") if (gameName == "")
GameSaver.saveGame(gamePreview, gamePreview.gameId) GameSaver.saveGame(gamePreview, gamePreview.gameId)
else else
@ -160,7 +159,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
} catch (ex: FileNotFoundException) { } catch (ex: FileNotFoundException) {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
try { try {
val gamePreview = OnlineMultiplayer().tryDownloadGame(gameId.trim()).asPreview() val gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId.trim()).asPreview()
if (gameName == "") if (gameName == "")
GameSaver.saveGame(gamePreview, gamePreview.gameId) GameSaver.saveGame(gamePreview, gamePreview.gameId)
else else
@ -172,13 +171,13 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
popup.reuseWith("Could not download game!", true) popup.reuseWith("Could not download game!", true)
} }
} }
} catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable {
popup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true)
}
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
popup.reuseWith("Could not download game!", true) val message = when (ex) {
is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds"
else -> "Could not download game!"
}
popup.reuseWith(message, true)
} }
} }
postCrashHandlingRunnable { postCrashHandlingRunnable {
@ -194,18 +193,18 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
loadingGamePopup.add("Loading latest game state...".tr()) loadingGamePopup.add("Loading latest game state...".tr())
loadingGamePopup.open() loadingGamePopup.open()
crashHandlingThread(name = "JoinMultiplayerGame") { launchCrashHandling("JoinMultiplayerGame") {
try { try {
val gameId = multiplayerGames[selectedGameFile]!!.gameId val gameId = multiplayerGames[selectedGameFile]!!.gameId
val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId) val gameInfo = OnlineMultiplayerGameSaver().tryDownloadGame(gameId)
postCrashHandlingRunnable { game.loadGame(gameInfo) } postCrashHandlingRunnable { game.loadGame(gameInfo) }
} catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable {
loadingGamePopup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true)
}
} catch (ex: Exception) { } catch (ex: Exception) {
val message = when (ex) {
is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds"
else -> "Could not download game!"
}
postCrashHandlingRunnable { postCrashHandlingRunnable {
loadingGamePopup.reuseWith("Could not download game!", true) loadingGamePopup.reuseWith(message, true)
} }
} }
} }
@ -280,7 +279,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
continue continue
} }
crashHandlingThread(name = "loadGameFile") { launchCrashHandling("loadGameFile") {
try { try {
val game = gameSaver.loadGamePreviewFromFile(gameSaveFile) val game = gameSaver.loadGamePreviewFromFile(gameSaveFile)
@ -301,7 +300,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
} catch (usx: UncivShowableException) { } catch (usx: UncivShowableException) {
//Gets thrown when mods are not installed //Gets thrown when mods are not installed
postCrashHandlingRunnable { postCrashHandlingRunnable {
val popup = Popup(this) val popup = Popup(this@MultiplayerScreen)
popup.addGoodSizedLabel(usx.message!! + " in ${gameSaveFile.name()}").row() popup.addGoodSizedLabel(usx.message!! + " in ${gameSaveFile.name()}").row()
popup.addCloseButton() popup.addCloseButton()
popup.open(true) popup.open(true)
@ -311,7 +310,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
} }
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Could not refresh!", this) ToastPopup("Could not refresh!", this@MultiplayerScreen)
turnIndicator.clear() turnIndicator.clear()
turnIndicator.add(ImageGetter.getImage("StatIcons/Malcontent")).size(50f) turnIndicator.add(ImageGetter.getImage("StatIcons/Malcontent")).size(50f)
} }
@ -330,12 +329,11 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
refreshButton.setText("Working...".tr()) refreshButton.setText("Working...".tr())
refreshButton.disable() refreshButton.disable()
//One thread for all downloads launchCrashHandling("multiplayerGameDownload") {
crashHandlingThread(name = "multiplayerGameDownload") {
for ((fileHandle, gameInfo) in multiplayerGames) { for ((fileHandle, gameInfo) in multiplayerGames) {
try { try {
// Update game without overriding multiplayer settings // Update game without overriding multiplayer settings
val game = gameInfo.updateCurrentTurn(OnlineMultiplayer().tryDownloadGamePreview(gameInfo.gameId)) val game = gameInfo.updateCurrentTurn(OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameInfo.gameId))
GameSaver.saveGame(game, fileHandle.name()) GameSaver.saveGame(game, fileHandle.name())
multiplayerGames[fileHandle] = game multiplayerGames[fileHandle] = game
@ -343,25 +341,25 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
try { try {
// Update game without overriding multiplayer settings // Update game without overriding multiplayer settings
val game = gameInfo.updateCurrentTurn(OnlineMultiplayer().tryDownloadGame(gameInfo.gameId)) val game = gameInfo.updateCurrentTurn(OnlineMultiplayerGameSaver().tryDownloadGame(gameInfo.gameId))
GameSaver.saveGame(game, fileHandle.name()) GameSaver.saveGame(game, fileHandle.name())
multiplayerGames[fileHandle] = game multiplayerGames[fileHandle] = game
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Could not download game!" + " ${fileHandle.name()}", this) ToastPopup("Could not download game!" + " ${fileHandle.name()}", this@MultiplayerScreen)
} }
} }
} catch (ex: FileStorageRateLimitReached) { } catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", this) ToastPopup("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", this@MultiplayerScreen)
} }
break // No need to keep trying if rate limit is reached break // No need to keep trying if rate limit is reached
} catch (ex: Exception) { } catch (ex: Exception) {
//skipping one is not fatal //skipping one is not fatal
//Trying to use as many prev. used strings as possible //Trying to use as many prev. used strings as possible
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Could not download game!" + " ${fileHandle.name()}", this) ToastPopup("Could not download game!" + " ${fileHandle.name()}", this@MultiplayerScreen)
} }
} }
} }

View File

@ -11,12 +11,12 @@ import com.unciv.UncivGame
import com.unciv.logic.* import com.unciv.logic.*
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.MapType import com.unciv.logic.map.MapType
import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.pickerscreens.PickerScreen
@ -160,9 +160,9 @@ class NewGameScreen(
rightSideButton.disable() rightSideButton.disable()
rightSideButton.setText("Working...".tr()) rightSideButton.setText("Working...".tr())
crashHandlingThread(name = "NewGame") {
// Creating a new game can take a while and we don't want ANRs // Creating a new game can take a while and we don't want ANRs
newGameThread() launchCrashHandling("NewGame", runAsDaemon = false) {
startNewGame()
} }
} }
} }
@ -226,7 +226,7 @@ class NewGameScreen(
} }
} }
private fun newGameThread() { suspend private fun startNewGame() {
val popup = Popup(this) val popup = Popup(this)
postCrashHandlingRunnable { postCrashHandlingRunnable {
popup.addGoodSizedLabel("Working...").row() popup.addGoodSizedLabel("Working...").row()
@ -255,7 +255,7 @@ class NewGameScreen(
if (gameSetupInfo.gameParameters.isOnlineMultiplayer) { 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! 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 { try {
OnlineMultiplayer().tryUploadGame(newGame, withPreview = true) OnlineMultiplayerGameSaver().tryUploadGame(newGame, withPreview = true)
GameSaver.autoSave(newGame) GameSaver.autoSave(newGame)

View File

@ -13,7 +13,7 @@ import com.unciv.models.ruleset.ModOptions
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
@ -23,6 +23,8 @@ import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.popup.YesNoPopup
import com.unciv.ui.utils.UncivDateFormat.formatDate import com.unciv.ui.utils.UncivDateFormat.formatDate
import com.unciv.ui.utils.UncivDateFormat.parseDate import com.unciv.ui.utils.UncivDateFormat.parseDate
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.math.max import kotlin.math.max
@ -67,11 +69,11 @@ class ModManagementScreen(
private var onlineScrollCurrentY = -1f private var onlineScrollCurrentY = -1f
// cleanup - background processing needs to be stopped on exit and memory freed // cleanup - background processing needs to be stopped on exit and memory freed
private var runningSearchThread: Thread? = null private var runningSearchJob: Job? = null
private var stopBackgroundTasks = false private var stopBackgroundTasks = false
override fun dispose() { override fun dispose() {
// make sure the worker threads will not continue trying their time-intensive job // make sure the worker threads will not continue trying their time-intensive job
runningSearchThread?.interrupt() runningSearchJob?.cancel()
stopBackgroundTasks = true stopBackgroundTasks = true
super.dispose() super.dispose()
} }
@ -189,20 +191,24 @@ class ModManagementScreen(
* calls itself for the next page of search results * calls itself for the next page of search results
*/ */
private fun tryDownloadPage(pageNum: Int) { private fun tryDownloadPage(pageNum: Int) {
runningSearchThread = crashHandlingThread(name="GitHubSearch") { runningSearchJob = launchCrashHandling("GitHubSearch") {
val repoSearch: Github.RepoSearch val repoSearch: Github.RepoSearch
try { try {
repoSearch = Github.tryGetGithubReposWithTopic(amountPerPage, pageNum)!! repoSearch = Github.tryGetGithubReposWithTopic(amountPerPage, pageNum)!!
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Could not download mod list", this) ToastPopup("Could not download mod list", this@ModManagementScreen)
} }
runningSearchThread = null runningSearchJob = null
return@crashHandlingThread return@launchCrashHandling
}
if (!isActive) {
return@launchCrashHandling
} }
postCrashHandlingRunnable { addModInfoFromRepoSearch(repoSearch, pageNum) } postCrashHandlingRunnable { addModInfoFromRepoSearch(repoSearch, pageNum) }
runningSearchThread = null runningSearchJob = null
} }
} }
@ -389,14 +395,14 @@ class ModManagementScreen(
/** Download and install a mod in the background, called both from the right-bottom button and the URL entry popup */ /** 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 = {}) { private fun downloadMod(repo: Github.Repo, postAction: () -> Unit = {}) {
crashHandlingThread(name="DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions launchCrashHandling("DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions
try { try {
val modFolder = Github.downloadAndExtract(repo.html_url, repo.default_branch, val modFolder = Github.downloadAndExtract(repo.html_url, repo.default_branch,
Gdx.files.local("mods")) Gdx.files.local("mods"))
?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something! ?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something!
Github.rewriteModOptions(repo, modFolder) Github.rewriteModOptions(repo, modFolder)
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("[${repo.name}] Downloaded!", this) ToastPopup("[${repo.name}] Downloaded!", this@ModManagementScreen)
RulesetCache.loadRulesets() RulesetCache.loadRulesets()
RulesetCache[repo.name]?.let { RulesetCache[repo.name]?.let {
installedModInfo[repo.name] = ModUIData(it) installedModInfo[repo.name] = ModUIData(it)
@ -408,7 +414,7 @@ class ModManagementScreen(
} }
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Could not download [${repo.name}]", this) ToastPopup("Could not download [${repo.name}]", this@ModManagementScreen)
postAction() postAction()
} }
} }
@ -538,7 +544,7 @@ class ModManagementScreen(
} }
internal fun refreshOnlineModTable() { internal fun refreshOnlineModTable() {
if (runningSearchThread != null) return // cowardice: prevent concurrent modification, avoid a manager layer if (runningSearchJob != null) return // cowardice: prevent concurrent modification, avoid a manager layer
val newHeaderText = optionsManager.getOnlineHeader() val newHeaderText = optionsManager.getOnlineHeader()
onlineHeaderLabel?.setText(newHeaderText) onlineHeaderLabel?.setText(newHeaderText)

View File

@ -1,9 +1,10 @@
package com.unciv.ui.popup package com.unciv.ui.popup
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.crashhandling.crashHandlingThread
import com.unciv.ui.utils.onClick import com.unciv.ui.utils.onClick
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import kotlinx.coroutines.delay
/** /**
* This is an unobtrusive popup which will close itself after a given amount of time. * This is an unobtrusive popup which will close itself after a given amount of time.
@ -23,9 +24,9 @@ class ToastPopup (message: String, screen: BaseScreen, val time: Long = 2000) :
} }
private fun startTimer(){ private fun startTimer(){
crashHandlingThread(name = "ResponsePopup") { launchCrashHandling("ResponsePopup") {
Thread.sleep(time) delay(time)
postCrashHandlingRunnable { this.close() } postCrashHandlingRunnable { this@ToastPopup.close() }
} }
} }

View File

@ -14,7 +14,7 @@ import com.unciv.logic.MissingModsException
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.pickerscreens.Github import com.unciv.ui.pickerscreens.Github
@ -51,7 +51,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
val loadingPopup = Popup( this) val loadingPopup = Popup( this)
loadingPopup.addGoodSizedLabel("Loading...") loadingPopup.addGoodSizedLabel("Loading...")
loadingPopup.open() loadingPopup.open()
crashHandlingThread(name = "Load Game") { launchCrashHandling("Load Game") {
try { try {
// This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread
val loadedGame = GameSaver.loadGameByName(selectedSave) val loadedGame = GameSaver.loadGameByName(selectedSave)
@ -59,7 +59,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
loadingPopup.close() loadingPopup.close()
val cantLoadGamePopup = Popup(this) val cantLoadGamePopup = Popup(this@LoadGameScreen)
cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row()
if (ex is UncivShowableException && ex.localizedMessage != null) { if (ex is UncivShowableException && ex.localizedMessage != null) {
// thrown exceptions are our own tests and can be shown to the user // thrown exceptions are our own tests and can be shown to the user
@ -155,7 +155,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
private fun loadMissingMods() { private fun loadMissingMods() {
loadMissingModsButton.isEnabled = false loadMissingModsButton.isEnabled = false
descriptionLabel.setText("Loading...".tr()) descriptionLabel.setText("Loading...".tr())
crashHandlingThread(name="DownloadMods") { launchCrashHandling("DownloadMods", runAsDaemon = false) {
try { try {
val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-") val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-")
for (modName in mods) { for (modName in mods) {
@ -175,7 +175,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
missingModsToLoad = "" missingModsToLoad = ""
loadMissingModsButton.isVisible = false loadMissingModsButton.isVisible = false
errorLabel.setText("") errorLabel.setText("")
ToastPopup("Missing mods are downloaded successfully.", this) ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen)
} }
} catch (ex: Exception) { } catch (ex: Exception) {
handleLoadGameException("Could not load the missing mods!", ex) handleLoadGameException("Could not load the missing mods!", ex)
@ -205,8 +205,9 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
loadImage.addAction(Actions.rotateBy(360f, 2f)) loadImage.addAction(Actions.rotateBy(360f, 2f))
saveTable.add(loadImage).size(50f) saveTable.add(loadImage).size(50f)
crashHandlingThread { // Apparently, even jut getting the list of saves can cause ANRs - // 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 // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times
launchCrashHandling("GetSaves") {
// .toList() because otherwise the lastModified will only be checked inside the postRunnable // .toList() because otherwise the lastModified will only be checked inside the postRunnable
val saves = GameSaver.getSaves().sortedByDescending { it.lastModified() }.toList() val saves = GameSaver.getSaves().sortedByDescending { it.lastModified() }.toList()
@ -235,7 +236,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
val savedAt = Date(save.lastModified()) val savedAt = Date(save.lastModified())
var textToSet = save.name() + "\n${"Saved at".tr()}: " + savedAt.formatDate() var textToSet = save.name() + "\n${"Saved at".tr()}: " + savedAt.formatDate()
crashHandlingThread { // Even loading the game to get its metadata can take a long time on older phones launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones
try { try {
val game = GameSaver.loadGamePreviewFromFile(save) val game = GameSaver.loadGamePreviewFromFile(save)
val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() }

View File

@ -9,7 +9,7 @@ import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.ToastPopup
@ -60,7 +60,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
errorLabel.setText("") errorLabel.setText("")
saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.setText("Saving...".tr())
saveToCustomLocation.disable() saveToCustomLocation.disable()
crashHandlingThread(name = "SaveGame") { launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> GameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e ->
if (e == null) { if (e == null) {
postCrashHandlingRunnable { game.setWorldScreen() } postCrashHandlingRunnable { game.setWorldScreen() }
@ -97,10 +97,10 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
private fun saveGame() { private fun saveGame() {
rightSideButton.setText("Saving...".tr()) rightSideButton.setText("Saving...".tr())
crashHandlingThread(name = "SaveGame") { launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGame(gameInfo, gameNameTextField.text) { GameSaver.saveGame(gameInfo, gameNameTextField.text) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
if (it != null) ToastPopup("Could not save game!", this) if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen)
else UncivGame.Current.setWorldScreen() else UncivGame.Current.setWorldScreen()
} }
} }

View File

@ -124,7 +124,7 @@ abstract class BaseScreen : Screen {
/** @return `true` if the screen is narrower than 4:3 landscape */ /** @return `true` if the screen is narrower than 4:3 landscape */
fun isNarrowerThan4to3() = stage.viewport.screenHeight * 4 > stage.viewport.screenWidth * 3 fun isNarrowerThan4to3() = stage.viewport.screenHeight * 4 > stage.viewport.screenWidth * 3
fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage) { fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, onClose: () -> Unit = {}) {
OptionsPopup(this, startingPage).open(force = true) OptionsPopup(this, startingPage, onClose).open(force = true)
} }
} }

View File

@ -13,7 +13,7 @@ import com.unciv.UncivGame
import com.unciv.models.UncivSound import com.unciv.models.UncivSound
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.audio.Sounds import com.unciv.ui.audio.Sounds
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.IconCircleGroup
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -67,7 +67,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) { fun Actor.onClickEvent(sound: UncivSound = UncivSound.Click, function: (event: InputEvent?, x: Float, y: Float) -> Unit) {
this.addListener(object : ClickListener() { this.addListener(object : ClickListener() {
override fun clicked(event: InputEvent?, x: Float, y: Float) { override fun clicked(event: InputEvent?, x: Float, y: Float) {
crashHandlingThread(name = "Sound") { Sounds.play(sound) } launchCrashHandling("Sound") { Sounds.play(sound) }
function(event, x, y) function(event, x, y)
} }
}) })

View File

@ -26,7 +26,7 @@ import com.unciv.models.*
import com.unciv.models.helpers.MapArrowType import com.unciv.models.helpers.MapArrowType
import com.unciv.models.helpers.MiscArrowTypes import com.unciv.models.helpers.MiscArrowTypes
import com.unciv.ui.audio.Sounds import com.unciv.ui.audio.Sounds
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.map.TileGroupMap import com.unciv.ui.map.TileGroupMap
@ -115,7 +115,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
override fun clicked(event: InputEvent?, x: Float, y: Float) { override fun clicked(event: InputEvent?, x: Float, y: Float) {
val unit = worldScreen.bottomUnitTable.selectedUnit val unit = worldScreen.bottomUnitTable.selectedUnit
?: return ?: return
crashHandlingThread { launchCrashHandling("WorldScreenClick") {
val tile = tileGroup.tileInfo val tile = tileGroup.tileInfo
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) { if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
@ -123,7 +123,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
swapMoveUnitToTargetTile(unit, tile) swapMoveUnitToTargetTile(unit, tile)
} }
// If we are in unit-swapping mode, we don't want to move or attack // If we are in unit-swapping mode, we don't want to move or attack
return@crashHandlingThread return@launchCrashHandling
} }
val attackableTile = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles()) val attackableTile = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
@ -131,13 +131,13 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
if (unit.canAttack() && attackableTile != null) { if (unit.canAttack() && attackableTile != null) {
Battle.moveAndAttack(MapUnitCombatant(unit), attackableTile) Battle.moveAndAttack(MapUnitCombatant(unit), attackableTile)
worldScreen.shouldUpdate = true worldScreen.shouldUpdate = true
return@crashHandlingThread return@launchCrashHandling
} }
val canUnitReachTile = unit.movement.canReach(tile) val canUnitReachTile = unit.movement.canReach(tile)
if (canUnitReachTile) { if (canUnitReachTile) {
moveUnitToTargetTile(listOf(unit), tile) moveUnitToTargetTile(listOf(unit), tile)
return@crashHandlingThread return@launchCrashHandling
} }
} }
} }
@ -214,7 +214,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
val selectedUnit = selectedUnits.first() val selectedUnit = selectedUnits.first()
crashHandlingThread(name = "TileToMoveTo") { launchCrashHandling("TileToMoveTo") {
// these are the heavy parts, finding where we want to go // these are the heavy parts, finding where we want to go
// Since this runs in a different thread, even if we check movement.canReach() // 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 // then it might change until we get to the getTileToMoveTo, so we just try/catch it
@ -224,7 +224,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
} catch (ex: Exception) { } catch (ex: Exception) {
println("Exception in getTileToMoveToThisTurn: ${ex.message}") println("Exception in getTileToMoveToThisTurn: ${ex.message}")
ex.printStackTrace() ex.printStackTrace()
return@crashHandlingThread return@launchCrashHandling
} // can't move here } // can't move here
postCrashHandlingRunnable { postCrashHandlingRunnable {
@ -270,7 +270,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
} }
private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tileInfo: TileInfo) { private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tileInfo: TileInfo) {
crashHandlingThread(name = "TurnsToGetThere") { launchCrashHandling("TurnsToGetThere") {
/** LibGdx sometimes has these weird errors when you try to edit the UI layout from 2 separate threads. /** 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. * 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, * The only "heavy lifting" that needs to be done is getting the turns to get there,

View File

@ -1,5 +1,7 @@
package com.unciv.ui.worldscreen package com.unciv.ui.worldscreen
import com.unciv.ui.worldscreen.status.NextTurnAction
import com.unciv.ui.worldscreen.status.NextTurnButton
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
@ -21,7 +23,6 @@ import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.ReligionState
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.map.MapVisualization import com.unciv.logic.map.MapVisualization
import com.unciv.logic.multiplayer.FileStorageRateLimitReached
import com.unciv.logic.trade.TradeEvaluation import com.unciv.logic.trade.TradeEvaluation
import com.unciv.models.Tutorial import com.unciv.models.Tutorial
import com.unciv.models.UncivSound import com.unciv.models.UncivSound
@ -40,8 +41,10 @@ import com.unciv.ui.utils.UncivDateFormat.formatDate
import com.unciv.ui.victoryscreen.VictoryScreen import com.unciv.ui.victoryscreen.VictoryScreen
import com.unciv.ui.worldscreen.bottombar.BattleTable import com.unciv.ui.worldscreen.bottombar.BattleTable
import com.unciv.ui.worldscreen.bottombar.TileInfoTable import com.unciv.ui.worldscreen.bottombar.TileInfoTable
import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
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.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.ExitGamePopup import com.unciv.ui.popup.ExitGamePopup
@ -51,6 +54,11 @@ import com.unciv.ui.popup.hasOpenPopups
import com.unciv.ui.worldscreen.minimap.MinimapHolder import com.unciv.ui.worldscreen.minimap.MinimapHolder
import com.unciv.ui.worldscreen.unit.UnitActionsTable import com.unciv.ui.worldscreen.unit.UnitActionsTable
import com.unciv.ui.worldscreen.unit.UnitTable import com.unciv.ui.worldscreen.unit.UnitTable
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import java.util.* import java.util.*
import kotlin.concurrent.timer import kotlin.concurrent.timer
@ -90,8 +98,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
private val techButtonHolder = Table() private val techButtonHolder = Table()
private val diplomacyButtonHolder = Table() private val diplomacyButtonHolder = Table()
private val fogOfWarButton = createFogOfWarButton() private val fogOfWarButton = createFogOfWarButton()
private val nextTurnButton = createNextTurnButton() private val nextTurnButton = NextTurnButton(keyPressDispatcher)
private var nextTurnAction: () -> Unit = {}
private val tutorialTaskTable = Table().apply { background = ImageGetter.getBackground( private val tutorialTaskTable = Table().apply { background = ImageGetter.getBackground(
ImageGetter.getBlue().darken(0.5f)) } ImageGetter.getBlue().darken(0.5f)) }
@ -102,8 +109,9 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
/** Switch for console logging of next turn duration */ /** Switch for console logging of next turn duration */
private const val consoleLog = false private const val consoleLog = false
private lateinit var multiPlayerRefresher: Flow<Unit>
// this object must not be created multiple times // this object must not be created multiple times
private var multiPlayerRefresher: Timer? = null private var multiPlayerRefresherJob: Job? = null
} }
init { init {
@ -195,12 +203,14 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// restart the timer // restart the timer
stopMultiPlayerRefresher() stopMultiPlayerRefresher()
// isDaemon = true, in order to not block the app closing multiPlayerRefresher = flow {
// DO NOT use Timer() since this seems to (maybe?) translate to com.badlogic.gdx.utils.Timer? Not sure about this. while (true) {
multiPlayerRefresher = timer("multiPlayerRefresh", true, period = 10000) {
loadLatestMultiplayerState() loadLatestMultiplayerState()
delay(10000)
} }
} }
multiPlayerRefresherJob = multiPlayerRefresher.launchIn(CRASH_HANDLING_DAEMON_SCOPE)
}
// don't run update() directly, because the UncivGame.worldScreen should be set so that the city buttons and tile groups // don't run update() directly, because the UncivGame.worldScreen should be set so that the city buttons and tile groups
// know what the viewing civ is. // know what the viewing civ is.
@ -208,9 +218,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
private fun stopMultiPlayerRefresher() { private fun stopMultiPlayerRefresher() {
if (multiPlayerRefresher != null) { if (multiPlayerRefresherJob != null) {
multiPlayerRefresher?.cancel() multiPlayerRefresherJob?.cancel()
multiPlayerRefresher?.purge()
} }
} }
@ -219,14 +228,14 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// GameSaver.autoSave, SaveGameScreen.saveGame, LoadGameScreen.rightSideButton.onClick,... // GameSaver.autoSave, SaveGameScreen.saveGame, LoadGameScreen.rightSideButton.onClick,...
val quickSave = { val quickSave = {
val toast = ToastPopup("Quicksaving...", this) val toast = ToastPopup("Quicksaving...", this)
crashHandlingThread(name = "SaveGame") { launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGame(gameInfo, "QuickSave") { GameSaver.saveGame(gameInfo, "QuickSave") {
postCrashHandlingRunnable { postCrashHandlingRunnable {
toast.close() toast.close()
if (it != null) if (it != null)
ToastPopup("Could not save game!", this) ToastPopup("Could not save game!", this@WorldScreen)
else { else {
ToastPopup("Quicksave successful.", this) ToastPopup("Quicksave successful.", this@WorldScreen)
} }
} }
} }
@ -235,17 +244,17 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
val quickLoad = { val quickLoad = {
val toast = ToastPopup("Quickloading...", this) val toast = ToastPopup("Quickloading...", this)
crashHandlingThread(name = "SaveGame") { launchCrashHandling("LoadGame") {
try { try {
val loadedGame = GameSaver.loadGameByName("QuickSave") val loadedGame = GameSaver.loadGameByName("QuickSave")
postCrashHandlingRunnable { postCrashHandlingRunnable {
toast.close() toast.close()
UncivGame.Current.loadGame(loadedGame) UncivGame.Current.loadGame(loadedGame)
ToastPopup("Quickload successful.", this) ToastPopup("Quickload successful.", this@WorldScreen)
} }
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
ToastPopup("Could not load game!", this) ToastPopup("Could not load game!", this@WorldScreen)
} }
} }
} }
@ -276,7 +285,12 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
if (!mapHolder.setCenterPosition(capital.location)) if (!mapHolder.setCenterPosition(capital.location))
game.setScreen(CityScreen(capital)) game.setScreen(CityScreen(capital))
} }
keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { this.openOptionsPopup() } // Game Options keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { // Game Options
this.openOptionsPopup(onClose = {
mapHolder.reloadMaxZoom()
nextTurnButton.update(hasOpenPopups(), isPlayersTurn, waitingForAutosave)
})
}
keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save
keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load
keyPressDispatcher[KeyCharAndCode.ctrl('Q')] = { ExitGamePopup(this, true) } // Quit keyPressDispatcher[KeyCharAndCode.ctrl('Q')] = { ExitGamePopup(this, true) } // Quit
@ -339,7 +353,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
private fun loadLatestMultiplayerState() { private suspend fun loadLatestMultiplayerState() {
// Since we're on a background thread, all the UI calls in this func need to run from the // Since we're on a background thread, all the UI calls in this func need to run from the
// main thread which has a GL context // main thread which has a GL context
val loadingGamePopup = Popup(this) val loadingGamePopup = Popup(this)
@ -349,7 +363,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
try { try {
val latestGame = OnlineMultiplayer().tryDownloadGame(gameInfo.gameId) val latestGame = OnlineMultiplayerGameSaver().tryDownloadGame(gameInfo.gameId)
// if we find the current player didn't change, don't update // if we find the current player didn't change, don't update
// Additionally, check if we are the current player, and in that case always stop // Additionally, check if we are the current player, and in that case always stop
@ -381,9 +395,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
val restartAfter : Long = ex.limitRemainingSeconds.toLong() * 1000 val restartAfter : Long = ex.limitRemainingSeconds.toLong() * 1000
timer("RestartTimerTimer", true, restartAfter, 0) { timer("RestartTimerTimer", true, restartAfter, 0) {
multiPlayerRefresher = timer("multiPlayerRefresh", true, period = 10000) { multiPlayerRefresherJob = multiPlayerRefresher.launchIn(CRASH_HANDLING_DAEMON_SCOPE)
loadLatestMultiplayerState()
}
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
@ -622,20 +634,6 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
private fun createNextTurnButton(): TextButton {
val nextTurnButton = TextButton("", skin) // text is set in update()
nextTurnButton.label.setFontSize(30)
nextTurnButton.labelCell.pad(10f)
val nextTurnActionWrapped = { nextTurnAction() }
nextTurnButton.onClick(nextTurnActionWrapped)
keyPressDispatcher[Input.Keys.SPACE] = nextTurnActionWrapped
keyPressDispatcher['n'] = nextTurnActionWrapped
return nextTurnButton
}
private fun createNewWorldScreen(gameInfo: GameInfo) { private fun createNewWorldScreen(gameInfo: GameInfo) {
game.gameInfo = gameInfo game.gameInfo = gameInfo
@ -661,8 +659,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
isPlayersTurn = false isPlayersTurn = false
shouldUpdate = true shouldUpdate = true
// on a separate thread so the user can explore their world while we're passing the turn
crashHandlingThread(name = "NextTurn") { // on a separate thread so the user can explore their world while we're passing the turn launchCrashHandling("NextTurn", runAsDaemon = false) {
if (consoleLog) if (consoleLog)
println("\nNext turn starting " + Date().formatDate()) println("\nNext turn starting " + Date().formatDate())
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
@ -674,31 +672,31 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
if (originalGameInfo.gameParameters.isOnlineMultiplayer) { if (originalGameInfo.gameParameters.isOnlineMultiplayer) {
try { try {
OnlineMultiplayer().tryUploadGame(gameInfoClone, withPreview = true) OnlineMultiplayerGameSaver().tryUploadGame(gameInfoClone, withPreview = true)
} catch (ex: FileStorageRateLimitReached) { } catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
val cantUploadNewGamePopup = Popup(this) val cantUploadNewGamePopup = Popup(this@WorldScreen)
cantUploadNewGamePopup.addGoodSizedLabel("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds").row() cantUploadNewGamePopup.addGoodSizedLabel("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds").row()
cantUploadNewGamePopup.addCloseButton() cantUploadNewGamePopup.addCloseButton()
cantUploadNewGamePopup.open() cantUploadNewGamePopup.open()
} }
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { // Since we're changing the UI, that should be done on the main thread postCrashHandlingRunnable { // Since we're changing the UI, that should be done on the main thread
val cantUploadNewGamePopup = Popup(this) val cantUploadNewGamePopup = Popup(this@WorldScreen)
cantUploadNewGamePopup.addGoodSizedLabel("Could not upload game!").row() cantUploadNewGamePopup.addGoodSizedLabel("Could not upload game!").row()
cantUploadNewGamePopup.addCloseButton() cantUploadNewGamePopup.addCloseButton()
cantUploadNewGamePopup.open() cantUploadNewGamePopup.open()
} }
isPlayersTurn = true // Since we couldn't push the new game clone, then it's like we never clicked the "next turn" button this@WorldScreen.isPlayersTurn = true // Since we couldn't push the new game clone, then it's like we never clicked the "next turn" button
shouldUpdate = true this@WorldScreen.shouldUpdate = true
return@crashHandlingThread return@launchCrashHandling
} }
} }
if (game.gameInfo != originalGameInfo) // while this was turning we loaded another game if (game.gameInfo != originalGameInfo) // while this was turning we loaded another game
return@crashHandlingThread return@launchCrashHandling
game.gameInfo = gameInfoClone this@WorldScreen.game.gameInfo = gameInfoClone
if (consoleLog) if (consoleLog)
println("Next turn took ${System.currentTimeMillis()-startTime}ms") println("Next turn took ${System.currentTimeMillis()-startTime}ms")
@ -716,7 +714,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
if (shouldAutoSave) { if (shouldAutoSave) {
val newWorldScreen = game.worldScreen val newWorldScreen = this@WorldScreen.game.worldScreen
newWorldScreen.waitingForAutosave = true newWorldScreen.waitingForAutosave = true
newWorldScreen.shouldUpdate = true newWorldScreen.shouldUpdate = true
GameSaver.autoSave(gameInfoClone) { GameSaver.autoSave(gameInfoClone) {
@ -729,27 +727,11 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
} }
private class NextTurnAction(val text: String, val color: Color, val action: () -> Unit)
private fun updateNextTurnButton(isSomethingOpen: Boolean) { private fun updateNextTurnButton(isSomethingOpen: Boolean) {
val action: NextTurnAction = getNextTurnAction() nextTurnButton.update(isSomethingOpen, isPlayersTurn, waitingForAutosave, getNextTurnAction())
nextTurnAction = action.action
nextTurnButton.setText(action.text.tr())
nextTurnButton.label.color = action.color
nextTurnButton.pack()
nextTurnButton.isEnabled = !isSomethingOpen && isPlayersTurn && !waitingForAutosave
nextTurnButton.setPosition(stage.width - nextTurnButton.width - 10f, topBar.y - nextTurnButton.height - 10f) nextTurnButton.setPosition(stage.width - nextTurnButton.width - 10f, topBar.y - nextTurnButton.height - 10f)
} }
/**
* Used by [OptionsPopup][com.unciv.ui.worldscreen.mainmenu.OptionsPopup]
* to re-enable the next turn button within its Close button action
*/
fun enableNextTurnButtonAfterOptions() {
mapHolder.reloadMaxZoom()
nextTurnButton.isEnabled = isPlayersTurn && !waitingForAutosave
}
private fun getNextTurnAction(): NextTurnAction { private fun getNextTurnAction(): NextTurnAction {
return when { return when {
@ -832,7 +814,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
viewingCiv.hasMovedAutomatedUnits = true viewingCiv.hasMovedAutomatedUnits = true
isPlayersTurn = false // Disable state changes isPlayersTurn = false // Disable state changes
nextTurnButton.disable() nextTurnButton.disable()
crashHandlingThread(name="Move automated units") { launchCrashHandling("Move automated units") {
for (unit in viewingCiv.getCivUnits()) for (unit in viewingCiv.getCivUnits())
unit.doAction() unit.doAction()
postCrashHandlingRunnable { postCrashHandlingRunnable {

View File

@ -13,7 +13,7 @@ import com.unciv.UncivGame
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.MapSaver import com.unciv.logic.MapSaver
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.multiplayer.SimpleHttp import com.unciv.logic.multiplayer.storage.SimpleHttp
import com.unciv.models.UncivSound import com.unciv.models.UncivSound
import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
@ -29,7 +29,7 @@ import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicTrackChooserFlags import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.civilopedia.MarkupRenderer import com.unciv.ui.civilopedia.MarkupRenderer
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.newgamescreen.TranslatedSelectBox import com.unciv.ui.newgamescreen.TranslatedSelectBox
@ -53,7 +53,8 @@ import com.badlogic.gdx.utils.Array as GdxArray
//region Fields //region Fields
class OptionsPopup( class OptionsPopup(
private val previousScreen: BaseScreen, private val previousScreen: BaseScreen,
private val selectPage: Int = defaultPage private val selectPage: Int = defaultPage,
private val onClose: () -> Unit = {}
) : Popup(previousScreen) { ) : Popup(previousScreen) {
private val settings = previousScreen.game.settings private val settings = previousScreen.game.settings
private val tabs: TabbedPager private val tabs: TabbedPager
@ -110,8 +111,7 @@ class OptionsPopup(
addCloseButton { addCloseButton {
previousScreen.game.musicController.onChange(null) previousScreen.game.musicController.onChange(null)
previousScreen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait) previousScreen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait)
if (previousScreen is WorldScreen) onClose()
previousScreen.enableNextTurnButtonAfterOptions()
}.padBottom(10f) }.padBottom(10f)
pack() // Needed to show the background. pack() // Needed to show the background.
@ -137,7 +137,7 @@ class OptionsPopup(
(previousScreen.game.screen as BaseScreen).openOptionsPopup(tabs.activePage) (previousScreen.game.screen as BaseScreen).openOptionsPopup(tabs.activePage)
} }
private fun successfullyConnectedToServer(action: (Boolean, String)->Unit){ private fun successfullyConnectedToServer(action: (Boolean, String, Int?) -> Unit){
SimpleHttp.sendGetRequest("${settings.multiplayerServer}/isalive", action) SimpleHttp.sendGetRequest("${settings.multiplayerServer}/isalive", action)
} }
@ -307,7 +307,7 @@ class OptionsPopup(
} }
popup.open(true) popup.open(true)
successfullyConnectedToServer { success: Boolean, _: String -> successfullyConnectedToServer { success, _, _ ->
popup.addGoodSizedLabel(if (success) "Success!" else "Failed!").row() popup.addGoodSizedLabel(if (success) "Success!" else "Failed!").row()
popup.addCloseButton() popup.addCloseButton()
} }
@ -394,7 +394,7 @@ class OptionsPopup(
modCheckResultTable.add("Checking mods for errors...".toLabel()).row() modCheckResultTable.add("Checking mods for errors...".toLabel()).row()
modCheckBaseSelect!!.isDisabled = true modCheckBaseSelect!!.isDisabled = true
crashHandlingThread(name="ModChecker") { launchCrashHandling("ModChecker") {
for (mod in RulesetCache.values.sortedBy { it.name }) { for (mod in RulesetCache.values.sortedBy { it.name }) {
if (base != modCheckWithoutBase && mod.modOptions.isBaseRuleset) continue if (base != modCheckWithoutBase && mod.modOptions.isBaseRuleset) continue
@ -807,7 +807,7 @@ class OptionsPopup(
errorTable.add("Downloading...".toLabel()) errorTable.add("Downloading...".toLabel())
// So the whole game doesn't get stuck while downloading the file // So the whole game doesn't get stuck while downloading the file
crashHandlingThread(name = "Music") { launchCrashHandling("MusicDownload") {
try { try {
previousScreen.game.musicController.downloadDefaultFile() previousScreen.game.musicController.downloadDefaultFile()
postCrashHandlingRunnable { postCrashHandlingRunnable {
@ -924,7 +924,7 @@ class OptionsPopup(
} }
} }
crashHandlingThread(name = "Add Font Select") { launchCrashHandling("Add Font Select") {
// This is a heavy operation and causes ANRs // This is a heavy operation and causes ANRs
val fonts = GdxArray<FontFamilyData>().apply { val fonts = GdxArray<FontFamilyData>().apply {
add(FontFamilyData.default) add(FontFamilyData.default)
@ -943,7 +943,7 @@ class OptionsPopup(
val generateAction: ()->Unit = { val generateAction: ()->Unit = {
tabs.selectPage("Advanced") tabs.selectPage("Advanced")
generateTranslationsButton.setText("Working...".tr()) generateTranslationsButton.setText("Working...".tr())
crashHandlingThread { launchCrashHandling("WriteTranslations") {
val result = TranslationFileWriter.writeNewTranslationFiles() val result = TranslationFileWriter.writeNewTranslationFiles()
postCrashHandlingRunnable { postCrashHandlingRunnable {
// notify about completion // notify about completion

View File

@ -0,0 +1,34 @@
package com.unciv.ui.worldscreen.status
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.unciv.models.translations.tr
import com.unciv.ui.utils.*
class NextTurnButton(
keyPressDispatcher: KeyPressDispatcher
) : TextButton("", BaseScreen.skin) {
lateinit var nextTurnAction: NextTurnAction
init {
label.setFontSize(30)
labelCell.pad(10f)
val action = { nextTurnAction.action() }
onClick(action)
keyPressDispatcher[Input.Keys.SPACE] = action
keyPressDispatcher['n'] = action
}
fun update(isSomethingOpen: Boolean, isPlayersTurn: Boolean, waitingForAutosave: Boolean, nextTurnAction: NextTurnAction? = null) {
if (nextTurnAction != null) {
this.nextTurnAction = nextTurnAction
setText(nextTurnAction.text.tr())
label.color = nextTurnAction.color
pack()
}
isEnabled = !isSomethingOpen && isPlayersTurn && !waitingForAutosave
}
}
class NextTurnAction(val text: String, val color: Color, val action: () -> Unit)

View File

@ -7,7 +7,7 @@ import com.unciv.UncivGame
import com.unciv.logic.map.MapUnit import com.unciv.logic.map.MapUnit
import com.unciv.models.UnitAction import com.unciv.models.UnitAction
import com.unciv.ui.audio.Sounds import com.unciv.ui.audio.Sounds
import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.IconTextButton
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable
@ -44,7 +44,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
actionButton.onClick(unitAction.uncivSound, action) actionButton.onClick(unitAction.uncivSound, action)
if (key != KeyCharAndCode.UNKNOWN) if (key != KeyCharAndCode.UNKNOWN)
worldScreen.keyPressDispatcher[key] = { worldScreen.keyPressDispatcher[key] = {
crashHandlingThread(name = "Sound") { Sounds.play(unitAction.uncivSound) } launchCrashHandling("UnitSound") { Sounds.play(unitAction.uncivSound) }
action() action()
worldScreen.mapHolder.removeUnitActionOverlay() worldScreen.mapHolder.removeUnitActionOverlay()
} }

View File

@ -59,7 +59,11 @@ tasks.register<Jar>("dist") { // Compiles the jar file
from(files(sourceSets.main.get().output.resourcesDir)) from(files(sourceSets.main.get().output.resourcesDir))
from(files(sourceSets.main.get().output.classesDirs)) from(files(sourceSets.main.get().output.classesDirs))
// see Laurent1967's comment on https://github.com/libgdx/libgdx/issues/5491 // see Laurent1967's comment on https://github.com/libgdx/libgdx/issues/5491
from({ configurations.compileClasspath.get().resolve().map { if (it.isDirectory) it else zipTree(it) } }) from({
(
configurations.runtimeClasspath.get().resolve() // kotlin coroutine classes live here, thanks https://stackoverflow.com/a/59021222
+ configurations.compileClasspath.get().resolve()
).map { if (it.isDirectory) it else zipTree(it) }})
from(files(assetsDir)) from(files(assetsDir))
// This is for the .dll and .so files to make the Discord RPC work on all desktops // This is for the .dll and .so files to make the Discord RPC work on all desktops
from(files(discordDir)) from(files(discordDir))

View File

@ -6,6 +6,7 @@ import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.restrictTo import com.github.ajalt.clikt.parameters.types.restrictTo
import io.ktor.application.* import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
@ -14,6 +15,7 @@ import io.ktor.utils.io.jvm.javaio.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileNotFoundException
internal object UncivServer { internal object UncivServer {
@ -60,7 +62,10 @@ private class UncivServerRunner : CliktCommand() {
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
println("Get file: $fileName") println("Get file: $fileName")
val file = File(fileFolderName, fileName) val file = File(fileFolderName, fileName)
if (!file.exists()) throw Exception("File does not exist!") if (!file.exists()) {
call.respond(HttpStatusCode.NotFound, "File does not exist")
return@get
}
val fileText = file.readText() val fileText = file.readText()
println("Text read: $fileText") println("Text read: $fileText")
call.respondText(fileText) call.respondText(fileText)
@ -68,7 +73,10 @@ private class UncivServerRunner : CliktCommand() {
delete("/files/{fileName}") { delete("/files/{fileName}") {
val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!")
val file = File(fileFolderName, fileName) val file = File(fileFolderName, fileName)
if (!file.exists()) throw Exception("File does not exist!") if (!file.exists()) {
call.respond(HttpStatusCode.NotFound, "File does not exist")
return@delete
}
file.delete() file.delete()
} }
} }