Fix multiplayer sometimes duplicating games (#6999)

* Fix multiplayer sometimes duplicating games

* Fix test compilation
This commit is contained in:
Timo T 2022-06-01 21:24:44 +02:00 committed by GitHub
parent 51e3349ecc
commit 1abc65163d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 41 deletions

View File

@ -264,12 +264,10 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
init { init {
// We can't use Gdx.files since that is only initialized within a com.badlogic.gdx.backends.android.AndroidApplication. // We can't use Gdx.files since that is only initialized within a com.badlogic.gdx.backends.android.AndroidApplication.
// Worker instances may be stopped & recreated by the Android WorkManager, so no AndroidApplication and thus no Gdx.files available // Worker instances may be stopped & recreated by the Android WorkManager, so no AndroidApplication and thus no Gdx.files available
val files = DefaultAndroidFiles(applicationContext.assets, ContextWrapper(applicationContext), false) val files = DefaultAndroidFiles(applicationContext.assets, ContextWrapper(applicationContext), true)
// GDX's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance // GDX's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance
Gdx.files = files Gdx.files = files
val externalFilesDirForAndroid = applicationContext.getExternalFilesDir(null)?.path gameSaver = GameSaver(files, null, true)
Log.d(LOG_TAG, "Creating new GameSaver with externalFilesDir=[${externalFilesDirForAndroid}]")
gameSaver = GameSaver(files, null, externalFilesDirForAndroid)
} }
override fun doWork(): Result = runBlocking { override fun doWork(): Result = runBlocking {

View File

@ -30,7 +30,9 @@ Sources for Info about current orientation in case need:
if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation
} }
override fun getExternalFilesDir(): String? { /**
return activity.getExternalFilesDir(null)?.path * On Android, local is some android-internal data directory which may or may not be accessible by the user.
} * External is probably on an SD-card or similar which is always accessible by the user.
*/
override fun shouldPreferExternalStorage(): Boolean = true
} }

View File

@ -87,7 +87,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
viewEntireMapForDebug = false viewEntireMapForDebug = false
} }
Current = this Current = this
gameSaver = GameSaver(Gdx.files, customSaveLocationHelper, platformSpecificHelper?.getExternalFilesDir()) gameSaver = GameSaver(Gdx.files, customSaveLocationHelper, platformSpecificHelper?.shouldPreferExternalStorage() == true)
// If this takes too long players, especially with older phones, get ANR problems. // If this takes too long players, especially with older phones, get ANR problems.
// Whatever needs graphics needs to be done on the main thread, // Whatever needs graphics needs to be done on the main thread,

View File

