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