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
8 changed files with 99 additions and 96 deletions

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
}
/**