Refactor: Extract all cross-platform code from CustomSaveLocationHelpers into core module (#6962)

* Also fixes the GameInfo.customSaveLocation to work for Android
This commit is contained in:
Timo T
2022-05-27 15:53:18 +02:00
committed by GitHub
parent c01d2a8893
commit 3a03799074
13 changed files with 346 additions and 290 deletions

View File

@ -15,13 +15,13 @@ import com.unciv.utils.Log
import java.io.File
open class AndroidLauncher : AndroidApplication() {
private var customSaveLocationHelper: CustomSaveLocationHelperAndroid? = null
private var customFileLocationHelper: CustomFileLocationHelperAndroid? = null
private var game: UncivGame? = null
private var deepLinkedMultiplayerGame: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.backend = AndroidLogBackend()
customSaveLocationHelper = CustomSaveLocationHelperAndroid(this)
customFileLocationHelper = CustomFileLocationHelperAndroid(this)
MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext)
copyMods()
@ -41,7 +41,7 @@ open class AndroidLauncher : AndroidApplication() {
version = BuildConfig.VERSION_NAME,
crashReportSysInfo = CrashReportSysInfoAndroid,
fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt(), fontFamily),
customSaveLocationHelper = customSaveLocationHelper,
customFileLocationHelper = customFileLocationHelper,
platformSpecificHelper = platformSpecificHelper
)
@ -72,9 +72,12 @@ open class AndroidLauncher : AndroidApplication() {
if (UncivGame.isCurrentInitialized()
&& UncivGame.Current.isGameInfoInitialized()
&& UncivGame.Current.settings.multiplayer.turnCheckerEnabled
&& UncivGame.Current.gameSaver.getMultiplayerSaves().any()) {
MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver,
UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer)
&& UncivGame.Current.gameSaver.getMultiplayerSaves().any()
) {
MultiplayerTurnCheckWorker.startTurnChecker(
applicationContext, UncivGame.Current.gameSaver,
UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer
)
}
super.onPause()
}
@ -115,7 +118,7 @@ open class AndroidLauncher : AndroidApplication() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
customSaveLocationHelper?.handleIntentData(requestCode, data?.data)
customFileLocationHelper?.onActivityResult(requestCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
}

View File

@ -0,0 +1,98 @@
package com.unciv.app
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import androidx.annotation.GuardedBy
import com.unciv.logic.CustomFileLocationHelper
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import java.io.InputStream
import java.io.OutputStream
class CustomFileLocationHelperAndroid(private val activity: Activity) : CustomFileLocationHelper() {
@GuardedBy("this")
private val callbacks = mutableListOf<ActivityCallback>()
@GuardedBy("this")
private var curActivityRequestCode = 100
override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) {
val requestCode = createActivityCallback(callback) { activity.contentResolver.openOutputStream(it, "rwt") }
// When we loaded, we returned a "content://" URI as file location.
val uri = Uri.parse(suggestedLocation)
val fileName = if (uri.scheme == "content") {
val cursor = activity.contentResolver.query(uri, null, null, null, null)
cursor.use {
// we should have a direct URI to a file, so first is enough
if (it?.moveToFirst() == true) {
it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
} else {
""
}
}
} else {
// if we didn't load, this is some file name entered by the user
suggestedLocation
}
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "application/json"
putExtra(Intent.EXTRA_TITLE, fileName)
if (uri.scheme == "content") {
putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
}
activity.startActivityForResult(this, requestCode)
}
}
override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) {
val callbackIndex = createActivityCallback(callback, activity.contentResolver::openInputStream)
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = "*/*"
// It is theoretically possible to use an initial URI here, however, the only Android URIs we have are obtained from here, so, no dice
activity.startActivityForResult(this, callbackIndex)
}
}
private fun <T> createActivityCallback(callback: (String?, T?, Exception?) -> Unit,
createValue: (Uri) -> T): Int {
synchronized(this) {
val requestCode = curActivityRequestCode++
val activityCallback = ActivityCallback(requestCode) { uri ->
if (uri == null) {
callback(null, null, null)
return@ActivityCallback
}
try {
val outputStream = createValue(uri)
callback(uri.toString(), outputStream, null)
} catch (ex: Exception) {
callback(null, null, ex)
}
}
callbacks.add(activityCallback)
return requestCode
}
}
fun onActivityResult(requestCode: Int, data: Intent?) {
val callback = synchronized(this) {
val index = callbacks.indexOfFirst { it.requestCode == requestCode }
if (index == -1) return
callbacks.removeAt(index)
}
postCrashHandlingRunnable {
callback.callback(data?.data)
}
}
}
private class ActivityCallback(
val requestCode: Int,
val callback: (Uri?) -> Unit
)

