mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
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:
@ -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)
|
||||
}
|
||||
}
|
||||
|
98
android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt
Normal file
98
android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt
Normal 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
|
||||
)
|
@ -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()
|
||||
)
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
95
core/src/com/unciv/logic/CustomFileLocationHelper.kt
Normal file
95
core/src/com/unciv/logic/CustomFileLocationHelper.kt
Normal 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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
Reference in New Issue
Block a user