Improved MultiplayerScreen performance (#5527)

* Added MultiplayerGameInfo

* Added usage of MultiplayerGameInfo

* Integrate MultiplayerGamInfo into GameInfoPreview

* Replaced MultiplayerGameInfo with GameInfoPreview

* Correction in function docs

* PR cleanup

* Added currentTurnStartTime from merge

* Fixed resign not propagating to preview
This commit is contained in:
GGGuenni
2021-10-27 15:30:25 +02:00
committed by GitHub
parent 446c3fb97a
commit ca1d070c81
6 changed files with 106 additions and 36 deletions

View File

@ -121,6 +121,7 @@ class GameInfo {
* @throws NoSuchElementException if no civ of than name is in the game (alive or dead)! */ * @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 getCivilization(civName: String) = civilizations.first { it.civName == civName }
fun getCurrentPlayerCivilization() = currentPlayerCiv fun getCurrentPlayerCivilization() = currentPlayerCiv
fun getCivilizationsAsPreviews() = civilizations.map { it.asPreview() }.toMutableList()
/** Get barbarian civ /** Get barbarian civ
* @throws NoSuchElementException in no-barbarians games! */ * @throws NoSuchElementException in no-barbarians games! */
fun getBarbarianCivilization() = getCivilization(Constants.barbarians) fun getBarbarianCivilization() = getCivilization(Constants.barbarians)
@ -367,15 +368,51 @@ class GameInfo {
} }
//endregion //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<CivilizationInfoPreview>() var civilizations = mutableListOf<CivilizationInfoPreview>()
var difficulty = "Chieftain" var difficulty = "Chieftain"
var gameParameters = GameParameters() var gameParameters = GameParameters()
var turns = 0 var turns = 0
var gameId = "" var gameId = ""
var currentPlayer = "" 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 } 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
}
} }

View File

