diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 08ed1917a9..b26f93db75 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -20,9 +20,7 @@ android { } } packagingOptions { - resources.excludes += "META-INF/robovm/ios/robovm.xml" - // part of kotlinx-coroutines-android, should not go into the apk - resources.excludes += "DebugProbesKt.bin" + resources.excludes.add("META-INF/robovm/ios/robovm.xml") } defaultConfig { applicationId = "com.unciv.app" diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 10917c5813..7bb2aa03b5 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -15,10 +15,9 @@ import androidx.work.* import com.badlogic.gdx.backends.android.AndroidApplication import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver -import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached +import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.models.metadata.GameSettings -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver -import kotlinx.coroutines.runBlocking +import com.unciv.logic.multiplayer.OnlineMultiplayer import java.io.FileNotFoundException import java.io.PrintWriter import java.io.StringWriter @@ -235,7 +234,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame } } - override fun doWork(): Result = runBlocking { + override fun doWork(): Result { val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true) val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5) val fileStorage = inputData.getString(FILE_STORAGE) @@ -254,7 +253,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame continue try { - val gamePreview = OnlineMultiplayerGameSaver(fileStorage).tryDownloadGamePreview(gameId) + val gamePreview = OnlineMultiplayer(fileStorage).tryDownloadGamePreview(gameId) val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer) //Save game so MultiplayerScreen gets updated @@ -303,7 +302,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame with(NotificationManagerCompat.from(applicationContext)) { cancel(NOTIFICATION_ID_SERVICE) } - return@runBlocking Result.failure() + return Result.failure() } else { if (showPersistNotific) { showPersistentNotification(applicationContext, applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) } @@ -314,9 +313,9 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame enqueue(applicationContext, 1, inputDataFailIncrease) } } catch (outOfMemory: OutOfMemoryError){ // no point in trying multiple times if this was an oom error - return@runBlocking Result.failure() + return Result.failure() } - return@runBlocking Result.success() + return Result.success() } private fun getStackTraceString(ex: Exception): String { diff --git a/build.gradle.kts b/build.gradle.kts index 67b38c7b7c..189299d762 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -93,7 +93,6 @@ project(":android") { dependencies { "implementation"(project(":core")) "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-arm64-v8a") natives("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86") @@ -120,7 +119,6 @@ project(":core") { dependencies { "implementation"("com.badlogicgames.gdx:gdx:$gdxVersion") - "implementation"("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1") } diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 0d12f14ed2..67d1f65dfe 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -20,7 +20,7 @@ import com.unciv.ui.MultiplayerScreen import com.unciv.ui.mapeditor.* import com.unciv.models.metadata.GameSetupInfo import com.unciv.ui.civilopedia.CivilopediaScreen -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.newgamescreen.NewGameScreen @@ -73,12 +73,12 @@ class MainMenuScreen: BaseScreen() { // will not exist unless we reset the ruleset and images ImageGetter.ruleset = RulesetCache.getVanillaRuleset() - launchCrashHandling("ShowMapBackground") { + crashHandlingThread(name = "ShowMapBackground") { val newMap = MapGenerator(RulesetCache.getVanillaRuleset()) .generateMap(MapParameters().apply { mapSize = MapSizeNew(MapSize.Small); type = MapType.default }) postCrashHandlingRunnable { // for GL context ImageGetter.setNewRuleset(RulesetCache.getVanillaRuleset()) - val mapHolder = EditorMapHolder(this@MainMenuScreen, newMap) {} + val mapHolder = EditorMapHolder(this, newMap) {} backgroundTable.addAction(Actions.sequence( Actions.fadeOut(0f), Actions.run { @@ -167,12 +167,12 @@ class MainMenuScreen: BaseScreen() { val loadingPopup = Popup(this) loadingPopup.addGoodSizedLabel("Loading...") loadingPopup.open() - launchCrashHandling("autoLoadGame") { + crashHandlingThread { // Load game from file to class on separate thread to avoid ANR... fun outOfMemory() { postCrashHandlingRunnable { loadingPopup.close() - ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen) + ToastPopup("Not enough memory on phone to load game!", this) } } @@ -181,7 +181,7 @@ class MainMenuScreen: BaseScreen() { savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName) } catch (oom: OutOfMemoryError) { outOfMemory() - return@launchCrashHandling + return@crashHandlingThread } catch (ex: Exception) { // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first // This can help for situations when the autosave is corrupted try { @@ -191,13 +191,13 @@ class MainMenuScreen: BaseScreen() { GameSaver.loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!) } catch (oom: OutOfMemoryError) { // The autosave could have oom problems as well... smh outOfMemory() - return@launchCrashHandling + return@crashHandlingThread } catch (ex: Exception) { postCrashHandlingRunnable { loadingPopup.close() - ToastPopup("Cannot resume game!", this@MainMenuScreen) + ToastPopup("Cannot resume game!", this) } - return@launchCrashHandling + return@crashHandlingThread } } @@ -215,14 +215,14 @@ class MainMenuScreen: BaseScreen() { private fun quickstartNewGame() { ToastPopup("Working...", this) val errorText = "Cannot start game with the default new game parameters!" - launchCrashHandling("QuickStart") { + crashHandlingThread { val newGame: GameInfo // Can fail when starting the game... try { newGame = GameStarter.startNewGame(GameSetupInfo.fromSettings("Chieftain")) } catch (ex: Exception) { - postCrashHandlingRunnable { ToastPopup(errorText, this@MainMenuScreen) } - return@launchCrashHandling + postCrashHandlingRunnable { ToastPopup(errorText, this) } + return@crashHandlingThread } // ...or when loading the game @@ -230,9 +230,9 @@ class MainMenuScreen: BaseScreen() { try { game.loadGame(newGame) } catch (outOfMemory: OutOfMemoryError) { - ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen) + ToastPopup("Not enough memory on phone to load game!", this) } catch (ex: Exception) { - ToastPopup(errorText, this@MainMenuScreen) + ToastPopup(errorText, this) } } } diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 12c98477b7..42f3507318 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -19,14 +19,11 @@ import com.unciv.ui.audio.MusicMood import com.unciv.ui.utils.* import com.unciv.ui.worldscreen.PlayerReadyScreen import com.unciv.ui.worldscreen.WorldScreen -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.ui.audio.Sounds -import com.unciv.ui.crashhandling.closeExecutors -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter -import com.unciv.ui.multiplayer.LoadDeepLinkScreen -import com.unciv.ui.popup.Popup import java.util.* class UncivGame(parameters: UncivGameParameters) : Game() { @@ -117,7 +114,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { Gdx.graphics.isContinuousRendering = settings.continuousRendering - launchCrashHandling("LoadJSON") { + crashHandlingThread(name = "LoadJSON") { RulesetCache.loadRulesets(printOutput = true) translations.tryReadTranslationForCurrentLanguage() translations.loadPercentageCompleteOfLanguages() @@ -172,26 +169,13 @@ class UncivGame(parameters: UncivGameParameters) : Game() { Gdx.graphics.requestRendering() } - fun tryLoadDeepLinkedGame() = launchCrashHandling("LoadDeepLinkedGame") { + fun tryLoadDeepLinkedGame() { if (deepLinkedMultiplayerGame != null) { - postCrashHandlingRunnable { - setScreen(LoadDeepLinkScreen()) - } try { - val onlineGame = OnlineMultiplayerGameSaver().tryDownloadGame(deepLinkedMultiplayerGame!!) - postCrashHandlingRunnable { - loadGame(onlineGame) - } + val onlineGame = OnlineMultiplayer().tryDownloadGame(deepLinkedMultiplayerGame!!) + loadGame(onlineGame) } catch (ex: Exception) { - 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() - } + setScreen(MainMenuScreen()) } } } @@ -226,7 +210,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() { cancelDiscordEvent?.invoke() Sounds.clearCache() if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out - closeExecutors() // Log still running threads (on desktop that should be only this one and "DestroyJavaVM") val numThreads = Thread.activeCount() diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index 8d762bc9a5..01cd907ffc 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -4,8 +4,9 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame import com.unciv.json.json +import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.models.metadata.GameSettings -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.saves.Gzip import java.io.File @@ -30,20 +31,20 @@ object GameSaver { //endregion //region Helpers - private fun getSavefolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder + private fun getSubfolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder fun getSave(GameName: String, multiplayer: Boolean = false): FileHandle { - val localFile = Gdx.files.local("${getSavefolder(multiplayer)}/$GameName") + val localFile = Gdx.files.local("${getSubfolder(multiplayer)}/$GameName") if (externalFilesDirForAndroid == "" || !Gdx.files.isExternalStorageAvailable) return localFile - val externalFile = Gdx.files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}/$GameName") + val externalFile = Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}/$GameName") if (localFile.exists() && !externalFile.exists()) return localFile return externalFile } fun getSaves(multiplayer: Boolean = false): Sequence { - val localSaves = Gdx.files.local(getSavefolder(multiplayer)).list().asSequence() + val localSaves = Gdx.files.local(getSubfolder(multiplayer)).list().asSequence() if (externalFilesDirForAndroid == "" || !Gdx.files.isExternalStorageAvailable) return localSaves - return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}").list().asSequence() + return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}").list().asSequence() } fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null @@ -55,21 +56,12 @@ object GameSaver { //endregion //region Saving - 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 }) { + fun saveGame(game: GameInfo, GameName: String, saveCompletionCallback: ((Exception?) -> Unit)? = null) { try { - file.writeString(gameInfoToString(game), false) - saveCompletionCallback(null) + getSave(GameName).writeString(gameInfoToString(game), false) + saveCompletionCallback?.invoke(null) } catch (ex: Exception) { - saveCompletionCallback(ex) + saveCompletionCallback?.invoke(ex) } } @@ -79,7 +71,7 @@ object GameSaver { return if (forceZip ?: saveZipped) Gzip.zip(plainJson) else plainJson } - /** Returns gzipped serialization of preview [game] - only called from [OnlineMultiplayerGameSaver] */ + /** Returns gzipped serialization of preview [game] - only called from [OnlineMultiplayer] */ fun gameInfoToString(game: GameInfoPreview): String { return Gzip.zip(json().toJson(game)) } @@ -87,21 +79,12 @@ object GameSaver { /** * Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder */ - 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 }) { + fun saveGame(game: GameInfoPreview, GameName: String, saveCompletionCallback: ((Exception?) -> Unit)? = null) { try { - json().toJson(game, file) - saveCompletionCallback(null) + json().toJson(game, getSave(GameName, true)) + saveCompletionCallback?.invoke(null) } catch (ex: Exception) { - saveCompletionCallback(ex) + saveCompletionCallback?.invoke(ex) } } @@ -138,7 +121,7 @@ object GameSaver { } } - /** Parses [gameData] as gzipped serialization of a [GameInfoPreview] - only called from [OnlineMultiplayerGameSaver] */ + /** Parses [gameData] as gzipped serialization of a [GameInfoPreview] - only called from [OnlineMultiplayer] */ fun gameInfoPreviewFromString(gameData: String): GameInfoPreview { return json().fromJson(GameInfoPreview::class.java, Gzip.unzip(gameData)) } @@ -201,7 +184,7 @@ object GameSaver { 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 - launchCrashHandling(autoSaveFileName, runAsDaemon = false) { + crashHandlingThread(name = autoSaveFileName) { autoSaveSingleThreaded(gameInfo) // do this on main thread postCrashHandlingRunnable ( postRunnable ) diff --git a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt b/core/src/com/unciv/logic/multiplayer/DropBox.kt similarity index 96% rename from core/src/com/unciv/logic/multiplayer/storage/DropBox.kt rename to core/src/com/unciv/logic/multiplayer/DropBox.kt index fe1ce5517a..af6f2f880c 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt +++ b/core/src/com/unciv/logic/multiplayer/DropBox.kt @@ -1,4 +1,4 @@ -package com.unciv.logic.multiplayer.storage +package com.unciv.logic.multiplayer import com.unciv.json.json import com.unciv.ui.utils.UncivDateFormat.parseDate @@ -11,7 +11,7 @@ import kotlin.collections.ArrayList import kotlin.concurrent.timer -object DropBox: FileStorage { +object DropBox: IFileStorage { private var remainingRateLimitSeconds = 0 private var rateLimitTimer: Timer? = null @@ -76,7 +76,7 @@ object DropBox: FileStorage { ) } - override fun getFileMetaData(fileName: String): FileMetaData { + override fun getFileMetaData(fileName: String): IFileMetaData { val stream = dropboxApi( url="https://api.dropboxapi.com/2/files/get_metadata", data="{\"path\":\"${getLocalGameLocation(fileName)}\"}", @@ -124,8 +124,8 @@ object DropBox: FileStorage { throw FileStorageRateLimitReached(remainingRateLimitSeconds) } - fun getFolderList(folder: String): ArrayList { - val folderList = ArrayList() + fun getFolderList(folder: String): ArrayList { + val folderList = ArrayList() // 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 // instead of the path. @@ -168,7 +168,7 @@ object DropBox: FileStorage { } @Suppress("PropertyName") - private class MetaData: FileMetaData { + private class MetaData: IFileMetaData { var name = "" private var server_modified = "" diff --git a/core/src/com/unciv/logic/multiplayer/Multiplayer.kt b/core/src/com/unciv/logic/multiplayer/Multiplayer.kt new file mode 100644 index 0000000000..81f3bf2073 --- /dev/null +++ b/core/src/com/unciv/logic/multiplayer/Multiplayer.kt @@ -0,0 +1,130 @@ +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) + } +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/multiplayer/storage/ServerMutex.kt b/core/src/com/unciv/logic/multiplayer/ServerMutex.kt similarity index 89% rename from core/src/com/unciv/logic/multiplayer/storage/ServerMutex.kt rename to core/src/com/unciv/logic/multiplayer/ServerMutex.kt index cb7426a6c0..021d19ed9b 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/ServerMutex.kt +++ b/core/src/com/unciv/logic/multiplayer/ServerMutex.kt @@ -1,4 +1,4 @@ -package com.unciv.logic.multiplayer.storage +package com.unciv.logic.multiplayer import com.unciv.json.json 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 // lock file to not overuse the dropbox file upload limit else it will return an error try { - val metaData = OnlineMultiplayerGameSaver().fileStorage().getFileMetaData(fileName) + val metaData = OnlineMultiplayer().fileStorage.getFileMetaData(fileName) val date = metaData.getLastModified() // 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) { return locked } else { - OnlineMultiplayerGameSaver().fileStorage().deleteFile(fileName) + OnlineMultiplayer().fileStorage.deleteFile(fileName) } } catch (ex: FileNotFoundException) { // Catching this exception means no lock file is present @@ -65,7 +65,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) { } try { - OnlineMultiplayerGameSaver().fileStorage().saveFileData(fileName, Gzip.zip(json().toJson(LockFile())), false) + OnlineMultiplayer().fileStorage.saveFileData(fileName, Gzip.zip(json().toJson(LockFile())), false) } catch (ex: FileStorageConflictException) { return locked } @@ -116,7 +116,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) { if (!locked) return - OnlineMultiplayerGameSaver().fileStorage().deleteFile("${gameInfo.gameId}_Lock") + OnlineMultiplayer().fileStorage.deleteFile("${gameInfo.gameId}_Lock") locked = false } diff --git a/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt b/core/src/com/unciv/logic/multiplayer/SimpleHttp.kt similarity index 85% rename from core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt rename to core/src/com/unciv/logic/multiplayer/SimpleHttp.kt index 11ff73e1ce..48b6b32b5f 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt +++ b/core/src/com/unciv/logic/multiplayer/SimpleHttp.kt @@ -1,4 +1,4 @@ -package com.unciv.logic.multiplayer.storage +package com.unciv.logic.multiplayer import com.badlogic.gdx.Net import com.unciv.UncivGame @@ -9,11 +9,11 @@ import java.net.* import java.nio.charset.Charset object SimpleHttp { - fun sendGetRequest(url: String, action: (success: Boolean, result: String, code: Int?)->Unit) { + fun sendGetRequest(url: String, action: (success: Boolean, result: String)->Unit) { sendRequest(Net.HttpMethods.GET, url, "", action) } - fun sendRequest(method: String, url: String, content: String, action: (success: Boolean, result: String, code: Int?)->Unit) { + fun sendRequest(method: String, url: String, content: String, action: (success: Boolean, result: String)->Unit) { var uri = URI(url) if (uri.host == null) uri = URI("http://$url") @@ -21,7 +21,7 @@ object SimpleHttp { try { urlObj = uri.toURL() } catch (t:Throwable){ - action(false, "Bad URL", null) + action(false, "Bad URL") return } @@ -43,14 +43,14 @@ object SimpleHttp { } val text = BufferedReader(InputStreamReader(inputStream)).readText() - action(true, text, responseCode) + action(true, text) } catch (t: Throwable) { println(t.message) val errorMessageToReturn = if (errorStream != null) BufferedReader(InputStreamReader(errorStream)).readText() else t.message!! println(errorMessageToReturn) - action(false, errorMessageToReturn, if (errorStream != null) responseCode else null) + action(false, errorMessageToReturn) } } } diff --git a/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt deleted file mode 100644 index c6e8c13515..0000000000 --- a/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt +++ /dev/null @@ -1,34 +0,0 @@ -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) -} diff --git a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt b/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt deleted file mode 100644 index f5219bc97d..0000000000 --- a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt +++ /dev/null @@ -1,58 +0,0 @@ -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) - } -} \ No newline at end of file diff --git a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt deleted file mode 100644 index b77cf01f86..0000000000 --- a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt +++ /dev/null @@ -1,51 +0,0 @@ -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) - } - } - } - } - -} diff --git a/core/src/com/unciv/models/simulation/Simulation.kt b/core/src/com/unciv/models/simulation/Simulation.kt index 22ff806b0a..e86892f63f 100644 --- a/core/src/com/unciv/models/simulation/Simulation.kt +++ b/core/src/com/unciv/models/simulation/Simulation.kt @@ -5,9 +5,7 @@ import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.GameStarter import com.unciv.models.metadata.GameSetupInfo -import com.unciv.ui.crashhandling.launchCrashHandling -import kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking +import com.unciv.ui.crashhandling.crashHandlingThread import kotlin.time.Duration import kotlin.math.max import kotlin.time.ExperimentalTime @@ -42,12 +40,12 @@ class Simulation( } } - fun start() = runBlocking { + fun start() { startTime = System.currentTimeMillis() - val jobs: ArrayList = ArrayList() + val threads: ArrayList = ArrayList() for (threadId in 1..threadsNumber) { - jobs.add(launchCrashHandling("simulation-${threadId}") { + threads.add(crashHandlingThread { for (i in 1..simulationsPerThread) { val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo)) gameInfo.simulateMaxTurns = maxTurns @@ -68,8 +66,8 @@ class Simulation( } }) } - // wait for all to finish - for (job in jobs) job.join() + // wait for all threads to finish + for (thread in threads) thread.join() endTime = System.currentTimeMillis() } diff --git a/core/src/com/unciv/ui/audio/MusicController.kt b/core/src/com/unciv/ui/audio/MusicController.kt index 74aac3cca1..c622e83675 100644 --- a/core/src/com/unciv/ui/audio/MusicController.kt +++ b/core/src/com/unciv/ui/audio/MusicController.kt @@ -6,7 +6,7 @@ import com.badlogic.gdx.audio.Music import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame import com.unciv.models.metadata.GameSettings -import com.unciv.logic.multiplayer.storage.DropBox +import com.unciv.logic.multiplayer.DropBox import java.util.* import kotlin.concurrent.thread import kotlin.concurrent.timer diff --git a/core/src/com/unciv/ui/audio/Sounds.kt b/core/src/com/unciv/ui/audio/Sounds.kt index 8598bc0fba..bf9fa732ff 100644 --- a/core/src/com/unciv/ui/audio/Sounds.kt +++ b/core/src/com/unciv/ui/audio/Sounds.kt @@ -6,8 +6,7 @@ import com.badlogic.gdx.audio.Sound import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame import com.unciv.models.UncivSound -import com.unciv.ui.crashhandling.launchCrashHandling -import kotlinx.coroutines.delay +import com.unciv.ui.crashhandling.crashHandlingThread import java.io.File /* @@ -165,10 +164,10 @@ object Sounds { val initialDelay = if (isFresh && Gdx.app.type == Application.ApplicationType.Android) 40 else 0 if (initialDelay > 0 || resource.play(volume) == -1L) { - launchCrashHandling("DelayedSound") { - delay(initialDelay.toLong()) + crashHandlingThread(name = "DelayedSound") { + Thread.sleep(initialDelay.toLong()) while (resource.play(volume) == -1L) { - delay(20L) + Thread.sleep(20L) } } } diff --git a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt index b7b2461431..38d8cb6227 100644 --- a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt @@ -17,7 +17,7 @@ import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.models.translations.tr import com.unciv.ui.audio.Sounds -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.popup.Popup @@ -207,7 +207,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { availableConstructionsTable.add("Loading...".toLabel()).pad(10f) } - launchCrashHandling("Construction info gathering - ${cityScreen.city.name}") { + crashHandlingThread(name = "Construction info gathering - ${cityScreen.city.name}") { // Since this can be a heavy operation and leads to many ANRs on older phones we put the metadata-gathering in another thread. val constructionButtonDTOList = getConstructionButtonDTOs() postCrashHandlingRunnable { diff --git a/core/src/com/unciv/ui/crashhandling/CrashHandlingThread.kt b/core/src/com/unciv/ui/crashhandling/CrashHandlingThread.kt index 0f78a78507..6af33983d0 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashHandlingThread.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashHandlingThread.kt @@ -2,45 +2,10 @@ package com.unciv.ui.crashhandling import com.badlogic.gdx.Gdx import com.unciv.ui.utils.wrapCrashHandlingUnit -import kotlinx.coroutines.* -import java.util.concurrent.Executors -import java.util.concurrent.ThreadFactory import kotlin.concurrent.thread -private val DAEMON_EXECUTOR = Executors.newCachedThreadPool(object : ThreadFactory { - var n = 0 - override fun newThread(r: java.lang.Runnable): Thread = - crashHandlingThread(name = "crash-handling-daemon-${n++}", start = false, isDaemon = true, block = r::run) -}).asCoroutineDispatcher() -/** - * Coroutine Scope that runs coroutines in separate daemon threads. - * - * Brings the main game loop to a [com.unciv.CrashScreen] if an exception happens. - */ -val CRASH_HANDLING_DAEMON_SCOPE = CoroutineScope(DAEMON_EXECUTOR) - -private val EXECUTOR = Executors.newCachedThreadPool(object : ThreadFactory { - var n = 0 - override fun newThread(r: java.lang.Runnable): Thread = - crashHandlingThread(name = "crash-handling-${n++}", start = false, isDaemon = false, block = r::run) -}).asCoroutineDispatcher() -/** - * Coroutine Scope that runs coroutines in separate threads that are not started as daemons. - * - * Brings the main game loop to a [com.unciv.CrashScreen] if an exception happens. - */ -val CRASH_HANDLING_SCOPE = CoroutineScope(EXECUTOR) - -/** - * Must be called only in [com.unciv.UncivGame.dispose] to not have any threads running that prevent JVM shutdown. - */ -fun closeExecutors() { - EXECUTOR.close() - DAEMON_EXECUTOR.close() -} - /** Wrapped version of [kotlin.concurrent.thread], that brings the main game loop to a [com.unciv.CrashScreen] if an exception happens. */ -private fun crashHandlingThread( +fun crashHandlingThread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, @@ -60,24 +25,3 @@ private fun crashHandlingThread( fun postCrashHandlingRunnable(runnable: () -> Unit) { Gdx.app.postRunnable(runnable.wrapCrashHandlingUnit()) } - -/** - * [launch]es a new coroutine that brings the game loop to a [com.unciv.CrashScreen] if an exception occurs. - * @see crashHandlingThread - */ -fun launchCrashHandling(name: String, runAsDaemon: Boolean = true, - flowBlock: suspend CoroutineScope.() -> Unit): Job { - return getCoroutineContext(runAsDaemon).launch(CoroutineName(name)) { flowBlock(this) } -} -/** - * 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 asyncCrashHandling(name: String, runAsDaemon: Boolean = true, - flowBlock: suspend CoroutineScope.() -> T): Deferred { - 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 -} \ No newline at end of file diff --git a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt index dce46eeec2..3bfbb96bcf 100644 --- a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt @@ -5,12 +5,12 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameSaver import com.unciv.logic.civilization.PlayerType -import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.utils.* -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.logic.multiplayer.OnlineMultiplayer +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.popup.Popup import com.unciv.ui.popup.YesNoPopup @@ -83,10 +83,10 @@ class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: St popup.addGoodSizedLabel("Working...").row() popup.open() - launchCrashHandling("Resign", runAsDaemon = false) { + crashHandlingThread { try { //download to work with newest game state - val gameInfo = OnlineMultiplayerGameSaver().tryDownloadGame(gameId) + val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId) val playerCiv = gameInfo.currentPlayerCiv //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 - val updatedSave = this@EditMultiplayerGameInfoScreen.gameInfo!!.updateCurrentTurn(gameInfo) + val updatedSave = this.gameInfo!!.updateCurrentTurn(gameInfo) GameSaver.saveGame(updatedSave, gameName) - OnlineMultiplayerGameSaver().tryUploadGame(gameInfo, withPreview = true) + OnlineMultiplayer().tryUploadGame(gameInfo, withPreview = true) postCrashHandlingRunnable { popup.close() diff --git a/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt b/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt deleted file mode 100644 index 4ace26c014..0000000000 --- a/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt +++ /dev/null @@ -1,14 +0,0 @@ -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) - } -} \ No newline at end of file diff --git a/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt b/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt index 072c41edfd..b8b4342594 100644 --- a/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt @@ -4,12 +4,12 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.scenes.scene2d.ui.* import com.unciv.logic.* -import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.utils.* -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.logic.multiplayer.OnlineMultiplayer +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.popup.Popup @@ -146,10 +146,11 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { addGameButton.setText("Working...".tr()) addGameButton.disable() - - launchCrashHandling("MultiplayerDownload", runAsDaemon = false) { + crashHandlingThread(name = "MultiplayerDownload") { try { - val gamePreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameId.trim()) + // The tryDownload can take more than 500ms. Therefore, to avoid ANRs, + // we need to run it in a different thread. + val gamePreview = OnlineMultiplayer().tryDownloadGamePreview(gameId.trim()) if (gameName == "") GameSaver.saveGame(gamePreview, gamePreview.gameId) else @@ -159,7 +160,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { } catch (ex: FileNotFoundException) { // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead try { - val gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId.trim()).asPreview() + val gamePreview = OnlineMultiplayer().tryDownloadGame(gameId.trim()).asPreview() if (gameName == "") GameSaver.saveGame(gamePreview, gamePreview.gameId) else @@ -171,13 +172,13 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { 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) { postCrashHandlingRunnable { - val message = when (ex) { - is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds" - else -> "Could not download game!" - } - popup.reuseWith(message, true) + popup.reuseWith("Could not download game!", true) } } postCrashHandlingRunnable { @@ -193,18 +194,18 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { loadingGamePopup.add("Loading latest game state...".tr()) loadingGamePopup.open() - launchCrashHandling("JoinMultiplayerGame") { + crashHandlingThread(name = "JoinMultiplayerGame") { try { val gameId = multiplayerGames[selectedGameFile]!!.gameId - val gameInfo = OnlineMultiplayerGameSaver().tryDownloadGame(gameId) + val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId) postCrashHandlingRunnable { game.loadGame(gameInfo) } - } catch (ex: Exception) { - val message = when (ex) { - is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds" - else -> "Could not download game!" - } + } catch (ex: FileStorageRateLimitReached) { postCrashHandlingRunnable { - loadingGamePopup.reuseWith(message, true) + loadingGamePopup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true) + } + } catch (ex: Exception) { + postCrashHandlingRunnable { + loadingGamePopup.reuseWith("Could not download game!", true) } } } @@ -279,7 +280,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { continue } - launchCrashHandling("loadGameFile") { + crashHandlingThread(name = "loadGameFile") { try { val game = gameSaver.loadGamePreviewFromFile(gameSaveFile) @@ -300,7 +301,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { } catch (usx: UncivShowableException) { //Gets thrown when mods are not installed postCrashHandlingRunnable { - val popup = Popup(this@MultiplayerScreen) + val popup = Popup(this) popup.addGoodSizedLabel(usx.message!! + " in ${gameSaveFile.name()}").row() popup.addCloseButton() popup.open(true) @@ -310,7 +311,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { } } catch (ex: Exception) { postCrashHandlingRunnable { - ToastPopup("Could not refresh!", this@MultiplayerScreen) + ToastPopup("Could not refresh!", this) turnIndicator.clear() turnIndicator.add(ImageGetter.getImage("StatIcons/Malcontent")).size(50f) } @@ -329,11 +330,12 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() { refreshButton.setText("Working...".tr()) refreshButton.disable() - launchCrashHandling("multiplayerGameDownload") { + //One thread for all downloads + crashHandlingThread(name = "multiplayerGameDownload") { for ((fileHandle, gameInfo) in multiplayerGames) { try { // Update game without overriding multiplayer settings - val game = gameInfo.updateCurrentTurn(OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameInfo.gameId)) + val game = gameInfo.updateCurrentTurn(OnlineMultiplayer().tryDownloadGamePreview(gameInfo.gameId)) GameSaver.saveGame(game, fileHandle.name()) multiplayerGames[fileHandle] = game @@ -341,25 +343,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 try { // Update game without overriding multiplayer settings - val game = gameInfo.updateCurrentTurn(OnlineMultiplayerGameSaver().tryDownloadGame(gameInfo.gameId)) + val game = gameInfo.updateCurrentTurn(OnlineMultiplayer().tryDownloadGame(gameInfo.gameId)) GameSaver.saveGame(game, fileHandle.name()) multiplayerGames[fileHandle] = game } catch (ex: Exception) { postCrashHandlingRunnable { - ToastPopup("Could not download game!" + " ${fileHandle.name()}", this@MultiplayerScreen) + ToastPopup("Could not download game!" + " ${fileHandle.name()}", this) } } } catch (ex: FileStorageRateLimitReached) { postCrashHandlingRunnable { - ToastPopup("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", this@MultiplayerScreen) + ToastPopup("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", this) } break // No need to keep trying if rate limit is reached } catch (ex: Exception) { //skipping one is not fatal //Trying to use as many prev. used strings as possible postCrashHandlingRunnable { - ToastPopup("Could not download game!" + " ${fileHandle.name()}", this@MultiplayerScreen) + ToastPopup("Could not download game!" + " ${fileHandle.name()}", this) } } } diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 6a529220ef..7326bcf7ea 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -11,12 +11,12 @@ import com.unciv.UncivGame import com.unciv.logic.* import com.unciv.logic.civilization.PlayerType import com.unciv.logic.map.MapType -import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.FileStorageRateLimitReached +import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.pickerscreens.PickerScreen @@ -160,9 +160,9 @@ class NewGameScreen( rightSideButton.disable() rightSideButton.setText("Working...".tr()) - // Creating a new game can take a while and we don't want ANRs - launchCrashHandling("NewGame", runAsDaemon = false) { - startNewGame() + crashHandlingThread(name = "NewGame") { + // Creating a new game can take a while and we don't want ANRs + newGameThread() } } } @@ -226,7 +226,7 @@ class NewGameScreen( } } - suspend private fun startNewGame() { + private fun newGameThread() { val popup = Popup(this) postCrashHandlingRunnable { popup.addGoodSizedLabel("Working...").row() @@ -255,7 +255,7 @@ class NewGameScreen( if (gameSetupInfo.gameParameters.isOnlineMultiplayer) { newGame.isUpToDate = true // So we don't try to download it from dropbox the second after we upload it - the file is not yet ready for loading! try { - OnlineMultiplayerGameSaver().tryUploadGame(newGame, withPreview = true) + OnlineMultiplayer().tryUploadGame(newGame, withPreview = true) GameSaver.autoSave(newGame) diff --git a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt index 82d405a019..30b2c0ecd5 100644 --- a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt @@ -13,7 +13,7 @@ import com.unciv.models.ruleset.ModOptions import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.utils.* @@ -23,8 +23,6 @@ import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.utils.UncivDateFormat.formatDate import com.unciv.ui.utils.UncivDateFormat.parseDate -import kotlinx.coroutines.Job -import kotlinx.coroutines.isActive import java.util.* import kotlin.collections.HashMap import kotlin.math.max @@ -69,11 +67,11 @@ class ModManagementScreen( private var onlineScrollCurrentY = -1f // cleanup - background processing needs to be stopped on exit and memory freed - private var runningSearchJob: Job? = null + private var runningSearchThread: Thread? = null private var stopBackgroundTasks = false override fun dispose() { // make sure the worker threads will not continue trying their time-intensive job - runningSearchJob?.cancel() + runningSearchThread?.interrupt() stopBackgroundTasks = true super.dispose() } @@ -191,24 +189,20 @@ class ModManagementScreen( * calls itself for the next page of search results */ private fun tryDownloadPage(pageNum: Int) { - runningSearchJob = launchCrashHandling("GitHubSearch") { + runningSearchThread = crashHandlingThread(name="GitHubSearch") { val repoSearch: Github.RepoSearch try { repoSearch = Github.tryGetGithubReposWithTopic(amountPerPage, pageNum)!! } catch (ex: Exception) { postCrashHandlingRunnable { - ToastPopup("Could not download mod list", this@ModManagementScreen) + ToastPopup("Could not download mod list", this) } - runningSearchJob = null - return@launchCrashHandling - } - - if (!isActive) { - return@launchCrashHandling + runningSearchThread = null + return@crashHandlingThread } postCrashHandlingRunnable { addModInfoFromRepoSearch(repoSearch, pageNum) } - runningSearchJob = null + runningSearchThread = null } } @@ -395,14 +389,14 @@ class ModManagementScreen( /** Download and install a mod in the background, called both from the right-bottom button and the URL entry popup */ private fun downloadMod(repo: Github.Repo, postAction: () -> Unit = {}) { - launchCrashHandling("DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions + crashHandlingThread(name="DownloadMod") { // to avoid ANRs - we've learnt our lesson from previous download-related actions try { val modFolder = Github.downloadAndExtract(repo.html_url, repo.default_branch, Gdx.files.local("mods")) ?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something! Github.rewriteModOptions(repo, modFolder) postCrashHandlingRunnable { - ToastPopup("[${repo.name}] Downloaded!", this@ModManagementScreen) + ToastPopup("[${repo.name}] Downloaded!", this) RulesetCache.loadRulesets() RulesetCache[repo.name]?.let { installedModInfo[repo.name] = ModUIData(it) @@ -414,7 +408,7 @@ class ModManagementScreen( } } catch (ex: Exception) { postCrashHandlingRunnable { - ToastPopup("Could not download [${repo.name}]", this@ModManagementScreen) + ToastPopup("Could not download [${repo.name}]", this) postAction() } } @@ -544,7 +538,7 @@ class ModManagementScreen( } internal fun refreshOnlineModTable() { - if (runningSearchJob != null) return // cowardice: prevent concurrent modification, avoid a manager layer + if (runningSearchThread != null) return // cowardice: prevent concurrent modification, avoid a manager layer val newHeaderText = optionsManager.getOnlineHeader() onlineHeaderLabel?.setText(newHeaderText) diff --git a/core/src/com/unciv/ui/popup/ToastPopup.kt b/core/src/com/unciv/ui/popup/ToastPopup.kt index 078c76a62f..1650738ec8 100644 --- a/core/src/com/unciv/ui/popup/ToastPopup.kt +++ b/core/src/com/unciv/ui/popup/ToastPopup.kt @@ -1,10 +1,9 @@ package com.unciv.ui.popup -import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.utils.BaseScreen +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.utils.onClick 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. @@ -24,9 +23,9 @@ class ToastPopup (message: String, screen: BaseScreen, val time: Long = 2000) : } private fun startTimer(){ - launchCrashHandling("ResponsePopup") { - delay(time) - postCrashHandlingRunnable { this@ToastPopup.close() } + crashHandlingThread(name = "ResponsePopup") { + Thread.sleep(time) + postCrashHandlingRunnable { this.close() } } } diff --git a/core/src/com/unciv/ui/saves/LoadGameScreen.kt b/core/src/com/unciv/ui/saves/LoadGameScreen.kt index ef8e9db4a1..5f9f162a64 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -14,7 +14,7 @@ import com.unciv.logic.MissingModsException import com.unciv.logic.UncivShowableException import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.pickerscreens.Github @@ -51,7 +51,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t val loadingPopup = Popup( this) loadingPopup.addGoodSizedLabel("Loading...") loadingPopup.open() - launchCrashHandling("Load Game") { + crashHandlingThread(name = "Load Game") { try { // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread val loadedGame = GameSaver.loadGameByName(selectedSave) @@ -59,7 +59,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t } catch (ex: Exception) { postCrashHandlingRunnable { loadingPopup.close() - val cantLoadGamePopup = Popup(this@LoadGameScreen) + val cantLoadGamePopup = Popup(this) cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() if (ex is UncivShowableException && ex.localizedMessage != null) { // 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() { loadMissingModsButton.isEnabled = false descriptionLabel.setText("Loading...".tr()) - launchCrashHandling("DownloadMods", runAsDaemon = false) { + crashHandlingThread(name="DownloadMods") { try { val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-") for (modName in mods) { @@ -175,7 +175,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t missingModsToLoad = "" loadMissingModsButton.isVisible = false errorLabel.setText("") - ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen) + ToastPopup("Missing mods are downloaded successfully.", this) } } catch (ex: Exception) { handleLoadGameException("Could not load the missing mods!", ex) @@ -205,9 +205,8 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t loadImage.addAction(Actions.rotateBy(360f, 2f)) saveTable.add(loadImage).size(50f) - // Apparently, even just getting the list of saves can cause ANRs - - // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times - launchCrashHandling("GetSaves") { + crashHandlingThread { // Apparently, even jut getting the list of saves can cause ANRs - + // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times // .toList() because otherwise the lastModified will only be checked inside the postRunnable val saves = GameSaver.getSaves().sortedByDescending { it.lastModified() }.toList() @@ -236,7 +235,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t val savedAt = Date(save.lastModified()) var textToSet = save.name() + "\n${"Saved at".tr()}: " + savedAt.formatDate() - launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones + crashHandlingThread { // Even loading the game to get its metadata can take a long time on older phones try { val game = GameSaver.loadGamePreviewFromFile(save) val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index 56aae0293d..7bb9bcb0ed 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -9,7 +9,7 @@ import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.popup.ToastPopup @@ -60,7 +60,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true errorLabel.setText("") saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.disable() - launchCrashHandling("SaveGame", runAsDaemon = false) { + crashHandlingThread(name = "SaveGame") { GameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> if (e == null) { postCrashHandlingRunnable { game.setWorldScreen() } @@ -97,10 +97,10 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true private fun saveGame() { rightSideButton.setText("Saving...".tr()) - launchCrashHandling("SaveGame", runAsDaemon = false) { + crashHandlingThread(name = "SaveGame") { GameSaver.saveGame(gameInfo, gameNameTextField.text) { postCrashHandlingRunnable { - if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen) + if (it != null) ToastPopup("Could not save game!", this) else UncivGame.Current.setWorldScreen() } } diff --git a/core/src/com/unciv/ui/utils/BaseScreen.kt b/core/src/com/unciv/ui/utils/BaseScreen.kt index 64796f6dfd..5fc3795496 100644 --- a/core/src/com/unciv/ui/utils/BaseScreen.kt +++ b/core/src/com/unciv/ui/utils/BaseScreen.kt @@ -124,7 +124,7 @@ abstract class BaseScreen : Screen { /** @return `true` if the screen is narrower than 4:3 landscape */ fun isNarrowerThan4to3() = stage.viewport.screenHeight * 4 > stage.viewport.screenWidth * 3 - fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, onClose: () -> Unit = {}) { - OptionsPopup(this, startingPage, onClose).open(force = true) + fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage) { + OptionsPopup(this, startingPage).open(force = true) } } diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index 2c6f054e84..b656901908 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -26,7 +26,7 @@ import com.unciv.models.* import com.unciv.models.helpers.MapArrowType import com.unciv.models.helpers.MiscArrowTypes import com.unciv.ui.audio.Sounds -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter 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) { val unit = worldScreen.bottomUnitTable.selectedUnit ?: return - launchCrashHandling("WorldScreenClick") { + crashHandlingThread { val tile = tileGroup.tileInfo if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) { @@ -123,7 +123,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap swapMoveUnitToTargetTile(unit, tile) } // If we are in unit-swapping mode, we don't want to move or attack - return@launchCrashHandling + return@crashHandlingThread } 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) { Battle.moveAndAttack(MapUnitCombatant(unit), attackableTile) worldScreen.shouldUpdate = true - return@launchCrashHandling + return@crashHandlingThread } val canUnitReachTile = unit.movement.canReach(tile) if (canUnitReachTile) { moveUnitToTargetTile(listOf(unit), tile) - return@launchCrashHandling + return@crashHandlingThread } } } @@ -214,7 +214,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap val selectedUnit = selectedUnits.first() - launchCrashHandling("TileToMoveTo") { + crashHandlingThread(name = "TileToMoveTo") { // these are the heavy parts, finding where we want to go // Since this runs in a different thread, even if we check movement.canReach() // then it might change until we get to the getTileToMoveTo, so we just try/catch it @@ -224,7 +224,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap } catch (ex: Exception) { println("Exception in getTileToMoveToThisTurn: ${ex.message}") ex.printStackTrace() - return@launchCrashHandling + return@crashHandlingThread } // can't move here postCrashHandlingRunnable { @@ -270,7 +270,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap } private fun addTileOverlaysWithUnitMovement(selectedUnits: List, tileInfo: TileInfo) { - launchCrashHandling("TurnsToGetThere") { + crashHandlingThread(name = "TurnsToGetThere") { /** LibGdx sometimes has these weird errors when you try to edit the UI layout from 2 separate threads. * And so, all UI editing will be done on the main thread. * The only "heavy lifting" that needs to be done is getting the turns to get there, diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index d56e431e05..0447178d6f 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -1,7 +1,5 @@ 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.Input import com.badlogic.gdx.graphics.Color @@ -23,6 +21,7 @@ import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.map.MapVisualization +import com.unciv.logic.multiplayer.FileStorageRateLimitReached import com.unciv.logic.trade.TradeEvaluation import com.unciv.models.Tutorial import com.unciv.models.UncivSound @@ -41,10 +40,8 @@ import com.unciv.ui.utils.UncivDateFormat.formatDate import com.unciv.ui.victoryscreen.VictoryScreen import com.unciv.ui.worldscreen.bottombar.BattleTable import com.unciv.ui.worldscreen.bottombar.TileInfoTable -import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -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.logic.multiplayer.OnlineMultiplayer +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.popup.ExitGamePopup @@ -54,11 +51,6 @@ import com.unciv.ui.popup.hasOpenPopups import com.unciv.ui.worldscreen.minimap.MinimapHolder import com.unciv.ui.worldscreen.unit.UnitActionsTable 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 kotlin.concurrent.timer @@ -98,7 +90,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas private val techButtonHolder = Table() private val diplomacyButtonHolder = Table() private val fogOfWarButton = createFogOfWarButton() - private val nextTurnButton = NextTurnButton(keyPressDispatcher) + private val nextTurnButton = createNextTurnButton() + private var nextTurnAction: () -> Unit = {} private val tutorialTaskTable = Table().apply { background = ImageGetter.getBackground( ImageGetter.getBlue().darken(0.5f)) } @@ -109,9 +102,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas /** Switch for console logging of next turn duration */ private const val consoleLog = false - private lateinit var multiPlayerRefresher: Flow // this object must not be created multiple times - private var multiPlayerRefresherJob: Job? = null + private var multiPlayerRefresher: Timer? = null } init { @@ -203,13 +195,11 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas // restart the timer stopMultiPlayerRefresher() - multiPlayerRefresher = flow { - while (true) { - loadLatestMultiplayerState() - delay(10000) - } + // isDaemon = true, in order to not block the app closing + // DO NOT use Timer() since this seems to (maybe?) translate to com.badlogic.gdx.utils.Timer? Not sure about this. + multiPlayerRefresher = timer("multiPlayerRefresh", true, period = 10000) { + loadLatestMultiplayerState() } - 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 @@ -218,8 +208,9 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } private fun stopMultiPlayerRefresher() { - if (multiPlayerRefresherJob != null) { - multiPlayerRefresherJob?.cancel() + if (multiPlayerRefresher != null) { + multiPlayerRefresher?.cancel() + multiPlayerRefresher?.purge() } } @@ -228,14 +219,14 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas // GameSaver.autoSave, SaveGameScreen.saveGame, LoadGameScreen.rightSideButton.onClick,... val quickSave = { val toast = ToastPopup("Quicksaving...", this) - launchCrashHandling("SaveGame", runAsDaemon = false) { + crashHandlingThread(name = "SaveGame") { GameSaver.saveGame(gameInfo, "QuickSave") { postCrashHandlingRunnable { toast.close() if (it != null) - ToastPopup("Could not save game!", this@WorldScreen) + ToastPopup("Could not save game!", this) else { - ToastPopup("Quicksave successful.", this@WorldScreen) + ToastPopup("Quicksave successful.", this) } } } @@ -244,17 +235,17 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } val quickLoad = { val toast = ToastPopup("Quickloading...", this) - launchCrashHandling("LoadGame") { + crashHandlingThread(name = "SaveGame") { try { val loadedGame = GameSaver.loadGameByName("QuickSave") postCrashHandlingRunnable { toast.close() UncivGame.Current.loadGame(loadedGame) - ToastPopup("Quickload successful.", this@WorldScreen) + ToastPopup("Quickload successful.", this) } } catch (ex: Exception) { postCrashHandlingRunnable { - ToastPopup("Could not load game!", this@WorldScreen) + ToastPopup("Could not load game!", this) } } } @@ -285,12 +276,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas if (!mapHolder.setCenterPosition(capital.location)) game.setScreen(CityScreen(capital)) } - keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { // Game Options - this.openOptionsPopup(onClose = { - mapHolder.reloadMaxZoom() - nextTurnButton.update(hasOpenPopups(), isPlayersTurn, waitingForAutosave) - }) - } + keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { this.openOptionsPopup() } // Game Options keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load keyPressDispatcher[KeyCharAndCode.ctrl('Q')] = { ExitGamePopup(this, true) } // Quit @@ -353,7 +339,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } - private suspend fun loadLatestMultiplayerState() { + private fun loadLatestMultiplayerState() { // Since we're on a background thread, all the UI calls in this func need to run from the // main thread which has a GL context val loadingGamePopup = Popup(this) @@ -363,13 +349,13 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } try { - val latestGame = OnlineMultiplayerGameSaver().tryDownloadGame(gameInfo.gameId) + val latestGame = OnlineMultiplayer().tryDownloadGame(gameInfo.gameId) // 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 // This fixes a bug where for some reason players were waiting for themselves. - if (gameInfo.currentPlayer == latestGame.currentPlayer - && gameInfo.turns == latestGame.turns + if (gameInfo.currentPlayer == latestGame.currentPlayer + && gameInfo.turns == latestGame.turns && latestGame.currentPlayer != gameInfo.getPlayerToViewAs().civName ) { postCrashHandlingRunnable { loadingGamePopup.close() } @@ -394,8 +380,10 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas stopMultiPlayerRefresher() val restartAfter : Long = ex.limitRemainingSeconds.toLong() * 1000 - timer("RestartTimerTimer", true, restartAfter, 0) { - multiPlayerRefresherJob = multiPlayerRefresher.launchIn(CRASH_HANDLING_DAEMON_SCOPE) + timer("RestartTimerTimer", true, restartAfter, 0 ) { + multiPlayerRefresher = timer("multiPlayerRefresh", true, period = 10000) { + loadLatestMultiplayerState() + } } } catch (ex: Throwable) { postCrashHandlingRunnable { @@ -634,6 +622,20 @@ 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) { game.gameInfo = gameInfo @@ -659,8 +661,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas isPlayersTurn = false shouldUpdate = true - // on a separate thread so the user can explore their world while we're passing the turn - launchCrashHandling("NextTurn", runAsDaemon = false) { + + crashHandlingThread(name = "NextTurn") { // on a separate thread so the user can explore their world while we're passing the turn if (consoleLog) println("\nNext turn starting " + Date().formatDate()) val startTime = System.currentTimeMillis() @@ -672,31 +674,31 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas if (originalGameInfo.gameParameters.isOnlineMultiplayer) { try { - OnlineMultiplayerGameSaver().tryUploadGame(gameInfoClone, withPreview = true) + OnlineMultiplayer().tryUploadGame(gameInfoClone, withPreview = true) } catch (ex: FileStorageRateLimitReached) { postCrashHandlingRunnable { - val cantUploadNewGamePopup = Popup(this@WorldScreen) + val cantUploadNewGamePopup = Popup(this) cantUploadNewGamePopup.addGoodSizedLabel("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds").row() cantUploadNewGamePopup.addCloseButton() cantUploadNewGamePopup.open() } } catch (ex: Exception) { postCrashHandlingRunnable { // Since we're changing the UI, that should be done on the main thread - val cantUploadNewGamePopup = Popup(this@WorldScreen) + val cantUploadNewGamePopup = Popup(this) cantUploadNewGamePopup.addGoodSizedLabel("Could not upload game!").row() cantUploadNewGamePopup.addCloseButton() cantUploadNewGamePopup.open() } - this@WorldScreen.isPlayersTurn = true // Since we couldn't push the new game clone, then it's like we never clicked the "next turn" button - this@WorldScreen.shouldUpdate = true - return@launchCrashHandling + isPlayersTurn = true // Since we couldn't push the new game clone, then it's like we never clicked the "next turn" button + shouldUpdate = true + return@crashHandlingThread } } if (game.gameInfo != originalGameInfo) // while this was turning we loaded another game - return@launchCrashHandling + return@crashHandlingThread - this@WorldScreen.game.gameInfo = gameInfoClone + game.gameInfo = gameInfoClone if (consoleLog) println("Next turn took ${System.currentTimeMillis()-startTime}ms") @@ -714,7 +716,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } if (shouldAutoSave) { - val newWorldScreen = this@WorldScreen.game.worldScreen + val newWorldScreen = game.worldScreen newWorldScreen.waitingForAutosave = true newWorldScreen.shouldUpdate = true GameSaver.autoSave(gameInfoClone) { @@ -727,11 +729,27 @@ 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) { - nextTurnButton.update(isSomethingOpen, isPlayersTurn, waitingForAutosave, getNextTurnAction()) + val action: NextTurnAction = 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) } + /** + * 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 { return when { @@ -814,7 +832,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas viewingCiv.hasMovedAutomatedUnits = true isPlayersTurn = false // Disable state changes nextTurnButton.disable() - launchCrashHandling("Move automated units") { + crashHandlingThread(name="Move automated units") { for (unit in viewingCiv.getCivUnits()) unit.doAction() postCrashHandlingRunnable { diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt index 81b6b04f91..4db915403b 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt @@ -13,7 +13,7 @@ import com.unciv.UncivGame import com.unciv.logic.GameSaver import com.unciv.logic.MapSaver import com.unciv.logic.civilization.PlayerType -import com.unciv.logic.multiplayer.storage.SimpleHttp +import com.unciv.logic.multiplayer.SimpleHttp import com.unciv.models.UncivSound import com.unciv.models.metadata.BaseRuleset 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.civilopedia.FormattedLine import com.unciv.ui.civilopedia.MarkupRenderer -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.newgamescreen.TranslatedSelectBox @@ -52,8 +52,7 @@ import com.badlogic.gdx.utils.Array as GdxArray //region Fields class OptionsPopup( private val previousScreen: BaseScreen, - private val selectPage: Int = defaultPage, - private val onClose: () -> Unit = {} + private val selectPage: Int = defaultPage ) : Popup(previousScreen) { private val settings = previousScreen.game.settings private val tabs: TabbedPager @@ -110,7 +109,8 @@ class OptionsPopup( addCloseButton { previousScreen.game.musicController.onChange(null) previousScreen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait) - onClose() + if (previousScreen is WorldScreen) + previousScreen.enableNextTurnButtonAfterOptions() }.padBottom(10f) pack() // Needed to show the background. @@ -136,7 +136,7 @@ class OptionsPopup( (previousScreen.game.screen as BaseScreen).openOptionsPopup(tabs.activePage) } - private fun successfullyConnectedToServer(action: (Boolean, String, Int?) -> Unit){ + private fun successfullyConnectedToServer(action: (Boolean, String)->Unit){ SimpleHttp.sendGetRequest("${settings.multiplayerServer}/isalive", action) } @@ -300,7 +300,7 @@ class OptionsPopup( } popup.open(true) - successfullyConnectedToServer { success, _, _ -> + successfullyConnectedToServer { success: Boolean, _: String -> popup.addGoodSizedLabel(if (success) "Success!" else "Failed!").row() popup.addCloseButton() } @@ -387,7 +387,7 @@ class OptionsPopup( modCheckResultTable.add("Checking mods for errors...".toLabel()).row() modCheckBaseSelect!!.isDisabled = true - launchCrashHandling("ModChecker") { + crashHandlingThread(name="ModChecker") { for (mod in RulesetCache.values.sortedBy { it.name }) { if (base != modCheckWithoutBase && mod.modOptions.isBaseRuleset) continue @@ -800,7 +800,7 @@ class OptionsPopup( errorTable.add("Downloading...".toLabel()) // So the whole game doesn't get stuck while downloading the file - launchCrashHandling("MusicDownload") { + crashHandlingThread(name = "Music") { try { previousScreen.game.musicController.downloadDefaultFile() postCrashHandlingRunnable { @@ -917,7 +917,7 @@ class OptionsPopup( } } - launchCrashHandling("Add Font Select") { + crashHandlingThread(name = "Add Font Select") { // This is a heavy operation and causes ANRs val fonts = GdxArray().apply { add(FontFamilyData.default) @@ -936,7 +936,7 @@ class OptionsPopup( val generateAction: ()->Unit = { tabs.selectPage("Advanced") generateTranslationsButton.setText("Working...".tr()) - launchCrashHandling("WriteTranslations") { + crashHandlingThread { val result = TranslationFileWriter.writeNewTranslationFiles() postCrashHandlingRunnable { // notify about completion diff --git a/core/src/com/unciv/ui/worldscreen/status/NextTurnButton.kt b/core/src/com/unciv/ui/worldscreen/status/NextTurnButton.kt deleted file mode 100644 index b6adecb22b..0000000000 --- a/core/src/com/unciv/ui/worldscreen/status/NextTurnButton.kt +++ /dev/null @@ -1,34 +0,0 @@ -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) \ No newline at end of file diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt index 3608d71563..75af10604f 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt @@ -7,7 +7,7 @@ import com.unciv.UncivGame import com.unciv.logic.map.MapUnit import com.unciv.models.UnitAction import com.unciv.ui.audio.Sounds -import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.images.IconTextButton import com.unciv.ui.utils.* import com.unciv.ui.utils.KeyPressDispatcher.Companion.keyboardAvailable @@ -44,7 +44,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { actionButton.onClick(unitAction.uncivSound, action) if (key != KeyCharAndCode.UNKNOWN) worldScreen.keyPressDispatcher[key] = { - launchCrashHandling("UnitSound") { Sounds.play(unitAction.uncivSound) } + crashHandlingThread(name = "Sound") { Sounds.play(unitAction.uncivSound) } action() worldScreen.mapHolder.removeUnitActionOverlay() } diff --git a/server/src/com/unciv/app/server/UncivServer.kt b/server/src/com/unciv/app/server/UncivServer.kt index c4d869b2b2..a660f88e3f 100644 --- a/server/src/com/unciv/app/server/UncivServer.kt +++ b/server/src/com/unciv/app/server/UncivServer.kt @@ -6,7 +6,6 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.restrictTo import io.ktor.application.* -import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* import io.ktor.server.engine.* @@ -15,7 +14,6 @@ import io.ktor.utils.io.jvm.javaio.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -import java.io.FileNotFoundException internal object UncivServer { @@ -62,10 +60,7 @@ private class UncivServerRunner : CliktCommand() { val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") println("Get file: $fileName") val file = File(fileFolderName, fileName) - if (!file.exists()) { - call.respond(HttpStatusCode.NotFound, "File does not exist") - return@get - } + if (!file.exists()) throw Exception("File does not exist!") val fileText = file.readText() println("Text read: $fileText") call.respondText(fileText) @@ -73,10 +68,7 @@ private class UncivServerRunner : CliktCommand() { delete("/files/{fileName}") { val fileName = call.parameters["fileName"] ?: throw Exception("No fileName!") val file = File(fileFolderName, fileName) - if (!file.exists()) { - call.respond(HttpStatusCode.NotFound, "File does not exist") - return@delete - } + if (!file.exists()) throw Exception("File does not exist!") file.delete() } }