Implement custom save locations for Android and Desktop (#3160)
* Implement custom save locations for Android and Desktop * Request write permission to save to external storage * Fix race condition for custom saves/loads caused by autosaves * Remove unnecessary WRITE_EXTERNAL_STORAGE permission for saving files * Fix padding for custom save/load location buttons * Use nullability checks as defined in coding style guide * Use nullability checks as defined in coding style guide * Use early return for readability * Rename save/load completion callbacks for custom locations and implement error handling
This commit is contained in:
parent
fded66b523
commit
205b5ccfea
11 changed files with 362 additions and 11 deletions
|
@ -1,5 +1,6 @@
|
|||
package com.unciv.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
@ -13,8 +14,12 @@ import com.unciv.ui.utils.ORIGINAL_FONT_SIZE
|
|||
import java.io.File
|
||||
|
||||
class AndroidLauncher : AndroidApplication() {
|
||||
private var customSaveLocationHelper: CustomSaveLocationHelperAndroid? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
customSaveLocationHelper = CustomSaveLocationHelperAndroid(this)
|
||||
}
|
||||
MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext)
|
||||
|
||||
// Only allow mods on KK+, to avoid READ_EXTERNAL_STORAGE permission earlier versions need
|
||||
|
@ -29,7 +34,8 @@ class AndroidLauncher : AndroidApplication() {
|
|||
version = BuildConfig.VERSION_NAME,
|
||||
crashReportSender = CrashReportSenderAndroid(this),
|
||||
exitEvent = this::finish,
|
||||
fontImplementation = NativeFontAndroid(ORIGINAL_FONT_SIZE.toInt())
|
||||
fontImplementation = NativeFontAndroid(ORIGINAL_FONT_SIZE.toInt()),
|
||||
customSaveLocationHelper = customSaveLocationHelper
|
||||
)
|
||||
val game = UncivGame ( androidParameters )
|
||||
initialize(game, config)
|
||||
|
@ -77,4 +83,13 @@ class AndroidLauncher : AndroidApplication() {
|
|||
catch (ex:Exception){}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
// This should only happen on API 19+ but it's wrapped in the if check to keep the
|
||||
// compiler happy
|
||||
customSaveLocationHelper?.handleIntentData(requestCode, data?.data)
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
125
android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt
Normal file
125
android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt
Normal file
|
@ -0,0 +1,125 @@
|
|||
package com.unciv.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.unciv.logic.CustomSaveLocationHelper
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.logic.GameSaver.json
|
||||
|
||||
// The Storage Access Framework is available from API 19 and up:
|
||||
// https://developer.android.com/guide/topics/providers/document-provider
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSaveLocationHelper {
|
||||
// This looks a little scary but it's really not so bad. Whenever a load or save operation is
|
||||
// attempted, the game automatically autosaves as well (but on a separate thread), so we end up
|
||||
// with a race condition when trying to handle both operations in parallel. In order to work
|
||||
// around that, the callbacks are given an arbitrary index beginning at 100 and incrementing
|
||||
// each time, and this index is used as the requestCode for the call to startActivityForResult()
|
||||
// so that we can map it back to the corresponding callback when onActivityResult is called
|
||||
@GuardedBy("this")
|
||||
@Volatile
|
||||
private var callbackIndex = 100
|
||||
|
||||
@GuardedBy("this")
|
||||
private val callbacks = ArrayList<IndexedCallback>()
|
||||
|
||||
override fun saveGame(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)
|
||||
saveCompleteCallback?.invoke(null)
|
||||
} else {
|
||||
saveCompleteCallback?.invoke(RuntimeException("Uri was null"))
|
||||
}
|
||||
}
|
||||
))
|
||||
index
|
||||
}
|
||||
if (!forcePrompt && gameInfo.customSaveLocation != null) {
|
||||
handleIntentData(callbackIndex, Uri.parse(gameInfo.customSaveLocation))
|
||||
return
|
||||
}
|
||||
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
type = "application/json"
|
||||
putExtra(Intent.EXTRA_TITLE, gameName)
|
||||
activity.startActivityForResult(this, callbackIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This will be called on the main thread
|
||||
fun handleIntentData(requestCode: Int, uri: Uri?) {
|
||||
val callback = synchronized(this) {
|
||||
val index = callbacks.indexOfFirst { it.index == requestCode }
|
||||
if (index == -1) return
|
||||
callbacks.removeAt(index)
|
||||
}
|
||||
callback.thread.run {
|
||||
callback.callback(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveGame(gameInfo: GameInfo, uri: Uri) {
|
||||
gameInfo.customSaveLocation = uri.toString()
|
||||
activity.contentResolver.openOutputStream(uri, "rwt")
|
||||
?.writer()
|
||||
?.use {
|
||||
it.write(json().toJson(gameInfo))
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) {
|
||||
val callbackIndex = synchronized(this) {
|
||||
val index = callbackIndex++
|
||||
callbacks.add(IndexedCallback(
|
||||
index,
|
||||
callback@{ uri ->
|
||||
if (uri == null) return@callback
|
||||
var exception: Exception? = null
|
||||
val game = try {
|
||||
activity.contentResolver.openInputStream(uri)
|
||||
?.reader()
|
||||
?.readText()
|
||||
?.run {
|
||||
GameSaver.gameInfoFromString(this)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
exception = e
|
||||
null
|
||||
}
|
||||
if (game != null) {
|
||||
// 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
|
||||
// here
|
||||
game.customSaveLocation = uri.toString()
|
||||
loadCompleteCallback(game, null)
|
||||
} else {
|
||||
loadCompleteCallback(null, RuntimeException("Failed to load save game", exception))
|
||||
}
|
||||
}
|
||||
))
|
||||
index
|
||||
}
|
||||
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = "*/*"
|
||||
activity.startActivityForResult(this, callbackIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class IndexedCallback(
|
||||
val index: Int,
|
||||
val callback: (Uri?) -> Unit,
|
||||
val thread: Thread = Thread.currentThread()
|
||||
)
|
|
@ -13,7 +13,10 @@ import com.unciv.models.metadata.GameSettings
|
|||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.translations.Translations
|
||||
import com.unciv.ui.LanguagePickerScreen
|
||||
import com.unciv.ui.utils.*
|
||||
import com.unciv.ui.utils.CameraStageBaseScreen
|
||||
import com.unciv.ui.utils.CrashController
|
||||
import com.unciv.ui.utils.ImageGetter
|
||||
import com.unciv.ui.utils.center
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
|
@ -28,6 +31,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||
val cancelDiscordEvent = parameters.cancelDiscordEvent
|
||||
val fontImplementation = parameters.fontImplementation
|
||||
val consoleMode = parameters.consoleMode
|
||||
val customSaveLocationHelper = parameters.customSaveLocationHelper
|
||||
|
||||
lateinit var gameInfo: GameInfo
|
||||
fun isGameInfoInitialized() = this::gameInfo.isInitialized
|
||||
|
@ -69,6 +73,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||
Current = this
|
||||
|
||||
|
||||
GameSaver.customSaveLocationHelper = customSaveLocationHelper
|
||||
// If this takes too long players, especially with older phones, get ANR problems.
|
||||
// Whatever needs graphics needs to be done on the main thread,
|
||||
// So it's basically a long set of deferred actions.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.unciv
|
||||
|
||||
import com.unciv.logic.CustomSaveLocationHelper
|
||||
import com.unciv.ui.utils.CrashReportSender
|
||||
import com.unciv.ui.utils.NativeFontImplementation
|
||||
|
||||
|
@ -8,5 +9,6 @@ class UncivGameParameters(val version: String,
|
|||
val exitEvent: (()->Unit)? = null,
|
||||
val cancelDiscordEvent: (()->Unit)? = null,
|
||||
val fontImplementation: NativeFontImplementation? = null,
|
||||
val consoleMode: Boolean = false) {
|
||||
val consoleMode: Boolean = false,
|
||||
val customSaveLocationHelper: CustomSaveLocationHelper? = null) {
|
||||
}
|
28
core/src/com/unciv/logic/CustomSaveLocationHelper.kt
Normal file
28
core/src/com/unciv/logic/CustomSaveLocationHelper.kt
Normal file
|
@ -0,0 +1,28 @@
|
|||
package com.unciv.logic
|
||||
|
||||
/**
|
||||
* Contract for platform-specific helper classes to handle saving and loading games to and from
|
||||
* arbitrary external locations
|
||||
*/
|
||||
interface CustomSaveLocationHelper {
|
||||
/**
|
||||
* Saves a game asynchronously with a given default name and then calls the [saveCompleteCallback] callback
|
||||
* upon completion. The [saveCompleteCallback] callback will be called from the same thread that this method
|
||||
* is called from. If the [GameInfo] object already has the
|
||||
* [customSaveLocation][GameInfo.customSaveLocation] property defined (not null), then the user
|
||||
* will not be prompted to select a location for the save unless [forcePrompt] is set to true
|
||||
* (think of this like "Save as...")
|
||||
*/
|
||||
fun saveGame(
|
||||
gameInfo: GameInfo,
|
||||
gameName: String,
|
||||
forcePrompt: Boolean = false,
|
||||
saveCompleteCallback: ((Exception?) -> Unit)? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Loads a game from an external source asynchronously, then calls [loadCompleteCallback] with the loaded
|
||||
* [GameInfo]
|
||||
*/
|
||||
fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit)
|
||||
}
|
|
@ -35,6 +35,8 @@ class GameInfo {
|
|||
var oneMoreTurnMode=false
|
||||
var currentPlayer=""
|
||||
var gameId = UUID.randomUUID().toString() // random string
|
||||
@Volatile
|
||||
var customSaveLocation: String? = null
|
||||
|
||||
/** Simulate until any player wins,
|
||||
* or turns exceeds indicated number
|
||||
|
@ -55,6 +57,7 @@ class GameInfo {
|
|||
toReturn.gameParameters = gameParameters
|
||||
toReturn.gameId = gameId
|
||||
toReturn.oneMoreTurnMode = oneMoreTurnMode
|
||||
toReturn.customSaveLocation = customSaveLocation
|
||||
return toReturn
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ object GameSaver {
|
|||
private const val saveFilesFolder = "SaveFiles"
|
||||
private const val multiplayerFilesFolder = "MultiplayerGames"
|
||||
private const val settingsFileName = "GameSettings.json"
|
||||
@Volatile
|
||||
var 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 */
|
||||
|
@ -37,8 +39,19 @@ object GameSaver {
|
|||
return localSaves + Gdx.files.absolute(externalFilesDirForAndroid + "/${getSubfolder(multiplayer)}").list().asSequence()
|
||||
}
|
||||
|
||||
fun saveGame(game: GameInfo, GameName: String, multiplayer: Boolean = false) {
|
||||
json().toJson(game,getSave(GameName, multiplayer))
|
||||
fun saveGame(game: GameInfo, GameName: String, multiplayer: Boolean = false, forcePrompt: Boolean = false, saveCompletionCallback: ((Exception?) -> Unit)? = null) {
|
||||
val customSaveLocation = game.customSaveLocation
|
||||
val customSaveLocationHelper = this.customSaveLocationHelper
|
||||
if (customSaveLocation != null && customSaveLocationHelper != null) {
|
||||
customSaveLocationHelper.saveGame(game, GameName, forcePrompt, saveCompletionCallback)
|
||||
} else {
|
||||
json().toJson(game, getSave(GameName, multiplayer))
|
||||
saveCompletionCallback?.invoke(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) {
|
||||
customSaveLocationHelper!!.saveGame(game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback)
|
||||
}
|
||||
|
||||
fun loadGameByName(GameName: String, multiplayer: Boolean = false) =
|
||||
|
@ -50,7 +63,15 @@ object GameSaver {
|
|||
return game
|
||||
}
|
||||
|
||||
fun gameInfoFromString(gameData:String): GameInfo {
|
||||
fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) {
|
||||
customSaveLocationHelper!!.loadGame { game, e ->
|
||||
loadCompletionCallback(game?.apply { setTransients() }, e)
|
||||
}
|
||||
}
|
||||
|
||||
fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null
|
||||
|
||||
fun gameInfoFromString(gameData: String): GameInfo {
|
||||
val game = json().fromJson(GameInfo::class.java, gameData)
|
||||
game.setTransients()
|
||||
return game
|
||||
|
@ -107,14 +128,26 @@ object GameSaver {
|
|||
}
|
||||
|
||||
fun autoSaveSingleThreaded (gameInfo: GameInfo) {
|
||||
// If the user has chosen a custom save location outside of the usual game directories,
|
||||
// they'll probably expect us to overwrite that instead. E.g. if the user is saving their
|
||||
// game to their Google Drive folder, they'll probably want that progress to be synced to
|
||||
// other devices automatically so they don't have to remember to manually save before
|
||||
// exiting the game.
|
||||
if (gameInfo.customSaveLocation != null) {
|
||||
// GameName is unused here since we're just overwriting the existing file
|
||||
saveGame(gameInfo, "", false)
|
||||
return
|
||||
}
|
||||
saveGame(gameInfo, "Autosave")
|
||||
|
||||
// keep auto-saves for the last 10 turns for debugging purposes
|
||||
val newAutosaveFilename = saveFilesFolder + File.separator + "Autosave-${gameInfo.currentPlayer}-${gameInfo.turns}"
|
||||
getSave("Autosave").copyTo(Gdx.files.local(newAutosaveFilename))
|
||||
|
||||
fun getAutosaves(): Sequence<FileHandle> { return getSaves().filter { it.name().startsWith("Autosave") } }
|
||||
while(getAutosaves().count()>10){
|
||||
fun getAutosaves(): Sequence<FileHandle> {
|
||||
return getSaves().filter { it.name().startsWith("Autosave") }
|
||||
}
|
||||
while (getAutosaves().count() > 10) {
|
||||
val saveToDelete = getAutosaves().minBy { it.lastModified() }!!
|
||||
deleteSave(saveToDelete.name())
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.unciv.ui.pickerscreens.PickerScreen
|
|||
import com.unciv.ui.utils.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.concurrent.thread
|
||||
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
|
||||
|
||||
|
@ -78,6 +79,20 @@ class LoadGameScreen(previousScreen:CameraStageBaseScreen) : PickerScreen() {
|
|||
}
|
||||
}
|
||||
rightSideTable.add(loadFromClipboardButton).row()
|
||||
if (GameSaver.canLoadFromCustomSaveLocation()) {
|
||||
val loadFromCustomLocation = "Load from custom location".toTextButton()
|
||||
loadFromCustomLocation.onClick {
|
||||
GameSaver.loadGameFromCustomLocation { gameInfo, exception ->
|
||||
if (gameInfo != null) {
|
||||
game.loadGame(gameInfo)
|
||||
} else if (exception !is CancellationException) {
|
||||
errorLabel.setText("Could not load game from custom location!".tr())
|
||||
exception?.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
rightSideTable.add(loadFromCustomLocation).pad(10f).row()
|
||||
}
|
||||
rightSideTable.add(errorLabel).row()
|
||||
|
||||
deleteSaveButton.onClick {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.unciv.ui.saves
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextField
|
||||
|
@ -10,6 +11,7 @@ import com.unciv.logic.GameSaver
|
|||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.utils.*
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.concurrent.thread
|
||||
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
|
||||
|
||||
|
@ -40,6 +42,29 @@ class SaveGameScreen : PickerScreen() {
|
|||
Gdx.app.clipboard.contents = base64Gzip
|
||||
}
|
||||
newSave.add(copyJsonButton).row()
|
||||
if (GameSaver.canLoadFromCustomSaveLocation()) {
|
||||
val saveToCustomLocation = "Save to custom location".toTextButton()
|
||||
val errorLabel = "".toLabel(Color.RED)
|
||||
saveToCustomLocation.enable()
|
||||
saveToCustomLocation.onClick {
|
||||
errorLabel.setText("")
|
||||
saveToCustomLocation.setText("Saving...".tr())
|
||||
saveToCustomLocation.disable()
|
||||
thread(name = "SaveGame") {
|
||||
GameSaver.saveGameToCustomLocation(UncivGame.Current.gameInfo, textField.text) { e ->
|
||||
if (e == null) {
|
||||
Gdx.app.postRunnable { UncivGame.Current.setWorldScreen() }
|
||||
} else if (e !is CancellationException) {
|
||||
errorLabel.setText("Could not save game to custom location".tr())
|
||||
e.printStackTrace()
|
||||
}
|
||||
saveToCustomLocation.enable()
|
||||
}
|
||||
}
|
||||
}
|
||||
newSave.add(saveToCustomLocation).pad(10f).row()
|
||||
newSave.add(errorLabel).pad(0f, 10f, 10f, 10f).row()
|
||||
}
|
||||
|
||||
|
||||
val showAutosavesCheckbox = CheckBox("Show autosaves".tr(), skin)
|
||||
|
@ -56,10 +81,11 @@ class SaveGameScreen : PickerScreen() {
|
|||
rightSideButton.onClick {
|
||||
rightSideButton.setText("Saving...".tr())
|
||||
thread(name = "SaveGame") {
|
||||
GameSaver.saveGame(UncivGame.Current.gameInfo, textField.text)
|
||||
GameSaver.saveGame(UncivGame.Current.gameInfo, textField.text) {
|
||||
Gdx.app.postRunnable { UncivGame.Current.setWorldScreen() }
|
||||
}
|
||||
}
|
||||
}
|
||||
rightSideButton.enable()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package com.unciv.app.desktop
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.unciv.logic.CustomSaveLocationHelper
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.GameSaver
|
||||
import com.unciv.logic.GameSaver.json
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.File
|
||||
import java.util.concurrent.CancellationException
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.JFrame
|
||||
|
||||
class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper {
|
||||
override fun saveGame(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(json().toJson(gameInfo))
|
||||
}
|
||||
saveCompleteCallback?.invoke(null)
|
||||
} catch (e: Exception) {
|
||||
saveCompleteCallback?.invoke(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val fileChooser = JFileChooser().apply fileChooser@{
|
||||
currentDirectory = Gdx.files.local("").file()
|
||||
selectedFile = File(gameInfo.customSaveLocation ?: gameName)
|
||||
}
|
||||
|
||||
JFrame().apply frame@{
|
||||
setLocationRelativeTo(null)
|
||||
isVisible = true
|
||||
toFront()
|
||||
fileChooser.showSaveDialog(this@frame)
|
||||
dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
|
||||
}
|
||||
val file = fileChooser.selectedFile
|
||||
var exception: Exception? = null
|
||||
if (file != null) {
|
||||
gameInfo.customSaveLocation = file.absolutePath
|
||||
try {
|
||||
file.outputStream()
|
||||
.writer()
|
||||
.use {
|
||||
it.write(json().toJson(gameInfo))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
exception = e
|
||||
}
|
||||
} else {
|
||||
exception = CancellationException()
|
||||
}
|
||||
saveCompleteCallback?.invoke(exception)
|
||||
}
|
||||
|
||||
override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) {
|
||||
val fileChooser = JFileChooser().apply fileChooser@{
|
||||
currentDirectory = Gdx.files.local("").file()
|
||||
}
|
||||
|
||||
JFrame().apply frame@{
|
||||
setLocationRelativeTo(null)
|
||||
isVisible = true
|
||||
toFront()
|
||||
fileChooser.showOpenDialog(this@frame)
|
||||
dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
|
||||
}
|
||||
val file = fileChooser.selectedFile
|
||||
var exception: Exception? = null
|
||||
var gameInfo: GameInfo? = null
|
||||
if (file != null) {
|
||||
try {
|
||||
file.inputStream()
|
||||
.reader()
|
||||
.readText()
|
||||
.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
|
||||
// here
|
||||
customSaveLocation = file.absolutePath
|
||||
gameInfo = this
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
exception = e
|
||||
}
|
||||
} else {
|
||||
exception = CancellationException()
|
||||
}
|
||||
loadCompleteCallback(gameInfo, exception)
|
||||
}
|
||||
}
|
|
@ -47,7 +47,8 @@ internal object DesktopLauncher {
|
|||
versionFromJar,
|
||||
exitEvent = { exitProcess(0) },
|
||||
cancelDiscordEvent = { discordTimer?.cancel() },
|
||||
fontImplementation = NativeFontDesktop(ORIGINAL_FONT_SIZE.toInt())
|
||||
fontImplementation = NativeFontDesktop(ORIGINAL_FONT_SIZE.toInt()),
|
||||
customSaveLocationHelper = CustomSaveLocationHelperDesktop()
|
||||
)
|
||||
|
||||
val game = UncivGame ( desktopParameters )
|
||||
|
|
Loading…
Reference in a new issue