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
This commit is contained in:
Timo T
2022-05-22 18:51:35 +02:00
committed by GitHub
parent a2c646ef8e
commit fc9668f2d0
24 changed files with 213 additions and 187 deletions

View File

@ -10,7 +10,6 @@ import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.UncivGameParameters import com.unciv.UncivGameParameters
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.models.metadata.GameSettings
import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.Fonts
import java.io.File import java.io.File
@ -24,14 +23,12 @@ open class AndroidLauncher : AndroidApplication() {
MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext) MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext)
copyMods() copyMods()
val externalFilesDir = getExternalFilesDir(null)
if (externalFilesDir != null) GameSaver.externalFilesDirForAndroid = externalFilesDir.path
val config = AndroidApplicationConfiguration().apply { val config = AndroidApplicationConfiguration().apply {
useImmersiveMode = true useImmersiveMode = true
} }
val settings = GameSettings.getSettingsForPlatformLaunchers(filesDir.path) val settings = GameSaver.getSettingsForPlatformLaunchers(filesDir.path)
val fontFamily = settings.fontFamily val fontFamily = settings.fontFamily
// Manage orientation lock // Manage orientation lock
@ -73,8 +70,8 @@ open class AndroidLauncher : AndroidApplication() {
if (UncivGame.isCurrentInitialized() if (UncivGame.isCurrentInitialized()
&& UncivGame.Current.isGameInfoInitialized() && UncivGame.Current.isGameInfoInitialized()
&& UncivGame.Current.settings.multiplayerTurnCheckerEnabled && UncivGame.Current.settings.multiplayerTurnCheckerEnabled
&& GameSaver.getSaves(true).any()) { && UncivGame.Current.gameSaver.getMultiplayerSaves().any()) {
MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, GameSaver, UncivGame.Current.gameInfo, UncivGame.Current.settings) MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver, UncivGame.Current.gameInfo, UncivGame.Current.settings)
} }
super.onPause() super.onPause()
} }

View File

