diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index 9d58ce85f5..052bcd287d 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -15,7 +15,7 @@ import com.badlogic.gdx.backends.android.AndroidGraphics import com.badlogic.gdx.math.Rectangle import com.unciv.UncivGame import com.unciv.UncivGameParameters -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.logic.event.EventBus import com.unciv.ui.UncivStage import com.unciv.ui.utils.BaseScreen @@ -40,7 +40,7 @@ open class AndroidLauncher : AndroidApplication() { useImmersiveMode = true } - val settings = GameSaver.getSettingsForPlatformLaunchers(filesDir.path) + val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path) val fontFamily = settings.fontFamily // Manage orientation lock and display cutout @@ -119,10 +119,10 @@ open class AndroidLauncher : AndroidApplication() { if (UncivGame.isCurrentInitialized() && UncivGame.Current.gameInfo != null && UncivGame.Current.settings.multiplayer.turnCheckerEnabled - && UncivGame.Current.gameSaver.getMultiplayerSaves().any() + && UncivGame.Current.files.getMultiplayerSaves().any() ) { MultiplayerTurnCheckWorker.startTurnChecker( - applicationContext, UncivGame.Current.gameSaver, + applicationContext, UncivGame.Current.files, UncivGame.Current.gameInfo!!, UncivGame.Current.settings.multiplayer ) } diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 2bff39f806..2394dfea2e 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -20,10 +20,9 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.backends.android.AndroidApplication import com.badlogic.gdx.backends.android.DefaultAndroidFiles import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -import com.unciv.models.metadata.GameSettings -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles import com.unciv.models.metadata.GameSettingsMultiplayer import kotlinx.coroutines.runBlocking import java.io.FileNotFoundException @@ -181,16 +180,16 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame } } - fun startTurnChecker(applicationContext: Context, gameSaver: GameSaver, currentGameInfo: GameInfo, settings: GameSettingsMultiplayer) { + fun startTurnChecker(applicationContext: Context, files: UncivFiles, currentGameInfo: GameInfo, settings: GameSettingsMultiplayer) { Log.i(LOG_TAG, "startTurnChecker") - val gameFiles = gameSaver.getMultiplayerSaves() + val gameFiles = files.getMultiplayerSaves() val gameIds = Array(gameFiles.count()) {""} val gameNames = Array(gameFiles.count()) {""} var count = 0 for (gameFile in gameFiles) { try { - val gamePreview = gameSaver.loadGamePreviewFromFile(gameFile) + val gamePreview = files.loadGamePreviewFromFile(gameFile) gameIds[count] = gamePreview.gameId gameNames[count] = gameFile.name() count++ @@ -260,14 +259,14 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame */ private val notFoundRemotely = mutableMapOf() - private val gameSaver: GameSaver + private val files: UncivFiles 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), true) + val gdxFiles = 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 - gameSaver = GameSaver(files, null, true) + Gdx.files = gdxFiles + files = UncivFiles(gdxFiles, null, true) } override fun doWork(): Result = runBlocking { @@ -297,7 +296,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame try { Log.d(LOG_TAG, "doWork download ${gameId}") - val gamePreview = OnlineMultiplayerGameSaver(fileStorage).tryDownloadGamePreview(gameId) + val gamePreview = OnlineMultiplayerFiles(fileStorage).tryDownloadGamePreview(gameId) Log.d(LOG_TAG, "doWork download ${gameId} done") val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer) @@ -310,7 +309,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame Lets hope it works with gamePreview as they are a lot smaller and faster to save */ Log.i(LOG_TAG, "doWork save gameName: ${gameNames[idx]}") - gameSaver.saveGame(gamePreview, gameNames[idx]) + files.saveGame(gamePreview, gameNames[idx]) Log.i(LOG_TAG, "doWork save ${gameNames[idx]} done") if (currentTurnPlayer.playerId == inputData.getString(USER_ID)!! && foundGame == null) { diff --git a/build.gradle.kts b/build.gradle.kts index 6cb83107a4..46c1c8a1d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ buildscript { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${com.unciv.build.BuildConfig.kotlinVersion}") classpath("de.richsource.gradle.plugins:gwt-gradle-plugin:0.6") - classpath("com.android.tools.build:gradle:7.1.3") + classpath("com.android.tools.build:gradle:7.0.4") classpath("com.mobidevelop.robovm:robovm-gradle-plugin:2.3.1") // This is for wrapping the .jar file into a standalone executable diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index f1abba6e2b..4e9774ae27 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -139,7 +139,7 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize { val column1 = Table().apply { defaults().pad(10f).fillX() } val column2 = if (singleColumn) column1 else Table().apply { defaults().pad(10f).fillX() } - if (game.gameSaver.autosaveExists()) { + if (game.files.autosaveExists()) { val resumeTable = getMenuButton("Resume","OtherIcons/Resume", 'r') { resumeGame() } column1.add(resumeTable).row() @@ -153,7 +153,7 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize { { game.pushScreen(NewGameScreen()) } column1.add(newGameButton).row() - if (game.gameSaver.getSaves().any()) { + if (game.files.getSaves().any()) { val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", 'l') { game.pushScreen(LoadGameScreen(this)) } column1.add(loadGameTable).row() diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 4b1ad5d4a4..9a6126cdba 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -8,7 +8,7 @@ import com.badlogic.gdx.Screen import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.utils.Align import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.logic.civilization.PlayerType import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.models.metadata.GameSettings @@ -37,6 +37,7 @@ import com.unciv.utils.concurrency.launchOnGLThread import com.unciv.utils.concurrency.withGLContext import com.unciv.utils.concurrency.withThreadPoolContext import com.unciv.utils.debug +import java.io.PrintWriter import java.util.* import kotlin.collections.ArrayDeque @@ -59,7 +60,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { lateinit var settings: GameSettings lateinit var musicController: MusicController lateinit var onlineMultiplayer: OnlineMultiplayer - lateinit var gameSaver: GameSaver + lateinit var files: UncivFiles /** * This exists so that when debugging we can see the entire map. @@ -95,7 +96,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { viewEntireMapForDebug = false } Current = this - gameSaver = GameSaver(Gdx.files, customSaveLocationHelper, platformSpecificHelper?.shouldPreferExternalStorage() == true) + files = UncivFiles(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, @@ -109,7 +110,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { * - Skin (hence BaseScreen.setSkin()) * - Font (hence Fonts.resetFont() inside setSkin()) */ - settings = gameSaver.getGeneralSettings() // needed for the screen + settings = files.getGeneralSettings() // needed for the screen setAsRootScreen(GameStartScreen()) // NOT dependent on any atlas or skin GameSounds.init() @@ -355,7 +356,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { override fun pause() { val curGameInfo = gameInfo - if (curGameInfo != null) gameSaver.requestAutoSave(curGameInfo) + if (curGameInfo != null) files.requestAutoSave(curGameInfo) musicController.pause() super.pause() } @@ -375,7 +376,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { val curGameInfo = gameInfo if (curGameInfo != null) { - val autoSaveJob = gameSaver.autoSaveJob + val autoSaveJob = files.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 @@ -383,7 +384,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { autoSaveJob.join() } } else { - gameSaver.autoSave(curGameInfo) // NO new thread + files.autoSave(curGameInfo) // NO new thread } } settings.save() @@ -405,6 +406,13 @@ class UncivGame(parameters: UncivGameParameters) : Game() { /** Handles an uncaught exception or error. First attempts a platform-specific handler, and if that didn't handle the exception or error, brings the game to a [CrashScreen]. */ fun handleUncaughtThrowable(ex: Throwable) { Log.error("Uncaught throwable", ex) + try { + PrintWriter(files.fileWriter("lasterror.txt")).use { + ex.printStackTrace(it) + } + } catch (ex: Exception) { + // ignore + } if (platformSpecificHelper?.handleUncaughtThrowable(ex) == true) return Gdx.app.postRunnable { setAsRootScreen(CrashScreen(ex)) @@ -419,7 +427,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { fun goToMainMenu(): MainMenuScreen { val curGameInfo = gameInfo if (curGameInfo != null) { - gameSaver.requestAutoSaveUnCloned(curGameInfo) // Can save gameInfo directly because the user can't modify it on the MainMenuScreen + files.requestAutoSaveUnCloned(curGameInfo) // Can save gameInfo directly because the user can't modify it on the MainMenuScreen } val mainMenuScreen = MainMenuScreen() pushScreen(mainMenuScreen) diff --git a/core/src/com/unciv/logic/CustomFileLocationHelper.kt b/core/src/com/unciv/logic/CustomFileLocationHelper.kt index 9d4b2b4917..21ef1785c0 100644 --- a/core/src/com/unciv/logic/CustomFileLocationHelper.kt +++ b/core/src/com/unciv/logic/CustomFileLocationHelper.kt @@ -1,7 +1,7 @@ package com.unciv.logic -import com.unciv.logic.GameSaver.CustomLoadResult -import com.unciv.logic.GameSaver.CustomSaveResult +import com.unciv.logic.UncivFiles.CustomLoadResult +import com.unciv.logic.UncivFiles.CustomSaveResult import com.unciv.utils.concurrency.Concurrency import java.io.InputStream import java.io.OutputStream diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/UncivFiles.kt similarity index 96% rename from core/src/com/unciv/logic/GameSaver.kt rename to core/src/com/unciv/logic/UncivFiles.kt index 83bab4f28a..7190467d48 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/UncivFiles.kt @@ -16,13 +16,14 @@ import com.unciv.utils.Log import com.unciv.utils.debug import kotlinx.coroutines.Job import java.io.File +import java.io.Writer 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" -class GameSaver( +class UncivFiles( /** * 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. @@ -32,7 +33,7 @@ class GameSaver( private val preferExternalStorage: Boolean = false ) { init { - debug("Creating GameSaver, localStoragePath: %s, externalStoragePath: %s, preferExternalStorage: %s", + debug("Creating UncivFiles, localStoragePath: %s, externalStoragePath: %s, preferExternalStorage: %s", files.localStoragePath, files.externalStoragePath, preferExternalStorage) } //region Data @@ -66,6 +67,18 @@ class GameSaver( return toReturn } + /** + * @throws GdxRuntimeException if the [path] represents a directory + */ + fun fileWriter(path: String, append: Boolean = false): Writer { + val file = if (preferExternalStorage && files.isExternalStorageAvailable) { + files.external(path) + } else { + files.local(path) + } + return file.writer(append) + } + fun getMultiplayerSaves(): Sequence { return getSaves(MULTIPLAYER_FILES_FOLDER) } diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt index c2387240b4..cd1871b245 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt @@ -8,7 +8,7 @@ import com.unciv.logic.GameInfoPreview import com.unciv.logic.civilization.PlayerType import com.unciv.logic.event.EventBus import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles import com.unciv.ui.utils.extensions.isLargerThan import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.Dispatcher @@ -39,8 +39,8 @@ private val FILE_UPDATE_THROTTLE_PERIOD = Duration.ofSeconds(60) * See the file of [com.unciv.logic.multiplayer.MultiplayerGameAdded] for all available [EventBus] events. */ class OnlineMultiplayer { - private val gameSaver = UncivGame.Current.gameSaver - private val onlineGameSaver = OnlineMultiplayerGameSaver() + private val files = UncivGame.Current.files + private val multiplayerFiles = OnlineMultiplayerFiles() private val savedGames: MutableMap = Collections.synchronizedMap(mutableMapOf()) @@ -100,7 +100,7 @@ class OnlineMultiplayer { } private suspend fun updateSavesFromFiles() { - val saves = gameSaver.getMultiplayerSaves() + val saves = files.getMultiplayerSaves() val removedSaves = savedGames.keys - saves.toSet() for (saveFile in removedSaves) { @@ -120,7 +120,7 @@ class OnlineMultiplayer { * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time */ suspend fun createGame(newGame: GameInfo) { - onlineGameSaver.tryUploadGame(newGame, withPreview = true) + multiplayerFiles.tryUploadGame(newGame, withPreview = true) addGame(newGame) } @@ -136,10 +136,10 @@ class OnlineMultiplayer { val saveFileName = if (gameName.isNullOrBlank()) gameId else gameName var gamePreview: GameInfoPreview try { - gamePreview = onlineGameSaver.tryDownloadGamePreview(gameId) + gamePreview = multiplayerFiles.tryDownloadGamePreview(gameId) } catch (ex: FileNotFoundException) { // 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 = multiplayerFiles.tryDownloadGame(gameId).asPreview() } addGame(gamePreview, saveFileName) } @@ -150,11 +150,11 @@ class OnlineMultiplayer { } private suspend fun addGame(preview: GameInfoPreview, saveFileName: String) { - val fileHandle = gameSaver.saveGame(preview, saveFileName) + val fileHandle = files.saveGame(preview, saveFileName) return addGame(fileHandle, preview) } - private suspend fun addGame(fileHandle: FileHandle, preview: GameInfoPreview = gameSaver.loadGamePreviewFromFile(fileHandle)) { + private suspend fun addGame(fileHandle: FileHandle, preview: GameInfoPreview = files.loadGamePreviewFromFile(fileHandle)) { debug("Adding game %s", preview.gameId) val game = OnlineMultiplayerGame(fileHandle, preview, Instant.now()) savedGames[fileHandle] = game @@ -184,7 +184,7 @@ class OnlineMultiplayer { suspend fun resign(game: OnlineMultiplayerGame): Boolean { val preview = game.preview ?: throw game.error!! // download to work with the latest game state - val gameInfo = onlineGameSaver.tryDownloadGame(preview.gameId) + val gameInfo = multiplayerFiles.tryDownloadGame(preview.gameId) val playerCiv = gameInfo.currentPlayerCiv if (!gameInfo.isUsersTurn()) { @@ -205,8 +205,8 @@ class OnlineMultiplayer { } val newPreview = gameInfo.asPreview() - gameSaver.saveGame(newPreview, game.fileHandle) - onlineGameSaver.tryUploadGame(gameInfo, withPreview = true) + files.saveGame(newPreview, game.fileHandle) + multiplayerFiles.tryUploadGame(gameInfo, withPreview = true) game.doManualUpdate(newPreview) return true } @@ -242,7 +242,7 @@ class OnlineMultiplayer { */ suspend fun loadGame(gameInfo: GameInfo) = coroutineScope { val gameId = gameInfo.gameId - val preview = onlineGameSaver.tryDownloadGamePreview(gameId) + val preview = multiplayerFiles.tryDownloadGamePreview(gameId) if (hasLatestGameState(gameInfo, preview)) { gameInfo.isUpToDate = true UncivGame.Current.loadGame(gameInfo) @@ -256,7 +256,7 @@ class OnlineMultiplayer { * @throws FileNotFoundException if the file can't be found */ suspend fun downloadGame(gameId: String): GameInfo { - val latestGame = onlineGameSaver.tryDownloadGame(gameId) + val latestGame = multiplayerFiles.tryDownloadGame(gameId) latestGame.isUpToDate = true return latestGame } @@ -271,7 +271,7 @@ class OnlineMultiplayer { } private fun deleteGame(fileHandle: FileHandle) { - gameSaver.deleteSave(fileHandle) + files.deleteSave(fileHandle) val game = savedGames[fileHandle] if (game == null) return @@ -291,8 +291,8 @@ class OnlineMultiplayer { val oldName = game.name savedGames.remove(game.fileHandle) - gameSaver.deleteSave(game.fileHandle) - val newFileHandle = gameSaver.saveGame(oldPreview, newName) + files.deleteSave(game.fileHandle) + val newFileHandle = files.saveGame(oldPreview, newName) val newGame = OnlineMultiplayerGame(newFileHandle, oldPreview, oldLastUpdate) savedGames[newFileHandle] = newGame @@ -305,7 +305,7 @@ class OnlineMultiplayer { */ suspend fun updateGame(gameInfo: GameInfo) { debug("Updating remote game %s", gameInfo.gameId) - onlineGameSaver.tryUploadGame(gameInfo, withPreview = true) + multiplayerFiles.tryUploadGame(gameInfo, withPreview = true) val game = getGameByGameId(gameInfo.gameId) debug("Existing OnlineMultiplayerGame: %s", game) if (game == null) { diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt index 53b3cd2f8b..cb8c9503fa 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt @@ -5,9 +5,8 @@ import com.unciv.UncivGame import com.unciv.logic.GameInfoPreview import com.unciv.logic.event.EventBus import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached -import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver +import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles import com.unciv.ui.utils.extensions.isLargerThan -import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread import com.unciv.utils.concurrency.withGLContext import com.unciv.utils.debug @@ -53,7 +52,7 @@ class OnlineMultiplayerGame( } private fun loadPreviewFromFile(): GameInfoPreview { - val previewFromFile = UncivGame.Current.gameSaver.loadGamePreviewFromFile(fileHandle) + val previewFromFile = UncivGame.Current.files.loadGamePreviewFromFile(fileHandle) preview = previewFromFile return previewFromFile } @@ -104,9 +103,9 @@ class OnlineMultiplayerGame( private suspend fun update(): GameUpdateResult { val curPreview = if (preview != null) preview!! else loadPreviewFromFile() - val newPreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(curPreview.gameId) + val newPreview = OnlineMultiplayerFiles().tryDownloadGamePreview(curPreview.gameId) if (newPreview.turns == curPreview.turns && newPreview.currentPlayer == curPreview.currentPlayer) return GameUpdateResult.UNCHANGED - UncivGame.Current.gameSaver.saveGame(newPreview, fileHandle) + UncivGame.Current.files.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/OnlineMultiplayerFiles.kt similarity index 90% rename from core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt rename to core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerFiles.kt index a184b7e002..a3eced65e8 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerGameSaver.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/OnlineMultiplayerFiles.kt @@ -4,7 +4,7 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.GameInfoPreview -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles /** * Allows access to games stored on a server for multiplayer purposes. @@ -17,7 +17,7 @@ import com.unciv.logic.GameSaver * @see UncivGame.Current.settings.multiplayerServer */ @Suppress("RedundantSuspendModifier") // Methods can take a long time, so force users to use them in a coroutine to not get ANRs on Android -class OnlineMultiplayerGameSaver( +class OnlineMultiplayerFiles( private var fileStorageIdentifier: String? = null ) { fun fileStorage(): FileStorage { @@ -28,7 +28,7 @@ class OnlineMultiplayerGameSaver( /** @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time */ suspend fun tryUploadGame(gameInfo: GameInfo, withPreview: Boolean) { - val zippedGameInfo = GameSaver.gameInfoToString(gameInfo, forceZip = true) + val zippedGameInfo = UncivFiles.gameInfoToString(gameInfo, forceZip = true) fileStorage().saveFileData(gameInfo.gameId, zippedGameInfo, true) // We upload the preview after the game because otherwise the following race condition will happen: @@ -53,7 +53,7 @@ class OnlineMultiplayerGameSaver( * @see GameInfo.asPreview */ suspend fun tryUploadGamePreview(gameInfo: GameInfoPreview) { - val zippedGameInfo = GameSaver.gameInfoToString(gameInfo) + val zippedGameInfo = UncivFiles.gameInfoToString(gameInfo) fileStorage().saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo, true) } @@ -63,7 +63,7 @@ class OnlineMultiplayerGameSaver( */ suspend fun tryDownloadGame(gameId: String): GameInfo { val zippedGameInfo = fileStorage().loadFileData(gameId) - return GameSaver.gameInfoFromString(zippedGameInfo) + return UncivFiles.gameInfoFromString(zippedGameInfo) } /** @@ -72,6 +72,6 @@ class OnlineMultiplayerGameSaver( */ suspend fun tryDownloadGamePreview(gameId: String): GameInfoPreview { val zippedGameInfo = fileStorage().loadFileData("${gameId}_Preview") - return GameSaver.gameInfoPreviewFromString(zippedGameInfo) + return UncivFiles.gameInfoPreviewFromString(zippedGameInfo) } } diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 7a81ba8a86..90924ef578 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -91,7 +91,7 @@ class GameSettings { if (!isFreshlyCreated && Gdx.app?.type == Application.ApplicationType.Desktop) { windowState = WindowState(Gdx.graphics.width, Gdx.graphics.height) } - UncivGame.Current.gameSaver.setGeneralSettings(this) + UncivGame.Current.files.setGeneralSettings(this) } fun addCompletedTutorialTask(tutorialTask: String) { diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt index 7b370db1c2..2bfd0e2c41 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt @@ -8,7 +8,7 @@ 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.logic.UncivFiles import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.ImageGetter @@ -19,10 +19,8 @@ import com.unciv.ui.utils.extensions.addBorder import com.unciv.ui.utils.extensions.onClick import com.unciv.ui.utils.extensions.setFontSize import com.unciv.ui.utils.extensions.toLabel -import com.unciv.utils.Log import java.io.PrintWriter import java.io.StringWriter -import kotlin.concurrent.thread /** Screen to crash to when an otherwise unhandled exception or error is thrown. */ class CrashScreen(val exception: Throwable): BaseScreen() { @@ -52,7 +50,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() { return "" return "\n**Save Data:**\n
Show Saved Game\n\n```\n" + try { - GameSaver.gameInfoToString(UncivGame.Current.gameInfo!!, forceZip = true) + UncivFiles.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 298ab1347f..e45e06cdeb 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -272,7 +272,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) - game.gameSaver.requestAutoSave(newGame) + game.files.requestAutoSave(newGame) } catch (ex: FileStorageRateLimitReached) { launchOnGLThread { 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 80475289c9..f6d7867664 100644 --- a/core/src/com/unciv/ui/options/DebugTab.kt +++ b/core/src/com/unciv/ui/options/DebugTab.kt @@ -2,7 +2,7 @@ package com.unciv.ui.options import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.UncivGame -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.logic.MapSaver import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.tile.ResourceType @@ -51,8 +51,8 @@ fun debugTab() = Table(BaseScreen.skin).apply { curGameInfo.gameParameters.godMode = it }).colspan(2).row() } - add("Save games compressed".toCheckBox(GameSaver.saveZipped) { - GameSaver.saveZipped = it + add("Save games compressed".toCheckBox(UncivFiles.saveZipped) { + UncivFiles.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 8ec750cf8a..e1149f8216 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -7,7 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.utils.SerializationException import com.unciv.Constants -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.logic.MissingModsException import com.unciv.logic.UncivShowableException import com.unciv.models.ruleset.RulesetCache @@ -82,7 +82,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { Concurrency.run(loadGame) { 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 = game.gameSaver.loadGameByName(selectedSave) + val loadedGame = game.files.loadGameByName(selectedSave) game.loadGame(loadedGame) } catch (ex: Exception) { launchOnGLThread { @@ -119,7 +119,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { Concurrency.run(loadFromClipboard) { try { val clipboardContentsString = Gdx.app.clipboard.contents.trim() - val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString) + val loadedGame = UncivFiles.gameInfoFromString(clipboardContentsString) game.loadGame(loadedGame) } catch (ex: Exception) { launchOnGLThread { handleLoadGameException("Could not load game from clipboard!", ex) } @@ -133,14 +133,14 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { } private fun Table.addLoadFromCustomLocationButton() { - if (!game.gameSaver.canLoadFromCustomSaveLocation()) return + if (!game.files.canLoadFromCustomSaveLocation()) return val loadFromCustomLocation = loadFromCustomLocation.toTextButton() loadFromCustomLocation.onClick { errorLabel.isVisible = false loadFromCustomLocation.setText(Constants.loading.tr()) loadFromCustomLocation.disable() Concurrency.run(Companion.loadFromCustomLocation) { - game.gameSaver.loadGameFromCustomLocation { result -> + game.files.loadGameFromCustomLocation { result -> if (result.isError()) { handleLoadGameException("Could not load game from custom location!", result.exception) } else if (result.isSuccessful()) { @@ -159,7 +159,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { copyButton.onActivation { Concurrency.run(copyExistingSaveToClipboard) { try { - val gameText = game.gameSaver.getSave(selectedSave).readString() + val gameText = game.files.getSave(selectedSave).readString() Gdx.app.clipboard.contents = if (gameText[0] == '{') Gzip.zip(gameText) else gameText } catch (ex: Throwable) { ex.printStackTrace() diff --git a/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt b/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt index ce6dc364fd..e0283c4126 100644 --- a/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt @@ -77,7 +77,7 @@ abstract class LoadOrSaveScreen( if (selectedSave.isEmpty()) return ConfirmPopup(this, "Are you sure you want to delete this save?", "Delete save") { val result = try { - if (game.gameSaver.deleteSave(selectedSave)) { + if (game.files.deleteSave(selectedSave)) { resetWindowState() "[$selectedSave] deleted successfully." } else { @@ -93,7 +93,7 @@ abstract class LoadOrSaveScreen( } private fun updateShownSaves(showAutosaves: Boolean) { - savesScrollPane.updateSaveGames(game.gameSaver, showAutosaves) + savesScrollPane.updateSaveGames(game.files, showAutosaves) } private fun selectExistingSave(saveGameFile: FileHandle) { @@ -109,7 +109,7 @@ abstract class LoadOrSaveScreen( Concurrency.run("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones val textToSet = try { val savedAt = Date(saveGameFile.lastModified()) - val game = game.gameSaver.loadGamePreviewFromFile(saveGameFile) + val game = game.files.loadGamePreviewFromFile(saveGameFile) val playerCivNames = game.civilizations .filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } val mods = if (game.gameParameters.mods.isEmpty()) "" diff --git a/core/src/com/unciv/ui/saves/QuickSave.kt b/core/src/com/unciv/ui/saves/QuickSave.kt index 56ad376041..6e549b7777 100644 --- a/core/src/com/unciv/ui/saves/QuickSave.kt +++ b/core/src/com/unciv/ui/saves/QuickSave.kt @@ -17,10 +17,10 @@ import com.unciv.utils.Log object QuickSave { fun save(gameInfo: GameInfo, screen: WorldScreen) { - val gameSaver = UncivGame.Current.gameSaver + val files = UncivGame.Current.files val toast = ToastPopup("Quicksaving...", screen) Concurrency.runOnNonDaemonThreadPool("QuickSaveGame") { - gameSaver.saveGame(gameInfo, "QuickSave") { + files.saveGame(gameInfo, "QuickSave") { launchOnGLThread { toast.close() if (it != null) @@ -33,11 +33,11 @@ object QuickSave { } fun load(screen: WorldScreen) { - val gameSaver = UncivGame.Current.gameSaver + val files = UncivGame.Current.files val toast = ToastPopup("Quickloading...", screen) Concurrency.run("QuickLoadGame") { try { - val loadedGame = gameSaver.loadGameByName("QuickSave") + val loadedGame = files.loadGameByName("QuickSave") launchOnGLThread { toast.close() UncivGame.Current.loadGame(loadedGame) @@ -67,7 +67,7 @@ object QuickSave { val savedGame: GameInfo try { - savedGame = screen.game.gameSaver.loadLatestAutosave() + savedGame = screen.game.files.loadLatestAutosave() } catch (oom: OutOfMemoryError) { outOfMemory() return@run diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index 764f6816f2..8912a71376 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -6,7 +6,7 @@ import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.UncivGame import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.models.translations.tr import com.unciv.ui.popup.ConfirmPopup import com.unciv.ui.popup.ToastPopup @@ -34,7 +34,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") rightSideButton.setText("Save game".tr()) rightSideButton.onActivation { - if (game.gameSaver.getSave(gameNameTextField.text).exists()) + if (game.files.getSave(gameNameTextField.text).exists()) ConfirmPopup( this, "Overwrite existing file?", @@ -78,7 +78,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") Concurrency.run("Copy game to clipboard") { // the Gzip rarely leads to ANRs try { - Gdx.app.clipboard.contents = GameSaver.gameInfoToString(gameInfo, forceZip = true) + Gdx.app.clipboard.contents = UncivFiles.gameInfoToString(gameInfo, forceZip = true) } catch (ex: Throwable) { ex.printStackTrace() launchOnGLThread { @@ -89,7 +89,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") } private fun Table.addSaveToCustomLocation() { - if (!game.gameSaver.canLoadFromCustomSaveLocation()) return + if (!game.files.canLoadFromCustomSaveLocation()) return val saveToCustomLocation = "Save to custom location".toTextButton() val errorLabel = "".toLabel(Color.RED) saveToCustomLocation.onClick { @@ -97,7 +97,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.disable() Concurrency.runOnNonDaemonThreadPool("Save to custom location") { - game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result -> + game.files.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result -> if (result.isError()) { errorLabel.setText("Could not save game to custom location!".tr()) result.exception?.printStackTrace() @@ -115,7 +115,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") private fun saveGame() { rightSideButton.setText("Saving...".tr()) Concurrency.runOnNonDaemonThreadPool("SaveGame") { - game.gameSaver.saveGame(gameInfo, gameNameTextField.text) { + game.files.saveGame(gameInfo, gameNameTextField.text) { launchOnGLThread { if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen) else UncivGame.Current.popScreen() diff --git a/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt b/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt index 11fa3f5da8..ff3df10a68 100644 --- a/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt +++ b/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt @@ -7,7 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.actions.Actions 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.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.ui.images.ImageGetter import com.unciv.ui.utils.AutoScrollPane import com.unciv.ui.utils.BaseScreen @@ -48,8 +48,8 @@ class VerticalFileListScrollPane( } /** repopulate with existing saved games */ - fun updateSaveGames(gameSaver: GameSaver, showAutosaves: Boolean) { - update(gameSaver.getSaves(showAutosaves) + fun updateSaveGames(files: UncivFiles, showAutosaves: Boolean) { + update(files.getSaves(showAutosaves) .sortedByDescending { it.lastModified() }) } diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index c1bf8da106..b20feaccf3 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -816,7 +816,7 @@ class WorldScreen( fun autoSave() { waitingForAutosave = true shouldUpdate = true - UncivGame.Current.gameSaver.requestAutoSave(gameInfo).invokeOnCompletion { + UncivGame.Current.files.requestAutoSave(gameInfo).invokeOnCompletion { // only enable the user to next turn once we've saved the current one waitingForAutosave = false shouldUpdate = true diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index e149a4cd1d..10d8c39f6d 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -9,7 +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.logic.UncivFiles import com.unciv.ui.utils.Fonts import com.unciv.utils.Log import com.unciv.utils.debug @@ -42,7 +42,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 = GameSaver.getSettingsForPlatformLaunchers() + val settings = UncivFiles.getSettingsForPlatformLaunchers() if (!settings.isFreshlyCreated) { config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80)) } diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt index 7c3910ad87..27e3564705 100644 --- a/tests/src/com/unciv/testing/SerializationTests.kt +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -4,7 +4,7 @@ import com.badlogic.gdx.Gdx import com.unciv.UncivGame import com.unciv.json.json import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver +import com.unciv.logic.UncivFiles import com.unciv.logic.GameStarter import com.unciv.logic.civilization.PlayerType import com.unciv.logic.map.MapParameters @@ -61,10 +61,10 @@ class SerializationTests { } val setup = GameSetupInfo(param, mapParameters) UncivGame.Current = UncivGame("") - UncivGame.Current.gameSaver = GameSaver(Gdx.files) + UncivGame.Current.files = UncivFiles(Gdx.files) // Both startNewGame and makeCivilizationsMeet will cause a save to storage of our empty settings - settingsBackup = UncivGame.Current.gameSaver.getGeneralSettings() + settingsBackup = UncivGame.Current.files.getGeneralSettings() UncivGame.Current.settings = GameSettings() game = GameStarter.startNewGame(setup)