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)! */
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<CivilizationInfoPreview>()
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
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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()

View File

@ -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<FileHandle, GameInfo>()
private var multiplayerGames = ConcurrentHashMap<FileHandle, GameInfoPreview>()
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)

View File

@ -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 {