Extracted File functions from OnlineMultiplayer to separate class, for separation of concerns between *local files* and *online data*

This commit is contained in:
yairm210 2024-08-08 00:50:10 +03:00
parent 0c4814eae4
commit cdf6d6169e
8 changed files with 136 additions and 118 deletions

View File

@ -0,0 +1,89 @@
package com.unciv.logic.multiplayer
import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.GameInfoPreview
import com.unciv.logic.event.EventBus
import com.unciv.utils.debug
import java.time.Instant
import java.util.*
/** Files that are stored locally */
class MultiplayerFiles {
internal val files = UncivGame.Current.files
internal val savedGames: MutableMap<FileHandle, OnlineMultiplayerGame> = Collections.synchronizedMap(mutableMapOf())
internal fun updateSavesFromFiles() {
val saves = files.getMultiplayerSaves()
val removedSaves = savedGames.keys - saves.toSet()
for (saveFile in removedSaves) {
deleteGame(saveFile)
}
val newSaves = saves - savedGames.keys
for (saveFile in newSaves) {
addGame(saveFile)
}
}
/**
* Deletes the game from disk, does not delete it remotely.
*/
fun deleteGame(onlineMultiplayerGame: OnlineMultiplayerGame) {
deleteGame(onlineMultiplayerGame.fileHandle)
}
private fun deleteGame(fileHandle: FileHandle) {
files.deleteSave(fileHandle)
val game = savedGames[fileHandle] ?: return
debug("Deleting game %s with id %s", fileHandle.name(), game.preview?.gameId)
savedGames.remove(game.fileHandle)
}
internal fun addGame(newGame: GameInfo) {
val newGamePreview = newGame.asPreview()
addGame(newGamePreview, newGamePreview.gameId)
}
internal fun addGame(preview: GameInfoPreview, saveFileName: String) {
val fileHandle = files.saveGame(preview, saveFileName)
return addGame(fileHandle, preview)
}
private fun addGame(fileHandle: FileHandle, preview: GameInfoPreview? = null) {
debug("Adding game %s", fileHandle.name())
val game = OnlineMultiplayerGame(fileHandle, preview, if (preview != null) Instant.now() else null)
savedGames[fileHandle] = game
}
fun getGameByName(name: String): OnlineMultiplayerGame? {
return savedGames.values.firstOrNull { it.name == name }
}
fun getGameByGameId(gameId: String): OnlineMultiplayerGame? {
return savedGames.values.firstOrNull { it.preview?.gameId == gameId }
}
/**
* Fires [MultiplayerGameNameChanged]
*/
fun changeGameName(game: OnlineMultiplayerGame, newName: String, onException: (Exception?)->Unit) {
debug("Changing name of game %s to", game.name, newName)
val oldPreview = game.preview ?: throw game.error!!
val oldLastUpdate = game.getLastUpdate()
val oldName = game.name
val newFileHandle = files.saveGame(oldPreview, newName, onException)
val newGame = OnlineMultiplayerGame(newFileHandle, oldPreview, oldLastUpdate)
savedGames[newFileHandle] = newGame
savedGames.remove(game.fileHandle)
files.deleteSave(game.fileHandle)
EventBus.send(MultiplayerGameNameChanged(oldName, newName))
}
}

View File

