mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 09:18:43 +07:00
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:
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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().")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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") {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user