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

View File

@ -95,16 +95,19 @@ class OnlineMultiplayer {
private fun updateSavesFromFiles() {
val saves = gameSaver.getMultiplayerSaves()
val removedSaves = savedGames.keys - saves.toSet()
removedSaves.forEach(savedGames::remove)
for (saveFile in removedSaves) {
deleteGame(saveFile)
}
val newSaves = saves - savedGames.keys
for (saveFile in newSaves) {
val game = OnlineMultiplayerGame(saveFile)
savedGames[saveFile] = game
postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(game.name)) }
addGame(saveFile)
}
}
/**
* 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 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
var gamePreview: GameInfoPreview
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
gamePreview = onlineGameSaver.tryDownloadGame(gameId).asPreview()
}
return addGame(gamePreview, saveFileName)
addGame(gamePreview, saveFileName)
}
private fun addGame(newGame: GameInfo) {
@ -140,12 +143,15 @@ class OnlineMultiplayer {
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)
return addGame(fileHandle, preview)
}
private fun addGame(fileHandle: FileHandle, preview: GameInfoPreview = gameSaver.loadGamePreviewFromFile(fileHandle)) {
val game = OnlineMultiplayerGame(fileHandle, preview, Instant.now())
savedGames[fileHandle] = game
postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(game.name)) }
return saveFileName
}
fun getGameByName(name: String): OnlineMultiplayerGame? {
@ -252,9 +258,17 @@ class OnlineMultiplayer {
* Fires [MultiplayerGameDeleted]
*/
fun deleteGame(multiplayerGame: OnlineMultiplayerGame) {
val name = multiplayerGame.name
gameSaver.deleteSave(multiplayerGame.fileHandle)
EventBus.send(MultiplayerGameDeleted(name))
deleteGame(multiplayerGame.fileHandle)
}
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() {}
/**
* @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()
}
/** 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
}