@ -38,9 +38,21 @@ object GameSaver {
return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}").list().asSequence() 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 { 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) saveCompletionCallback?.invoke(null)
} catch (ex: Exception) { } catch (ex: Exception) {
saveCompletionCallback?.invoke(ex) saveCompletionCallback?.invoke(ex)

View File

@ -1212,14 +1212,27 @@ class CivilizationInfo {
} }
//endregion //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 civName = ""
var playerType = PlayerType.AI var playerType = PlayerType.AI
var playerId = "" var playerId = ""
fun isPlayerCivilization() = playerType == PlayerType.Human 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 { enum class CivFlags {

View File

@ -3,7 +3,7 @@ package com.unciv.ui
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.TextField 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.GameSaver
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
@ -14,7 +14,7 @@ import kotlin.concurrent.thread
/** Subscreen of MultiplayerScreen to edit and delete saves /** 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 */ * 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 { init {
val textField = TextField(gameName, skin) val textField = TextField(gameName, skin)
@ -24,7 +24,7 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree
val deleteButton = "Delete save".toTextButton() val deleteButton = "Delete save".toTextButton()
deleteButton.onClick { deleteButton.onClick {
val askPopup = YesNoPopup("Are you sure you want to delete this map?", { 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.game.setScreen(backScreen)
backScreen.reloadGameListUI() backScreen.reloadGameListUI()
}, this) }, this)
@ -34,7 +34,7 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree
val giveUpButton = "Resign".toTextButton() val giveUpButton = "Resign".toTextButton()
giveUpButton.onClick { giveUpButton.onClick {
val askPopup = YesNoPopup("Are you sure you want to resign?", { val askPopup = YesNoPopup("Are you sure you want to resign?", {
resign(game!!.gameId, gameName, backScreen) resign(gameInfo!!.gameId, gameName, backScreen)
}, this) }, this)
askPopup.open() askPopup.open()
} }
@ -55,14 +55,14 @@ class EditMultiplayerGameInfoScreen(game: GameInfo?, gameName: String, backScree
rightSideButton.onClick { rightSideButton.onClick {
rightSideButton.setText("Saving...".tr()) rightSideButton.setText("Saving...".tr())
//remove the old game file //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 //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.game.setScreen(backScreen)
backScreen.reloadGameListUI() backScreen.reloadGameListUI()
} }
if (game == null){ if (gameInfo == null){
textField.isDisabled = true textField.isDisabled = true
textField.color = Color.GRAY textField.color = Color.GRAY
rightSideButton.disable() 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) civ.addNotification("[${playerCiv.civName}] resigned and is now controlled by AI", playerCiv.civName)
} }
//save game so multiplayer list stays up to date //save game so multiplayer list stays up to date but do not override multiplayer settings
GameSaver.saveGame(gameInfo, gameName, true) val updatedSave = this.gameInfo!!.updateCurrentTurn(gameInfo)
GameSaver.saveGame(updatedSave, gameName)
OnlineMultiplayer().tryUploadGame(gameInfo) OnlineMultiplayer().tryUploadGame(gameInfo)
Gdx.app.postRunnable { Gdx.app.postRunnable {
popup.close() popup.close()

View File

@ -3,10 +3,7 @@ package com.unciv.ui
import com.badlogic.gdx.Gdx 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.GameInfo import com.unciv.logic.*
import com.unciv.logic.GameSaver
import com.unciv.logic.IdChecker
import com.unciv.logic.UncivShowableException
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.*
@ -21,7 +18,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
private lateinit var selectedGameFile: FileHandle private lateinit var selectedGameFile: FileHandle
// Concurrent because we can get concurrent modification errors if we change things around while running redownloadAllGames() in another thread // Concurrent because we can get concurrent modification errors if we change things around while running redownloadAllGames() in another thread
private var multiplayerGames = ConcurrentHashMap<FileHandle, GameInfo>() private var multiplayerGames = ConcurrentHashMap<FileHandle, GameInfoPreview>()
private val rightSideTable = Table() private val rightSideTable = Table()
private val leftSideTable = Table() private val leftSideTable = Table()
@ -117,7 +114,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
//RightSideButton Setup //RightSideButton Setup
rightSideButton.setText("Join game".tr()) rightSideButton.setText("Join game".tr())
rightSideButton.onClick { rightSideButton.onClick {
joinMultiplaerGame() joinMultiplayerGame()
} }
} }
@ -147,11 +144,11 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
try { try {
// The tryDownload can take more than 500ms. Therefore, to avoid ANRs, // The tryDownload can take more than 500ms. Therefore, to avoid ANRs,
// we need to run it in a different thread. // we need to run it in a different thread.
val game = OnlineMultiplayer().tryDownloadGame(gameId.trim()) val gamePreview = OnlineMultiplayer().tryDownloadGame(gameId.trim()).asPreview()
if (gameName == "") if (gameName == "")
GameSaver.saveGame(game, game.gameId, true) GameSaver.saveGame(gamePreview, gamePreview.gameId)
else else
GameSaver.saveGame(game, gameName, true) GameSaver.saveGame(gamePreview, gameName)
Gdx.app.postRunnable { reloadGameListUI() } Gdx.app.postRunnable { reloadGameListUI() }
} catch (ex: Exception) { } catch (ex: Exception) {
@ -170,12 +167,21 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
} }
} }
//just loads the game from savefile //Download game and use the popup to cover ANRs
//the game will be downloaded opon joining it anyway private fun joinMultiplayerGame() {
private fun joinMultiplaerGame() { val loadingGamePopup = Popup(this)
loadingGamePopup.add("Loading latest game state...".tr())
loadingGamePopup.open()
try { 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) { } catch (ex: Exception) {
loadingGamePopup.close()
val errorPopup = Popup(this) val errorPopup = Popup(this)
errorPopup.addGoodSizedLabel("Could not download game!") errorPopup.addGoodSizedLabel("Could not download game!")
errorPopup.row() errorPopup.row()
@ -255,7 +261,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
thread(name = "loadGameFile") { thread(name = "loadGameFile") {
try { 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 //Add games to list so saves don't have to be loaded as Files so often
if (!gameIsAlreadySavedAsMultiplayer(game.gameId)) { if (!gameIsAlreadySavedAsMultiplayer(game.gameId)) {
@ -265,7 +271,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
Gdx.app.postRunnable { Gdx.app.postRunnable {
turnIndicator.clear() turnIndicator.clear()
if (isUsersTurn(game)) { 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 //set variable so it can be displayed when gameButton.onClick gets called
currentTurnUser = game.currentPlayer currentTurnUser = game.currentPlayer
@ -307,8 +313,8 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
thread(name = "multiplayerGameDownload") { thread(name = "multiplayerGameDownload") {
for ((fileHandle, gameInfo) in multiplayerGames) { for ((fileHandle, gameInfo) in multiplayerGames) {
try { try {
val game = OnlineMultiplayer().tryDownloadGame(gameInfo.gameId) val game = gameInfo.updateCurrentTurn(OnlineMultiplayer().tryDownloadGame(gameInfo.gameId))
GameSaver.saveGame(game, fileHandle.name(), true) GameSaver.saveGame(game, fileHandle.name())
multiplayerGames[fileHandle] = game multiplayerGames[fileHandle] = game
} catch (ex: Exception) { } catch (ex: Exception) {
//skipping one is not fatal //skipping one is not fatal
@ -342,7 +348,7 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
if (gameIsAlreadySavedAsMultiplayer(currentlyRunningGame.gameId)) if (gameIsAlreadySavedAsMultiplayer(currentlyRunningGame.gameId))
return@onClick return@onClick
try { try {
GameSaver.saveGame(currentlyRunningGame, currentlyRunningGame.gameId, true) GameSaver.saveGame(currentlyRunningGame, currentlyRunningGame.gameId)
reloadGameListUI() reloadGameListUI()
} catch (ex: Exception) { } catch (ex: Exception) {
val errorPopup = Popup(this) val errorPopup = Popup(this)
@ -366,9 +372,9 @@ class MultiplayerScreen(previousScreen: CameraStageBaseScreen) : PickerScreen()
} }
//check if its the users turn //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 val games = multiplayerGames.filterValues { it == gameInfo }.keys
try { try {
GameSaver.deleteSave(gameName, true) GameSaver.deleteSave(gameName, true)

View File

@ -207,7 +207,8 @@ class NewGameScreen(
GameSaver.autoSave(newGame!!) {} GameSaver.autoSave(newGame!!) {}
// Saved as Multiplayer game to show up in the session browser // 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) { } catch (ex: Exception) {
Gdx.app.postRunnable { Gdx.app.postRunnable {
Popup(this).apply { Popup(this).apply {