Use kotlin coroutines instead of raw threads (+ refactorings) (#6801)

* Refactor: Move classes into their own files

* Handle 404 FileNotFound in UncivServer

* Refactor: Rename method

* Make GameSaver.saveGame throw the exception by default if it is not handled

* Add possibility to work with FileHandles in GameSaver

* Make OnlineMultiplayerGameSaver load the multiplayerServer setting each time it does something

This theoretically makes it unnecessary for the OnlineMultiplayerGameSaver to be re-instantiated each time it is used

* Refactor: Remove duplicated code

* Refactor: Extract NextTurnButton from WorldScreen

* Refactor: Remove WorldScreen-specific code from OptionsPopup

* Use kotlin coroutines everywhere instead of plain threads

This probably also cuts down on our raw thread usage, improving performance, since we now use a cached thread pool (in addition to coroutines being able to reuse threads anyway)

* Improve deep load from notification

* Refactor: Give music download coroutine the proper name
This commit is contained in:
Timo T 2022-05-14 23:52:45 +02:00 committed by GitHub
parent 47728afafa
commit f8e0f572e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 534 additions and 379 deletions

View File

@ -20,7 +20,9 @@ android {
}
}
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 {
applicationId = "com.unciv.app"

View File

@ -15,9 +15,10 @@ 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.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
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.PrintWriter
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 configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5)
val fileStorage = inputData.getString(FILE_STORAGE)
@ -253,7 +254,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
continue
try {
val gamePreview = OnlineMultiplayer(fileStorage).tryDownloadGamePreview(gameId)
val gamePreview = OnlineMultiplayerGameSaver(fileStorage).tryDownloadGamePreview(gameId)
val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer)
//Save game so MultiplayerScreen gets updated
@ -302,7 +303,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
with(NotificationManagerCompat.from(applicationContext)) {
cancel(NOTIFICATION_ID_SERVICE)
}
return Result.failure()
return@runBlocking Result.failure()
} else {
if (showPersistNotific) { showPersistentNotification(applicationContext,
applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) }
@ -313,9 +314,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 Result.failure()
return@runBlocking Result.failure()
}
return Result.success()
return@runBlocking Result.success()
}
private fun getStackTraceString(ex: Exception): String {

View File

@ -93,6 +93,7 @@ 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")
@ -119,6 +120,7 @@ project(":core") {
dependencies {
"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.models.metadata.GameSetupInfo
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.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()
crashHandlingThread(name = "ShowMapBackground") {
launchCrashHandling("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, newMap) {}
val mapHolder = EditorMapHolder(this@MainMenuScreen, 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()
crashHandlingThread {
launchCrashHandling("autoLoadGame") {
// 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)
ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen)
}
}
@ -181,7 +181,7 @@ class MainMenuScreen: BaseScreen() {
savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName)
} catch (oom: OutOfMemoryError) {
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
// 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@crashHandlingThread
return@launchCrashHandling
} catch (ex: Exception) {
postCrashHandlingRunnable {
loadingPopup.close()
ToastPopup("Cannot resume game!", this)
ToastPopup("Cannot resume game!", this@MainMenuScreen)
}
return@crashHandlingThread
return@launchCrashHandling
}
}
@ -215,14 +215,14 @@ class MainMenuScreen: BaseScreen() {
private fun quickstartNewGame() {
ToastPopup("Working...", this)
val errorText = "Cannot start game with the default new game parameters!"
crashHandlingThread {
launchCrashHandling("QuickStart") {
val newGame: GameInfo
// Can fail when starting the game...
try {
newGame = GameStarter.startNewGame(GameSetupInfo.fromSettings("Chieftain"))
} catch (ex: Exception) {
postCrashHandlingRunnable { ToastPopup(errorText, this) }
return@crashHandlingThread
postCrashHandlingRunnable { ToastPopup(errorText, this@MainMenuScreen) }
return@launchCrashHandling
}
// ...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)
ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen)
} 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.worldscreen.PlayerReadyScreen
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.crashhandling.crashHandlingThread
import com.unciv.ui.crashhandling.closeExecutors
import com.unciv.ui.crashhandling.launchCrashHandling
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() {
@ -114,7 +117,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
Gdx.graphics.isContinuousRendering = settings.continuousRendering
crashHandlingThread(name = "LoadJSON") {
launchCrashHandling("LoadJSON") {
RulesetCache.loadRulesets(printOutput = true)
translations.tryReadTranslationForCurrentLanguage()
translations.loadPercentageCompleteOfLanguages()
@ -169,13 +172,26 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
Gdx.graphics.requestRendering()
}
fun tryLoadDeepLinkedGame() {
fun tryLoadDeepLinkedGame() = launchCrashHandling("LoadDeepLinkedGame") {
if (deepLinkedMultiplayerGame != null) {
postCrashHandlingRunnable {
setScreen(LoadDeepLinkScreen())
}
try {
val onlineGame = OnlineMultiplayer().tryDownloadGame(deepLinkedMultiplayerGame!!)
val onlineGame = OnlineMultiplayerGameSaver().tryDownloadGame(deepLinkedMultiplayerGame!!)
postCrashHandlingRunnable {
loadGame(onlineGame)
}
} 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()
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()

View File

@ -4,9 +4,8 @@ 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.crashHandlingThread
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.saves.Gzip
import java.io.File
@ -31,20 +30,20 @@ object GameSaver {
//endregion
//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 {
val localFile = Gdx.files.local("${getSubfolder(multiplayer)}/$GameName")
val localFile = Gdx.files.local("${getSavefolder(multiplayer)}/$GameName")
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
return externalFile
}
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
return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}").list().asSequence()
return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}").list().asSequence()
}
fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null
@ -56,12 +55,21 @@ object GameSaver {
//endregion
//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 {
getSave(GameName).writeString(gameInfoToString(game), false)
saveCompletionCallback?.invoke(null)
file.writeString(gameInfoToString(game), false)
saveCompletionCallback(null)
} catch (ex: Exception) {
saveCompletionCallback?.invoke(ex)
saveCompletionCallback(ex)
}
}
@ -71,7 +79,7 @@ object GameSaver {
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 {
return Gzip.zip(json().toJson(game))
}
@ -79,12 +87,21 @@ object GameSaver {
/**
* 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 {
json().toJson(game, getSave(GameName, true))
saveCompletionCallback?.invoke(null)
json().toJson(game, file)
saveCompletionCallback(null)
} 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 {
return json().fromJson(GameInfoPreview::class.java, Gzip.unzip(gameData))
}
@ -184,7 +201,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
crashHandlingThread(name = autoSaveFileName) {
launchCrashHandling(autoSaveFileName, runAsDaemon = false) {
autoSaveSingleThreaded(gameInfo)
// do this on main thread
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.ui.utils.UncivDateFormat.parseDate
@ -11,7 +11,7 @@ import kotlin.collections.ArrayList
import kotlin.concurrent.timer
object DropBox: IFileStorage {
object DropBox: FileStorage {
private var remainingRateLimitSeconds = 0
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(
url="https://api.dropboxapi.com/2/files/get_metadata",
data="{\"path\":\"${getLocalGameLocation(fileName)}\"}",
@ -124,8 +124,8 @@ object DropBox: IFileStorage {
throw FileStorageRateLimitReached(remainingRateLimitSeconds)
}
fun getFolderList(folder: String): ArrayList<IFileMetaData> {
val folderList = ArrayList<IFileMetaData>()
fun getFolderList(folder: String): ArrayList<FileMetaData> {
val folderList = ArrayList<FileMetaData>()
// 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: IFileStorage {
}
@Suppress("PropertyName")
private class MetaData: IFileMetaData {
private class MetaData: FileMetaData {
var name = ""
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.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 = OnlineMultiplayer().fileStorage.getFileMetaData(fileName)
val metaData = OnlineMultiplayerGameSaver().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 {
OnlineMultiplayer().fileStorage.deleteFile(fileName)
OnlineMultiplayerGameSaver().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 {
OnlineMultiplayer().fileStorage.saveFileData(fileName, Gzip.zip(json().toJson(LockFile())), false)
OnlineMultiplayerGameSaver().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
OnlineMultiplayer().fileStorage.deleteFile("${gameInfo.gameId}_Lock")
OnlineMultiplayerGameSaver().fileStorage().deleteFile("${gameInfo.gameId}_Lock")
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.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)->Unit) {
fun sendGetRequest(url: String, action: (success: Boolean, result: String, code: Int?)->Unit) {
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)
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")
action(false, "Bad URL", null)
return
}
@ -43,14 +43,14 @@ object SimpleHttp {
}
val text = BufferedReader(InputStreamReader(inputStream)).readText()
action(true, text)
action(true, text, responseCode)
} catch (t: Throwable) {
println(t.message)
val errorMessageToReturn =
if (errorStream != null) BufferedReader(InputStreamReader(errorStream)).readText()
else t.message!!
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.GameStarter
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.math.max
import kotlin.time.ExperimentalTime
@ -40,12 +42,12 @@ class Simulation(
}
}
fun start() {
fun start() = runBlocking {
startTime = System.currentTimeMillis()
val threads: ArrayList<Thread> = ArrayList()
val jobs: ArrayList<Job> = ArrayList()
for (threadId in 1..threadsNumber) {
threads.add(crashHandlingThread {
jobs.add(launchCrashHandling("simulation-${threadId}") {
for (i in 1..simulationsPerThread) {
val gameInfo = GameStarter.startNewGame(GameSetupInfo(newGameInfo))
gameInfo.simulateMaxTurns = maxTurns
@ -66,8 +68,8 @@ class Simulation(
}
})
}
// wait for all threads to finish
for (thread in threads) thread.join()
// wait for all to finish
for (job in jobs) job.join()
endTime = System.currentTimeMillis()
}

View File

@ -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.DropBox
import com.unciv.logic.multiplayer.storage.DropBox
import java.util.*
import kotlin.concurrent.thread
import kotlin.concurrent.timer

View File

@ -6,7 +6,8 @@ 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.crashHandlingThread
import com.unciv.ui.crashhandling.launchCrashHandling
import kotlinx.coroutines.delay
import java.io.File
/*
@ -164,10 +165,10 @@ object Sounds {
val initialDelay = if (isFresh && Gdx.app.type == Application.ApplicationType.Android) 40 else 0
if (initialDelay > 0 || resource.play(volume) == -1L) {
crashHandlingThread(name = "DelayedSound") {
Thread.sleep(initialDelay.toLong())
launchCrashHandling("DelayedSound") {
delay(initialDelay.toLong())
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.translations.tr
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.images.ImageGetter
import com.unciv.ui.popup.Popup
@ -207,7 +207,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
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.
val constructionButtonDTOList = getConstructionButtonDTOs()
postCrashHandlingRunnable {

View File

@ -2,10 +2,45 @@ 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. */
fun crashHandlingThread(
private fun crashHandlingThread(
start: Boolean = true,
isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null,
@ -25,3 +60,24 @@ 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 <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.GameSaver
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.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.*
import com.unciv.logic.multiplayer.OnlineMultiplayer
import com.unciv.ui.crashhandling.crashHandlingThread
import com.unciv.ui.crashhandling.launchCrashHandling
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()
crashHandlingThread {
launchCrashHandling("Resign", runAsDaemon = false) {
try {
//download to work with newest game state
val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId)
val gameInfo = OnlineMultiplayerGameSaver().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.gameInfo!!.updateCurrentTurn(gameInfo)
val updatedSave = this@EditMultiplayerGameInfoScreen.gameInfo!!.updateCurrentTurn(gameInfo)
GameSaver.saveGame(updatedSave, gameName)
OnlineMultiplayer().tryUploadGame(gameInfo, withPreview = true)
OnlineMultiplayerGameSaver().tryUploadGame(gameInfo, withPreview = true)
postCrashHandlingRunnable {
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.scenes.scene2d.ui.*
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.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.*
import com.unciv.logic.multiplayer.OnlineMultiplayer
import com.unciv.ui.crashhandling.crashHandlingThread
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.Popup
@ -146,11 +146,10 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
addGameButton.setText("Working...".tr())
addGameButton.disable()
crashHandlingThread(name = "MultiplayerDownload") {
launchCrashHandling("MultiplayerDownload", runAsDaemon = false) {
try {
// 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())
val gamePreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameId.trim())
if (gameName == "")
GameSaver.saveGame(gamePreview, gamePreview.gameId)
else
@ -160,7 +159,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 = OnlineMultiplayer().tryDownloadGame(gameId.trim()).asPreview()
val gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId.trim()).asPreview()
if (gameName == "")
GameSaver.saveGame(gamePreview, gamePreview.gameId)
else
@ -172,13 +171,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 {
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 {
@ -194,18 +193,18 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
loadingGamePopup.add("Loading latest game state...".tr())
loadingGamePopup.open()
crashHandlingThread(name = "JoinMultiplayerGame") {
launchCrashHandling("JoinMultiplayerGame") {
try {
val gameId = multiplayerGames[selectedGameFile]!!.gameId
val gameInfo = OnlineMultiplayer().tryDownloadGame(gameId)
val gameInfo = OnlineMultiplayerGameSaver().tryDownloadGame(gameId)
postCrashHandlingRunnable { game.loadGame(gameInfo) }
} catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable {
loadingGamePopup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true)
}
} catch (ex: Exception) {
val message = when (ex) {
is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds"
else -> "Could not download game!"
}
postCrashHandlingRunnable {
loadingGamePopup.reuseWith("Could not download game!", true)
loadingGamePopup.reuseWith(message, true)
}
}
}
@ -280,7 +279,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
continue
}
crashHandlingThread(name = "loadGameFile") {
launchCrashHandling("loadGameFile") {
try {
val game = gameSaver.loadGamePreviewFromFile(gameSaveFile)
@ -301,7 +300,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
} catch (usx: UncivShowableException) {
//Gets thrown when mods are not installed
postCrashHandlingRunnable {
val popup = Popup(this)
val popup = Popup(this@MultiplayerScreen)
popup.addGoodSizedLabel(usx.message!! + " in ${gameSaveFile.name()}").row()
popup.addCloseButton()
popup.open(true)
@ -311,7 +310,7 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
}
} catch (ex: Exception) {
postCrashHandlingRunnable {
ToastPopup("Could not refresh!", this)
ToastPopup("Could not refresh!", this@MultiplayerScreen)
turnIndicator.clear()
turnIndicator.add(ImageGetter.getImage("StatIcons/Malcontent")).size(50f)
}
@ -330,12 +329,11 @@ class MultiplayerScreen(previousScreen: BaseScreen) : PickerScreen() {
refreshButton.setText("Working...".tr())
refreshButton.disable()
//One thread for all downloads
crashHandlingThread(name = "multiplayerGameDownload") {
launchCrashHandling("multiplayerGameDownload") {
for ((fileHandle, gameInfo) in multiplayerGames) {
try {
// 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())
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
try {
// 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())
multiplayerGames[fileHandle] = game
} catch (ex: Exception) {
postCrashHandlingRunnable {
ToastPopup("Could not download game!" + " ${fileHandle.name()}", this)
ToastPopup("Could not download game!" + " ${fileHandle.name()}", this@MultiplayerScreen)
}
}
} catch (ex: FileStorageRateLimitReached) {
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
} 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)
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.civilization.PlayerType
import com.unciv.logic.map.MapType
import com.unciv.logic.multiplayer.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.OnlineMultiplayer
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.RulesetCache
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.images.ImageGetter
import com.unciv.ui.pickerscreens.PickerScreen
@ -160,9 +160,9 @@ class NewGameScreen(
rightSideButton.disable()
rightSideButton.setText("Working...".tr())
crashHandlingThread(name = "NewGame") {
// 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)
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 {
OnlineMultiplayer().tryUploadGame(newGame, withPreview = true)
OnlineMultiplayerGameSaver().tryUploadGame(newGame, withPreview = true)
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.RulesetCache
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.images.ImageGetter
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.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
@ -67,11 +69,11 @@ class ModManagementScreen(
private var onlineScrollCurrentY = -1f
// 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
override fun dispose() {
// make sure the worker threads will not continue trying their time-intensive job
runningSearchThread?.interrupt()
runningSearchJob?.cancel()
stopBackgroundTasks = true
super.dispose()
}
@ -189,20 +191,24 @@ class ModManagementScreen(
* calls itself for the next page of search results
*/
private fun tryDownloadPage(pageNum: Int) {
runningSearchThread = crashHandlingThread(name="GitHubSearch") {
runningSearchJob = launchCrashHandling("GitHubSearch") {
val repoSearch: Github.RepoSearch
try {
repoSearch = Github.tryGetGithubReposWithTopic(amountPerPage, pageNum)!!
} catch (ex: Exception) {
postCrashHandlingRunnable {
ToastPopup("Could not download mod list", this)
ToastPopup("Could not download mod list", this@ModManagementScreen)
}
runningSearchThread = null
return@crashHandlingThread
runningSearchJob = null
return@launchCrashHandling
}
if (!isActive) {
return@launchCrashHandling
}
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 */
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 {
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)
ToastPopup("[${repo.name}] Downloaded!", this@ModManagementScreen)
RulesetCache.loadRulesets()
RulesetCache[repo.name]?.let {
installedModInfo[repo.name] = ModUIData(it)
@ -408,7 +414,7 @@ class ModManagementScreen(
}
} catch (ex: Exception) {
postCrashHandlingRunnable {
ToastPopup("Could not download [${repo.name}]", this)
ToastPopup("Could not download [${repo.name}]", this@ModManagementScreen)
postAction()
}
}
@ -538,7 +544,7 @@ class ModManagementScreen(
}
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()
onlineHeaderLabel?.setText(newHeaderText)

View File

@ -1,9 +1,10 @@
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.
@ -23,9 +24,9 @@ class ToastPopup (message: String, screen: BaseScreen, val time: Long = 2000) :
}
private fun startTimer(){
crashHandlingThread(name = "ResponsePopup") {
Thread.sleep(time)
postCrashHandlingRunnable { this.close() }
launchCrashHandling("ResponsePopup") {
delay(time)
postCrashHandlingRunnable { this@ToastPopup.close() }
}
}

View File

@ -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.crashHandlingThread
import com.unciv.ui.crashhandling.launchCrashHandling
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()
crashHandlingThread(name = "Load Game") {
launchCrashHandling("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)
val cantLoadGamePopup = Popup(this@LoadGameScreen)
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())
crashHandlingThread(name="DownloadMods") {
launchCrashHandling("DownloadMods", runAsDaemon = false) {
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)
ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen)
}
} catch (ex: Exception) {
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))
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
launchCrashHandling("GetSaves") {
// .toList() because otherwise the lastModified will only be checked inside the postRunnable
val saves = GameSaver.getSaves().sortedByDescending { it.lastModified() }.toList()
@ -235,7 +236,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
val savedAt = Date(save.lastModified())
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 {
val game = GameSaver.loadGamePreviewFromFile(save)
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.GameSaver
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.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()
crashHandlingThread(name = "SaveGame") {
launchCrashHandling("SaveGame", runAsDaemon = false) {
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())
crashHandlingThread(name = "SaveGame") {
launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGame(gameInfo, gameNameTextField.text) {
postCrashHandlingRunnable {
if (it != null) ToastPopup("Could not save game!", this)
if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen)
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 */
fun isNarrowerThan4to3() = stage.viewport.screenHeight * 4 > stage.viewport.screenWidth * 3
fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage) {
OptionsPopup(this, startingPage).open(force = true)
fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, onClose: () -> Unit = {}) {
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.translations.tr
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.ImageGetter
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) {
this.addListener(object : ClickListener() {
override fun clicked(event: InputEvent?, x: Float, y: Float) {
crashHandlingThread(name = "Sound") { Sounds.play(sound) }
launchCrashHandling("Sound") { Sounds.play(sound) }
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.MiscArrowTypes
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.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
crashHandlingThread {
launchCrashHandling("WorldScreenClick") {
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@crashHandlingThread
return@launchCrashHandling
}
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@crashHandlingThread
return@launchCrashHandling
}
val canUnitReachTile = unit.movement.canReach(tile)
if (canUnitReachTile) {
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()
crashHandlingThread(name = "TileToMoveTo") {
launchCrashHandling("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@crashHandlingThread
return@launchCrashHandling
} // can't move here
postCrashHandlingRunnable {
@ -270,7 +270,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
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.
* 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,

View File

@ -1,5 +1,7 @@
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
@ -21,7 +23,6 @@ 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
@ -40,8 +41,10 @@ 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.OnlineMultiplayer
import com.unciv.ui.crashhandling.crashHandlingThread
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.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter
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.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
@ -90,8 +98,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
private val techButtonHolder = Table()
private val diplomacyButtonHolder = Table()
private val fogOfWarButton = createFogOfWarButton()
private val nextTurnButton = createNextTurnButton()
private var nextTurnAction: () -> Unit = {}
private val nextTurnButton = NextTurnButton(keyPressDispatcher)
private val tutorialTaskTable = Table().apply { background = ImageGetter.getBackground(
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 */
private const val consoleLog = false
private lateinit var multiPlayerRefresher: Flow<Unit>
// this object must not be created multiple times
private var multiPlayerRefresher: Timer? = null
private var multiPlayerRefresherJob: Job? = null
}
init {
@ -195,12 +203,14 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// restart the timer
stopMultiPlayerRefresher()
// 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) {
multiPlayerRefresher = flow {
while (true) {
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
// know what the viewing civ is.
@ -208,9 +218,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
}
private fun stopMultiPlayerRefresher() {
if (multiPlayerRefresher != null) {
multiPlayerRefresher?.cancel()
multiPlayerRefresher?.purge()
if (multiPlayerRefresherJob != null) {
multiPlayerRefresherJob?.cancel()
}
}
@ -219,14 +228,14 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
// GameSaver.autoSave, SaveGameScreen.saveGame, LoadGameScreen.rightSideButton.onClick,...
val quickSave = {
val toast = ToastPopup("Quicksaving...", this)
crashHandlingThread(name = "SaveGame") {
launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGame(gameInfo, "QuickSave") {
postCrashHandlingRunnable {
toast.close()
if (it != null)
ToastPopup("Could not save game!", this)
ToastPopup("Could not save game!", this@WorldScreen)
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 toast = ToastPopup("Quickloading...", this)
crashHandlingThread(name = "SaveGame") {
launchCrashHandling("LoadGame") {
try {
val loadedGame = GameSaver.loadGameByName("QuickSave")
postCrashHandlingRunnable {
toast.close()
UncivGame.Current.loadGame(loadedGame)
ToastPopup("Quickload successful.", this)
ToastPopup("Quickload successful.", this@WorldScreen)
}
} catch (ex: Exception) {
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))
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('L')] = { game.setScreen(LoadGameScreen(this)) } // Load
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
// main thread which has a GL context
val loadingGamePopup = Popup(this)
@ -349,7 +363,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
}
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
// 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
timer("RestartTimerTimer", true, restartAfter, 0) {
multiPlayerRefresher = timer("multiPlayerRefresh", true, period = 10000) {
loadLatestMultiplayerState()
}
multiPlayerRefresherJob = multiPlayerRefresher.launchIn(CRASH_HANDLING_DAEMON_SCOPE)
}
} catch (ex: Throwable) {
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) {
game.gameInfo = gameInfo
@ -661,8 +659,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
isPlayersTurn = false
shouldUpdate = true
crashHandlingThread(name = "NextTurn") { // on a separate thread so the user can explore their world while we're passing the turn
// on a separate thread so the user can explore their world while we're passing the turn
launchCrashHandling("NextTurn", runAsDaemon = false) {
if (consoleLog)
println("\nNext turn starting " + Date().formatDate())
val startTime = System.currentTimeMillis()
@ -674,31 +672,31 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
if (originalGameInfo.gameParameters.isOnlineMultiplayer) {
try {
OnlineMultiplayer().tryUploadGame(gameInfoClone, withPreview = true)
OnlineMultiplayerGameSaver().tryUploadGame(gameInfoClone, withPreview = true)
} catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable {
val cantUploadNewGamePopup = Popup(this)
val cantUploadNewGamePopup = Popup(this@WorldScreen)
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)
val cantUploadNewGamePopup = Popup(this@WorldScreen)
cantUploadNewGamePopup.addGoodSizedLabel("Could not upload game!").row()
cantUploadNewGamePopup.addCloseButton()
cantUploadNewGamePopup.open()
}
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
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
}
}
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)
println("Next turn took ${System.currentTimeMillis()-startTime}ms")
@ -716,7 +714,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
}
if (shouldAutoSave) {
val newWorldScreen = game.worldScreen
val newWorldScreen = this@WorldScreen.game.worldScreen
newWorldScreen.waitingForAutosave = true
newWorldScreen.shouldUpdate = true
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) {
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.update(isSomethingOpen, isPlayersTurn, waitingForAutosave, getNextTurnAction())
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 {
@ -832,7 +814,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
viewingCiv.hasMovedAutomatedUnits = true
isPlayersTurn = false // Disable state changes
nextTurnButton.disable()
crashHandlingThread(name="Move automated units") {
launchCrashHandling("Move automated units") {
for (unit in viewingCiv.getCivUnits())
unit.doAction()
postCrashHandlingRunnable {

View File

@ -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.SimpleHttp
import com.unciv.logic.multiplayer.storage.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.crashHandlingThread
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.newgamescreen.TranslatedSelectBox
@ -52,7 +52,8 @@ import com.badlogic.gdx.utils.Array as GdxArray
//region Fields
class OptionsPopup(
private val previousScreen: BaseScreen,
private val selectPage: Int = defaultPage
private val selectPage: Int = defaultPage,
private val onClose: () -> Unit = {}
) : Popup(previousScreen) {
private val settings = previousScreen.game.settings
private val tabs: TabbedPager
@ -109,8 +110,7 @@ class OptionsPopup(
addCloseButton {
previousScreen.game.musicController.onChange(null)
previousScreen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait)
if (previousScreen is WorldScreen)
previousScreen.enableNextTurnButtonAfterOptions()
onClose()
}.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)->Unit){
private fun successfullyConnectedToServer(action: (Boolean, String, Int?) -> Unit){
SimpleHttp.sendGetRequest("${settings.multiplayerServer}/isalive", action)
}
@ -300,7 +300,7 @@ class OptionsPopup(
}
popup.open(true)
successfullyConnectedToServer { success: Boolean, _: String ->
successfullyConnectedToServer { success, _, _ ->
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
crashHandlingThread(name="ModChecker") {
launchCrashHandling("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
crashHandlingThread(name = "Music") {
launchCrashHandling("MusicDownload") {
try {
previousScreen.game.musicController.downloadDefaultFile()
postCrashHandlingRunnable {
@ -917,7 +917,7 @@ class OptionsPopup(
}
}
crashHandlingThread(name = "Add Font Select") {
launchCrashHandling("Add Font Select") {
// This is a heavy operation and causes ANRs
val fonts = GdxArray<FontFamilyData>().apply {
add(FontFamilyData.default)
@ -936,7 +936,7 @@ class OptionsPopup(
val generateAction: ()->Unit = {
tabs.selectPage("Advanced")
generateTranslationsButton.setText("Working...".tr())
crashHandlingThread {
launchCrashHandling("WriteTranslations") {
val result = TranslationFileWriter.writeNewTranslationFiles()
postCrashHandlingRunnable {
// 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.models.UnitAction
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.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] = {
crashHandlingThread(name = "Sound") { Sounds.play(unitAction.uncivSound) }
launchCrashHandling("UnitSound") { Sounds.play(unitAction.uncivSound) }
action()
worldScreen.mapHolder.removeUnitActionOverlay()
}

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.restrictTo
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
@ -14,6 +15,7 @@ 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 {
@ -60,7 +62,10 @@ 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()) throw Exception("File does not exist!")
if (!file.exists()) {
call.respond(HttpStatusCode.NotFound, "File does not exist")
return@get
}
val fileText = file.readText()
println("Text read: $fileText")
call.respondText(fileText)
@ -68,7 +73,10 @@ private class UncivServerRunner : CliktCommand() {
delete("/files/{fileName}") {
val fileName = call.parameters["fileName"] ?: throw Exception("No 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()
}
}