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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 346 additions and 290 deletions

View File

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

View File

@ -1,6 +1,6 @@
package com.unciv package com.unciv
import com.unciv.logic.CustomSaveLocationHelper import com.unciv.logic.CustomFileLocationHelper
import com.unciv.ui.crashhandling.CrashReportSysInfo import com.unciv.ui.crashhandling.CrashReportSysInfo
import com.unciv.ui.utils.AudioExceptionHelper import com.unciv.ui.utils.AudioExceptionHelper
import com.unciv.ui.utils.GeneralPlatformSpecificHelpers import com.unciv.ui.utils.GeneralPlatformSpecificHelpers
@ -11,7 +11,7 @@ class UncivGameParameters(val version: String,
val cancelDiscordEvent: (() -> Unit)? = null, val cancelDiscordEvent: (() -> Unit)? = null,
val fontImplementation: NativeFontImplementation? = null, val fontImplementation: NativeFontImplementation? = null,
val consoleMode: Boolean = false, val consoleMode: Boolean = false,
val customSaveLocationHelper: CustomSaveLocationHelper? = null, val customFileLocationHelper: CustomFileLocationHelper? = null,
val platformSpecificHelper: GeneralPlatformSpecificHelpers? = null, val platformSpecificHelper: GeneralPlatformSpecificHelpers? = null,
val audioExceptionHelper: AudioExceptionHelper? = 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. * which is normally responsible for keeping the [Gdx] static variables from being garbage collected.
*/ */
private val files: Files, 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 /** 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 */
private val externalFilesDirForAndroid: String? = null private val externalFilesDirForAndroid: String? = null
@ -71,7 +71,7 @@ class GameSaver(
return localSaves + files.absolute(externalFilesDirForAndroid + "/${saveFolder}").list().asSequence() return localSaves + files.absolute(externalFilesDirForAndroid + "/${saveFolder}").list().asSequence()
} }
fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null
fun deleteSave(gameName: String) { fun deleteSave(gameName: String) {
getSave(gameName).delete() getSave(gameName).delete()
@ -88,6 +88,15 @@ class GameSaver(
file.delete() 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 //endregion
//region Saving //region Saving
@ -130,8 +139,31 @@ class GameSaver(
} }
} }
fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) { class CustomSaveResult(
customSaveLocationHelper!!.saveGame(game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback) 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 //endregion
@ -151,9 +183,34 @@ class GameSaver(
return json().fromJson(GameInfoPreview::class.java, gameFile) return json().fromJson(GameInfoPreview::class.java, gameFile)
} }
fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) { class CustomLoadResult<T>(
customSaveLocationHelper!!.loadGame { game, e -> private val locationAndGameData: Pair<String, T>? = null,
loadCompletionCallback(game?.apply { setTransients() }, e) 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()) { if (game.gameSaver.canLoadFromCustomSaveLocation()) {
val loadFromCustomLocation = "Load from custom location".toTextButton() val loadFromCustomLocation = "Load from custom location".toTextButton()
loadFromCustomLocation.onClick { loadFromCustomLocation.onClick {
game.gameSaver.loadGameFromCustomLocation { gameInfo, exception -> game.gameSaver.loadGameFromCustomLocation { result ->
if (gameInfo != null) { if (result.isError()) {
postCrashHandlingRunnable { handleLoadGameException("Could not load game from custom location!", result.exception)
game.loadGame(gameInfo) } else if (result.isSuccessful()) {
game.loadGame(result.gameData!!)
} }
} else if (exception !is CancellationException)
handleLoadGameException("Could not load game from custom location!", exception)
} }
} }
rightSideTable.add(loadFromCustomLocation).row() 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.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Table 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.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
@ -53,7 +54,8 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
newSave.add(copyJsonButton).row() newSave.add(copyJsonButton).row()
if (game.gameSaver.canLoadFromCustomSaveLocation()) { 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) val errorLabel = "".toLabel(Color.RED)
saveToCustomLocation.enable() saveToCustomLocation.enable()
saveToCustomLocation.onClick { saveToCustomLocation.onClick {
@ -61,14 +63,15 @@ 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) {
game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result ->
if (e == null) { if (result.isError()) {
postCrashHandlingRunnable { game.resetToWorldScreen() }
} else if (e !is CancellationException) {
errorLabel.setText("Could not save game to custom location!".tr()) errorLabel.setText("Could not save game to custom location!".tr())
e.printStackTrace() result.exception?.printStackTrace()
} else if (result.isSuccessful()) {
game.resetToWorldScreen()
} }
saveToCustomLocation.enable() 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, versionFromJar,
cancelDiscordEvent = { discordTimer?.cancel() }, cancelDiscordEvent = { discordTimer?.cancel() },
fontImplementation = NativeFontDesktop(Fonts.ORIGINAL_FONT_SIZE.toInt(), settings.fontFamily), fontImplementation = NativeFontDesktop(Fonts.ORIGINAL_FONT_SIZE.toInt(), settings.fontFamily),
customSaveLocationHelper = CustomSaveLocationHelperDesktop(), customFileLocationHelper = CustomFileLocationHelperDesktop(),
crashReportSysInfo = CrashReportSysInfoDesktop(), crashReportSysInfo = CrashReportSysInfoDesktop(),
platformSpecificHelper = platformSpecificHelper, platformSpecificHelper = platformSpecificHelper,
audioExceptionHelper = HardenGdxAudio() audioExceptionHelper = HardenGdxAudio()