mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 14:24:43 -04: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:
parent
c01d2a8893
commit
3a03799074
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
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.
|
* 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user