From fc9668f2d04906d3382cccbd38a40fa88a7f8fe7 Mon Sep 17 00:00:00 2001 From: Timo T Date: Sun, 22 May 2022 18:51:35 +0200 Subject: [PATCH] Refactor: change GameSaver from singleton to single instance pattern & move autosave logic into GameSaver (#6846) * Refactor: change GameSaver from singleton to single instance pattern & move autosave logic info GameSaver Singleton just doesn't make sense anymore when we have to `init(..)` with different arguments, then we should just make a normal class out of it * Fix not correctly checking for missing external files dir * Refactor: use more appropriate library method * Add logging for external files dir --- android/src/com/unciv/app/AndroidLauncher.kt | 9 +- .../app/CustomSaveLocationHelperAndroid.kt | 12 +- .../unciv/app/MultiplayerTurnCheckWorker.kt | 8 +- .../app/PlatformSpecificHelpersAndroid.kt | 4 + core/src/com/unciv/MainMenuScreen.kt | 31 ++-- core/src/com/unciv/UncivGame.kt | 40 +++-- .../unciv/logic/CustomSaveLocationHelper.kt | 3 +- core/src/com/unciv/logic/GameSaver.kt | 152 ++++++++++++------ .../logic/multiplayer/OnlineMultiplayer.kt | 19 +-- .../multiplayer/OnlineMultiplayerGame.kt | 5 +- .../storage/OnlineMultiplayerGameSaver.kt | 10 +- .../com/unciv/models/metadata/GameSettings.kt | 26 +-- .../com/unciv/ui/crashhandling/CrashScreen.kt | 3 +- .../unciv/ui/newgamescreen/NewGameScreen.kt | 2 +- core/src/com/unciv/ui/options/DebugTab.kt | 4 +- core/src/com/unciv/ui/saves/LoadGameScreen.kt | 18 +-- core/src/com/unciv/ui/saves/SaveGameScreen.kt | 14 +- .../utils/GeneralPlatformSpecificHelpers.kt | 5 + .../com/unciv/ui/worldscreen/WorldScreen.kt | 7 +- .../mainmenu/WorldScreenMenuPopup.kt | 3 +- .../CustomSaveLocationHelperDesktop.kt | 8 +- .../com/unciv/app/desktop/DesktopLauncher.kt | 3 +- .../src/com/unciv/testing/GdxTestRunner.java | 10 +- .../com/unciv/testing/SerializationTests.kt | 4 +- 24 files changed, 213 insertions(+), 187 deletions(-) diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index e9f983482a..258a27fb48 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -10,7 +10,6 @@ import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration import com.unciv.UncivGame import com.unciv.UncivGameParameters import com.unciv.logic.GameSaver -import com.unciv.models.metadata.GameSettings import com.unciv.ui.utils.Fonts import java.io.File @@ -24,14 +23,12 @@ open class AndroidLauncher : AndroidApplication() { MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext) copyMods() - val externalFilesDir = getExternalFilesDir(null) - if (externalFilesDir != null) GameSaver.externalFilesDirForAndroid = externalFilesDir.path val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true } - val settings = GameSettings.getSettingsForPlatformLaunchers(filesDir.path) + val settings = GameSaver.getSettingsForPlatformLaunchers(filesDir.path) val fontFamily = settings.fontFamily // Manage orientation lock @@ -73,8 +70,8 @@ open class AndroidLauncher : AndroidApplication() { if (UncivGame.isCurrentInitialized() && UncivGame.Current.isGameInfoInitialized() && UncivGame.Current.settings.multiplayerTurnCheckerEnabled - && GameSaver.getSaves(true).any()) { - MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, GameSaver, UncivGame.Current.gameInfo, UncivGame.Current.settings) + && UncivGame.Current.gameSaver.getMultiplayerSaves().any()) { + MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver, UncivGame.Current.gameInfo, UncivGame.Current.settings) } super.onPause() } diff --git a/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt b/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt index a12502ef34..428cafb6dc 100644 --- a/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt +++ b/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt @@ -27,14 +27,14 @@ class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSa @GuardedBy("this") private val callbacks = ArrayList() - override fun saveGame(gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) { + override fun saveGame(gameSaver: GameSaver, gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) { val callbackIndex = synchronized(this) { val index = callbackIndex++ callbacks.add(IndexedCallback( index, { uri -> if (uri != null) { - saveGame(gameInfo, uri) + saveGame(gameSaver, gameInfo, uri) saveCompleteCallback?.invoke(null) } else { saveCompleteCallback?.invoke(RuntimeException("Uri was null")) @@ -68,16 +68,16 @@ class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSa } } - private fun saveGame(gameInfo: GameInfo, uri: Uri) { + private fun saveGame(gameSaver: GameSaver, gameInfo: GameInfo, uri: Uri) { gameInfo.customSaveLocation = uri.toString() activity.contentResolver.openOutputStream(uri, "rwt") ?.writer() ?.use { - it.write(GameSaver.gameInfoToString(gameInfo)) + it.write(gameSaver.gameInfoToString(gameInfo)) } } - override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { + override fun loadGame(gameSaver: GameSaver, loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { val callbackIndex = synchronized(this) { val index = callbackIndex++ callbacks.add(IndexedCallback( @@ -90,7 +90,7 @@ class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSa ?.reader() ?.readText() ?.run { - GameSaver.gameInfoFromString(this) + gameSaver.gameInfoFromString(this) } } catch (e: Exception) { exception = e diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index a3493abf76..dbb4c21a91 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -182,7 +182,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame fun startTurnChecker(applicationContext: Context, gameSaver: GameSaver, currentGameInfo: GameInfo, settings: GameSettings) { Log.i(LOG_TAG, "startTurnChecker") - val gameFiles = gameSaver.getSaves(true) + val gameFiles = gameSaver.getMultiplayerSaves() val gameIds = Array(gameFiles.count()) {""} val gameNames = Array(gameFiles.count()) {""} @@ -255,14 +255,16 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame */ private val notFoundRemotely = mutableMapOf() - private val gameSaver = GameSaver + private val gameSaver: GameSaver 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) // GDX's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance Gdx.files = files - gameSaver.init(files, null) + val externalFilesDirForAndroid = applicationContext.getExternalFilesDir(null)?.path + Log.d(LOG_TAG, "Creating new GameSaver with externalFilesDir=[${externalFilesDirForAndroid}]") + gameSaver = GameSaver(files, null, externalFilesDirForAndroid) } override fun doWork(): Result = runBlocking { diff --git a/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt b/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt index 8746b845b6..37d1d1190b 100644 --- a/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt +++ b/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt @@ -29,4 +29,8 @@ Sources for Info about current orientation in case need: // Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation } + + override fun getExternalFilesDir(): String? { + return activity.getExternalFilesDir(null)?.path + } } diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 837e1d77e2..8e7bdac87d 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -7,7 +7,6 @@ import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver import com.unciv.logic.GameStarter import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapSize @@ -97,8 +96,7 @@ class MainMenuScreen: BaseScreen() { val column1 = Table().apply { defaults().pad(10f).fillX() } val column2 = if (singleColumn) column1 else Table().apply { defaults().pad(10f).fillX() } - val autosaveGame = GameSaver.getSave(GameSaver.autoSaveFileName, false) - if (autosaveGame.exists()) { + if (game.gameSaver.autosaveExists()) { val resumeTable = getMenuButton("Resume","OtherIcons/Resume", 'r') { autoLoadGame() } column1.add(resumeTable).row() @@ -112,7 +110,7 @@ class MainMenuScreen: BaseScreen() { { game.setScreen(NewGameScreen(this)) } column1.add(newGameButton).row() - if (GameSaver.getSaves(false).any()) { + if (game.gameSaver.getSaves().any()) { val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", 'l') { game.setScreen(LoadGameScreen(this)) } column1.add(loadGameTable).row() @@ -180,29 +178,18 @@ class MainMenuScreen: BaseScreen() { } } - var savedGame: GameInfo + val savedGame: GameInfo try { - savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName) + savedGame = game.gameSaver.loadLatestAutosave() } catch (oom: OutOfMemoryError) { outOfMemory() return@launchCrashHandling - } catch (ex: Exception) { // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first - // This can help for situations when the autosave is corrupted - try { - val autosaves = GameSaver.getSaves() - .filter { it.name() != GameSaver.autoSaveFileName && it.name().startsWith(GameSaver.autoSaveFileName) } - savedGame = - GameSaver.loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!) - } catch (oom: OutOfMemoryError) { // The autosave could have oom problems as well... smh - outOfMemory() - return@launchCrashHandling - } catch (ex: Exception) { - postCrashHandlingRunnable { - loadingPopup.close() - ToastPopup("Cannot resume game!", this@MainMenuScreen) - } - return@launchCrashHandling + } catch (ex: Exception) { + postCrashHandlingRunnable { + loadingPopup.close() + ToastPopup("Cannot resume game!", this@MainMenuScreen) } + return@launchCrashHandling } postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index b20cfae92a..1c31c8c32f 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -27,6 +27,7 @@ import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.multiplayer.LoadDeepLinkScreen import com.unciv.ui.popup.Popup +import kotlinx.coroutines.runBlocking import java.util.* class UncivGame(parameters: UncivGameParameters) : Game() { @@ -48,6 +49,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { lateinit var settings: GameSettings lateinit var musicController: MusicController lateinit var onlineMultiplayer: OnlineMultiplayer + lateinit var gameSaver: GameSaver /** * This exists so that when debugging we can see the entire map. @@ -87,7 +89,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { viewEntireMapForDebug = false } Current = this - GameSaver.init(Gdx.files, customSaveLocationHelper) + gameSaver = GameSaver(Gdx.files, customSaveLocationHelper, platformSpecificHelper?.getExternalFilesDir()) // If this takes too long players, especially with older phones, get ANR problems. // Whatever needs graphics needs to be done on the main thread, @@ -101,7 +103,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { * - Skin (hence BaseScreen.setSkin()) * - Font (hence Fonts.resetFont() inside setSkin()) */ - settings = GameSaver.getGeneralSettings() // needed for the screen + settings = gameSaver.getGeneralSettings() // needed for the screen screen = LoadingScreen() // NOT dependent on any atlas or skin musicController = MusicController() // early, but at this point does only copy volume from settings audioExceptionHelper?.installHooks( @@ -170,7 +172,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { } /** - * If called with null [newWorldScreen], disposes of the current screen and sets it to the current stored world screen. + * If called with null [newWorldScreen], disposes of the current screen and sets it to the current stored world screen. * If the current screen is already the world screen, the only thing that happens is that the world screen updates. */ fun resetToWorldScreen(newWorldScreen: WorldScreen? = null) { @@ -221,7 +223,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { } override fun pause() { - if (isGameInfoInitialized()) GameSaver.autoSave(this.gameInfo) + if (isGameInfoInitialized()) gameSaver.autoSave(this.gameInfo) musicController.pause() super.pause() } @@ -232,7 +234,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { override fun render() = wrappedCrashHandlingRender() - override fun dispose() { + override fun dispose() = runBlocking { Gdx.input.inputProcessor = null // don't allow ANRs when shutting down, that's silly cancelDiscordEvent?.invoke() @@ -240,24 +242,28 @@ class UncivGame(parameters: UncivGameParameters) : Game() { if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out closeExecutors() - // Log still running threads (on desktop that should be only this one and "DestroyJavaVM") - val numThreads = Thread.activeCount() - val threadList = Array(numThreads) { Thread() } - Thread.enumerate(threadList) - if (isGameInfoInitialized()) { - val autoSaveThread = threadList.firstOrNull { it.name == GameSaver.autoSaveFileName } - if (autoSaveThread != null && autoSaveThread.isAlive) { + val autoSaveJob = gameSaver.autoSaveJob + if (autoSaveJob != null && autoSaveJob.isActive) { // auto save is already in progress (e.g. started by onPause() event) // let's allow it to finish and do not try to autosave second time - autoSaveThread.join() - } else - GameSaver.autoSaveSingleThreaded(gameInfo) // NO new thread + autoSaveJob.join() + } else { + gameSaver.autoSaveSingleThreaded(gameInfo) // NO new thread + } } settings.save() - threadList.filter { it !== Thread.currentThread() && it.name != "DestroyJavaVM"}.forEach { - println (" Thread ${it.name} still running in UncivGame.dispose().") + // On desktop this should only be this one and "DestroyJavaVM" + logRunningThreads() + } + + private fun logRunningThreads() { + val numThreads = Thread.activeCount() + val threadList = Array(numThreads) { _ -> Thread() } + Thread.enumerate(threadList) + threadList.filter { it !== Thread.currentThread() && it.name != "DestroyJavaVM" }.forEach { + println(" Thread ${it.name} still running in UncivGame.dispose().") } } diff --git a/core/src/com/unciv/logic/CustomSaveLocationHelper.kt b/core/src/com/unciv/logic/CustomSaveLocationHelper.kt index dd95bd5d4e..829d3f589c 100644 --- a/core/src/com/unciv/logic/CustomSaveLocationHelper.kt +++ b/core/src/com/unciv/logic/CustomSaveLocationHelper.kt @@ -20,6 +20,7 @@ interface CustomSaveLocationHelper { * @param saveCompleteCallback Action to call upon completion (success _and_ failure) */ fun saveGame( + gameSaver: GameSaver, gameInfo: GameInfo, gameName: String, forcePrompt: Boolean = false, @@ -33,5 +34,5 @@ interface CustomSaveLocationHelper { * * @param loadCompleteCallback Action to call upon completion (success _and_ failure) */ - fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) + fun loadGame(gameSaver: GameSaver, loadCompleteCallback: (GameInfo?, Exception?) -> Unit) } diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index f716640bc8..e418fec407 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -4,68 +4,83 @@ import com.badlogic.gdx.Files import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame +import com.unciv.json.fromJsonFile import com.unciv.json.json import com.unciv.models.metadata.GameSettings import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.saves.Gzip +import kotlinx.coroutines.Job import java.io.File +private const val SAVE_FILES_FOLDER = "SaveFiles" +private const val MULTIPLAYER_FILES_FOLDER = "MultiplayerGames" +private const val AUTOSAVE_FILE_NAME = "Autosave" +private const val SETTINGS_FILE_NAME = "GameSettings.json" -object GameSaver { - //region Data - - private const val saveFilesFolder = "SaveFiles" - private const val multiplayerFilesFolder = "MultiplayerGames" - const val autoSaveFileName = "Autosave" - const val settingsFileName = "GameSettings.json" - var saveZipped = false - +class GameSaver( /** * This is necessary because the Android turn check background worker does not hold any reference to the actual [com.badlogic.gdx.Application], * which is normally responsible for keeping the [Gdx] static variables from being garbage collected. */ - private lateinit var files: Files - - private var customSaveLocationHelper: CustomSaveLocationHelper? = null - + private val files: Files, + private val customSaveLocationHelper: CustomSaveLocationHelper? = 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 */ - var externalFilesDirForAndroid = "" + private val externalFilesDirForAndroid: String? = null +) { + //region Data - /** Needs to be called before the class can be used */ - fun init(files: Files, customSaveLocationHelper: CustomSaveLocationHelper?) { - this.files = files - this.customSaveLocationHelper = customSaveLocationHelper - } + var saveZipped = false + + var autoSaveJob: Job? = null //endregion //region Helpers - private fun getSavefolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder + fun getSave(gameName: String): FileHandle { + return getSave(SAVE_FILES_FOLDER, gameName) + } + fun getMultiplayerSave(gameName: String): FileHandle { + return getSave(MULTIPLAYER_FILES_FOLDER, gameName) + } - fun getSave(GameName: String, multiplayer: Boolean = false): FileHandle { - val localFile = files.local("${getSavefolder(multiplayer)}/$GameName") - if (externalFilesDirForAndroid == "" || !files.isExternalStorageAvailable) return localFile - val externalFile = files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}/$GameName") + 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 } - fun getSaves(multiplayer: Boolean = false): Sequence { - val localSaves = files.local(getSavefolder(multiplayer)).list().asSequence() - if (externalFilesDirForAndroid == "" || !files.isExternalStorageAvailable) return localSaves - return localSaves + files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}").list().asSequence() + fun getMultiplayerSaves(): Sequence { + return getSaves(MULTIPLAYER_FILES_FOLDER) + } + + fun getSaves(autoSaves: Boolean = true): Sequence { + val saves = getSaves(SAVE_FILES_FOLDER) + val filteredSaves = if (autoSaves) { saves } else { saves.filter { !it.name().startsWith(AUTOSAVE_FILE_NAME) }} + return filteredSaves + } + + private fun getSaves(saveFolder: String): Sequence { + val localSaves = files.local(saveFolder).list().asSequence() + if (externalFilesDirForAndroid.isNullOrBlank() || !files.isExternalStorageAvailable) return localSaves + return localSaves + files.absolute(externalFilesDirForAndroid + "/${saveFolder}").list().asSequence() } fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null - fun deleteSave(GameName: String, multiplayer: Boolean = false) { - getSave(GameName, multiplayer).delete() + fun deleteSave(gameName: String) { + getSave(gameName).delete() + } + + fun deleteMultiplayerSave(gameName: String) { + getMultiplayerSave(gameName).delete() } /** - * Only use this with a [FileHandle] returned by [getSaves]! + * Only use this with a [FileHandle] obtained by one of the methods of this class! */ fun deleteSave(file: FileHandle) { file.delete() @@ -81,7 +96,7 @@ object GameSaver { } /** - * Only use this with a [FileHandle] obtained by [getSaves]! + * Only use this with a [FileHandle] obtained by one of the methods of this class! */ fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) { try { @@ -98,7 +113,7 @@ object GameSaver { return if (forceZip ?: saveZipped) Gzip.zip(plainJson) else plainJson } - /** Returns gzipped serialization of preview [game] - only called from [OnlineMultiplayerGameSaver] */ + /** Returns gzipped serialization of preview [game] */ fun gameInfoToString(game: GameInfoPreview): String { return Gzip.zip(json().toJson(game)) } @@ -106,14 +121,14 @@ object GameSaver { /** * Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder */ - fun saveGame(game: GameInfoPreview, GameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle { - val file = getSave(GameName, true) + fun saveGame(game: GameInfoPreview, gameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle { + val file = getMultiplayerSave(gameName) saveGame(game, file, saveCompletionCallback) return file } /** - * Only use this with a [FileHandle] obtained by [getSaves]! + * Only use this with a [FileHandle] obtained by one of the methods of this class! */ fun saveGame(game: GameInfoPreview, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) { try { @@ -125,28 +140,28 @@ object GameSaver { } fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) { - customSaveLocationHelper!!.saveGame(game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback) + customSaveLocationHelper!!.saveGame(this, game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback) } //endregion //region Loading - fun loadGameByName(GameName: String) = - loadGameFromFile(getSave(GameName)) + fun loadGameByName(gameName: String) = + loadGameFromFile(getSave(gameName)) fun loadGameFromFile(gameFile: FileHandle): GameInfo { return gameInfoFromString(gameFile.readString()) } - fun loadGamePreviewByName(GameName: String) = - loadGamePreviewFromFile(getSave(GameName, true)) + fun loadGamePreviewByName(gameName: String) = + loadGamePreviewFromFile(getMultiplayerSave(gameName)) fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview { return json().fromJson(GameInfoPreview::class.java, gameFile) } fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) { - customSaveLocationHelper!!.loadGame { game, e -> + customSaveLocationHelper!!.loadGame(this) { game, e -> loadCompletionCallback(game?.apply { setTransients() }, e) } } @@ -158,7 +173,7 @@ object GameSaver { } /** - * Parses [gameData] as gzipped serialization of a [GameInfoPreview] - only called from [OnlineMultiplayerGameSaver] + * Parses [gameData] as gzipped serialization of a [GameInfoPreview] * @throws SerializationException */ fun gameInfoPreviewFromString(gameData: String): GameInfoPreview { @@ -185,8 +200,8 @@ object GameSaver { //region Settings private fun getGeneralSettingsFile(): FileHandle { - return if (UncivGame.Current.consoleMode) FileHandle(settingsFileName) - else files.local(settingsFileName) + return if (UncivGame.Current.consoleMode) FileHandle(SETTINGS_FILE_NAME) + else files.local(SETTINGS_FILE_NAME) } fun getGeneralSettings(): GameSettings { @@ -213,9 +228,30 @@ object GameSaver { getGeneralSettingsFile().writeString(json().toJson(gameSettings), false) } + companion object { + /** Specialized function to access settings before Gdx is initialized. + * + * @param base Path to the directory where the file should be - if not set, the OS current directory is used (which is "/" on Android) + */ + fun getSettingsForPlatformLaunchers(base: String = "."): GameSettings { + // FileHandle is Gdx, but the class and JsonParser are not dependent on app initialization + // In fact, at this point Gdx.app or Gdx.files are null but this still works. + val file = FileHandle(base + File.separator + SETTINGS_FILE_NAME) + return if (file.exists()) + json().fromJsonFile( + GameSettings::class.java, + file + ) + else GameSettings().apply { isFreshlyCreated = true } + } + } + //endregion //region Autosave + /** + * Runs autoSave + */ fun autoSave(gameInfo: GameInfo, postRunnable: () -> Unit = {}) { // The save takes a long time (up to a few seconds on large games!) and we can do it while the player continues his game. // On the other hand if we alter the game data while it's being serialized we could get a concurrent modification exception. @@ -225,7 +261,7 @@ object GameSaver { fun autoSaveUnCloned(gameInfo: GameInfo, postRunnable: () -> Unit = {}) { // This is used when returning from WorldScreen to MainMenuScreen - no clone since UI access to it should be gone - launchCrashHandling(autoSaveFileName, runAsDaemon = false) { + autoSaveJob = launchCrashHandling(AUTOSAVE_FILE_NAME) { autoSaveSingleThreaded(gameInfo) // do this on main thread postCrashHandlingRunnable ( postRunnable ) @@ -234,22 +270,38 @@ object GameSaver { fun autoSaveSingleThreaded(gameInfo: GameInfo) { try { - saveGame(gameInfo, autoSaveFileName) + saveGame(gameInfo, AUTOSAVE_FILE_NAME) } catch (oom: OutOfMemoryError) { return // not much we can do here } // keep auto-saves for the last 10 turns for debugging purposes val newAutosaveFilename = - saveFilesFolder + File.separator + autoSaveFileName + "-${gameInfo.currentPlayer}-${gameInfo.turns}" - getSave(autoSaveFileName).copyTo(files.local(newAutosaveFilename)) + SAVE_FILES_FOLDER + File.separator + AUTOSAVE_FILE_NAME + "-${gameInfo.currentPlayer}-${gameInfo.turns}" + getSave(AUTOSAVE_FILE_NAME).copyTo(files.local(newAutosaveFilename)) fun getAutosaves(): Sequence { - return getSaves().filter { it.name().startsWith(autoSaveFileName) } + return getSaves().filter { it.name().startsWith(AUTOSAVE_FILE_NAME) } } while (getAutosaves().count() > 10) { val saveToDelete = getAutosaves().minByOrNull { it.lastModified() }!! deleteSave(saveToDelete.name()) } } + + fun loadLatestAutosave(): GameInfo { + try { + return loadGameByName(AUTOSAVE_FILE_NAME) + } catch (ex: Exception) { + // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first + val autosaves = getSaves().filter { it.name() != AUTOSAVE_FILE_NAME && it.name().startsWith(AUTOSAVE_FILE_NAME) } + return loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!) + } + } + + fun autosaveExists(): Boolean { + return getSave(AUTOSAVE_FILE_NAME).exists() + } + + // endregion } diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt index 7858f4c781..7c30ec3d18 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt @@ -38,7 +38,8 @@ private val FILE_UPDATE_THROTTLE_INTERVAL = Duration.ofSeconds(60) * * See the file of [com.unciv.logic.multiplayer.MultiplayerGameAdded] for all available [EventBus] events. */ -class OnlineMultiplayer { +class OnlineMultiplayer() { + private val gameSaver = UncivGame.Current.gameSaver private val savedGames: MutableMap = Collections.synchronizedMap(mutableMapOf()) private var lastFileUpdate: AtomicReference = AtomicReference() @@ -80,7 +81,7 @@ class OnlineMultiplayer { private fun fileUpdateNeeded(it: Instant?) = it == null || Duration.between(it, Instant.now()).isLargerThan(FILE_UPDATE_THROTTLE_INTERVAL) private fun updateSavesFromFiles() { - val saves = GameSaver.getSaves(true) + val saves = gameSaver.getMultiplayerSaves() val removedSaves = savedGames.keys - saves removedSaves.forEach(savedGames::remove) val newSaves = saves - savedGames.keys @@ -99,7 +100,7 @@ class OnlineMultiplayer { suspend fun createGame(newGame: GameInfo) { OnlineMultiplayerGameSaver().tryUploadGame(newGame, withPreview = true) val newGamePreview = newGame.asPreview() - val file = GameSaver.saveGame(newGamePreview, newGamePreview.gameId) + val file = gameSaver.saveGame(newGamePreview, newGamePreview.gameId) val onlineMultiplayerGame = OnlineMultiplayerGame(file, newGamePreview, Instant.now()) savedGames[file] = onlineMultiplayerGame postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(onlineMultiplayerGame.name)) } @@ -119,11 +120,11 @@ class OnlineMultiplayer { var fileHandle: FileHandle try { gamePreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameId) - fileHandle = GameSaver.saveGame(gamePreview, saveFileName) + fileHandle = gameSaver.saveGame(gamePreview, saveFileName) } catch (ex: FileNotFoundException) { // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId).asPreview() - fileHandle = GameSaver.saveGame(gamePreview, saveFileName) + fileHandle = gameSaver.saveGame(gamePreview, saveFileName) } val game = OnlineMultiplayerGame(fileHandle, gamePreview, Instant.now()) savedGames[fileHandle] = game @@ -172,7 +173,7 @@ class OnlineMultiplayer { } val newPreview = gameInfo.asPreview() - GameSaver.saveGame(newPreview, game.fileHandle) + gameSaver.saveGame(newPreview, game.fileHandle) OnlineMultiplayerGameSaver().tryUploadGame(gameInfo, withPreview = true) game.doManualUpdate(newPreview) postCrashHandlingRunnable { EventBus.send(MultiplayerGameUpdated(game.name, newPreview)) } @@ -208,7 +209,7 @@ class OnlineMultiplayer { */ fun deleteGame(multiplayerGame: OnlineMultiplayerGame) { val name = multiplayerGame.name - GameSaver.deleteSave(multiplayerGame.fileHandle) + gameSaver.deleteSave(multiplayerGame.fileHandle) EventBus.send(MultiplayerGameDeleted(name)) } @@ -224,8 +225,8 @@ class OnlineMultiplayer { val oldName = game.name savedGames.remove(game.fileHandle) - GameSaver.deleteSave(game.fileHandle) - val newFileHandle = GameSaver.saveGame(oldPreview, newName) + gameSaver.deleteSave(game.fileHandle) + val newFileHandle = gameSaver.saveGame(oldPreview, newName) val newGame = OnlineMultiplayerGame(newFileHandle, oldPreview, oldLastUpdate) savedGames[newFileHandle] = newGame diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt index 321c201cff..dbc1b14831 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt @@ -4,7 +4,6 @@ import com.badlogic.gdx.files.FileHandle import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.GameInfoPreview -import com.unciv.logic.GameSaver import com.unciv.logic.event.EventBus import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver @@ -51,7 +50,7 @@ class OnlineMultiplayerGame( } private fun loadPreviewFromFile(): GameInfoPreview { - val previewFromFile = GameSaver.loadGamePreviewFromFile(fileHandle) + val previewFromFile = UncivGame.Current.gameSaver.loadGamePreviewFromFile(fileHandle) preview = previewFromFile return previewFromFile } @@ -91,7 +90,7 @@ class OnlineMultiplayerGame( val curPreview = if (preview != null) preview!! else loadPreviewFromFile() val newPreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(curPreview.gameId) if (newPreview.turns == curPreview.turns && newPreview.currentPlayer == curPreview.currentPlayer) return GameUpdateResult.UNCHANGED - GameSaver.saveGame(newPreview, fileHandle) + UncivGame.Current.gameSaver.saveGame(newPreview, fileHandle) preview = newPreview return GameUpdateResult.CHANGED } diff --git a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt b/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt index 5f8d726008..2c17a99be7 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt @@ -4,7 +4,6 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.GameInfoPreview -import com.unciv.logic.GameSaver /** * Allows access to games stored on a server for multiplayer purposes. @@ -20,6 +19,7 @@ import com.unciv.logic.GameSaver class OnlineMultiplayerGameSaver( private var fileStorageIdentifier: String? = null ) { + private val gameSaver = UncivGame.Current.gameSaver fun fileStorage(): FileStorage { val identifier = if (fileStorageIdentifier == null) UncivGame.Current.settings.multiplayerServer else fileStorageIdentifier @@ -34,7 +34,7 @@ class OnlineMultiplayerGameSaver( tryUploadGamePreview(gameInfo.asPreview()) } - val zippedGameInfo = GameSaver.gameInfoToString(gameInfo, forceZip = true) + val zippedGameInfo = gameSaver.gameInfoToString(gameInfo, forceZip = true) fileStorage().saveFileData(gameInfo.gameId, zippedGameInfo, true) } @@ -49,7 +49,7 @@ class OnlineMultiplayerGameSaver( * @see GameInfo.asPreview */ suspend fun tryUploadGamePreview(gameInfo: GameInfoPreview) { - val zippedGameInfo = GameSaver.gameInfoToString(gameInfo) + val zippedGameInfo = gameSaver.gameInfoToString(gameInfo) fileStorage().saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo, true) } @@ -59,7 +59,7 @@ class OnlineMultiplayerGameSaver( */ suspend fun tryDownloadGame(gameId: String): GameInfo { val zippedGameInfo = fileStorage().loadFileData(gameId) - return GameSaver.gameInfoFromString(zippedGameInfo) + return gameSaver.gameInfoFromString(zippedGameInfo) } /** @@ -68,6 +68,6 @@ class OnlineMultiplayerGameSaver( */ suspend fun tryDownloadGamePreview(gameId: String): GameInfoPreview { val zippedGameInfo = fileStorage().loadFileData("${gameId}_Preview") - return GameSaver.gameInfoPreviewFromString(zippedGameInfo) + return gameSaver.gameInfoPreviewFromString(zippedGameInfo) } } \ No newline at end of file diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 80a296eab9..39b623dcdc 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -2,13 +2,9 @@ package com.unciv.models.metadata import com.badlogic.gdx.Application import com.badlogic.gdx.Gdx -import com.badlogic.gdx.files.FileHandle import com.unciv.Constants -import com.unciv.json.fromJsonFile -import com.unciv.json.json -import com.unciv.logic.GameSaver +import com.unciv.UncivGame import com.unciv.ui.utils.Fonts -import java.io.File import java.text.Collator import java.util.* import kotlin.collections.HashSet @@ -86,7 +82,7 @@ class GameSettings { if (!isFreshlyCreated && Gdx.app?.type == Application.ApplicationType.Desktop) { windowState = WindowState(Gdx.graphics.width, Gdx.graphics.height) } - GameSaver.setGeneralSettings(this) + UncivGame.Current.gameSaver.setGeneralSettings(this) } fun addCompletedTutorialTask(tutorialTask: String) { @@ -114,24 +110,6 @@ class GameSettings { fun getCollatorFromLocale(): Collator { return Collator.getInstance(getCurrentLocale()) } - - companion object { - /** Specialized function to access settings before Gdx is initialized. - * - * @param base Path to the directory where the file should be - if not set, the OS current directory is used (which is "/" on Android) - */ - fun getSettingsForPlatformLaunchers(base: String = "."): GameSettings { - // FileHandle is Gdx, but the class and JsonParser are not dependent on app initialization - // In fact, at this point Gdx.app or Gdx.files are null but this still works. - val file = FileHandle(base + File.separator + GameSaver.settingsFileName) - return if (file.exists()) - json().fromJsonFile( - GameSettings::class.java, - file - ) - else GameSettings().apply { isFreshlyCreated = true } - } - } } enum class LocaleCode(var language: String, var country: String) { diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt index 29ea0d7bbe..212de149cb 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt @@ -8,7 +8,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame -import com.unciv.logic.GameSaver import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.ImageGetter @@ -56,7 +55,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() { return "" return "\n**Save Data:**\n
Show Saved Game\n\n```" + try { - GameSaver.gameInfoToString(UncivGame.Current.gameInfo, forceZip = true) + game.gameSaver.gameInfoToString(UncivGame.Current.gameInfo, forceZip = true) } catch (e: Throwable) { "No save data: $e" // In theory .toString() could still error here. } + "\n```\n
\n" diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index f659fd512c..5d4b0adc10 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -256,7 +256,7 @@ class NewGameScreen( newGame.isUpToDate = true // So we don't try to download it from dropbox the second after we upload it - the file is not yet ready for loading! try { game.onlineMultiplayer.createGame(newGame) - GameSaver.autoSave(newGame) + game.gameSaver.autoSave(newGame) } catch (ex: FileStorageRateLimitReached) { postCrashHandlingRunnable { popup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true) diff --git a/core/src/com/unciv/ui/options/DebugTab.kt b/core/src/com/unciv/ui/options/DebugTab.kt index 070bc83ef9..4873ba464d 100644 --- a/core/src/com/unciv/ui/options/DebugTab.kt +++ b/core/src/com/unciv/ui/options/DebugTab.kt @@ -42,8 +42,8 @@ fun debugTab() = Table(BaseScreen.skin).apply { game.gameInfo.gameParameters.godMode = it }).colspan(2).row() } - add("Save games compressed".toCheckBox(GameSaver.saveZipped) { - GameSaver.saveZipped = it + add("Save games compressed".toCheckBox(game.gameSaver.saveZipped) { + game.gameSaver.saveZipped = it }).colspan(2).row() add("Save maps compressed".toCheckBox(MapSaver.saveZipped) { MapSaver.saveZipped = it diff --git a/core/src/com/unciv/ui/saves/LoadGameScreen.kt b/core/src/com/unciv/ui/saves/LoadGameScreen.kt index ef8e9db4a1..247ba80eea 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -9,7 +9,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.utils.Align import com.unciv.UncivGame -import com.unciv.logic.GameSaver import com.unciv.logic.MissingModsException import com.unciv.logic.UncivShowableException import com.unciv.models.ruleset.RulesetCache @@ -54,7 +53,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t launchCrashHandling("Load Game") { try { // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread - val loadedGame = GameSaver.loadGameByName(selectedSave) + val loadedGame = game.gameSaver.loadGameByName(selectedSave) postCrashHandlingRunnable { UncivGame.Current.loadGame(loadedGame) } } catch (ex: Exception) { postCrashHandlingRunnable { @@ -89,17 +88,17 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t loadFromClipboardButton.onClick { try { val clipboardContentsString = Gdx.app.clipboard.contents.trim() - val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString) + val loadedGame = game.gameSaver.gameInfoFromString(clipboardContentsString) UncivGame.Current.loadGame(loadedGame) } catch (ex: Exception) { handleLoadGameException("Could not load game from clipboard!", ex) } } rightSideTable.add(loadFromClipboardButton).row() - if (GameSaver.canLoadFromCustomSaveLocation()) { + if (game.gameSaver.canLoadFromCustomSaveLocation()) { val loadFromCustomLocation = "Load from custom location".toTextButton() loadFromCustomLocation.onClick { - GameSaver.loadGameFromCustomLocation { gameInfo, exception -> + game.gameSaver.loadGameFromCustomLocation { gameInfo, exception -> if (gameInfo != null) { postCrashHandlingRunnable { game.loadGame(gameInfo) @@ -119,7 +118,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t rightSideTable.add(loadMissingModsButton).row() deleteSaveButton.onClick { - GameSaver.deleteSave(selectedSave) + game.gameSaver.deleteSave(selectedSave) resetWindowState() } deleteSaveButton.disable() @@ -127,7 +126,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t copySavedGameToClipboardButton.disable() copySavedGameToClipboardButton.onClick { - val gameText = GameSaver.getSave(selectedSave).readString() + val gameText = game.gameSaver.getSave(selectedSave).readString() val gzippedGameText = Gzip.zip(gameText) Gdx.app.clipboard.contents = gzippedGameText } @@ -209,12 +208,11 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times launchCrashHandling("GetSaves") { // .toList() because otherwise the lastModified will only be checked inside the postRunnable - val saves = GameSaver.getSaves().sortedByDescending { it.lastModified() }.toList() + val saves = game.gameSaver.getSaves(autoSaves = showAutosaves).sortedByDescending { it.lastModified() }.toList() postCrashHandlingRunnable { saveTable.clear() for (save in saves) { - if (save.name().startsWith(GameSaver.autoSaveFileName) && !showAutosaves) continue val textButton = TextButton(save.name(), skin) textButton.onClick { onSaveSelected(save) } saveTable.add(textButton).pad(5f).row() @@ -238,7 +236,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t var textToSet = save.name() + "\n${"Saved at".tr()}: " + savedAt.formatDate() launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones try { - val game = GameSaver.loadGamePreviewFromFile(save) + val game = game.gameSaver.loadGamePreviewFromFile(save) val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } textToSet += "\n" + playerCivNames + ", " + game.difficulty.tr() + ", ${Fonts.turn}" + game.turns diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index 3d3d3283cd..985cb1b7d2 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -7,7 +7,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.unciv.UncivGame import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver import com.unciv.models.translations.tr import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.postCrashHandlingRunnable @@ -44,7 +43,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true copyJsonButton.onClick { thread(name="Copy to clipboard") { // the Gzip rarely leads to ANRs try { - Gdx.app.clipboard.contents = GameSaver.gameInfoToString(gameInfo, forceZip = true) + Gdx.app.clipboard.contents = game.gameSaver.gameInfoToString(gameInfo, forceZip = true) } catch (OOM: OutOfMemoryError) { // you don't get a special toast, this isn't nearly common enough, this is a total edge-case } @@ -52,7 +51,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true } newSave.add(copyJsonButton).row() - if (GameSaver.canLoadFromCustomSaveLocation()) { + if (game.gameSaver.canLoadFromCustomSaveLocation()) { val saveToCustomLocation = "Save to custom location".toTextButton() val errorLabel = "".toLabel(Color.RED) saveToCustomLocation.enable() @@ -61,7 +60,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.disable() launchCrashHandling("SaveGame", runAsDaemon = false) { - GameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> + game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> if (e == null) { postCrashHandlingRunnable { game.resetToWorldScreen() } } else if (e !is CancellationException) { @@ -88,7 +87,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true rightSideButton.setText("Save game".tr()) rightSideButton.onClick { - if (GameSaver.getSave(gameNameTextField.text).exists()) + if (game.gameSaver.getSave(gameNameTextField.text).exists()) YesNoPopup("Overwrite existing file?", { saveGame() }, this).open() else saveGame() } @@ -98,7 +97,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true private fun saveGame() { rightSideButton.setText("Saving...".tr()) launchCrashHandling("SaveGame", runAsDaemon = false) { - GameSaver.saveGame(gameInfo, gameNameTextField.text) { + game.gameSaver.saveGame(gameInfo, gameNameTextField.text) { postCrashHandlingRunnable { if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen) else UncivGame.Current.resetToWorldScreen() @@ -109,10 +108,9 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true private fun updateShownSaves(showAutosaves: Boolean) { currentSaves.clear() - val saves = GameSaver.getSaves() + val saves = game.gameSaver.getSaves(autoSaves = showAutosaves) .sortedByDescending { it.lastModified() } for (saveGameFile in saves) { - if (saveGameFile.name().startsWith(GameSaver.autoSaveFileName) && !showAutosaves) continue val textButton = saveGameFile.name().toTextButton() textButton.onClick { gameNameTextField.text = saveGameFile.name() diff --git a/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt b/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt index acc2a3d493..0b59c2e7ee 100644 --- a/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt +++ b/core/src/com/unciv/ui/utils/GeneralPlatformSpecificHelpers.kt @@ -17,4 +17,9 @@ interface GeneralPlatformSpecificHelpers { * Notifies the user that it's their turn while the game is running */ fun notifyTurnStarted() {} + + /** + * @return an additional external directory for save files, if applicable on the platform + */ + fun getExternalFilesDir(): String? { return null } } diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 9d79255827..10fa7bdc05 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -16,7 +16,6 @@ import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.diplomacy.DiplomaticStatus @@ -229,7 +228,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas val quickSave = { val toast = ToastPopup("Quicksaving...", this) launchCrashHandling("SaveGame", runAsDaemon = false) { - GameSaver.saveGame(gameInfo, "QuickSave") { + game.gameSaver.saveGame(gameInfo, "QuickSave") { postCrashHandlingRunnable { toast.close() if (it != null) @@ -246,7 +245,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas val toast = ToastPopup("Quickloading...", this) launchCrashHandling("LoadGame") { try { - val loadedGame = GameSaver.loadGameByName("QuickSave") + val loadedGame = game.gameSaver.loadGameByName("QuickSave") postCrashHandlingRunnable { toast.close() UncivGame.Current.loadGame(loadedGame) @@ -713,7 +712,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas val newWorldScreen = this@WorldScreen.game.worldScreen newWorldScreen.waitingForAutosave = true newWorldScreen.shouldUpdate = true - GameSaver.autoSave(gameInfoClone) { + game.gameSaver.autoSave(gameInfoClone) { // only enable the user to next turn once we've saved the current one newWorldScreen.waitingForAutosave = false newWorldScreen.shouldUpdate = true diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt index 3303a59238..a056609f42 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt @@ -2,7 +2,6 @@ package com.unciv.ui.worldscreen.mainmenu import com.badlogic.gdx.Gdx import com.unciv.MainMenuScreen -import com.unciv.logic.GameSaver import com.unciv.ui.civilopedia.CivilopediaScreen import com.unciv.models.metadata.GameSetupInfo import com.unciv.ui.newgamescreen.NewGameScreen @@ -17,7 +16,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) { defaults().fillX() addButton("Main menu") { - GameSaver.autoSaveUnCloned(worldScreen.gameInfo) + worldScreen.game.gameSaver.autoSaveUnCloned(worldScreen.gameInfo) worldScreen.game.setScreen(MainMenuScreen()) } addButton("Civilopedia") { diff --git a/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt b/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt index af6e281a9a..fc025adfc3 100644 --- a/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt @@ -12,14 +12,14 @@ import javax.swing.JFileChooser import javax.swing.JFrame class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper { - override fun saveGame(gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) { + override fun saveGame(gameSaver: GameSaver, gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) { val customSaveLocation = gameInfo.customSaveLocation if (customSaveLocation != null && !forcePrompt) { try { File(customSaveLocation).outputStream() .writer() .use { writer -> - writer.write(GameSaver.gameInfoToString(gameInfo)) + writer.write(gameSaver.gameInfoToString(gameInfo)) } saveCompleteCallback?.invoke(null) } catch (e: Exception) { @@ -59,7 +59,7 @@ class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper { saveCompleteCallback?.invoke(exception) } - override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { + override fun loadGame(gameSaver: GameSaver, loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { val fileChooser = JFileChooser().apply fileChooser@{ currentDirectory = Gdx.files.local("").file() } @@ -79,7 +79,7 @@ class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper { file.inputStream() .reader() .readText() - .run { GameSaver.gameInfoFromString(this) } + .run { gameSaver.gameInfoFromString(this) } .apply { // If the user has saved the game from another platform (like Android), // then the save location might not be right so we have to correct for that diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index 0a1f216bfe..ba7efaaa06 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -9,6 +9,7 @@ import com.badlogic.gdx.graphics.glutils.HdpiMode import com.sun.jna.Native import com.unciv.UncivGame import com.unciv.UncivGameParameters +import com.unciv.logic.GameSaver import com.unciv.models.metadata.GameSettings import com.unciv.ui.utils.Fonts import java.util.* @@ -35,7 +36,7 @@ internal object DesktopLauncher { // Note that means config.setAudioConfig() would be ignored too, those would need to go into the HardenedGdxAudio constructor. config.disableAudio(true) - val settings = GameSettings.getSettingsForPlatformLaunchers() + val settings = GameSaver.getSettingsForPlatformLaunchers() if (!settings.isFreshlyCreated) { config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80)) } diff --git a/tests/src/com/unciv/testing/GdxTestRunner.java b/tests/src/com/unciv/testing/GdxTestRunner.java index 11e3988889..1878dcf854 100644 --- a/tests/src/com/unciv/testing/GdxTestRunner.java +++ b/tests/src/com/unciv/testing/GdxTestRunner.java @@ -16,21 +16,20 @@ package com.unciv.testing; +import java.util.HashMap; +import java.util.Map; + import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.backends.headless.HeadlessApplication; import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration; import com.badlogic.gdx.graphics.GL20; -import com.unciv.logic.GameSaver; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; -import java.util.HashMap; -import java.util.Map; - -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; public class GdxTestRunner extends BlockJUnit4ClassRunner implements ApplicationListener { @@ -46,7 +45,6 @@ public class GdxTestRunner extends BlockJUnit4ClassRunner implements Application @Override public void create() { - GameSaver.INSTANCE.init(Gdx.files, null); } @Override diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt index 577f93bf77..2ce41d9fed 100644 --- a/tests/src/com/unciv/testing/SerializationTests.kt +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -1,5 +1,6 @@ package com.unciv.testing +import com.badlogic.gdx.Gdx import com.unciv.UncivGame import com.unciv.json.json import com.unciv.logic.GameInfo @@ -59,9 +60,10 @@ class SerializationTests { } val setup = GameSetupInfo(param, mapParameters) UncivGame.Current = UncivGame("") + UncivGame.Current.gameSaver = GameSaver(Gdx.files) // Both startNewGame and makeCivilizationsMeet will cause a save to storage of our empty settings - settingsBackup = GameSaver.getGeneralSettings() + settingsBackup = UncivGame.Current.gameSaver.getGeneralSettings() UncivGame.Current.settings = GameSettings() game = GameStarter.startNewGame(setup)