View File

@ -1,124 +0,0 @@
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
// 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(GameSaver.gameInfoToString(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()
)

View File

@ -41,7 +41,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
val cancelDiscordEvent = parameters.cancelDiscordEvent
var fontImplementation = parameters.fontImplementation
val consoleMode = parameters.consoleMode
private val customSaveLocationHelper = parameters.customSaveLocationHelper
private val customSaveLocationHelper = parameters.customFileLocationHelper
val platformSpecificHelper = parameters.platformSpecificHelper
private val audioExceptionHelper = parameters.audioExceptionHelper

View File

@ -1,6 +1,6 @@
package com.unciv
import com.unciv.logic.CustomSaveLocationHelper
import com.unciv.logic.CustomFileLocationHelper
import com.unciv.ui.crashhandling.CrashReportSysInfo
import com.unciv.ui.utils.AudioExceptionHelper
import com.unciv.ui.utils.GeneralPlatformSpecificHelpers
@ -11,7 +11,7 @@ class UncivGameParameters(val version: String,
val cancelDiscordEvent: (() -> Unit)? = null,
val fontImplementation: NativeFontImplementation? = null,
val consoleMode: Boolean = false,
val customSaveLocationHelper: CustomSaveLocationHelper? = null,
val customFileLocationHelper: CustomFileLocationHelper? = null,
val platformSpecificHelper: GeneralPlatformSpecificHelpers? = null,
val audioExceptionHelper: AudioExceptionHelper? = null
)

View File

@ -0,0 +1,95 @@
package com.unciv.logic
import com.unciv.logic.GameSaver.CustomLoadResult
import com.unciv.logic.GameSaver.CustomSaveResult
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import java.io.InputStream
import java.io.OutputStream
/**
* Contract for platform-specific helper classes to handle saving and loading games to and from
* arbitrary external locations.
*
* Implementation note: If a game is loaded with [loadGame] and the same game is saved with [saveGame],
* the suggestedLocation in [saveGame] will be the location returned by [loadGame].
*/
abstract class CustomFileLocationHelper {
/**
* Saves a game asynchronously to a location selected by the user.
*
* Prefills their UI with a [suggestedLocation].
*
* Calls the [saveCompleteCallback] on the main thread with the save location on success or the [Exception] on error or null in both on cancel.
*/
fun saveGame(
gameData: String,
suggestedLocation: String,
saveCompleteCallback: (CustomSaveResult) -> Unit = {}
) {
createOutputStream(suggestedLocation) { location, outputStream, exception ->
if (outputStream == null) {
callSaveCallback(saveCompleteCallback, exception = exception)
return@createOutputStream
}
try {
outputStream.writer().use { it.write(gameData) }
callSaveCallback(saveCompleteCallback, location)
} catch (ex: Exception) {
callSaveCallback(saveCompleteCallback, exception = ex)
}
}
}
/**
* Loads a game asynchronously from a location selected by the user.
*
* Calls the [loadCompleteCallback] on the main thread.
*/
fun loadGame(loadCompleteCallback: (CustomLoadResult<String>) -> Unit) {
createInputStream { location, inputStream, exception ->
if (inputStream == null) {
callLoadCallback(loadCompleteCallback, exception = exception)
return@createInputStream
}
try {
val gameData = inputStream.reader().use { it.readText() }
callLoadCallback(loadCompleteCallback, location, gameData)
} catch (ex: Exception) {
callLoadCallback(loadCompleteCallback, exception = ex)
}
}
}
/**
* [callback] should be called with the actual selected location and an OutputStream to the location, or an exception if something failed.
*/
protected abstract fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit)
/**
* [callback] should be called with the actual selected location and an InputStream to read the location, or an exception if something failed.
*/
protected abstract fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit)
}
private fun callLoadCallback(loadCompleteCallback: (CustomLoadResult<String>) -> Unit,
location: String? = null,
gameData: String? = null,
exception: Exception? = null) {
val result = if (location != null && gameData != null && exception == null) {
CustomLoadResult(location to gameData)
} else {
CustomLoadResult(null, exception)
}
postCrashHandlingRunnable {
loadCompleteCallback(result)
}
}
private fun callSaveCallback(saveCompleteCallback: (CustomSaveResult) -> Unit,
location: String? = null,
exception: Exception? = null) {
postCrashHandlingRunnable {
saveCompleteCallback(CustomSaveResult(location, exception))
}
}

View File