@ -1,6 +1,5 @@
package com.unciv.logic.multiplayer
import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
@ -18,17 +17,11 @@ import com.unciv.utils.Concurrency
import com.unciv.utils.Dispatcher
import com.unciv.utils.debug
import com.unciv.utils.launchOnThreadPool
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.isActive
import java.time.Duration
import java.time.Instant
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
@ -39,21 +32,24 @@ import java.util.concurrent.atomic.AtomicReference
private val FILE_UPDATE_THROTTLE_PERIOD = Duration.ofSeconds(60)
/**
* Provides multiplayer functionality to the rest of the game.
* Provides *online* multiplayer functionality to the rest of the game.
* Multiplayer data is a mix of local files ([multiplayerFiles]) and server data ([multiplayerServer]).
* This class handles functions that require a mix of both.
*
* See the file of [com.unciv.logic.multiplayer.HasMultiplayerGameName] for all available [EventBus] events.
*/
class OnlineMultiplayer {
private val files = UncivGame.Current.files
/** Handles SERVER DATA only */
val multiplayerServer = OnlineMultiplayerServer()
/** Handles LOCAL FILES only */
val multiplayerFiles = MultiplayerFiles()
private val savedGames: MutableMap<FileHandle, OnlineMultiplayerGame> = Collections.synchronizedMap(mutableMapOf())
private val lastFileUpdate: AtomicReference<Instant?> = AtomicReference()
private val lastAllGamesRefresh: AtomicReference<Instant?> = AtomicReference()
private val lastCurGameRefresh: AtomicReference<Instant?> = AtomicReference()
val games: Set<OnlineMultiplayerGame> get() = savedGames.values.toSet()
val games: Set<OnlineMultiplayerGame> get() = multiplayerFiles.savedGames.values.toSet()
val multiplayerGameUpdater: Job
init {
@ -86,7 +82,7 @@ class OnlineMultiplayer {
private fun getCurrentGame(): OnlineMultiplayerGame? {
val gameInfo = UncivGame.Current.gameInfo
return if (gameInfo != null && gameInfo.gameParameters.isOnlineMultiplayer) {
getGameByGameId(gameInfo.gameId)
multiplayerFiles.getGameByGameId(gameInfo.gameId)
} else null
}
@ -101,9 +97,9 @@ class OnlineMultiplayer {
Concurrency.run("Update all multiplayer games") {
val fileThrottleInterval = if (forceUpdate) Duration.ZERO else FILE_UPDATE_THROTTLE_PERIOD
// An exception only happens here if the files can't be listed, should basically never happen
throttle(lastFileUpdate, fileThrottleInterval, {}, action = ::updateSavesFromFiles)
throttle(lastFileUpdate, fileThrottleInterval, {}, action = {multiplayerFiles.updateSavesFromFiles()})
for (game in savedGames.values) {
for (game in multiplayerFiles.savedGames.values) {
if (game in doNotUpdate) continue
launchOnThreadPool {
game.requestUpdate(forceUpdate)
@ -112,27 +108,13 @@ class OnlineMultiplayer {
}
}
private fun updateSavesFromFiles() {
val saves = files.getMultiplayerSaves()
val removedSaves = savedGames.keys - saves.toSet()
for (saveFile in removedSaves) {
deleteGame(saveFile)
}
val newSaves = saves - savedGames.keys
for (saveFile in newSaves) {
addGame(saveFile)
}
}
/**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
*/
suspend fun createGame(newGame: GameInfo) {
multiplayerServer.tryUploadGame(newGame, withPreview = true)
addGame(newGame)
multiplayerFiles.addGame(newGame)
}
/**
@ -149,32 +131,9 @@ class OnlineMultiplayer {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
multiplayerServer.tryDownloadGame(gameId).asPreview()
}
addGame(gamePreview, saveFileName)
multiplayerFiles.addGame(gamePreview, saveFileName)
}
private fun addGame(newGame: GameInfo) {
val newGamePreview = newGame.asPreview()
addGame(newGamePreview, newGamePreview.gameId)
}
private fun addGame(preview: GameInfoPreview, saveFileName: String) {
val fileHandle = files.saveGame(preview, saveFileName)
return addGame(fileHandle, preview)
}
private fun addGame(fileHandle: FileHandle, preview: GameInfoPreview? = null) {
debug("Adding game %s", fileHandle.name())
val game = OnlineMultiplayerGame(fileHandle, preview, if (preview != null) Instant.now() else null)
savedGames[fileHandle] = game
}
fun getGameByName(name: String): OnlineMultiplayerGame? {
return savedGames.values.firstOrNull { it.name == name }
}
fun getGameByGameId(gameId: String): OnlineMultiplayerGame? {
return savedGames.values.firstOrNull { it.preview?.gameId == gameId }
}
/**
* Resigns from the given multiplayer [game]. Can only resign if it's currently the user's turn,
@ -212,7 +171,7 @@ class OnlineMultiplayer {
}
val newPreview = gameInfo.asPreview()
files.saveGame(newPreview, game.fileHandle)
multiplayerFiles.files.saveGame(newPreview, game.fileHandle)
multiplayerServer.tryUploadGame(gameInfo, withPreview = true)
game.doManualUpdate(newPreview)
return true
@ -227,14 +186,14 @@ class OnlineMultiplayer {
loadGame(preview.gameId)
}
/**
/** Downloads game, and updates it locally
* @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
*/
suspend fun loadGame(gameId: String) = coroutineScope {
val gameInfo = downloadGame(gameId)
val gameInfo = multiplayerServer.downloadGame(gameId)
val preview = gameInfo.asPreview()
val onlineGame = getGameByGameId(gameId)
val onlineGame = multiplayerFiles.getGameByGameId(gameId)
val onlinePreview = onlineGame?.preview
if (onlineGame == null) {
createGame(gameInfo)
@ -258,49 +217,8 @@ class OnlineMultiplayer {
}
}
/**
* @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
*/
suspend fun downloadGame(gameId: String): GameInfo {
val latestGame = multiplayerServer.tryDownloadGame(gameId)
latestGame.isUpToDate = true
return latestGame
}
/**
* Deletes the game from disk, does not delete it remotely.
*/
fun deleteGame(multiplayerGame: OnlineMultiplayerGame) {
deleteGame(multiplayerGame.fileHandle)
}
private fun deleteGame(fileHandle: FileHandle) {
files.deleteSave(fileHandle)
val game = savedGames[fileHandle] ?: return
debug("Deleting game %s with id %s", fileHandle.name(), game.preview?.gameId)
savedGames.remove(game.fileHandle)
}
/**
* Fires [MultiplayerGameNameChanged]
*/
fun changeGameName(game: OnlineMultiplayerGame, newName: String, onException: (Exception?)->Unit) {
debug("Changing name of game %s to", game.name, newName)
val oldPreview = game.preview ?: throw game.error!!
val oldLastUpdate = game.getLastUpdate()
val oldName = game.name
val newFileHandle = files.saveGame(oldPreview, newName, onException)
val newGame = OnlineMultiplayerGame(newFileHandle, oldPreview, oldLastUpdate)
savedGames[newFileHandle] = newGame
savedGames.remove(game.fileHandle)
files.deleteSave(game.fileHandle)
EventBus.send(MultiplayerGameNameChanged(oldName, newName))
}
/**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
@ -310,10 +228,10 @@ class OnlineMultiplayer {
suspend fun updateGame(gameInfo: GameInfo) {
debug("Updating remote game %s", gameInfo.gameId)
multiplayerServer.tryUploadGame(gameInfo, withPreview = true)
val game = getGameByGameId(gameInfo.gameId)
val game = multiplayerFiles.getGameByGameId(gameInfo.gameId)
debug("Existing OnlineMultiplayerGame: %s", game)
if (game == null) {
addGame(gameInfo)
multiplayerFiles.addGame(gameInfo)
} else {
game.doManualUpdate(gameInfo.asPreview())
}
@ -340,7 +258,6 @@ class OnlineMultiplayer {
fun usesCustomServer() = UncivGame.Current.settings.multiplayer.server != Constants.dropboxMultiplayerServer
fun usesDropbox() = !usesCustomServer()
}
}
/**

View File

@ -16,7 +16,7 @@ import com.unciv.logic.multiplayer.ServerFeatureSet
*
* @param fileStorageIdentifier must be given if UncivGame.Current might not be initialized
* @see FileStorage
* @see UncivGame.Current.settings.multiplayerServer
* @see UncivGame.settings.multiplayer.server
*/
@Suppress("RedundantSuspendModifier") // Methods can take a long time, so force users to use them in a coroutine to not get ANRs on Android
class OnlineMultiplayerServer(
@ -142,6 +142,17 @@ class OnlineMultiplayerServer(
return gameInfo
}
/**
* @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
*/
suspend fun downloadGame(gameId: String): GameInfo {
val latestGame = tryDownloadGame(gameId)
latestGame.isUpToDate = true
return latestGame
}
/**
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time
* @throws FileNotFoundException if the file can't be found

View File

@ -106,12 +106,12 @@ private fun addMultiplayerServerOptions(
val connectionToServerButton = "Check connection to server".toTextButton()
val textToShowForMultiplayerAddress = if (OnlineMultiplayer.usesCustomServer()) {
val textToShowForOnlineMultiplayerAddress = if (OnlineMultiplayer.usesCustomServer()) {
settings.multiplayer.server
} else {
"https://"
}
val multiplayerServerTextField = UncivTextField("Server address", textToShowForMultiplayerAddress)
val multiplayerServerTextField = UncivTextField("Server address", textToShowForOnlineMultiplayerAddress)
multiplayerServerTextField.setTextFieldFilter { _, c -> c !in " \r\n\t\\" }
multiplayerServerTextField.programmaticChangeEvents = true
val serverIpTable = Table()

View File

@ -35,16 +35,16 @@ object MultiplayerHelpers {
}
}
fun buildDescriptionText(multiplayerGame: OnlineMultiplayerGame): StringBuilder {
fun buildDescriptionText(onlineMultiplayerGame: OnlineMultiplayerGame): StringBuilder {
val descriptionText = StringBuilder()
val ex = multiplayerGame.error
val ex = onlineMultiplayerGame.error
if (ex != null) {
val (message) = LoadGameScreen.getLoadExceptionMessage(ex, "Error while refreshing:")
descriptionText.appendLine(message)
}
val lastUpdate = multiplayerGame.getLastUpdate()
val lastUpdate = onlineMultiplayerGame.getLastUpdate()
descriptionText.appendLine("Last refresh: [${Duration.between(lastUpdate, Instant.now()).formatShort()}] ago".tr())
val preview = multiplayerGame.preview
val preview = onlineMultiplayerGame.preview
if (preview?.currentPlayer != null) {
val currentTurnStartTime = Instant.ofEpochMilli(preview.currentTurnStartTime)
descriptionText.appendLine("Current Turn: [${preview.currentPlayer}] since [${Duration.between(currentTurnStartTime, Instant.now()).formatShort()}] ago".tr())

View File

@ -127,7 +127,7 @@ class MultiplayerScreen : PickerScreen() {
* Helper function to decrease indentation
* Turns the current playerCiv into an AI civ and uploads the game afterwards.
*/
private fun resign(multiplayerGame: OnlineMultiplayerGame) {
private fun resign(onlineMultiplayerGame: OnlineMultiplayerGame) {
//Create a popup
val popup = Popup(this)
popup.addGoodSizedLabel(Constants.working).row()
@ -135,7 +135,7 @@ class MultiplayerScreen : PickerScreen() {
Concurrency.runOnNonDaemonThreadPool("Resign") {
try {
val resignSuccess = game.onlineMultiplayer.resign(multiplayerGame)
val resignSuccess = game.onlineMultiplayer.resign(onlineMultiplayerGame)
launchOnGLThread {
if (resignSuccess) {
@ -151,7 +151,7 @@ class MultiplayerScreen : PickerScreen() {
if (ex is MultiplayerAuthException) {
launchOnGLThread {
AuthPopup(this@MultiplayerScreen) { success ->
if (success) resign(multiplayerGame)
if (success) resign(onlineMultiplayerGame)
}.open(true)
}
return@runOnNonDaemonThreadPool
@ -174,7 +174,7 @@ class MultiplayerScreen : PickerScreen() {
"Delete save",
) {
try {
game.onlineMultiplayer.deleteGame(selectedGame!!)
game.onlineMultiplayer.multiplayerFiles.deleteGame(selectedGame!!)
onGameDeleted(selectedGame!!.name)
} catch (ex: Exception) {
Log.error("Could not delete game!", ex)
@ -196,7 +196,7 @@ class MultiplayerScreen : PickerScreen() {
val saveNewNameFunction = {
val newName = textField.text.trim()
game.onlineMultiplayer.changeGameName(selectedGame!!, newName) {
game.onlineMultiplayer.multiplayerFiles.changeGameName(selectedGame!!, newName) {
if (it != null) reuseWith("Could not save game!", true)
}
gameList.update()
@ -289,7 +289,7 @@ class MultiplayerScreen : PickerScreen() {
}
fun selectGame(name: String) {
val multiplayerGame = game.onlineMultiplayer.getGameByName(name)
val multiplayerGame = game.onlineMultiplayer.multiplayerFiles.getGameByName(name)
if (multiplayerGame == null) {
// Should never happen
unselectGame()

View File

@ -329,7 +329,7 @@ class WorldScreen(
try {
debug("loadLatestMultiplayerState current game: gameId: %s, turn: %s, curCiv: %s",
gameInfo.gameId, gameInfo.turns, gameInfo.currentPlayer)
val latestGame = game.onlineMultiplayer.downloadGame(gameInfo.gameId)
val latestGame = game.onlineMultiplayer.multiplayerServer.downloadGame(gameInfo.gameId)
debug("loadLatestMultiplayerState downloaded game: gameId: %s, turn: %s, curCiv: %s",
latestGame.gameId, latestGame.turns, latestGame.currentPlayer)
if (viewingCiv.civName == latestGame.currentPlayer || viewingCiv.civName == Constants.spectator) {
@ -697,7 +697,8 @@ class WorldScreen(
private fun updateMultiplayerStatusButton() {
if (gameInfo.gameParameters.isOnlineMultiplayer || game.settings.multiplayer.statusButtonInSinglePlayer) {
if (statusButtons.multiplayerStatusButton != null) return
statusButtons.multiplayerStatusButton = MultiplayerStatusButton(this, game.onlineMultiplayer.getGameByGameId(gameInfo.gameId))
statusButtons.multiplayerStatusButton = MultiplayerStatusButton(this,
game.onlineMultiplayer.multiplayerFiles.getGameByGameId(gameInfo.gameId))
} else {
if (statusButtons.multiplayerStatusButton == null) return
statusButtons.multiplayerStatusButton = null

View File

@ -38,7 +38,7 @@ class MultiplayerStatusPopup(
}
private fun gameSelected(gameName: String) {
val multiplayerGame = UncivGame.Current.onlineMultiplayer.getGameByName(gameName)!!
val multiplayerGame = UncivGame.Current.onlineMultiplayer.multiplayerFiles.getGameByName(gameName)!!
selectedGame = multiplayerGame
pickerPane.setRightSideButtonEnabled(true)
pickerPane.rightSideButton.setText("Load [$gameName]".tr())