mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
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:
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user