diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index ed779b2e4d..293590e935 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -121,6 +121,7 @@ class GameInfo { * @throws NoSuchElementException if no civ of than name is in the game (alive or dead)! */ fun getCivilization(civName: String) = civilizations.first { it.civName == civName } fun getCurrentPlayerCivilization() = currentPlayerCiv + fun getCivilizationsAsPreviews() = civilizations.map { it.asPreview() }.toMutableList() /** Get barbarian civ * @throws NoSuchElementException in no-barbarians games! */ fun getBarbarianCivilization() = getCivilization(Constants.barbarians) @@ -367,15 +368,51 @@ class GameInfo { } //endregion + + fun asPreview() = GameInfoPreview(this) } -// reduced variant only for load preview -class GameInfoPreview { +/** + * Reduced variant of GameInfo used for load preview and multiplayer saves. + * Contains additional data for multiplayer settings. + */ +class GameInfoPreview() { var civilizations = mutableListOf() var difficulty = "Chieftain" var gameParameters = GameParameters() var turns = 0 var gameId = "" var currentPlayer = "" + var currentTurnStartTime = 0L + var turnNotification = true //used as setting in the MultiplayerScreen + + /** + * Converts a GameInfo object (can be uninitialized) into a GameInfoPreview object. + * Sets all multiplayer settings to default. + */ + constructor(gameInfo: GameInfo) : this() { + civilizations = gameInfo.getCivilizationsAsPreviews() + difficulty = gameInfo.difficulty + gameParameters = gameInfo.gameParameters + turns = gameInfo.turns + gameId = gameInfo.gameId + currentPlayer = gameInfo.currentPlayer + currentTurnStartTime = gameInfo.currentTurnStartTime + } + fun getCivilization(civName: String) = civilizations.first { it.civName == civName } + + /** + * Updates the current player and turn information in the GameInfoPreview object with the help of a + * GameInfo object (can be uninitialized). + */ + fun updateCurrentTurn(gameInfo: GameInfo) : GameInfoPreview { + currentPlayer = gameInfo.currentPlayer + turns = gameInfo.turns + currentTurnStartTime = gameInfo.currentTurnStartTime + //We update the civilizations in case someone is removed from the game (resign/kick) + civilizations = gameInfo.getCivilizationsAsPreviews() + + return this + } } diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index b5c44c8677..67b0f8ff82 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -38,9 +38,21 @@ object GameSaver { return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}").list().asSequence() } - fun saveGame(game: GameInfo, GameName: String, multiplayer: Boolean = false, saveCompletionCallback: ((Exception?) -> Unit)? = null) { + fun saveGame(game: GameInfo, GameName: String, saveCompletionCallback: ((Exception?) -> Unit)? = null) { try { - json().toJson(game, getSave(GameName, multiplayer)) + json().toJson(game, getSave(GameName)) + saveCompletionCallback?.invoke(null) + } catch (ex: Exception) { + saveCompletionCallback?.invoke(ex) + } + } + + /** + * Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder + */ + fun saveGame(game: GameInfoPreview, GameName: String, saveCompletionCallback: ((Exception?) -> Unit)? = null) { + try { + json().toJson(game, getSave(GameName, true)) saveCompletionCallback?.invoke(null) } catch (ex: Exception) { saveCompletionCallback?.invoke(ex) diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 0753b488e8..2bb54ff28f 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -1212,14 +1212,27 @@ class CivilizationInfo { } //endregion + + fun asPreview() = CivilizationInfoPreview(this) } -// reduced variant only for load preview -class CivilizationInfoPreview { +/** + * Reduced variant of CivilizationInfo used for load preview. + */ +class CivilizationInfoPreview() { var civName = "" var playerType = PlayerType.AI var playerId = "" fun isPlayerCivilization() = playerType == PlayerType.Human + + /** + * Converts a CivilizationInfo object (can be uninitialized) into a CivilizationInfoPreview object. + */ + constructor(civilizationInfo: CivilizationInfo) : this() { + civName = civilizationInfo.civName + playerType = civilizationInfo.playerType + playerId = civilizationInfo.playerId + } } enum class CivFlags { diff --git a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt index 50a02a1421..cf5ef88e56 100644 --- a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt @@ -3,7 +3,7 @@ package com.unciv.ui import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.TextField -import com.unciv.logic.GameInfo +import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameSaver import com.unciv.logic.civilization.PlayerType import com.unciv.models.translations.tr @@ -14,7 +14,7 @@ import kotlin.concurrent.thread /** Subscreen of MultiplayerScreen to edit and delete saves * backScreen is used for getting back to the MultiplayerScreen so it doesn't have to be created over and over again */ -class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScreen: MultiplayerScreen): PickerScreen(){ +class EditMultiplayerGameInfoScreen(val gameInfo: GameInfoPreview?, gameName: String, backScreen: MultiplayerScreen): PickerScreen(){ init { val textField = TextField(gameName, skin) @@ -24,7 +24,7 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree val deleteButton = "Delete save".toTextButton() deleteButton.onClick { val askPopup = YesNoPopup("Are you sure you want to delete this map?", { - backScreen.removeMultiplayerGame(game, gameName) + backScreen.removeMultiplayerGame(gameInfo, gameName) backScreen.game.setScreen(backScreen) backScreen.reloadGameListUI() }, this) @@ -34,7 +34,7 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree val giveUpButton = "Resign".toTextButton() giveUpButton.onClick { val askPopup = YesNoPopup("Are you sure you want to resign?", { - resign(game!!.gameId, gameName, backScreen) + resign(gameInfo!!.gameId, gameName, backScreen) }, this) askPopup.open() } @@ -55,14 +55,14 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree rightSideButton.onClick { rightSideButton.setText("Saving...".tr()) //remove the old game file - backScreen.removeMultiplayerGame(game, gameName) + backScreen.removeMultiplayerGame(gameInfo, gameName) //using addMultiplayerGame will download the game from Dropbox so the descriptionLabel displays the right things - backScreen.addMultiplayerGame(game!!.gameId, textField.text) + backScreen.addMultiplayerGame(gameInfo!!.gameId, textField.text) backScreen.game.setScreen(backScreen) backScreen.reloadGameListUI() } - if (game == null){ + if (gameInfo == null){ textField.isDisabled = true textField.color = Color.GRAY rightSideButton.disable() @@ -102,8 +102,9 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree civ.addNotification("[${playerCiv.civName}] resigned and is now controlled by AI", playerCiv.civName) } - //save game so multiplayer list stays up to date - GameSaver.saveGame(gameInfo, gameName, true) + //save game so multiplayer list stays up to date but do not override multiplayer settings + val updatedSave = this.gameInfo!!.updateCurrentTurn(gameInfo) + GameSaver.saveGame(updatedSave, gameName) OnlineMultiplayer().tryUploadGame(gameInfo) Gdx.app.postRunnable { popup.close() diff --git a/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt b/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt index 22dc9a3ef9..b3c5e3f9b4 100644 --- a/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/MultiplayerScreen.kt @@ -3,10 +3,7 @@ package com.unciv.ui import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.scenes.scene2d.ui.* -import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver -import com.unciv.logic.IdChecker -import com.unciv.logic.UncivShowableException +import com.unciv.logic.* import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.utils.* @@ -21,7 +18,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() private lateinit var selectedGameFile: FileHandle // Concurrent because we can get concurrent modification errors if we change things around while running redownloadAllGames() in another thread - private var multiplayerGames = ConcurrentHashMap() + private var multiplayerGames = ConcurrentHashMap() private val rightSideTable = Table() private val leftSideTable = Table() @@ -117,7 +114,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() //RightSideButton Setup rightSideButton.setText("Join game".tr()) rightSideButton.onClick { - joinMultiplaerGame() + joinMultiplayerGame() } } @@ -147,11 +144,11 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() try { // The tryDownload can take more than 500ms. Therefore, to avoid ANRs, // we need to run it in a different thread. - val game = OnlineMultiplayer().tryDownloadGame(gameId.trim()) + val gamePreview = OnlineMultiplayer().tryDownloadGame(gameId.trim()).asPreview() if (gameName == "") - GameSaver.saveGame(game, game.gameId, true) + GameSaver.saveGame(gamePreview, gamePreview.gameId) else - GameSaver.saveGame(game, gameName, true) + GameSaver.saveGame(gamePreview, gameName) Gdx.app.postRunnable { reloadGameListUI() } } catch (ex: Exception) { @@ -170,12 +167,21 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() } } - //just loads the game from savefile - //the game will be downloaded opon joining it anyway - private fun joinMultiplaerGame() { + //Download game and use the popup to cover ANRs + private fun joinMultiplayerGame() { + val loadingGamePopup = Popup(this) + loadingGamePopup.add("Loading latest game state...".tr()) + loadingGamePopup.open() + try { - game.loadGame(multiplayerGames[selectedGameFile]!!) + // For whatever reason, the only way to show the popup before the ANRs started was to + // call the loadGame explicitly with a runnable on the main thread. + // Maybe this adds just enough lag for the popup to show up + Gdx.app.postRunnable { + game.loadGame(OnlineMultiplayer().tryDownloadGame((multiplayerGames[selectedGameFile]!!.gameId))) + } } catch (ex: Exception) { + loadingGamePopup.close() val errorPopup = Popup(this) errorPopup.addGoodSizedLabel("Could not download game!") errorPopup.row() @@ -255,7 +261,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() thread(name = "loadGameFile") { try { - val game = gameSaver.loadGameFromFile(gameSaveFile) + val game = gameSaver.loadGamePreviewFromFile(gameSaveFile) //Add games to list so saves don't have to be loaded as Files so often if (!gameIsAlreadySavedAsMultiplayer(game.gameId)) { @@ -265,7 +271,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() Gdx.app.postRunnable { turnIndicator.clear() if (isUsersTurn(game)) { - turnIndicator.add(ImageGetter.getNationIndicator(game.currentPlayerCiv.nation, 50f)) + turnIndicator.add(ImageGetter.getImage("OtherIcons/ExclamationMark")).size(50f) } //set variable so it can be displayed when gameButton.onClick gets called currentTurnUser = game.currentPlayer @@ -307,8 +313,8 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() thread(name = "multiplayerGameDownload") { for ((fileHandle, gameInfo) in multiplayerGames) { try { - val game = OnlineMultiplayer().tryDownloadGame(gameInfo.gameId) - GameSaver.saveGame(game, fileHandle.name(), true) + val game = gameInfo.updateCurrentTurn(OnlineMultiplayer().tryDownloadGame(gameInfo.gameId)) + GameSaver.saveGame(game, fileHandle.name()) multiplayerGames[fileHandle] = game } catch (ex: Exception) { //skipping one is not fatal @@ -342,7 +348,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() if (gameIsAlreadySavedAsMultiplayer(currentlyRunningGame.gameId)) return@onClick try { - GameSaver.saveGame(currentlyRunningGame, currentlyRunningGame.gameId, true) + GameSaver.saveGame(currentlyRunningGame, currentlyRunningGame.gameId) reloadGameListUI() } catch (ex: Exception) { val errorPopup = Popup(this) @@ -366,9 +372,9 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen() } //check if its the users turn - private fun isUsersTurn(gameInfo: GameInfo) = gameInfo.currentPlayerCiv.playerId == game.settings.userId + private fun isUsersTurn(gameInfo: GameInfoPreview) = gameInfo.getCivilization(gameInfo.currentPlayer).playerId == game.settings.userId - fun removeMultiplayerGame(gameInfo: GameInfo?, gameName: String) { + fun removeMultiplayerGame(gameInfo: GameInfoPreview?, gameName: String) { val games = multiplayerGames.filterValues { it == gameInfo }.keys try { GameSaver.deleteSave(gameName, true) diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 99fcf04dcf..cf925b2b76 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -207,7 +207,8 @@ class NewGameScreen( GameSaver.autoSave(newGame!!) {} // Saved as Multiplayer game to show up in the session browser - GameSaver.saveGame(newGame!!, newGame!!.gameId, true) + val newGamePreview = newGame!!.asPreview() + GameSaver.saveGame(newGamePreview, newGamePreview.gameId) } catch (ex: Exception) { Gdx.app.postRunnable { Popup(this).apply {