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

View File

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

View File

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

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
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.utils.Align
import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.logic.GameStarter
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapSize
@ -97,8 +96,7 @@ class MainMenuScreen: BaseScreen() {
val column1 = Table().apply { defaults().pad(10f).fillX() }
val column2 = if (singleColumn) column1 else Table().apply { defaults().pad(10f).fillX() }
val autosaveGame = GameSaver.getSave(GameSaver.autoSaveFileName, false)
if (autosaveGame.exists()) {
if (game.gameSaver.autosaveExists()) {
val resumeTable = getMenuButton("Resume","OtherIcons/Resume", 'r')
{ autoLoadGame() }
column1.add(resumeTable).row()
@ -112,7 +110,7 @@ class MainMenuScreen: BaseScreen() {
{ game.setScreen(NewGameScreen(this)) }
column1.add(newGameButton).row()
if (GameSaver.getSaves(false).any()) {
if (game.gameSaver.getSaves().any()) {
val loadGameTable = getMenuButton("Load game", "OtherIcons/Load", 'l')
{ game.setScreen(LoadGameScreen(this)) }
column1.add(loadGameTable).row()
@ -180,22 +178,12 @@ class MainMenuScreen: BaseScreen() {
}
}
var savedGame: GameInfo
val savedGame: GameInfo
try {
savedGame = GameSaver.loadGameByName(GameSaver.autoSaveFileName)
savedGame = game.gameSaver.loadLatestAutosave()
} catch (oom: OutOfMemoryError) {
outOfMemory()
return@launchCrashHandling
} catch (ex: Exception) { // silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first
// This can help for situations when the autosave is corrupted
try {
val autosaves = GameSaver.getSaves()
.filter { it.name() != GameSaver.autoSaveFileName && it.name().startsWith(GameSaver.autoSaveFileName) }
savedGame =
GameSaver.loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!)
} catch (oom: OutOfMemoryError) { // The autosave could have oom problems as well... smh
outOfMemory()
return@launchCrashHandling
} catch (ex: Exception) {
postCrashHandlingRunnable {
loadingPopup.close()
@ -203,7 +191,6 @@ class MainMenuScreen: BaseScreen() {
}
return@launchCrashHandling
}
}
postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context
try {

View File

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

View File

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

View File

@ -4,68 +4,83 @@ import com.badlogic.gdx.Files
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame
import com.unciv.json.fromJsonFile
import com.unciv.json.json
import com.unciv.models.metadata.GameSettings
import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.saves.Gzip
import kotlinx.coroutines.Job
import java.io.File
private const val SAVE_FILES_FOLDER = "SaveFiles"
private const val MULTIPLAYER_FILES_FOLDER = "MultiplayerGames"
private const val AUTOSAVE_FILE_NAME = "Autosave"
private const val SETTINGS_FILE_NAME = "GameSettings.json"
object GameSaver {
//region Data
private const val saveFilesFolder = "SaveFiles"
private const val multiplayerFilesFolder = "MultiplayerGames"
const val autoSaveFileName = "Autosave"
const val settingsFileName = "GameSettings.json"
var saveZipped = false
class GameSaver(
/**
* This is necessary because the Android turn check background worker does not hold any reference to the actual [com.badlogic.gdx.Application],
* which is normally responsible for keeping the [Gdx] static variables from being garbage collected.
*/
private lateinit var files: Files
private var customSaveLocationHelper: CustomSaveLocationHelper? = null
private val files: Files,
private val customSaveLocationHelper: CustomSaveLocationHelper? = null,
/** When set, we know we're on Android and can save to the app's personal external file directory
* See https://developer.android.com/training/data-storage/app-specific#external-access-files */
var externalFilesDirForAndroid = ""
private val externalFilesDirForAndroid: String? = null
) {
//region Data
/** Needs to be called before the class can be used */
fun init(files: Files, customSaveLocationHelper: CustomSaveLocationHelper?) {
this.files = files
this.customSaveLocationHelper = customSaveLocationHelper
}
var saveZipped = false
var autoSaveJob: Job? = null
//endregion
//region Helpers
private fun getSavefolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder
fun getSave(gameName: String): FileHandle {
return getSave(SAVE_FILES_FOLDER, gameName)
}
fun getMultiplayerSave(gameName: String): FileHandle {
return getSave(MULTIPLAYER_FILES_FOLDER, gameName)
}
fun getSave(GameName: String, multiplayer: Boolean = false): FileHandle {
val localFile = files.local("${getSavefolder(multiplayer)}/$GameName")
if (externalFilesDirForAndroid == "" || !files.isExternalStorageAvailable) return localFile
val externalFile = files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}/$GameName")
private fun getSave(saveFolder: String, gameName: String): FileHandle {
val localFile = files.local("${saveFolder}/$gameName")
if (externalFilesDirForAndroid.isNullOrBlank() || !files.isExternalStorageAvailable) return localFile
val externalFile = files.absolute(externalFilesDirForAndroid + "/${saveFolder}/$gameName")
if (localFile.exists() && !externalFile.exists()) return localFile
return externalFile
}
fun getSaves(multiplayer: Boolean = false): Sequence<FileHandle> {
val localSaves = files.local(getSavefolder(multiplayer)).list().asSequence()
if (externalFilesDirForAndroid == "" || !files.isExternalStorageAvailable) return localSaves
return localSaves + files.absolute(externalFilesDirForAndroid + "/${getSavefolder(multiplayer)}").list().asSequence()
fun getMultiplayerSaves(): Sequence<FileHandle> {
return getSaves(MULTIPLAYER_FILES_FOLDER)
}
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 deleteSave(GameName: String, multiplayer: Boolean = false) {
getSave(GameName, multiplayer).delete()
fun deleteSave(gameName: String) {
getSave(gameName).delete()
}
fun deleteMultiplayerSave(gameName: String) {
getMultiplayerSave(gameName).delete()
}
/**
* Only use this with a [FileHandle] returned by [getSaves]!
* Only use this with a [FileHandle] obtained by one of the methods of this class!
*/
fun deleteSave(file: FileHandle) {
file.delete()
@ -81,7 +96,7 @@ object GameSaver {
}
/**
* Only use this with a [FileHandle] obtained by [getSaves]!
* Only use this with a [FileHandle] obtained by one of the methods of this class!
*/
fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try {
@ -98,7 +113,7 @@ object GameSaver {
return if (forceZip ?: saveZipped) Gzip.zip(plainJson) else plainJson
}
/** Returns gzipped serialization of preview [game] - only called from [OnlineMultiplayerGameSaver] */
/** Returns gzipped serialization of preview [game] */
fun gameInfoToString(game: GameInfoPreview): String {
return Gzip.zip(json().toJson(game))
}
@ -106,14 +121,14 @@ object GameSaver {
/**
* Overload of function saveGame to save a GameInfoPreview in the MultiplayerGames folder
*/
fun saveGame(game: GameInfoPreview, GameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle {
val file = getSave(GameName, true)
fun saveGame(game: GameInfoPreview, gameName: String, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }): FileHandle {
val file = getMultiplayerSave(gameName)
saveGame(game, file, saveCompletionCallback)
return file
}
/**
* Only use this with a [FileHandle] obtained by [getSaves]!
* Only use this with a [FileHandle] obtained by one of the methods of this class!
*/
fun saveGame(game: GameInfoPreview, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try {
@ -125,28 +140,28 @@ object GameSaver {
}
fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) {
customSaveLocationHelper!!.saveGame(game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback)
customSaveLocationHelper!!.saveGame(this, game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback)
}
//endregion
//region Loading
fun loadGameByName(GameName: String) =
loadGameFromFile(getSave(GameName))
fun loadGameByName(gameName: String) =
loadGameFromFile(getSave(gameName))
fun loadGameFromFile(gameFile: FileHandle): GameInfo {
return gameInfoFromString(gameFile.readString())
}
fun loadGamePreviewByName(GameName: String) =
loadGamePreviewFromFile(getSave(GameName, true))
fun loadGamePreviewByName(gameName: String) =
loadGamePreviewFromFile(getMultiplayerSave(gameName))
fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview {
return json().fromJson(GameInfoPreview::class.java, gameFile)
}
fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) {
customSaveLocationHelper!!.loadGame { game, e ->
customSaveLocationHelper!!.loadGame(this) { game, e ->
loadCompletionCallback(game?.apply { setTransients() }, e)
}
}
@ -158,7 +173,7 @@ object GameSaver {
}
/**
* Parses [gameData] as gzipped serialization of a [GameInfoPreview] - only called from [OnlineMultiplayerGameSaver]
* Parses [gameData] as gzipped serialization of a [GameInfoPreview]
* @throws SerializationException
*/
fun gameInfoPreviewFromString(gameData: String): GameInfoPreview {
@ -185,8 +200,8 @@ object GameSaver {
//region Settings
private fun getGeneralSettingsFile(): FileHandle {
return if (UncivGame.Current.consoleMode) FileHandle(settingsFileName)
else files.local(settingsFileName)
return if (UncivGame.Current.consoleMode) FileHandle(SETTINGS_FILE_NAME)
else files.local(SETTINGS_FILE_NAME)
}
fun getGeneralSettings(): GameSettings {
@ -213,9 +228,30 @@ object GameSaver {
getGeneralSettingsFile().writeString(json().toJson(gameSettings), false)
}
companion object {
/** Specialized function to access settings before Gdx is initialized.
*
* @param base Path to the directory where the file should be - if not set, the OS current directory is used (which is "/" on Android)
*/
fun getSettingsForPlatformLaunchers(base: String = "."): GameSettings {
// FileHandle is Gdx, but the class and JsonParser are not dependent on app initialization
// In fact, at this point Gdx.app or Gdx.files are null but this still works.
val file = FileHandle(base + File.separator + SETTINGS_FILE_NAME)
return if (file.exists())
json().fromJsonFile(
GameSettings::class.java,
file
)
else GameSettings().apply { isFreshlyCreated = true }
}
}
//endregion
//region Autosave
/**
* Runs autoSave
*/
fun autoSave(gameInfo: GameInfo, postRunnable: () -> Unit = {}) {
// The save takes a long time (up to a few seconds on large games!) and we can do it while the player continues his game.
// On the other hand if we alter the game data while it's being serialized we could get a concurrent modification exception.
@ -225,7 +261,7 @@ object GameSaver {
fun autoSaveUnCloned(gameInfo: GameInfo, postRunnable: () -> Unit = {}) {
// This is used when returning from WorldScreen to MainMenuScreen - no clone since UI access to it should be gone
launchCrashHandling(autoSaveFileName, runAsDaemon = false) {
autoSaveJob = launchCrashHandling(AUTOSAVE_FILE_NAME) {
autoSaveSingleThreaded(gameInfo)
// do this on main thread
postCrashHandlingRunnable ( postRunnable )
@ -234,22 +270,38 @@ object GameSaver {
fun autoSaveSingleThreaded(gameInfo: GameInfo) {
try {
saveGame(gameInfo, autoSaveFileName)
saveGame(gameInfo, AUTOSAVE_FILE_NAME)
} catch (oom: OutOfMemoryError) {
return // not much we can do here
}
// keep auto-saves for the last 10 turns for debugging purposes
val newAutosaveFilename =
saveFilesFolder + File.separator + autoSaveFileName + "-${gameInfo.currentPlayer}-${gameInfo.turns}"
getSave(autoSaveFileName).copyTo(files.local(newAutosaveFilename))
SAVE_FILES_FOLDER + File.separator + AUTOSAVE_FILE_NAME + "-${gameInfo.currentPlayer}-${gameInfo.turns}"
getSave(AUTOSAVE_FILE_NAME).copyTo(files.local(newAutosaveFilename))
fun getAutosaves(): Sequence<FileHandle> {
return getSaves().filter { it.name().startsWith(autoSaveFileName) }
return getSaves().filter { it.name().startsWith(AUTOSAVE_FILE_NAME) }
}
while (getAutosaves().count() > 10) {
val saveToDelete = getAutosaves().minByOrNull { it.lastModified() }!!
deleteSave(saveToDelete.name())
}
}
fun loadLatestAutosave(): GameInfo {
try {
return loadGameByName(AUTOSAVE_FILE_NAME)
} catch (ex: Exception) {
// silent fail if we can't read the autosave for any reason - try to load the last autosave by turn number first
val autosaves = getSaves().filter { it.name() != AUTOSAVE_FILE_NAME && it.name().startsWith(AUTOSAVE_FILE_NAME) }
return loadGameFromFile(autosaves.maxByOrNull { it.lastModified() }!!)
}
}
fun autosaveExists(): Boolean {
return getSave(AUTOSAVE_FILE_NAME).exists()
}
// endregion
}

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

View File

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

View File

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

View File

@ -2,13 +2,9 @@ package com.unciv.models.metadata
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants
import com.unciv.json.fromJsonFile
import com.unciv.json.json
import com.unciv.logic.GameSaver
import com.unciv.UncivGame
import com.unciv.ui.utils.Fonts
import java.io.File
import java.text.Collator
import java.util.*
import kotlin.collections.HashSet
@ -86,7 +82,7 @@ class GameSettings {
if (!isFreshlyCreated && Gdx.app?.type == Application.ApplicationType.Desktop) {
windowState = WindowState(Gdx.graphics.width, Gdx.graphics.height)
}
GameSaver.setGeneralSettings(this)
UncivGame.Current.gameSaver.setGeneralSettings(this)
}
fun addCompletedTutorialTask(tutorialTask: String) {
@ -114,24 +110,6 @@ class GameSettings {
fun getCollatorFromLocale(): Collator {
return Collator.getInstance(getCurrentLocale())
}
companion object {
/** Specialized function to access settings before Gdx is initialized.
*
* @param base Path to the directory where the file should be - if not set, the OS current directory is used (which is "/" on Android)
*/
fun getSettingsForPlatformLaunchers(base: String = "."): GameSettings {
// FileHandle is Gdx, but the class and JsonParser are not dependent on app initialization
// In fact, at this point Gdx.app or Gdx.files are null but this still works.
val file = FileHandle(base + File.separator + GameSaver.settingsFileName)
return if (file.exists())
json().fromJsonFile(
GameSettings::class.java,
file
)
else GameSettings().apply { isFreshlyCreated = true }
}
}
}
enum class LocaleCode(var language: String, var country: String) {

View File

@ -8,7 +8,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameSaver
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter
@ -56,7 +55,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() {
return ""
return "\n**Save Data:**\n<details><summary>Show Saved Game</summary>\n\n```" +
try {
GameSaver.gameInfoToString(UncivGame.Current.gameInfo, forceZip = true)
game.gameSaver.gameInfoToString(UncivGame.Current.gameInfo, forceZip = true)
} catch (e: Throwable) {
"No save data: $e" // In theory .toString() could still error here.
} + "\n```\n</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!
try {
game.onlineMultiplayer.createGame(newGame)
GameSaver.autoSave(newGame)
game.gameSaver.autoSave(newGame)
} catch (ex: FileStorageRateLimitReached) {
postCrashHandlingRunnable {
popup.reuseWith("Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds", true)

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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