@ -14,6 +14,7 @@ import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.saves.Gzip import com.unciv.ui.saves.Gzip
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.io.File import java.io.File
@ -29,10 +30,12 @@ class GameSaver(
*/ */
private val files: Files, private val files: Files,
private val customFileLocationHelper: CustomFileLocationHelper? = null, private val customFileLocationHelper: CustomFileLocationHelper? = null,
/** When set, we know we're on Android and can save to the app's personal external file directory private val preferExternalStorage: Boolean = false
* See https://developer.android.com/training/data-storage/app-specific#external-access-files */
private val externalFilesDirForAndroid: String? = null
) { ) {
init {
debug("Creating GameSaver, localStoragePath: %s, externalStoragePath: %s, preferExternalStorage: %s",
files.localStoragePath, files.externalStoragePath, preferExternalStorage)
}
//region Data //region Data
var autoSaveJob: Job? = null var autoSaveJob: Job? = null
@ -48,11 +51,20 @@ class GameSaver(
} }
private fun getSave(saveFolder: String, gameName: String): FileHandle { private fun getSave(saveFolder: String, gameName: String): FileHandle {
val localFile = files.local("${saveFolder}/$gameName") debug("Getting save %s from folder %s, preferExternal: %s",
if (externalFilesDirForAndroid.isNullOrBlank() || !files.isExternalStorageAvailable) return localFile gameName, saveFolder, preferExternalStorage, files.externalStoragePath)
val externalFile = files.absolute(externalFilesDirForAndroid + "/${saveFolder}/$gameName") val location = "${saveFolder}/$gameName"
if (localFile.exists() && !externalFile.exists()) return localFile val localFile = files.local(location)
return externalFile val externalFile = files.external(location)
val toReturn = if (preferExternalStorage && files.isExternalStorageAvailable && (externalFile.exists() || !localFile.exists())) {
externalFile
} else {
localFile
}
debug("Save found: %s", toReturn.file().absolutePath)
return toReturn
} }
fun getMultiplayerSaves(): Sequence<FileHandle> { fun getMultiplayerSaves(): Sequence<FileHandle> {
@ -66,30 +78,48 @@ class GameSaver(
} }
private fun getSaves(saveFolder: String): Sequence<FileHandle> { private fun getSaves(saveFolder: String): Sequence<FileHandle> {
val localSaves = files.local(saveFolder).list().asSequence() debug("Getting saves from folder %s, externalStoragePath: %s", saveFolder, files.externalStoragePath)
if (externalFilesDirForAndroid.isNullOrBlank() || !files.isExternalStorageAvailable) return localSaves val localFiles = files.local(saveFolder).list().asSequence()
return localSaves + files.absolute(externalFilesDirForAndroid + "/${saveFolder}").list().asSequence()
val externalFiles = if (files.isExternalStorageAvailable) {
files.external(saveFolder).list().asSequence()
} else {
emptySequence()
}
debug("Local files: %s, external files: %s",
{ localFiles.joinToString(prefix = "[", postfix = "]", transform = { it.file().absolutePath }) },
{ externalFiles.joinToString(prefix = "[", postfix = "]", transform = { it.file().absolutePath }) })
return localFiles + externalFiles
} }
fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null
/** Deletes a save. /**
* @return `true` if successful. * @return `true` if successful.
* @throws SecurityException when delete access was denied * @throws SecurityException when delete access was denied
*/ */
fun deleteSave(gameName: String): Boolean { fun deleteSave(gameName: String): Boolean {
return getSave(gameName).delete() return deleteSave(getSave(gameName))
} }
fun deleteMultiplayerSave(gameName: String) { /**
getMultiplayerSave(gameName).delete() * @return `true` if successful.
* @throws SecurityException when delete access was denied
*/
fun deleteMultiplayerSave(gameName: String): Boolean {
return deleteSave(getMultiplayerSave(gameName))
} }
/** /**
* Only use this with a [FileHandle] obtained by one of the methods of this class! * Only use this with a [FileHandle] obtained by one of the methods of this class!
*
* @return `true` if successful.
* @throws SecurityException when delete access was denied
*/ */
fun deleteSave(file: FileHandle) { fun deleteSave(file: FileHandle): Boolean {
file.delete() debug("Deleting save %s", file.path())
return file.delete()
} }
interface ChooseLocationResult { interface ChooseLocationResult {
@ -115,6 +145,7 @@ class GameSaver(
*/ */
fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) { fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
debug("Saving GameInfo %s to %s", game.gameId, file.path())
file.writeString(gameInfoToString(game), false) file.writeString(gameInfoToString(game), false)
saveCompletionCallback(null) saveCompletionCallback(null)
} catch (ex: Exception) { } catch (ex: Exception) {
@ -136,6 +167,7 @@ class GameSaver(
*/ */
fun saveGame(game: GameInfoPreview, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) { fun saveGame(game: GameInfoPreview, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
debug("Saving GameInfoPreview %s to %s", game.gameId, file.path())
json().toJson(game, file) json().toJson(game, file)
saveCompletionCallback(null) saveCompletionCallback(null)
} catch (ex: Exception) { } catch (ex: Exception) {
@ -162,6 +194,7 @@ class GameSaver(
postCrashHandlingRunnable { saveCompletionCallback(CustomSaveResult(exception = ex)) } postCrashHandlingRunnable { saveCompletionCallback(CustomSaveResult(exception = ex)) }
return return
} }
debug("Saving GameInfo %s to custom location %s", game.gameId, saveLocation)
customFileLocationHelper!!.saveGame(gameData, saveLocation) { customFileLocationHelper!!.saveGame(gameData, saveLocation) {
if (it.isSuccessful()) { if (it.isSuccessful()) {
game.customSaveLocation = it.location game.customSaveLocation = it.location

View File

@ -95,16 +95,19 @@ class OnlineMultiplayer {
private fun updateSavesFromFiles() { private fun updateSavesFromFiles() {
val saves = gameSaver.getMultiplayerSaves() val saves = gameSaver.getMultiplayerSaves()
val removedSaves = savedGames.keys - saves.toSet() val removedSaves = savedGames.keys - saves.toSet()
removedSaves.forEach(savedGames::remove) for (saveFile in removedSaves) {
deleteGame(saveFile)
}
val newSaves = saves - savedGames.keys val newSaves = saves - savedGames.keys
for (saveFile in newSaves) { for (saveFile in newSaves) {
val game = OnlineMultiplayerGame(saveFile) addGame(saveFile)
savedGames[saveFile] = game
postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(game.name)) }
} }
} }
/** /**
* Fires [MultiplayerGameAdded] * Fires [MultiplayerGameAdded]
* *
@ -123,7 +126,7 @@ class OnlineMultiplayer {
* @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time * @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 * @throws FileNotFoundException if the file can't be found
*/ */
suspend fun addGame(gameId: String, gameName: String? = null): String { suspend fun addGame(gameId: String, gameName: String? = null) {
val saveFileName = if (gameName.isNullOrBlank()) gameId else gameName val saveFileName = if (gameName.isNullOrBlank()) gameId else gameName
var gamePreview: GameInfoPreview var gamePreview: GameInfoPreview
try { try {
@ -132,7 +135,7 @@ class OnlineMultiplayer {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
gamePreview = onlineGameSaver.tryDownloadGame(gameId).asPreview() gamePreview = onlineGameSaver.tryDownloadGame(gameId).asPreview()
} }
return addGame(gamePreview, saveFileName) addGame(gamePreview, saveFileName)
} }
private fun addGame(newGame: GameInfo) { private fun addGame(newGame: GameInfo) {
@ -140,12 +143,15 @@ class OnlineMultiplayer {
addGame(newGamePreview, newGamePreview.gameId) addGame(newGamePreview, newGamePreview.gameId)
} }
private fun addGame(preview: GameInfoPreview, saveFileName: String): String { private fun addGame(preview: GameInfoPreview, saveFileName: String) {
val fileHandle = gameSaver.saveGame(preview, saveFileName) val fileHandle = gameSaver.saveGame(preview, saveFileName)
return addGame(fileHandle, preview)
}
private fun addGame(fileHandle: FileHandle, preview: GameInfoPreview = gameSaver.loadGamePreviewFromFile(fileHandle)) {
val game = OnlineMultiplayerGame(fileHandle, preview, Instant.now()) val game = OnlineMultiplayerGame(fileHandle, preview, Instant.now())
savedGames[fileHandle] = game savedGames[fileHandle] = game
postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(game.name)) } postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(game.name)) }
return saveFileName
} }
fun getGameByName(name: String): OnlineMultiplayerGame? { fun getGameByName(name: String): OnlineMultiplayerGame? {
@ -252,9 +258,17 @@ class OnlineMultiplayer {
* Fires [MultiplayerGameDeleted] * Fires [MultiplayerGameDeleted]
*/ */
fun deleteGame(multiplayerGame: OnlineMultiplayerGame) { fun deleteGame(multiplayerGame: OnlineMultiplayerGame) {
val name = multiplayerGame.name deleteGame(multiplayerGame.fileHandle)
gameSaver.deleteSave(multiplayerGame.fileHandle) }
EventBus.send(MultiplayerGameDeleted(name))
private fun deleteGame(fileHandle: FileHandle) {
gameSaver.deleteSave(fileHandle)
val game = savedGames[fileHandle]
if (game == null) return
savedGames.remove(game.fileHandle)
postCrashHandlingRunnable { EventBus.send(MultiplayerGameDeleted(game.name)) }
} }
/** /**

View File

@ -19,7 +19,8 @@ interface GeneralPlatformSpecificHelpers {
fun notifyTurnStarted() {} fun notifyTurnStarted() {}
/** /**
* @return an additional external directory for save files, if applicable on the platform * If the GDX [com.badlogic.gdx.Files.getExternalStoragePath] should be preferred for this platform,
* otherwise uses [com.badlogic.gdx.Files.getLocalStoragePath]
*/ */
fun getExternalFilesDir(): String? { return null } fun shouldPreferExternalStorage(): Boolean
} }

View File

@ -13,4 +13,6 @@ class PlatformSpecificHelpersDesktop(config: Lwjgl3ApplicationConfiguration) : G
turnNotifier.turnStarted() turnNotifier.turnStarted()
} }
/** On desktop, external is likely some document folder, while local is the game directory. We'd like to keep everything in the game directory */
override fun shouldPreferExternalStorage(): Boolean = false
} }