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