@ -27,14 +27,14 @@ class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSa
@GuardedBy("this") @GuardedBy("this")
private val callbacks = ArrayList<IndexedCallback>() private val callbacks = ArrayList<IndexedCallback>()
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 callbackIndex = synchronized(this) {
val index = callbackIndex++ val index = callbackIndex++
callbacks.add(IndexedCallback( callbacks.add(IndexedCallback(
index, index,
{ uri -> { uri ->
if (uri != null) { if (uri != null) {
saveGame(gameInfo, uri) saveGame(gameSaver, gameInfo, uri)
saveCompleteCallback?.invoke(null) saveCompleteCallback?.invoke(null)
} else { } else {
saveCompleteCallback?.invoke(RuntimeException("Uri was null")) 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() gameInfo.customSaveLocation = uri.toString()
activity.contentResolver.openOutputStream(uri, "rwt") activity.contentResolver.openOutputStream(uri, "rwt")
?.writer() ?.writer()
?.use { ?.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 callbackIndex = synchronized(this) {
val index = callbackIndex++ val index = callbackIndex++
callbacks.add(IndexedCallback( callbacks.add(IndexedCallback(
@ -90,7 +90,7 @@ class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSa
?.reader() ?.reader()
?.readText() ?.readText()
?.run { ?.run {
GameSaver.gameInfoFromString(this) gameSaver.gameInfoFromString(this)
} }
} catch (e: Exception) { } catch (e: Exception) {
exception = e exception = e

View File

@ -182,7 +182,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
fun startTurnChecker(applicationContext: Context, gameSaver: GameSaver, currentGameInfo: GameInfo, settings: GameSettings) { fun startTurnChecker(applicationContext: Context, gameSaver: GameSaver, currentGameInfo: GameInfo, settings: GameSettings) {
Log.i(LOG_TAG, "startTurnChecker") Log.i(LOG_TAG, "startTurnChecker")
val gameFiles = gameSaver.getSaves(true) val gameFiles = gameSaver.getMultiplayerSaves()
val gameIds = Array(gameFiles.count()) {""} val gameIds = Array(gameFiles.count()) {""}
val gameNames = Array(gameFiles.count()) {""} val gameNames = Array(gameFiles.count()) {""}
@ -255,14 +255,16 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
*/ */
private val notFoundRemotely = mutableMapOf<String, Boolean>() private val notFoundRemotely = mutableMapOf<String, Boolean>()
private val gameSaver = GameSaver private val gameSaver: GameSaver
init { init {
// We can't use Gdx.files since that is only initialized within a com.badlogic.gdx.backends.android.AndroidApplication. // We can't use Gdx.files since that is only initialized within a com.badlogic.gdx.backends.android.AndroidApplication.
// Worker instances may be stopped & recreated by the Android WorkManager, so no AndroidApplication and thus no Gdx.files available // Worker instances may be stopped & recreated by the Android WorkManager, so no AndroidApplication and thus no Gdx.files available
val files = DefaultAndroidFiles(applicationContext.assets, ContextWrapper(applicationContext), false) val files = DefaultAndroidFiles(applicationContext.assets, ContextWrapper(applicationContext), false)
// GDX's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance // GDX's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance
Gdx.files = files Gdx.files = files
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 { override fun doWork(): Result = runBlocking {

View File

@ -29,4 +29,8 @@ Sources for Info about current orientation in case need:
// Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary // Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary
if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation
} }
override fun getExternalFilesDir(): String? {
return activity.getExternalFilesDir(null)?.path
}
} }

View File

@ -7,7 +7,6 @@ import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapSize import com.unciv.logic.map.MapSize
@ -97,8 +96,7 @@ class MainMenuScreen: BaseScreen() {
val column1 = Table().apply { defaults().pad(10f).fillX() } val column1 = Table().apply { defaults().pad(10f).fillX() }
val column2 = if (singleColumn) column1 else 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 (game.gameSaver.autosaveExists()) {
if (autosaveGame.exists()) {
val resumeTable = getMenuButton("Resume","OtherIcons/Resume", 'r') val resumeTable = getMenuButton("Resume","OtherIcons/Resume", 'r')
{ autoLoadGame() } { autoLoadGame() }
column1.add(resumeTable).row() column1.add(resumeTable).row()
@ -112,7 +110,7 @@ class MainMenuScreen: BaseScreen() {
{ game.setScreen(NewGameScreen(this)) } { game.setScreen(NewGameScreen(this)) }
column1.add(newGameButton).row() column1.add(newGameButton).row()
if (GameSaver.getSaves(false).any()) { if (game.gameSaver.getSaves().any()) {
val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", 'l') val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", 'l')
{ game.setScreen(LoadGameScreen(this)) } { game.setScreen(LoadGameScreen(this)) }
column1.add(loadGameTable).row() column1.add(loadGameTable).row()
@ -180,22 +178,12 @@ class MainMenuScreen: BaseScreen() {
} }
} }
var savedGame: GameInfo val savedGame: GameInfo
try { try {
savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName) savedGame = game.gameSaver.loadLatestAutosave()
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
outOfMemory() outOfMemory()
return@launchCrashHandling 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) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
loadingPopup.close() loadingPopup.close()
@ -203,7 +191,6 @@ class MainMenuScreen: BaseScreen() {
} }
return@launchCrashHandling return@launchCrashHandling
} }
}
postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context
try { try {

View File

@ -27,6 +27,7 @@ import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.multiplayer.LoadDeepLinkScreen import com.unciv.ui.multiplayer.LoadDeepLinkScreen
import com.unciv.ui.popup.Popup import com.unciv.ui.popup.Popup
import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
class UncivGame(parameters: UncivGameParameters) : Game() { class UncivGame(parameters: UncivGameParameters) : Game() {
@ -48,6 +49,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
lateinit var settings: GameSettings lateinit var settings: GameSettings
lateinit var musicController: MusicController lateinit var musicController: MusicController
lateinit var onlineMultiplayer: OnlineMultiplayer lateinit var onlineMultiplayer: OnlineMultiplayer
lateinit var gameSaver: GameSaver
/** /**
* This exists so that when debugging we can see the entire map. * This exists so that when debugging we can see the entire map.
@ -87,7 +89,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
viewEntireMapForDebug = false viewEntireMapForDebug = false
} }
Current = this 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. // If this takes too long players, especially with older phones, get ANR problems.
// Whatever needs graphics needs to be done on the main thread, // Whatever needs graphics needs to be done on the main thread,
@ -101,7 +103,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
* - Skin (hence BaseScreen.setSkin()) * - Skin (hence BaseScreen.setSkin())
* - Font (hence Fonts.resetFont() inside 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 screen = LoadingScreen() // NOT dependent on any atlas or skin
musicController = MusicController() // early, but at this point does only copy volume from settings musicController = MusicController() // early, but at this point does only copy volume from settings
audioExceptionHelper?.installHooks( audioExceptionHelper?.installHooks(
@ -221,7 +223,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
} }
override fun pause() { override fun pause() {
if (isGameInfoInitialized()) GameSaver.autoSave(this.gameInfo) if (isGameInfoInitialized()) gameSaver.autoSave(this.gameInfo)
musicController.pause() musicController.pause()
super.pause() super.pause()
} }
@ -232,7 +234,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
override fun render() = wrappedCrashHandlingRender() 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 Gdx.input.inputProcessor = null // don't allow ANRs when shutting down, that's silly
cancelDiscordEvent?.invoke() cancelDiscordEvent?.invoke()
@ -240,24 +242,28 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out
closeExecutors() 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()) { if (isGameInfoInitialized()) {
val autoSaveThread = threadList.firstOrNull { it.name == GameSaver.autoSaveFileName } val autoSaveJob = gameSaver.autoSaveJob
if (autoSaveThread != null && autoSaveThread.isAlive) { if (autoSaveJob != null && autoSaveJob.isActive) {
// auto save is already in progress (e.g. started by onPause() event) // 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 // let's allow it to finish and do not try to autosave second time
autoSaveThread.join() autoSaveJob.join()
} else } else {
GameSaver.autoSaveSingleThreaded(gameInfo) // NO new thread gameSaver.autoSaveSingleThreaded(gameInfo) // NO new thread
}
} }
settings.save() settings.save()
threadList.filter { it !== Thread.currentThread() && it.name != "DestroyJavaVM"}.forEach { // On desktop this should only be this one and "DestroyJavaVM"
println (" Thread ${it.name} still running in UncivGame.dispose().") 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().")
} }
} }

View File

@ -20,6 +20,7 @@ interface CustomSaveLocationHelper {
* @param saveCompleteCallback Action to call upon completion (success _and_ failure) * @param saveCompleteCallback Action to call upon completion (success _and_ failure)
*/ */
fun saveGame( fun saveGame(
gameSaver: GameSaver,
gameInfo: GameInfo, gameInfo: GameInfo,
gameName: String, gameName: String,
forcePrompt: Boolean = false, forcePrompt: Boolean = false,
@ -33,5 +34,5 @@ interface CustomSaveLocationHelper {
* *
* @param loadCompleteCallback Action to call upon completion (success _and_ failure) * @param loadCompleteCallback Action to call upon completion (success _and_ failure)
*/ */
fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) fun loadGame(gameSaver: GameSaver, loadCompleteCallback: (GameInfo?, Exception?) -> Unit)
} }

View File

@ -4,68 +4,83 @@ import com.badlogic.gdx.Files
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.json.fromJsonFile
import com.unciv.json.json import com.unciv.json.json
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.saves.Gzip import com.unciv.ui.saves.Gzip
import kotlinx.coroutines.Job
import java.io.File 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 { class 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
/** /**
* This is necessary because the Android turn check background worker does not hold any reference to the actual [com.badlogic.gdx.Application], * 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. * which is normally responsible for keeping the [Gdx] static variables from being garbage collected.
*/ */
private lateinit var files: Files private val files: Files,
private val customSaveLocationHelper: CustomSaveLocationHelper? = null,
private var customSaveLocationHelper: CustomSaveLocationHelper? = null
/** When set, we know we're on Android and can save to the app's personal external file directory /** 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 */ * 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 */ var saveZipped = false
fun init(files: Files, customSaveLocationHelper: CustomSaveLocationHelper?) {
this.files = files var autoSaveJob: Job? = null
this.customSaveLocationHelper = customSaveLocationHelper
}
//endregion //endregion
//region Helpers //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 { private fun getSave(saveFolder: String, gameName: String): FileHandle {
val localFile = files.local("${getSavefolder(multiplayer)}/$GameName") val localFile = files.local("${saveFolder}/$gameName")
if (externalFilesDirForAndroid == "" || !files.isExternalStorageAvailable) return localFile if (externalFilesDirForAndroid.isNullOrBlank() || !files.isExternalStorageAvailable) return localFile
val externalFile = files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}/$GameName") val externalFile = files.absolute(externalFilesDirForAndroid + "/${saveFolder}/$gameName")
if (localFile.exists() && !externalFile.exists()) return localFile if (localFile.exists() && !externalFile.exists()) return localFile
return externalFile return externalFile
} }
fun getSaves(multiplayer: Boolean = false): Sequence<FileHandle> { fun getMultiplayerSaves(): Sequence<FileHandle> {
val localSaves = files.local(getSavefolder(multiplayer)).list().asSequence() return getSaves(MULTIPLAYER_FILES_FOLDER)
if (externalFilesDirForAndroid == "" || !files.isExternalStorageAvailable) return localSaves }
return localSaves + files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}").list().asSequence()
fun getSaves(autoSaves: Boolean = true): Sequence<FileHandle> {
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<FileHandle> {
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 canLoadFromCustomSaveLocation() = customSaveLocationHelper != null
fun deleteSave(GameName: String, multiplayer: Boolean = false) { fun deleteSave(gameName: String) {
getSave(GameName, multiplayer).delete() 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) { fun deleteSave(file: FileHandle) {
file.delete() 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 }) { fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
@ -98,7 +113,7 @@ object GameSaver {
return if (forceZip ?: saveZipped) Gzip.zip(plainJson) else plainJson 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 { fun gameInfoToString(game: GameInfoPreview): String {
return Gzip.zip(json().toJson(game)) return Gzip.zip(json().toJson(game))
} }
@ -106,14 +121,14 @@ object GameSaver {
/** /**
* Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder * 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 { fun saveGame(game: GameInfoPreview, gameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle {
val file = getSave(GameName, true) val file = getMultiplayerSave(gameName)
saveGame(game, file, saveCompletionCallback) saveGame(game, file, saveCompletionCallback)
return file 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 }) { fun saveGame(game: GameInfoPreview, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
@ -125,28 +140,28 @@ object GameSaver {
} }
fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) { 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 //endregion
//region Loading //region Loading
fun loadGameByName(GameName: String) = fun loadGameByName(gameName: String) =
loadGameFromFile(getSave(GameName)) loadGameFromFile(getSave(gameName))
fun loadGameFromFile(gameFile: FileHandle): GameInfo { fun loadGameFromFile(gameFile: FileHandle): GameInfo {
return gameInfoFromString(gameFile.readString()) return gameInfoFromString(gameFile.readString())
} }
fun loadGamePreviewByName(GameName: String) = fun loadGamePreviewByName(gameName: String) =
loadGamePreviewFromFile(getSave(GameName, true)) loadGamePreviewFromFile(getMultiplayerSave(gameName))
fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview { fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview {
return json().fromJson(GameInfoPreview::class.java, gameFile) return json().fromJson(GameInfoPreview::class.java, gameFile)
} }
fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) { fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) {
customSaveLocationHelper!!.loadGame { game, e -> customSaveLocationHelper!!.loadGame(this) { game, e ->
loadCompletionCallback(game?.apply { setTransients() }, 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 * @throws SerializationException
*/ */
fun gameInfoPreviewFromString(gameData: String): GameInfoPreview { fun gameInfoPreviewFromString(gameData: String): GameInfoPreview {
@ -185,8 +200,8 @@ object GameSaver {
//region Settings //region Settings
private fun getGeneralSettingsFile(): FileHandle { private fun getGeneralSettingsFile(): FileHandle {
return if (UncivGame.Current.consoleMode) FileHandle(settingsFileName) return if (UncivGame.Current.consoleMode) FileHandle(SETTINGS_FILE_NAME)
else files.local(settingsFileName) else files.local(SETTINGS_FILE_NAME)
} }
fun getGeneralSettings(): GameSettings { fun getGeneralSettings(): GameSettings {
@ -213,9 +228,30 @@ object GameSaver {
getGeneralSettingsFile().writeString(json().toJson(gameSettings), false) 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 //endregion
//region Autosave //region Autosave
/**
* Runs autoSave
*/
fun autoSave(gameInfo: GameInfo, postRunnable: () -> Unit = {}) { 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. // 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. // 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 = {}) { 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 // 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) autoSaveSingleThreaded(gameInfo)
// do this on main thread // do this on main thread
postCrashHandlingRunnable ( postRunnable ) postCrashHandlingRunnable ( postRunnable )
@ -234,22 +270,38 @@ object GameSaver {
fun autoSaveSingleThreaded(gameInfo: GameInfo) { fun autoSaveSingleThreaded(gameInfo: GameInfo) {
try { try {
saveGame(gameInfo, autoSaveFileName) saveGame(gameInfo, AUTOSAVE_FILE_NAME)
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
return // not much we can do here return // not much we can do here
} }
// keep auto-saves for the last 10 turns for debugging purposes // keep auto-saves for the last 10 turns for debugging purposes
val newAutosaveFilename = val newAutosaveFilename =
saveFilesFolder + File.separator + autoSaveFileName + "-${gameInfo.currentPlayer}-${gameInfo.turns}" SAVE_FILES_FOLDER + File.separator + AUTOSAVE_FILE_NAME + "-${gameInfo.currentPlayer}-${gameInfo.turns}"
getSave(autoSaveFileName).copyTo(files.local(newAutosaveFilename)) getSave(AUTOSAVE_FILE_NAME).copyTo(files.local(newAutosaveFilename))
fun getAutosaves(): Sequence<FileHandle> { fun getAutosaves(): Sequence<FileHandle> {
return getSaves().filter { it.name().startsWith(autoSaveFileName) } return getSaves().filter { it.name().startsWith(AUTOSAVE_FILE_NAME) }
} }
while (getAutosaves().count() > 10) { while (getAutosaves().count() > 10) {
val saveToDelete = getAutosaves().minByOrNull { it.lastModified() }!! val saveToDelete = getAutosaves().minByOrNull { it.lastModified() }!!
deleteSave(saveToDelete.name()) 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
} }

View File

@ -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. * 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<FileHandle, OnlineMultiplayerGame> = Collections.synchronizedMap(mutableMapOf()) private val savedGames: MutableMap<FileHandle, OnlineMultiplayerGame> = Collections.synchronizedMap(mutableMapOf())
private var lastFileUpdate: AtomicReference<Instant?> = AtomicReference() private var lastFileUpdate: AtomicReference<Instant?> = 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 fileUpdateNeeded(it: Instant?) = it == null || Duration.between(it, Instant.now()).isLargerThan(FILE_UPDATE_THROTTLE_INTERVAL)
private fun updateSavesFromFiles() { private fun updateSavesFromFiles() {
val saves = GameSaver.getSaves(true) val saves = gameSaver.getMultiplayerSaves()
val removedSaves = savedGames.keys - saves val removedSaves = savedGames.keys - saves
removedSaves.forEach(savedGames::remove) removedSaves.forEach(savedGames::remove)
val newSaves = saves - savedGames.keys val newSaves = saves - savedGames.keys
@ -99,7 +100,7 @@ class OnlineMultiplayer {
suspend fun createGame(newGame: GameInfo) { suspend fun createGame(newGame: GameInfo) {
OnlineMultiplayerGameSaver().tryUploadGame(newGame, withPreview = true) OnlineMultiplayerGameSaver().tryUploadGame(newGame, withPreview = true)
val newGamePreview = newGame.asPreview() 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()) val onlineMultiplayerGame = OnlineMultiplayerGame(file, newGamePreview, Instant.now())
savedGames[file] = onlineMultiplayerGame savedGames[file] = onlineMultiplayerGame
postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(onlineMultiplayerGame.name)) } postCrashHandlingRunnable { EventBus.send(MultiplayerGameAdded(onlineMultiplayerGame.name)) }
@ -119,11 +120,11 @@ class OnlineMultiplayer {
var fileHandle: FileHandle var fileHandle: FileHandle
try { try {
gamePreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameId) gamePreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(gameId)
fileHandle = GameSaver.saveGame(gamePreview, saveFileName) fileHandle = gameSaver.saveGame(gamePreview, saveFileName)
} catch (ex: FileNotFoundException) { } catch (ex: FileNotFoundException) {
// Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead
gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId).asPreview() gamePreview = OnlineMultiplayerGameSaver().tryDownloadGame(gameId).asPreview()
fileHandle = GameSaver.saveGame(gamePreview, saveFileName) fileHandle = gameSaver.saveGame(gamePreview, saveFileName)
} }
val game = OnlineMultiplayerGame(fileHandle, gamePreview, Instant.now()) val game = OnlineMultiplayerGame(fileHandle, gamePreview, Instant.now())
savedGames[fileHandle] = game savedGames[fileHandle] = game
@ -172,7 +173,7 @@ class OnlineMultiplayer {
} }
val newPreview = gameInfo.asPreview() val newPreview = gameInfo.asPreview()
GameSaver.saveGame(newPreview, game.fileHandle) gameSaver.saveGame(newPreview, game.fileHandle)
OnlineMultiplayerGameSaver().tryUploadGame(gameInfo, withPreview = true) OnlineMultiplayerGameSaver().tryUploadGame(gameInfo, withPreview = true)
game.doManualUpdate(newPreview) game.doManualUpdate(newPreview)
postCrashHandlingRunnable { EventBus.send(MultiplayerGameUpdated(game.name, newPreview)) } postCrashHandlingRunnable { EventBus.send(MultiplayerGameUpdated(game.name, newPreview)) }
@ -208,7 +209,7 @@ class OnlineMultiplayer {
*/ */
fun deleteGame(multiplayerGame: OnlineMultiplayerGame) { fun deleteGame(multiplayerGame: OnlineMultiplayerGame) {
val name = multiplayerGame.name val name = multiplayerGame.name
GameSaver.deleteSave(multiplayerGame.fileHandle) gameSaver.deleteSave(multiplayerGame.fileHandle)
EventBus.send(MultiplayerGameDeleted(name)) EventBus.send(MultiplayerGameDeleted(name))
} }
@ -224,8 +225,8 @@ class OnlineMultiplayer {
val oldName = game.name val oldName = game.name
savedGames.remove(game.fileHandle) savedGames.remove(game.fileHandle)
GameSaver.deleteSave(game.fileHandle) gameSaver.deleteSave(game.fileHandle)
val newFileHandle = GameSaver.saveGame(oldPreview, newName) val newFileHandle = gameSaver.saveGame(oldPreview, newName)
val newGame = OnlineMultiplayerGame(newFileHandle, oldPreview, oldLastUpdate) val newGame = OnlineMultiplayerGame(newFileHandle, oldPreview, oldLastUpdate)
savedGames[newFileHandle] = newGame savedGames[newFileHandle] = newGame

View File

@ -4,7 +4,6 @@ import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameInfoPreview
import com.unciv.logic.GameSaver
import com.unciv.logic.event.EventBus import com.unciv.logic.event.EventBus
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
@ -51,7 +50,7 @@ class OnlineMultiplayerGame(
} }
private fun loadPreviewFromFile(): GameInfoPreview { private fun loadPreviewFromFile(): GameInfoPreview {
val previewFromFile = GameSaver.loadGamePreviewFromFile(fileHandle) val previewFromFile = UncivGame.Current.gameSaver.loadGamePreviewFromFile(fileHandle)
preview = previewFromFile preview = previewFromFile
return previewFromFile return previewFromFile
} }
@ -91,7 +90,7 @@ class OnlineMultiplayerGame(
val curPreview = if (preview != null) preview!! else loadPreviewFromFile() val curPreview = if (preview != null) preview!! else loadPreviewFromFile()
val newPreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(curPreview.gameId) val newPreview = OnlineMultiplayerGameSaver().tryDownloadGamePreview(curPreview.gameId)
if (newPreview.turns == curPreview.turns && newPreview.currentPlayer == curPreview.currentPlayer) return GameUpdateResult.UNCHANGED if (newPreview.turns == curPreview.turns && newPreview.currentPlayer == curPreview.currentPlayer) return GameUpdateResult.UNCHANGED
GameSaver.saveGame(newPreview, fileHandle) UncivGame.Current.gameSaver.saveGame(newPreview, fileHandle)
preview = newPreview preview = newPreview
return GameUpdateResult.CHANGED return GameUpdateResult.CHANGED
} }

View File

@ -4,7 +4,6 @@ import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameInfoPreview
import com.unciv.logic.GameSaver
/** /**
* Allows access to games stored on a server for multiplayer purposes. * Allows access to games stored on a server for multiplayer purposes.
@ -20,6 +19,7 @@ import com.unciv.logic.GameSaver
class OnlineMultiplayerGameSaver( class OnlineMultiplayerGameSaver(
private var fileStorageIdentifier: String? = null private var fileStorageIdentifier: String? = null
) { ) {
private val gameSaver = UncivGame.Current.gameSaver
fun fileStorage(): FileStorage { fun fileStorage(): FileStorage {
val identifier = if (fileStorageIdentifier == null) UncivGame.Current.settings.multiplayerServer else fileStorageIdentifier val identifier = if (fileStorageIdentifier == null) UncivGame.Current.settings.multiplayerServer else fileStorageIdentifier
@ -34,7 +34,7 @@ class OnlineMultiplayerGameSaver(
tryUploadGamePreview(gameInfo.asPreview()) tryUploadGamePreview(gameInfo.asPreview())
} }
val zippedGameInfo = GameSaver.gameInfoToString(gameInfo, forceZip = true) val zippedGameInfo = gameSaver.gameInfoToString(gameInfo, forceZip = true)
fileStorage().saveFileData(gameInfo.gameId, zippedGameInfo, true) fileStorage().saveFileData(gameInfo.gameId, zippedGameInfo, true)
} }
@ -49,7 +49,7 @@ class OnlineMultiplayerGameSaver(
* @see GameInfo.asPreview * @see GameInfo.asPreview
*/ */
suspend fun tryUploadGamePreview(gameInfo: GameInfoPreview) { suspend fun tryUploadGamePreview(gameInfo: GameInfoPreview) {
val zippedGameInfo = GameSaver.gameInfoToString(gameInfo) val zippedGameInfo = gameSaver.gameInfoToString(gameInfo)
fileStorage().saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo, true) fileStorage().saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo, true)
} }
@ -59,7 +59,7 @@ class OnlineMultiplayerGameSaver(
*/ */
suspend fun tryDownloadGame(gameId: String): GameInfo { suspend fun tryDownloadGame(gameId: String): GameInfo {
val zippedGameInfo = fileStorage().loadFileData(gameId) 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 { suspend fun tryDownloadGamePreview(gameId: String): GameInfoPreview {
val zippedGameInfo = fileStorage().loadFileData("${gameId}_Preview") val zippedGameInfo = fileStorage().loadFileData("${gameId}_Preview")
return GameSaver.gameInfoPreviewFromString(zippedGameInfo) return gameSaver.gameInfoPreviewFromString(zippedGameInfo)
} }
} }

View File

@ -2,13 +2,9 @@ package com.unciv.models.metadata
import com.badlogic.gdx.Application import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants import com.unciv.Constants
import com.unciv.json.fromJsonFile import com.unciv.UncivGame
import com.unciv.json.json
import com.unciv.logic.GameSaver
import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.Fonts
import java.io.File
import java.text.Collator import java.text.Collator
import java.util.* import java.util.*
import kotlin.collections.HashSet import kotlin.collections.HashSet
@ -86,7 +82,7 @@ class GameSettings {
if (!isFreshlyCreated && Gdx.app?.type == Application.ApplicationType.Desktop) { if (!isFreshlyCreated && Gdx.app?.type == Application.ApplicationType.Desktop) {
windowState = WindowState(Gdx.graphics.width, Gdx.graphics.height) windowState = WindowState(Gdx.graphics.width, Gdx.graphics.height)
} }
GameSaver.setGeneralSettings(this) UncivGame.Current.gameSaver.setGeneralSettings(this)
} }
fun addCompletedTutorialTask(tutorialTask: String) { fun addCompletedTutorialTask(tutorialTask: String) {
@ -114,24 +110,6 @@ class GameSettings {
fun getCollatorFromLocale(): Collator { fun getCollatorFromLocale(): Collator {
return Collator.getInstance(getCurrentLocale()) 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) { enum class LocaleCode(var language: String, var country: String) {

View File

@ -8,7 +8,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameSaver
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
@ -56,7 +55,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() {
return "" return ""
return "\n**Save Data:**\n<details><summary>Show Saved Game</summary>\n\n```" + return "\n**Save Data:**\n<details><summary>Show Saved Game</summary>\n\n```" +
try { try {
GameSaver.gameInfoToString(UncivGame.Current.gameInfo, forceZip = true) game.gameSaver.gameInfoToString(UncivGame.Current.gameInfo, forceZip = true)
} catch (e: Throwable) { } catch (e: Throwable) {
"No save data: $e" // In theory .toString() could still error here. "No save data: $e" // In theory .toString() could still error here.
} + "\n```\n</details>\n" } + "\n```\n</details>\n"

View File

@ -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! 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 { try {
game.onlineMultiplayer.createGame(newGame) game.onlineMultiplayer.createGame(newGame)
GameSaver.autoSave(newGame) game.gameSaver.autoSave(newGame)
} catch (ex: FileStorageRateLimitReached) { } catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
popup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true) popup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true)

View File

@ -42,8 +42,8 @@ fun debugTab() = Table(BaseScreen.skin).apply {
game.gameInfo.gameParameters.godMode = it game.gameInfo.gameParameters.godMode = it
}).colspan(2).row() }).colspan(2).row()
} }
add("Save games compressed".toCheckBox(GameSaver.saveZipped) { add("Save games compressed".toCheckBox(game.gameSaver.saveZipped) {
GameSaver.saveZipped = it game.gameSaver.saveZipped = it
}).colspan(2).row() }).colspan(2).row()
add("Save maps compressed".toCheckBox(MapSaver.saveZipped) { add("Save maps compressed".toCheckBox(MapSaver.saveZipped) {
MapSaver.saveZipped = it MapSaver.saveZipped = it

View File

@ -9,7 +9,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameSaver
import com.unciv.logic.MissingModsException import com.unciv.logic.MissingModsException
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
@ -54,7 +53,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
launchCrashHandling("Load Game") { launchCrashHandling("Load Game") {
try { try {
// This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread // 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) } postCrashHandlingRunnable { UncivGame.Current.loadGame(loadedGame) }
} catch (ex: Exception) { } catch (ex: Exception) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
@ -89,17 +88,17 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
loadFromClipboardButton.onClick { loadFromClipboardButton.onClick {
try { try {
val clipboardContentsString = Gdx.app.clipboard.contents.trim() val clipboardContentsString = Gdx.app.clipboard.contents.trim()
val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString) val loadedGame = game.gameSaver.gameInfoFromString(clipboardContentsString)
UncivGame.Current.loadGame(loadedGame) UncivGame.Current.loadGame(loadedGame)
} catch (ex: Exception) { } catch (ex: Exception) {
handleLoadGameException("Could not load game from clipboard!", ex) handleLoadGameException("Could not load game from clipboard!", ex)
} }
} }
rightSideTable.add(loadFromClipboardButton).row() rightSideTable.add(loadFromClipboardButton).row()
if (GameSaver.canLoadFromCustomSaveLocation()) { if (game.gameSaver.canLoadFromCustomSaveLocation()) {
val loadFromCustomLocation = "Load from custom location".toTextButton() val loadFromCustomLocation = "Load from custom location".toTextButton()
loadFromCustomLocation.onClick { loadFromCustomLocation.onClick {
GameSaver.loadGameFromCustomLocation { gameInfo, exception -> game.gameSaver.loadGameFromCustomLocation { gameInfo, exception ->
if (gameInfo != null) { if (gameInfo != null) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
game.loadGame(gameInfo) game.loadGame(gameInfo)
@ -119,7 +118,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
rightSideTable.add(loadMissingModsButton).row() rightSideTable.add(loadMissingModsButton).row()
deleteSaveButton.onClick { deleteSaveButton.onClick {
GameSaver.deleteSave(selectedSave) game.gameSaver.deleteSave(selectedSave)
resetWindowState() resetWindowState()
} }
deleteSaveButton.disable() deleteSaveButton.disable()
@ -127,7 +126,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
copySavedGameToClipboardButton.disable() copySavedGameToClipboardButton.disable()
copySavedGameToClipboardButton.onClick { copySavedGameToClipboardButton.onClick {
val gameText = GameSaver.getSave(selectedSave).readString() val gameText = game.gameSaver.getSave(selectedSave).readString()
val gzippedGameText = Gzip.zip(gameText) val gzippedGameText = Gzip.zip(gameText)
Gdx.app.clipboard.contents = gzippedGameText 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 // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times
launchCrashHandling("GetSaves") { launchCrashHandling("GetSaves") {
// .toList() because otherwise the lastModified will only be checked inside the postRunnable // .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 { postCrashHandlingRunnable {
saveTable.clear() saveTable.clear()
for (save in saves) { for (save in saves) {
if (save.name().startsWith(GameSaver.autoSaveFileName) && !showAutosaves) continue
val textButton = TextButton(save.name(), skin) val textButton = TextButton(save.name(), skin)
textButton.onClick { onSaveSelected(save) } textButton.onClick { onSaveSelected(save) }
saveTable.add(textButton).pad(5f).row() 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() 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 launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones
try { try {
val game = GameSaver.loadGamePreviewFromFile(save) val game = game.gameSaver.loadGamePreviewFromFile(save)
val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() }
textToSet += "\n" + playerCivNames + textToSet += "\n" + playerCivNames +
", " + game.difficulty.tr() + ", ${Fonts.turn}" + game.turns ", " + game.difficulty.tr() + ", ${Fonts.turn}" + game.turns

View File

@ -7,7 +7,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
@ -44,7 +43,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
copyJsonButton.onClick { copyJsonButton.onClick {
thread(name="Copy to clipboard") { // the Gzip rarely leads to ANRs thread(name="Copy to clipboard") { // the Gzip rarely leads to ANRs
try { try {
Gdx.app.clipboard.contents = GameSaver.gameInfoToString(gameInfo, forceZip = true) Gdx.app.clipboard.contents = game.gameSaver.gameInfoToString(gameInfo, forceZip = true)
} catch (OOM: OutOfMemoryError) { } catch (OOM: OutOfMemoryError) {
// you don't get a special toast, this isn't nearly common enough, this is a total edge-case // 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() newSave.add(copyJsonButton).row()
if (GameSaver.canLoadFromCustomSaveLocation()) { if (game.gameSaver.canLoadFromCustomSaveLocation()) {
val saveToCustomLocation = "Save to custom location".toTextButton() val saveToCustomLocation = "Save to custom location".toTextButton()
val errorLabel = "".toLabel(Color.RED) val errorLabel = "".toLabel(Color.RED)
saveToCustomLocation.enable() saveToCustomLocation.enable()
@ -61,7 +60,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.setText("Saving...".tr())
saveToCustomLocation.disable() saveToCustomLocation.disable()
launchCrashHandling("SaveGame", runAsDaemon = false) { launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e ->
if (e == null) { if (e == null) {
postCrashHandlingRunnable { game.resetToWorldScreen() } postCrashHandlingRunnable { game.resetToWorldScreen() }
} else if (e !is CancellationException) { } else if (e !is CancellationException) {
@ -88,7 +87,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
rightSideButton.setText("Save game".tr()) rightSideButton.setText("Save game".tr())
rightSideButton.onClick { rightSideButton.onClick {
if (GameSaver.getSave(gameNameTextField.text).exists()) if (game.gameSaver.getSave(gameNameTextField.text).exists())
YesNoPopup("Overwrite existing file?", { saveGame() }, this).open() YesNoPopup("Overwrite existing file?", { saveGame() }, this).open()
else saveGame() else saveGame()
} }
@ -98,7 +97,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
private fun saveGame() { private fun saveGame() {
rightSideButton.setText("Saving...".tr()) rightSideButton.setText("Saving...".tr())
launchCrashHandling("SaveGame", runAsDaemon = false) { launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGame(gameInfo, gameNameTextField.text) { game.gameSaver.saveGame(gameInfo, gameNameTextField.text) {
postCrashHandlingRunnable { postCrashHandlingRunnable {
if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen) if (it != null) ToastPopup("Could not save game!", this@SaveGameScreen)
else UncivGame.Current.resetToWorldScreen() else UncivGame.Current.resetToWorldScreen()
@ -109,10 +108,9 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
private fun updateShownSaves(showAutosaves: Boolean) { private fun updateShownSaves(showAutosaves: Boolean) {
currentSaves.clear() currentSaves.clear()
val saves = GameSaver.getSaves() val saves = game.gameSaver.getSaves(autoSaves = showAutosaves)
.sortedByDescending { it.lastModified() } .sortedByDescending { it.lastModified() }
for (saveGameFile in saves) { for (saveGameFile in saves) {
if (saveGameFile.name().startsWith(GameSaver.autoSaveFileName) && !showAutosaves) continue
val textButton = saveGameFile.name().toTextButton() val textButton = saveGameFile.name().toTextButton()
textButton.onClick { textButton.onClick {
gameNameTextField.text = saveGameFile.name() gameNameTextField.text = saveGameFile.name()

View File

@ -17,4 +17,9 @@ interface GeneralPlatformSpecificHelpers {
* Notifies the user that it's their turn while the game is running * Notifies the user that it's their turn while the game is running
*/ */
fun notifyTurnStarted() {} fun notifyTurnStarted() {}
/**
* @return an additional external directory for save files, if applicable on the platform
*/
fun getExternalFilesDir(): String? { return null }
} }

View File

@ -16,7 +16,6 @@ import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.ReligionState
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
@ -229,7 +228,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
val quickSave = { val quickSave = {
val toast = ToastPopup("Quicksaving...", this) val toast = ToastPopup("Quicksaving...", this)
launchCrashHandling("SaveGame", runAsDaemon = false) { launchCrashHandling("SaveGame", runAsDaemon = false) {
GameSaver.saveGame(gameInfo, "QuickSave") { game.gameSaver.saveGame(gameInfo, "QuickSave") {
postCrashHandlingRunnable { postCrashHandlingRunnable {
toast.close() toast.close()
if (it != null) if (it != null)
@ -246,7 +245,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
val toast = ToastPopup("Quickloading...", this) val toast = ToastPopup("Quickloading...", this)
launchCrashHandling("LoadGame") { launchCrashHandling("LoadGame") {
try { try {
val loadedGame = GameSaver.loadGameByName("QuickSave") val loadedGame = game.gameSaver.loadGameByName("QuickSave")
postCrashHandlingRunnable { postCrashHandlingRunnable {
toast.close() toast.close()
UncivGame.Current.loadGame(loadedGame) UncivGame.Current.loadGame(loadedGame)
@ -713,7 +712,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
val newWorldScreen = this@WorldScreen.game.worldScreen val newWorldScreen = this@WorldScreen.game.worldScreen
newWorldScreen.waitingForAutosave = true newWorldScreen.waitingForAutosave = true
newWorldScreen.shouldUpdate = 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 // only enable the user to next turn once we've saved the current one
newWorldScreen.waitingForAutosave = false newWorldScreen.waitingForAutosave = false
newWorldScreen.shouldUpdate = true newWorldScreen.shouldUpdate = true

View File

@ -2,7 +2,6 @@ package com.unciv.ui.worldscreen.mainmenu
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.unciv.MainMenuScreen import com.unciv.MainMenuScreen
import com.unciv.logic.GameSaver
import com.unciv.ui.civilopedia.CivilopediaScreen import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.GameSetupInfo
import com.unciv.ui.newgamescreen.NewGameScreen import com.unciv.ui.newgamescreen.NewGameScreen
@ -17,7 +16,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
defaults().fillX() defaults().fillX()
addButton("Main menu") { addButton("Main menu") {
GameSaver.autoSaveUnCloned(worldScreen.gameInfo) worldScreen.game.gameSaver.autoSaveUnCloned(worldScreen.gameInfo)
worldScreen.game.setScreen(MainMenuScreen()) worldScreen.game.setScreen(MainMenuScreen())
} }
addButton("Civilopedia") { addButton("Civilopedia") {

View File

@ -12,14 +12,14 @@ import javax.swing.JFileChooser
import javax.swing.JFrame import javax.swing.JFrame
class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper { 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 val customSaveLocation = gameInfo.customSaveLocation
if (customSaveLocation != null && !forcePrompt) { if (customSaveLocation != null && !forcePrompt) {
try { try {
File(customSaveLocation).outputStream() File(customSaveLocation).outputStream()
.writer() .writer()
.use { writer -> .use { writer ->
writer.write(GameSaver.gameInfoToString(gameInfo)) writer.write(gameSaver.gameInfoToString(gameInfo))
} }
saveCompleteCallback?.invoke(null) saveCompleteCallback?.invoke(null)
} catch (e: Exception) { } catch (e: Exception) {
@ -59,7 +59,7 @@ class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper {
saveCompleteCallback?.invoke(exception) saveCompleteCallback?.invoke(exception)
} }
override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { override fun loadGame(gameSaver: GameSaver, loadCompleteCallback: (GameInfo?, Exception?) -> Unit) {
val fileChooser = JFileChooser().apply fileChooser@{ val fileChooser = JFileChooser().apply fileChooser@{
currentDirectory = Gdx.files.local("").file() currentDirectory = Gdx.files.local("").file()
} }
@ -79,7 +79,7 @@ class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper {
file.inputStream() file.inputStream()
.reader() .reader()
.readText() .readText()
.run { GameSaver.gameInfoFromString(this) } .run { gameSaver.gameInfoFromString(this) }
.apply { .apply {
// If the user has saved the game from another platform (like Android), // 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 // then the save location might not be right so we have to correct for that

View File

@ -9,6 +9,7 @@ import com.badlogic.gdx.graphics.glutils.HdpiMode
import com.sun.jna.Native import com.sun.jna.Native
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.UncivGameParameters import com.unciv.UncivGameParameters
import com.unciv.logic.GameSaver
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.Fonts
import java.util.* 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. // Note that means config.setAudioConfig() would be ignored too, those would need to go into the HardenedGdxAudio constructor.
config.disableAudio(true) config.disableAudio(true)
val settings = GameSettings.getSettingsForPlatformLaunchers() val settings = GameSaver.getSettingsForPlatformLaunchers()
if (!settings.isFreshlyCreated) { if (!settings.isFreshlyCreated) {
config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80)) config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80))
} }

View File

@ -16,21 +16,20 @@
package com.unciv.testing; package com.unciv.testing;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.headless.HeadlessApplication; import com.badlogic.gdx.backends.headless.HeadlessApplication;
import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration; import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration;
import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.GL20;
import com.unciv.logic.GameSaver;
import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError; import org.junit.runners.model.InitializationError;
import java.util.HashMap; import static org.mockito.Mockito.*;
import java.util.Map;
import static org.mockito.Mockito.mock;
public class GdxTestRunner extends BlockJUnit4ClassRunner implements ApplicationListener { public class GdxTestRunner extends BlockJUnit4ClassRunner implements ApplicationListener {
@ -46,7 +45,6 @@ public class GdxTestRunner extends BlockJUnit4ClassRunner implements Application
@Override @Override
public void create() { public void create() {
GameSaver.INSTANCE.init(Gdx.files, null);
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package com.unciv.testing package com.unciv.testing
import com.badlogic.gdx.Gdx
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.json.json import com.unciv.json.json
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
@ -59,9 +60,10 @@ class SerializationTests {
} }
val setup = GameSetupInfo(param, mapParameters) val setup = GameSetupInfo(param, mapParameters)
UncivGame.Current = UncivGame("") UncivGame.Current = UncivGame("")
UncivGame.Current.gameSaver = GameSaver(Gdx.files)
// Both startNewGame and makeCivilizationsMeet will cause a save to storage of our empty settings // 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() UncivGame.Current.settings = GameSettings()
game = GameStarter.startNewGame(setup) game = GameStarter.startNewGame(setup)