Multi-server preparations: Save server info in game, save server functionality in specific class (#9379)

* Save server info in game

* Moved server functionality into server class

* Fix Android multiplayer update
This commit is contained in:
Yair Morgenstern 2023-05-13 21:51:44 +03:00 committed by GitHub
parent 97769b89b6
commit 48bd416347
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 99 additions and 96 deletions

View File

@ -29,7 +29,7 @@ import com.badlogic.gdx.backends.android.DefaultAndroidFiles
import com.unciv.logic.GameInfo
import com.unciv.logic.files.UncivFiles
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerServer
import com.unciv.models.metadata.GameSettingsMultiplayer
import kotlinx.coroutines.runBlocking
import java.io.FileNotFoundException
@ -307,7 +307,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
try {
Log.d(LOG_TAG, "doWork download $gameId")
val gamePreview = OnlineMultiplayerFiles(fileStorage, mapOf("Authorization" to authHeader)).tryDownloadGamePreview(gameId)
val gamePreview = OnlineMultiplayerServer(fileStorage, mapOf("Authorization" to authHeader)).tryDownloadGamePreview(gameId)
Log.d(LOG_TAG, "doWork download $gameId done")
val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer)

View File

@ -182,7 +182,7 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci
Concurrency.run {
// Check if the server is available in case the feature set has changed
try {
onlineMultiplayer.checkServerStatus()
onlineMultiplayer.multiplayerServer.checkServerStatus()
} catch (ex: Exception) {
debug("Couldn't connect to server: " + ex.message)
}

View File

@ -3,7 +3,6 @@ package com.unciv.logic.multiplayer
import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.json.json
import com.unciv.logic.GameInfo
import com.unciv.logic.GameInfoPreview
import com.unciv.logic.civilization.NotificationCategory
@ -12,15 +11,13 @@ import com.unciv.logic.event.EventBus
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.MultiplayerAuthException
import com.unciv.logic.multiplayer.storage.MultiplayerFileNotFoundException
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerServer
import com.unciv.ui.components.extensions.isLargerThan
import com.unciv.logic.multiplayer.storage.SimpleHttp
import com.unciv.utils.Log
import com.unciv.utils.Concurrency
import com.unciv.utils.Dispatcher
import com.unciv.utils.debug
import com.unciv.utils.launchOnThreadPool
import com.unciv.utils.withGLContext
import com.unciv.utils.debug
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
@ -45,8 +42,7 @@ private val FILE_UPDATE_THROTTLE_PERIOD = Duration.ofSeconds(60)
*/
class OnlineMultiplayer {
private val files = UncivGame.Current.files
private val multiplayerFiles = OnlineMultiplayerFiles()
private var featureSet = ServerFeatureSet()
val multiplayerServer = OnlineMultiplayerServer()
private val savedGames: MutableMap<FileHandle, OnlineMultiplayerGame> = Collections.synchronizedMap(mutableMapOf())
@ -55,7 +51,6 @@ class OnlineMultiplayer {
private val lastCurGameRefresh: AtomicReference<Instant?> = AtomicReference()
val games: Set<OnlineMultiplayerGame> get() = savedGames.values.toSet()
val serverFeatureSet: ServerFeatureSet get() = featureSet
init {
flow<Unit> {
@ -125,7 +120,7 @@ class OnlineMultiplayer {
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
*/
suspend fun createGame(newGame: GameInfo) {
multiplayerFiles.tryUploadGame(newGame, withPreview = true)
multiplayerServer.tryUploadGame(newGame, withPreview = true)
addGame(newGame)
}
@ -139,11 +134,12 @@ class OnlineMultiplayer {
*/
suspend fun addGame(gameId: String, gameName: String? = null) {
val saveFileName = if (gameName.isNullOrBlank()) gameId else gameName
val gamePreview: GameInfoPreview = try {
multiplayerFiles.tryDownloadGamePreview(gameId)
var gamePreview: GameInfoPreview
try {
gamePreview = multiplayerServer.tryDownloadGamePreview(gameId)
} catch (ex: MultiplayerFileNotFoundException) {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
multiplayerFiles.tryDownloadGame(gameId).asPreview()
gamePreview = multiplayerServer.tryDownloadGame(gameId).asPreview()
}
addGame(gamePreview, saveFileName)
}
@ -189,7 +185,7 @@ class OnlineMultiplayer {
suspend fun resign(game: OnlineMultiplayerGame): Boolean {
val preview = game.preview ?: throw game.error!!
// download to work with the latest game state
val gameInfo = multiplayerFiles.tryDownloadGame(preview.gameId)
val gameInfo = multiplayerServer.tryDownloadGame(preview.gameId)
val playerCiv = gameInfo.getCurrentPlayerCivilization()
if (!gameInfo.isUsersTurn()) {
@ -212,7 +208,7 @@ class OnlineMultiplayer {
val newPreview = gameInfo.asPreview()
files.saveGame(newPreview, game.fileHandle)
multiplayerFiles.tryUploadGame(gameInfo, withPreview = true)
multiplayerServer.tryUploadGame(gameInfo, withPreview = true)
game.doManualUpdate(newPreview)
return true
}
@ -248,7 +244,7 @@ class OnlineMultiplayer {
*/
suspend fun loadGame(gameInfo: GameInfo) = coroutineScope {
val gameId = gameInfo.gameId
val preview = multiplayerFiles.tryDownloadGamePreview(gameId)
val preview = multiplayerServer.tryDownloadGamePreview(gameId)
if (hasLatestGameState(gameInfo, preview)) {
gameInfo.isUpToDate = true
UncivGame.Current.loadGame(gameInfo)
@ -262,7 +258,7 @@ class OnlineMultiplayer {
* @throws MultiplayerFileNotFoundException if the file can't be found
*/
suspend fun downloadGame(gameId: String): GameInfo {
val latestGame = multiplayerFiles.tryDownloadGame(gameId)
val latestGame = multiplayerServer.tryDownloadGame(gameId)
latestGame.isUpToDate = true
return latestGame
}
@ -311,7 +307,7 @@ class OnlineMultiplayer {
*/
suspend fun updateGame(gameInfo: GameInfo) {
debug("Updating remote game %s", gameInfo.gameId)
multiplayerFiles.tryUploadGame(gameInfo, withPreview = true)
multiplayerServer.tryUploadGame(gameInfo, withPreview = true)
val game = getGameByGameId(gameInfo.gameId)
debug("Existing OnlineMultiplayerGame: %s", game)
if (game == null) {
@ -330,66 +326,6 @@ class OnlineMultiplayer {
&& gameInfo.turns == preview.turns
}
/**
* Checks if the server is alive and sets the [serverFeatureSet] accordingly.
* @return true if the server is alive, false otherwise
*/
fun checkServerStatus(): Boolean {
var statusOk = false
SimpleHttp.sendGetRequest("${UncivGame.Current.settings.multiplayer.server}/isalive") { success, result, _ ->
statusOk = success
if (result.isNotEmpty()) {
featureSet = try {
json().fromJson(ServerFeatureSet::class.java, result)
} catch (ex: Exception) {
Log.error("${UncivGame.Current.settings.multiplayer.server} does not support server feature set", ex)
ServerFeatureSet()
}
}
}
return statusOk
}
/**
* @return true if the authentication was successful or the server does not support authentication.
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws MultiplayerAuthException if the authentication failed
*/
fun authenticate(password: String?): Boolean {
if (featureSet.authVersion == 0) {
return true
}
val settings = UncivGame.Current.settings.multiplayer
val success = multiplayerFiles.fileStorage().authenticate(
userId=settings.userId,
password=password ?: settings.passwords[settings.server] ?: ""
)
if (password != null && success) {
settings.passwords[settings.server] = password
}
return success
}
/**
* @return true if setting the password was successful, false otherwise.
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws MultiplayerAuthException if the authentication failed
*/
fun setPassword(password: String): Boolean {
if (
featureSet.authVersion > 0 &&
multiplayerFiles.fileStorage().setPassword(newPassword = password)
) {
val settings = UncivGame.Current.settings.multiplayer
settings.passwords[settings.server] = password
return true
}
return false
}
/**
* Checks if [preview1] has a more recent game state than [preview2]

View File

@ -8,11 +8,11 @@ import com.unciv.logic.multiplayer.GameUpdateResult.Type.CHANGED
import com.unciv.logic.multiplayer.GameUpdateResult.Type.FAILURE
import com.unciv.logic.multiplayer.GameUpdateResult.Type.UNCHANGED
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerServer
import com.unciv.ui.components.extensions.isLargerThan
import com.unciv.utils.debug
import com.unciv.utils.launchOnGLThread
import com.unciv.utils.withGLContext
import com.unciv.utils.debug
import kotlinx.coroutines.coroutineScope
import java.time.Duration
import java.time.Instant
@ -65,7 +65,7 @@ class OnlineMultiplayerGame(
* Fires: [MultiplayerGameUpdateStarted], [MultiplayerGameUpdated], [MultiplayerGameUpdateUnchanged], [MultiplayerGameUpdateFailed]
*
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws MultiplayerFileNotFoundException if the file can't be found
* @throws MultiplayerFileNotFoundException if the file can't be found
*/
suspend fun requestUpdate(forceUpdate: Boolean = false) = coroutineScope {
val onUnchanged = { GameUpdateResult(UNCHANGED, preview!!) }
@ -106,7 +106,8 @@ class OnlineMultiplayerGame(
private suspend fun update(): GameUpdateResult {
val curPreview = if (preview != null) preview!! else loadPreviewFromFile()
val newPreview = OnlineMultiplayerFiles().tryDownloadGamePreview(curPreview.gameId)
val serverIdentifier = curPreview.gameParameters.multiplayerServerUrl
val newPreview = OnlineMultiplayerServer(serverIdentifier).tryDownloadGamePreview(curPreview.gameId)
if (newPreview.turns == curPreview.turns && newPreview.currentPlayer == curPreview.currentPlayer) return GameUpdateResult(UNCHANGED, newPreview)
UncivGame.Current.files.saveGame(newPreview, fileHandle)
preview = newPreview

View File

@ -2,9 +2,12 @@ package com.unciv.logic.multiplayer.storage
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.json.json
import com.unciv.logic.GameInfo
import com.unciv.logic.GameInfoPreview
import com.unciv.logic.files.UncivFiles
import com.unciv.logic.multiplayer.ServerFeatureSet
import com.unciv.utils.Log
/**
* Allows access to games stored on a server for multiplayer purposes.
@ -17,12 +20,14 @@ import com.unciv.logic.files.UncivFiles
* @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 OnlineMultiplayerFiles(
private var fileStorageIdentifier: String? = null,
class OnlineMultiplayerServer(
fileStorageIdentifier: String? = null,
private var authenticationHeader: Map<String, String>? = null
) {
internal var featureSet = ServerFeatureSet()
val serverUrl = fileStorageIdentifier ?: UncivGame.Current.settings.multiplayer.server
fun fileStorage(): FileStorage {
val identifier = if (fileStorageIdentifier == null) UncivGame.Current.settings.multiplayer.server else fileStorageIdentifier
val authHeader = if (authenticationHeader == null) {
val settings = UncivGame.Current.settings.multiplayer
mapOf("Authorization" to settings.getAuthHeader())
@ -30,16 +35,73 @@ class OnlineMultiplayerFiles(
authenticationHeader
}
return if (identifier == Constants.dropboxMultiplayerServer) {
return if (serverUrl == Constants.dropboxMultiplayerServer) {
DropBox
} else {
UncivServerFileStorage.apply {
serverUrl = identifier!!
serverUrl = this@OnlineMultiplayerServer.serverUrl
this.authHeader = authHeader
}
}
}
/**
* Checks if the server is alive and sets the [serverFeatureSet] accordingly.
* @return true if the server is alive, false otherwise
*/
fun checkServerStatus(): Boolean {
var statusOk = false
SimpleHttp.sendGetRequest("${serverUrl}/isalive") { success, result, _ ->
statusOk = success
if (result.isNotEmpty()) {
featureSet = try {
json().fromJson(ServerFeatureSet::class.java, result)
} catch (ex: Exception) {
Log.error("${UncivGame.Current.settings.multiplayer.server} does not support server feature set", ex)
ServerFeatureSet()
}
}
}
return statusOk
}
/**
* @return true if the authentication was successful or the server does not support authentication.
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws MultiplayerAuthException if the authentication failed
*/
fun authenticate(password: String?): Boolean {
if (featureSet.authVersion == 0) return true
val settings = UncivGame.Current.settings.multiplayer
val success = fileStorage().authenticate(
userId=settings.userId,
password=password ?: settings.passwords[settings.server] ?: ""
)
if (password != null && success) {
settings.passwords[settings.server] = password
}
return success
}
/**
* @return true if setting the password was successful, false otherwise.
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws MultiplayerAuthException if the authentication failed
*/
fun setPassword(password: String): Boolean {
if (featureSet.authVersion > 0 && fileStorage().setPassword(newPassword = password)) {
val settings = UncivGame.Current.settings.multiplayer
settings.passwords[settings.server] = password
return true
}
return false
}
/**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws MultiplayerAuthException if the authentication failed
@ -81,7 +143,9 @@ class OnlineMultiplayerFiles(
*/
suspend fun tryDownloadGame(gameId: String): GameInfo {
val zippedGameInfo = fileStorage().loadFileData(gameId)
return UncivFiles.gameInfoFromString(zippedGameInfo)
val gameInfo = UncivFiles.gameInfoFromString(zippedGameInfo)
gameInfo.gameParameters.multiplayerServerUrl = UncivGame.Current.settings.multiplayer.server
return gameInfo
}
/**

View File

@ -43,6 +43,7 @@ class GameParameters : IsPartOfGameInfoSerialization { // Default values are the
var startingEra = "Ancient era"
var isOnlineMultiplayer = false
var multiplayerServerUrl: String? = null
var anyoneCanSpectate = true
var baseRuleset: String = BaseRuleset.Civ_V_GnK.fullName
var mods = LinkedHashSet<String>()
@ -74,6 +75,7 @@ class GameParameters : IsPartOfGameInfoSerialization { // Default values are the
parameters.victoryTypes = ArrayList(victoryTypes)
parameters.startingEra = startingEra
parameters.isOnlineMultiplayer = isOnlineMultiplayer
parameters.multiplayerServerUrl = multiplayerServerUrl
parameters.anyoneCanSpectate = anyoneCanSpectate
parameters.baseRuleset = baseRuleset
parameters.mods = LinkedHashSet(mods)

View File

@ -20,7 +20,7 @@ class AuthPopup(stage: Stage, authSuccessful: ((Boolean) -> Unit)? = null)
button.onClick {
try {
UncivGame.Current.onlineMultiplayer.authenticate(passwordField.text)
UncivGame.Current.onlineMultiplayer.multiplayerServer.authenticate(passwordField.text)
authSuccessful?.invoke(true)
close()
} catch (ex: Exception) {

View File

@ -192,7 +192,7 @@ private fun addMultiplayerServerOptions(
}
}).row()
if (UncivGame.Current.onlineMultiplayer.serverFeatureSet.authVersion > 0) {
if (UncivGame.Current.onlineMultiplayer.multiplayerServer.featureSet.authVersion > 0) {
val passwordTextField = UncivTextField.create(
settings.multiplayer.passwords[settings.multiplayer.server] ?: "Password"
)
@ -255,11 +255,11 @@ private fun addTurnCheckerOptions(
private fun successfullyConnectedToServer(action: (Boolean, Boolean) -> Unit) {
Concurrency.run("TestIsAlive") {
try {
val connectionSuccess = UncivGame.Current.onlineMultiplayer.checkServerStatus()
val connectionSuccess = UncivGame.Current.onlineMultiplayer.multiplayerServer.checkServerStatus()
var authSuccess = false
if (connectionSuccess) {
try {
authSuccess = UncivGame.Current.onlineMultiplayer.authenticate(null)
authSuccess = UncivGame.Current.onlineMultiplayer.multiplayerServer.authenticate(null)
} catch (_: Exception) {
// We ignore the exception here, because we handle the failed auth onGLThread
}
@ -289,7 +289,7 @@ private fun setPassword(password: String, optionsPopup: OptionsPopup) {
return
}
if (UncivGame.Current.onlineMultiplayer.serverFeatureSet.authVersion == 0) {
if (UncivGame.Current.onlineMultiplayer.multiplayerServer.featureSet.authVersion == 0) {
popup.reuseWith("This server does not support authentication", true)
return
}
@ -327,7 +327,7 @@ private fun setPassword(password: String, optionsPopup: OptionsPopup) {
private fun successfullySetPassword(password: String, action: (Boolean, Exception?) -> Unit) {
Concurrency.run("SetPassword") {
try {
val setSuccess = UncivGame.Current.onlineMultiplayer.setPassword(password)
val setSuccess = UncivGame.Current.onlineMultiplayer.multiplayerServer.setPassword(password)
launchOnGLThread {
action(setSuccess, null)
}