From 48bd416347c6c1c0d21bf4af8e62687763fd3336 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Sat, 13 May 2023 21:51:44 +0300 Subject: [PATCH] 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 --- .../unciv/app/MultiplayerTurnCheckWorker.kt | 4 +- core/src/com/unciv/UncivGame.kt | 2 +- .../logic/multiplayer/OnlineMultiplayer.kt | 90 +++---------------- .../multiplayer/OnlineMultiplayerGame.kt | 9 +- ...yerFiles.kt => OnlineMultiplayerServer.kt} | 76 ++++++++++++++-- .../unciv/models/metadata/GameParameters.kt | 2 + core/src/com/unciv/ui/popups/AuthPopup.kt | 2 +- .../unciv/ui/popups/options/MultiplayerTab.kt | 10 +-- 8 files changed, 99 insertions(+), 96 deletions(-) rename core/src/com/unciv/logic/multiplayer/storage/{OnlineMultiplayerFiles.kt => OnlineMultiplayerServer.kt} (57%) diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 77c14b16ce..67cead5dcf 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -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) diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 78747d5413..08f495fa68 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -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) } diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt index a740b9604c..f139620e0c 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt @@ -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 = Collections.synchronizedMap(mutableMapOf()) @@ -55,7 +51,6 @@ class OnlineMultiplayer { private val lastCurGameRefresh: AtomicReference = AtomicReference() val games: Set get() = savedGames.values.toSet() - val serverFeatureSet: ServerFeatureSet get() = featureSet init { flow { @@ -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] diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt index 4981e0ff6f..e7ffc6994a 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt @@ -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 diff --git a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerFiles.kt b/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerServer.kt similarity index 57% rename from core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerFiles.kt rename to core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerServer.kt index ca151f6927..36314554ee 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerFiles.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerServer.kt @@ -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? = 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 } /** diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index aaed938ece..7785799686 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -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() @@ -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) diff --git a/core/src/com/unciv/ui/popups/AuthPopup.kt b/core/src/com/unciv/ui/popups/AuthPopup.kt index 0945b856ba..706bdab565 100644 --- a/core/src/com/unciv/ui/popups/AuthPopup.kt +++ b/core/src/com/unciv/ui/popups/AuthPopup.kt @@ -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) { diff --git a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt index ab911cbb4f..c3c1723f42 100644 --- a/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt +++ b/core/src/com/unciv/ui/popups/options/MultiplayerTab.kt @@ -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) }