mirror of
https://github.com/yairm210/Unciv.git
synced 2024-12-22 19:54:24 +07:00
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:
parent
47728afafa
commit
f8e0f572e4
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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!!)
|
||||
loadGame(onlineGame)
|
||||
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()
|
||||
|
@ -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 )
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 = ""
|
||||
|
34
core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt
Normal file
34
core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
|
14
core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt
Normal file
14
core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
// Creating a new game can take a while and we don't want ANRs
|
||||
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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 -
|
||||
// not sure how many saves these guys had but Google Play reports this to have happened hundreds of times
|
||||
// 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() }
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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,11 +203,13 @@ 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) {
|
||||
loadLatestMultiplayerState()
|
||||
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
|
||||
@ -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,13 +363,13 @@ 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
|
||||
// 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() }
|
||||
@ -380,10 +394,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
stopMultiPlayerRefresher()
|
||||
val restartAfter : Long = ex.limitRemainingSeconds.toLong() * 1000
|
||||
|
||||
timer("RestartTimerTimer", true, restartAfter, 0 ) {
|
||||
multiPlayerRefresher = timer("multiPlayerRefresh", true, period = 10000) {
|
||||
loadLatestMultiplayerState()
|
||||
}
|
||||
timer("RestartTimerTimer", true, restartAfter, 0) {
|
||||
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 {
|
||||
|
@ -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
|
||||
|
34
core/src/com/unciv/ui/worldscreen/status/NextTurnButton.kt
Normal file
34
core/src/com/unciv/ui/worldscreen/status/NextTurnButton.kt
Normal 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)
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user