@ -1,37 +0,0 @@
package com.unciv.logic
/**
* Contract for platform-specific helper classes to handle saving and loading games to and from
* arbitrary external locations
*/
interface CustomSaveLocationHelper {
/**### Save to custom location
* 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...")
* On success, this is also expected to set [customSaveLocation][GameInfo.customSaveLocation].
*
* @param gameInfo Game data to save
* @param gameName Suggestion for the save name
* @param forcePrompt Bypass UI if location contained in [gameInfo] and [forcePrompt]==`false`
* @param saveCompleteCallback Action to call upon completion (success _and_ failure)
*/
fun saveGame(
gameInfo: GameInfo,
gameName: String,
forcePrompt: Boolean = false,
saveCompleteCallback: ((Exception?) -> Unit)? = null
)
/**### Load from custom location
* Loads a game from an external source asynchronously, then calls [loadCompleteCallback] with the loaded [GameInfo].
* On success, this is also expected to set the loaded [GameInfo]'s property [customSaveLocation][GameInfo.customSaveLocation].
* Note that there is no hint so pass a default location or a way to remember the folder the user chose last time.
*
* @param loadCompleteCallback Action to call upon completion (success _and_ failure)
*/
fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit)
}

View File

@ -28,7 +28,7 @@ class GameSaver(
* which is normally responsible for keeping the [Gdx] static variables from being garbage collected.
*/
private val files: Files,
private val customSaveLocationHelper: CustomSaveLocationHelper? = null,
private val customFileLocationHelper: CustomFileLocationHelper? = 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 */
private val externalFilesDirForAndroid: String? = null
@ -71,7 +71,7 @@ class GameSaver(
return localSaves + files.absolute(externalFilesDirForAndroid + "/${saveFolder}").list().asSequence()
}
fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null
fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null
fun deleteSave(gameName: String) {
getSave(gameName).delete()
@ -88,6 +88,15 @@ class GameSaver(
file.delete()
}
interface ChooseLocationResult {
val location: String?
val exception: Exception?
fun isCanceled(): Boolean = location == null && exception == null
fun isError(): Boolean = exception != null
fun isSuccessful(): Boolean = location != null
}
//endregion
//region Saving
@ -130,8 +139,31 @@ class GameSaver(
}
}
fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) {
customSaveLocationHelper!!.saveGame(game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback)
class CustomSaveResult(
override val location: String? = null,
override val exception: Exception? = null
) : ChooseLocationResult
/**
* [gameName] is a suggested name for the file. If the file has already been saved to or loaded from a custom location,
* this previous custom location will be used.
*
* Calls the [saveCompleteCallback] on the main thread with the save location on success, an [Exception] on error, or both null on cancel.
*/
fun saveGameToCustomLocation(game: GameInfo, gameName: String, saveCompletionCallback: (CustomSaveResult) -> Unit) {
val saveLocation = game.customSaveLocation ?: Gdx.files.local(gameName).path()
val gameData = try {
gameInfoToString(game)
} catch (ex: Exception) {
postCrashHandlingRunnable { saveCompletionCallback(CustomSaveResult(exception = ex)) }
return
}
customFileLocationHelper!!.saveGame(gameData, saveLocation) {
if (it.isSuccessful()) {
game.customSaveLocation = it.location
}
saveCompletionCallback(it)
}
}
//endregion
@ -151,9 +183,34 @@ class GameSaver(
return json().fromJson(GameInfoPreview::class.java, gameFile)
}
fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) {
customSaveLocationHelper!!.loadGame { game, e ->
loadCompletionCallback(game?.apply { setTransients() }, e)
class CustomLoadResult<T>(
private val locationAndGameData: Pair<String, T>? = null,
override val exception: Exception? = null
) : ChooseLocationResult {
override val location: String? get() = locationAndGameData?.first
val gameData: T? get() = locationAndGameData?.second
}
/**
* Calls the [loadCompleteCallback] on the main thread with the [GameInfo] on success or the [Exception] on error or null in both on cancel.
*/
fun loadGameFromCustomLocation(loadCompletionCallback: (CustomLoadResult<GameInfo>) -> Unit) {
customFileLocationHelper!!.loadGame { result ->
val location = result.location
val gameData = result.gameData
if (location == null || gameData == null) {
loadCompletionCallback(CustomLoadResult(exception = result.exception))
return@loadGame
}
try {
val gameInfo = gameInfoFromString(gameData)
gameInfo.customSaveLocation = location
gameInfo.setTransients()
loadCompletionCallback(CustomLoadResult(location to gameInfo))
} catch (ex: Exception) {
loadCompletionCallback(CustomLoadResult(exception = ex))
}
}
}

View File

@ -99,13 +99,12 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
if (game.gameSaver.canLoadFromCustomSaveLocation()) {
val loadFromCustomLocation = "Load from custom location".toTextButton()
loadFromCustomLocation.onClick {
game.gameSaver.loadGameFromCustomLocation { gameInfo, exception ->
if (gameInfo != null) {
postCrashHandlingRunnable {
game.loadGame(gameInfo)
}
} else if (exception !is CancellationException)
handleLoadGameException("Could not load game from custom location!", exception)
game.gameSaver.loadGameFromCustomLocation { result ->
if (result.isError()) {
handleLoadGameException("Could not load game from custom location!", result.exception)
} else if (result.isSuccessful()) {
game.loadGame(result.gameData!!)
}
}
}
rightSideTable.add(loadFromCustomLocation).row()

View File

@ -4,6 +4,7 @@ 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.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
@ -53,7 +54,8 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
newSave.add(copyJsonButton).row()
if (game.gameSaver.canLoadFromCustomSaveLocation()) {
val saveToCustomLocation = "Save to custom location".toTextButton()
val saveText = "Save to custom location".tr()
val saveToCustomLocation = TextButton(saveText, BaseScreen.skin)
val errorLabel = "".toLabel(Color.RED)
saveToCustomLocation.enable()
saveToCustomLocation.onClick {
@ -61,14 +63,15 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
saveToCustomLocation.setText("Saving...".tr())
saveToCustomLocation.disable()
launchCrashHandling("SaveGame", runAsDaemon = false) {
game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e ->
if (e == null) {
postCrashHandlingRunnable { game.resetToWorldScreen() }
} else if (e !is CancellationException) {
game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result ->
if (result.isError()) {
errorLabel.setText("Could not save game to custom location!".tr())
e.printStackTrace()
result.exception?.printStackTrace()
} else if (result.isSuccessful()) {
game.resetToWorldScreen()
}
saveToCustomLocation.enable()
saveToCustomLocation.setText(saveText)
}
}
}

View File

@ -0,0 +1,60 @@
package com.unciv.app.desktop
import com.badlogic.gdx.Gdx
import com.unciv.logic.CustomFileLocationHelper
import java.awt.Component
import java.awt.EventQueue
import java.awt.event.WindowEvent
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.swing.JFileChooser
import javax.swing.JFrame
class CustomFileLocationHelperDesktop : CustomFileLocationHelper() {
override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) {
pickFile(callback, JFileChooser::showSaveDialog, File::outputStream, suggestedLocation)
}
override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) {
pickFile(callback, JFileChooser::showOpenDialog, File::inputStream)
}
private fun <T> pickFile(callback: (String?, T?, Exception?) -> Unit,
chooseAction: (JFileChooser, Component) -> Int,
createValue: (File) -> T,
suggestedLocation: String? = null) {
EventQueue.invokeLater {
try {
val fileChooser = JFileChooser().apply fileChooser@{
if (suggestedLocation == null) {
currentDirectory = Gdx.files.local("").file()
} else {
selectedFile = File(suggestedLocation)
}
}
val result: Int
val frame = JFrame().apply frame@{
setLocationRelativeTo(null)
isVisible = true
toFront()
result = chooseAction(fileChooser, this@frame)
dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
}
frame.dispose()
if (result == JFileChooser.CANCEL_OPTION) {
callback(null, null, null)
} else {
val value = createValue(fileChooser.selectedFile)
callback(fileChooser.selectedFile.absolutePath, value, null)
}
} catch (ex: Exception) {
callback(null, null, ex)
}
}
}
}

View File

@ -1,98 +0,0 @@
package com.unciv.app.desktop
import com.badlogic.gdx.Gdx
import com.unciv.json.json
import com.unciv.logic.CustomSaveLocationHelper
import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
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(GameSaver.gameInfoToString(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)
}
}

View File

@ -54,7 +54,7 @@ internal object DesktopLauncher {
versionFromJar,
cancelDiscordEvent = { discordTimer?.cancel() },
fontImplementation = NativeFontDesktop(Fonts.ORIGINAL_FONT_SIZE.toInt(), settings.fontFamily),
customSaveLocationHelper = CustomSaveLocationHelperDesktop(),
customFileLocationHelper = CustomFileLocationHelperDesktop(),
crashReportSysInfo = CrashReportSysInfoDesktop(),
platformSpecificHelper = platformSpecificHelper,
audioExceptionHelper = HardenGdxAudio()