diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 4b2b9bce59..a004234d9c 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -598,6 +598,9 @@ Copy saved game to clipboard = Could not load game = Load [saveFileName] = Delete save = +[saveFileName] deleted successfully. = +Insufficient permissions to delete [saveFileName]. = +Failed to delete [saveFileName]. = Saved at = Saving... = Overwrite existing file? = @@ -610,6 +613,7 @@ Load from custom location = Could not load game from custom location! = Save to custom location = Could not save game to custom location! = +Download missing mods = Missing mods are downloaded successfully. = Could not load the missing mods! = Could not download mod list. = diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 8ce05ad0de..a0c9b7b45a 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -66,6 +66,7 @@ object Constants { const val close = "Close" const val yes = "Yes" const val no = "No" + const val loading = "Loading..." const val barbarians = "Barbarians" const val spectator = "Spectator" diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 3346217028..5e91c29414 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -27,6 +27,7 @@ import com.unciv.ui.newgamescreen.NewGameScreen import com.unciv.ui.pickerscreens.ModManagementScreen import com.unciv.ui.popup.* import com.unciv.ui.saves.LoadGameScreen +import com.unciv.ui.saves.QuickSave import com.unciv.ui.utils.* import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip import com.unciv.ui.worldscreen.mainmenu.WorldScreenMenuPopup @@ -175,49 +176,7 @@ class MainMenuScreen: BaseScreen() { curWorldScreen.popups.filterIsInstance(WorldScreenMenuPopup::class.java).forEach(Popup::close) return } - - val loadingPopup = Popup(this) - loadingPopup.addGoodSizedLabel("Loading...") - loadingPopup.open() - launchCrashHandling("autoLoadGame") { - // Load game from file to class on separate thread to avoid ANR... - fun outOfMemory() { - postCrashHandlingRunnable { - loadingPopup.close() - ToastPopup("Not enough memory on phone to load game!", this@MainMenuScreen) - } - } - - val savedGame: GameInfo - try { - savedGame = game.gameSaver.loadLatestAutosave() - } catch (oom: OutOfMemoryError) { - outOfMemory() - return@launchCrashHandling - } catch (ex: Exception) { - postCrashHandlingRunnable { - loadingPopup.close() - ToastPopup("Cannot resume game!", this@MainMenuScreen) - } - return@launchCrashHandling - } - - if (savedGame.gameParameters.isOnlineMultiplayer) { - try { - game.onlineMultiplayer.loadGame(savedGame) - } catch (oom: OutOfMemoryError) { - outOfMemory() - } - } else { - postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context - try { - game.loadGame(savedGame) - } catch (oom: OutOfMemoryError) { - outOfMemory() - } - } - } - } + QuickSave.autoLoadGame(this) } private fun quickstartNewGame() { diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index e05bd52952..7c015f5c65 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -73,8 +73,12 @@ class GameSaver( fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null - fun deleteSave(gameName: String) { - getSave(gameName).delete() + /** Deletes a save. + * @return `true` if successful. + * @throws SecurityException when delete access was denied + */ + fun deleteSave(gameName: String): Boolean { + return getSave(gameName).delete() } fun deleteMultiplayerSave(gameName: String) { diff --git a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt index 776d77f166..5f7744f359 100644 --- a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt @@ -204,7 +204,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { val constructionsScrollY = availableConstructionsScrollPane.scrollY if (!availableConstructionsTable.hasChildren()) { // - availableConstructionsTable.add("Loading...".toLabel()).pad(10f) + availableConstructionsTable.add(Constants.loading.toLabel()).pad(10f) } launchCrashHandling("Construction info gathering - ${cityScreen.city.name}") { diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt index f95807777c..e7573c884e 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt @@ -5,6 +5,7 @@ import com.badlogic.gdx.Input import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.Constants import com.unciv.logic.MapSaver import com.unciv.logic.UncivShowableException import com.unciv.models.ruleset.RulesetCache @@ -89,7 +90,7 @@ class MapEditorLoadTab( Gdx.app.postRunnable { if (!needPopup) return@postRunnable popup = Popup(editorScreen).apply { - addGoodSizedLabel("Loading...") + addGoodSizedLabel(Constants.loading) open() } } diff --git a/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt b/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt index 112ad5f599..54a6c32a69 100644 --- a/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/LoadDeepLinkScreen.kt @@ -1,13 +1,14 @@ package com.unciv.ui.multiplayer +import com.unciv.Constants import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.center import com.unciv.ui.utils.toLabel class LoadDeepLinkScreen : BaseScreen() { init { - val loadingLabel = "Loading...".toLabel() + val loadingLabel = Constants.loading.toLabel() stage.addActor(loadingLabel) loadingLabel.center(stage) } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt index e58466ce5d..f3502254b7 100644 --- a/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/PickerScreen.kt @@ -26,7 +26,7 @@ open class PickerScreen(disableScroll: Boolean = false) : BaseScreen() { init { pickerPane.setFillParent(true) stage.addActor(pickerPane) - ensureLayout() + ensureLayout() } /** Make sure that anyone relying on sizes of the tables within this class during construction gets correct size readings. @@ -39,7 +39,7 @@ open class PickerScreen(disableScroll: Boolean = false) : BaseScreen() { * Initializes the [Close button][closeButton]'s action (and the Back/ESC handler) * to return to the [previousScreen] if specified, or else to the world screen. */ - fun setDefaultCloseAction(previousScreen: BaseScreen?=null) { + fun setDefaultCloseAction(previousScreen: BaseScreen? = null) { val closeAction = { if (previousScreen != null) game.setScreen(previousScreen) else game.resetToWorldScreen() diff --git a/core/src/com/unciv/ui/saves/LoadGameScreen.kt b/core/src/com/unciv/ui/saves/LoadGameScreen.kt index 8c48f37a6b..e96532dfe6 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -3,12 +3,10 @@ package com.unciv.ui.saves import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.actions.Actions -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.utils.Align -import com.unciv.UncivGame +import com.badlogic.gdx.utils.SerializationException +import com.unciv.Constants import com.unciv.logic.GameSaver import com.unciv.logic.MissingModsException import com.unciv.logic.UncivShowableException @@ -16,90 +14,126 @@ import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.postCrashHandlingRunnable -import com.unciv.ui.images.ImageGetter import com.unciv.ui.pickerscreens.Github -import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.popup.Popup import com.unciv.ui.popup.ToastPopup import com.unciv.ui.utils.* -import com.unciv.ui.utils.UncivDateFormat.formatDate -import java.util.* +import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip +import java.io.FileNotFoundException import java.util.concurrent.CancellationException -import com.unciv.ui.utils.AutoScrollPane as ScrollPane -class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = true) { - lateinit var selectedSave: String - private val copySavedGameToClipboardButton = "Copy saved game to clipboard".toTextButton() - private val saveTable = Table() - private val deleteSaveButton = "Delete save".toTextButton() - private val errorLabel = "".toLabel(Color.RED) - private val loadMissingModsButton = "Download missing mods".toTextButton() - private val showAutosavesCheckbox = CheckBox("Show autosaves".tr(), skin) +class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { + private val copySavedGameToClipboardButton = getCopyExistingSaveToClipboardButton() + private val errorLabel = "".toLabel(Color.RED).apply { isVisible = false } + private val loadMissingModsButton = getLoadMissingModsButton() private var missingModsToLoad = "" + companion object { + private const val loadGame = "Load game" + private const val loadFromCustomLocation = "Load from custom location" + private const val loadFromClipboard = "Load copied data" + private const val copyExistingSaveToClipboard = "Copy saved game to clipboard" + private const val downloadMissingMods = "Download missing mods" + } + init { setDefaultCloseAction(previousScreen) + rightSideTable.initRightSideTable() + rightSideButton.onClick(::onLoadGame) + keyPressDispatcher[KeyCharAndCode.RETURN] = ::onLoadGame + } - resetWindowState() - topTable.add(ScrollPane(saveTable)) + override fun resetWindowState() { + super.resetWindowState() + copySavedGameToClipboardButton.disable() + rightSideButton.setText(loadGame.tr()) + rightSideButton.disable() + } - val rightSideTable = getRightSideTable() + override fun onExistingSaveSelected(saveGameFile: FileHandle) { + copySavedGameToClipboardButton.enable() + rightSideButton.setText("Load [$selectedSave]".tr()) + rightSideButton.enable() + } - topTable.add(rightSideTable) + private fun Table.initRightSideTable() { + add(getLoadFromClipboardButton()).row() + addLoadFromCustomLocationButton() + add(errorLabel).row() + add(loadMissingModsButton).row() + add(deleteSaveButton).row() + add(copySavedGameToClipboardButton).row() + add(showAutosavesCheckbox).row() + } - rightSideButton.onClick { - val loadingPopup = Popup( this) - loadingPopup.addGoodSizedLabel("Loading...") - loadingPopup.open() - launchCrashHandling("Load Game") { - try { - // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread - val loadedGame = game.gameSaver.loadGameByName(selectedSave) - postCrashHandlingRunnable { UncivGame.Current.loadGame(loadedGame) } - } catch (ex: Exception) { - postCrashHandlingRunnable { - loadingPopup.close() - val cantLoadGamePopup = Popup(this@LoadGameScreen) - cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() - if (ex is UncivShowableException && ex.localizedMessage != null) { - // thrown exceptions are our own tests and can be shown to the user - cantLoadGamePopup.addGoodSizedLabel(ex.localizedMessage).row() - cantLoadGamePopup.addCloseButton() - cantLoadGamePopup.open() - } else { - cantLoadGamePopup.addGoodSizedLabel("If you could copy your game data (\"Copy saved game to clipboard\" - ").row() - cantLoadGamePopup.addGoodSizedLabel(" paste into an email to yairm210@hotmail.com)").row() - cantLoadGamePopup.addGoodSizedLabel("I could maybe help you figure out what went wrong, since this isn't supposed to happen!").row() - cantLoadGamePopup.addCloseButton() - cantLoadGamePopup.open() - ex.printStackTrace() - } + private fun onLoadGame() { + if (selectedSave.isEmpty()) return + val loadingPopup = Popup( this) + loadingPopup.addGoodSizedLabel(Constants.loading) + loadingPopup.open() + launchCrashHandling(loadGame) { + try { + // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread + val loadedGame = game.gameSaver.loadGameByName(selectedSave) + postCrashHandlingRunnable { game.loadGame(loadedGame) } + } catch (ex: Exception) { + postCrashHandlingRunnable { + loadingPopup.close() + if (ex is MissingModsException) { + handleLoadGameException("Could not load game", ex) + return@postCrashHandlingRunnable } + val cantLoadGamePopup = Popup(this@LoadGameScreen) + cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() + if (ex is SerializationException) + cantLoadGamePopup.addGoodSizedLabel("The file data seems to be corrupted.").row() + if (ex.cause is FileNotFoundException && (ex.cause as FileNotFoundException).message?.contains("Permission denied") == true) { + cantLoadGamePopup.addGoodSizedLabel("You do not have sufficient permissions to access the file.").row() + } else if (ex is UncivShowableException) { + // thrown exceptions are our own tests and can be shown to the user + cantLoadGamePopup.addGoodSizedLabel(ex.message).row() + } else { + cantLoadGamePopup.addGoodSizedLabel("If you could copy your game data (\"Copy saved game to clipboard\" - ").row() + cantLoadGamePopup.addGoodSizedLabel(" paste into an email to yairm210@hotmail.com)").row() + cantLoadGamePopup.addGoodSizedLabel("I could maybe help you figure out what went wrong, since this isn't supposed to happen!").row() + ex.printStackTrace() + } + cantLoadGamePopup.addCloseButton() + cantLoadGamePopup.open() } } } - } - private fun getRightSideTable(): Table { - val rightSideTable = Table() - rightSideTable.defaults().pad(10f) - - val loadFromClipboardButton = "Load copied data".toTextButton() - loadFromClipboardButton.onClick { - try { - val clipboardContentsString = Gdx.app.clipboard.contents.trim() - val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString) - UncivGame.Current.loadGame(loadedGame) - } catch (ex: Exception) { - handleLoadGameException("Could not load game from clipboard!", ex) + private fun getLoadFromClipboardButton(): TextButton { + val pasteButton = loadFromClipboard.toTextButton() + val pasteHandler: ()->Unit = { + launchCrashHandling(loadFromClipboard) { + try { + val clipboardContentsString = Gdx.app.clipboard.contents.trim() + val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString) + postCrashHandlingRunnable { game.loadGame(loadedGame) } + } catch (ex: Exception) { + postCrashHandlingRunnable { handleLoadGameException("Could not load game from clipboard!", ex) } + } } } - rightSideTable.add(loadFromClipboardButton).row() - if (game.gameSaver.canLoadFromCustomSaveLocation()) { - val loadFromCustomLocation = "Load from custom location".toTextButton() - loadFromCustomLocation.onClick { - game.gameSaver.loadGameFromCustomLocation { result -> + pasteButton.onClick(pasteHandler) + val ctrlV = KeyCharAndCode.ctrl('v') + keyPressDispatcher[ctrlV] = pasteHandler + pasteButton.addTooltip(ctrlV) + return pasteButton + } + + private fun Table.addLoadFromCustomLocationButton() { + if (!game.gameSaver.canLoadFromCustomSaveLocation()) return + val loadFromCustomLocation = loadFromCustomLocation.toTextButton() + loadFromCustomLocation.onClick { + errorLabel.isVisible = false + loadFromCustomLocation.setText(Constants.loading.tr()) + loadFromCustomLocation.disable() + launchCrashHandling(Companion.loadFromCustomLocation) { + game.gameSaver.loadGameFromCustomLocation { result -> if (result.isError()) { handleLoadGameException("Could not load game from custom location!", result.exception) } else if (result.isSuccessful()) { @@ -107,73 +141,82 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t } } } - rightSideTable.add(loadFromCustomLocation).row() } - rightSideTable.add(errorLabel).row() + add(loadFromCustomLocation).row() + } - loadMissingModsButton.onClick { + private fun getCopyExistingSaveToClipboardButton(): TextButton { + val copyButton = copyExistingSaveToClipboard.toTextButton() + val copyHandler: ()->Unit = { + launchCrashHandling(copyExistingSaveToClipboard) { + try { + val gameText = game.gameSaver.getSave(selectedSave).readString() + Gdx.app.clipboard.contents = if (gameText[0] == '{') Gzip.zip(gameText) else gameText + } catch (ex: Throwable) { + ex.printStackTrace() + ToastPopup("Could not save game to clipboard!", this@LoadGameScreen) + } + } + } + copyButton.onClick(copyHandler) + copyButton.disable() + val ctrlC = KeyCharAndCode.ctrl('c') + keyPressDispatcher[ctrlC] = copyHandler + copyButton.addTooltip(ctrlC) + return copyButton + } + + private fun getLoadMissingModsButton(): TextButton { + val button = downloadMissingMods.toTextButton() + button.onClick { loadMissingMods() } - loadMissingModsButton.isVisible = false - rightSideTable.add(loadMissingModsButton).row() - - deleteSaveButton.onClick { - game.gameSaver.deleteSave(selectedSave) - resetWindowState() - } - deleteSaveButton.disable() - rightSideTable.add(deleteSaveButton).row() - - copySavedGameToClipboardButton.disable() - copySavedGameToClipboardButton.onClick { - val gameText = game.gameSaver.getSave(selectedSave).readString() - val gzippedGameText = Gzip.zip(gameText) - Gdx.app.clipboard.contents = gzippedGameText - } - rightSideTable.add(copySavedGameToClipboardButton).row() - - showAutosavesCheckbox.isChecked = false - showAutosavesCheckbox.onChange { - updateLoadableGames(showAutosavesCheckbox.isChecked) - } - rightSideTable.add(showAutosavesCheckbox).row() - return rightSideTable + button.isVisible = false + return button } private fun handleLoadGameException(primaryText: String, ex: Exception?) { var errorText = primaryText.tr() - if (ex is UncivShowableException) errorText += "\n${ex.message}" - errorLabel.setText(errorText) + if (ex is UncivShowableException) errorText += "\n${ex.localizedMessage}" ex?.printStackTrace() - if (ex is MissingModsException) { - loadMissingModsButton.isVisible = true - missingModsToLoad = ex.missingMods + postCrashHandlingRunnable { + errorLabel.setText(errorText) + errorLabel.isVisible = true + if (ex is MissingModsException) { + loadMissingModsButton.isVisible = true + missingModsToLoad = ex.missingMods + } } } private fun loadMissingMods() { loadMissingModsButton.isEnabled = false - descriptionLabel.setText("Loading...".tr()) - launchCrashHandling("DownloadMods", runAsDaemon = false) { + descriptionLabel.setText(Constants.loading.tr()) + launchCrashHandling(downloadMissingMods, runAsDaemon = false) { try { val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-") for (modName in mods) { val repos = Github.tryGetGithubReposWithTopic(10, 1, modName) - ?: throw UncivShowableException("Could not download mod list.".tr()) + ?: throw UncivShowableException("Could not download mod list.") val repo = repos.items.firstOrNull { it.name.lowercase() == modName } - ?: throw UncivShowableException("Could not find a mod named \"[$modName]\".".tr()) + ?: throw UncivShowableException("Could not find a mod named \"[$modName]\".") val modFolder = Github.downloadAndExtract( repo.html_url, repo.default_branch, Gdx.files.local("mods") ) ?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something! Github.rewriteModOptions(repo, modFolder) + val labelText = descriptionLabel.text // Surprise - a StringBuilder + labelText.appendLine() + labelText.append("[${repo.name}] Downloaded!".tr()) + postCrashHandlingRunnable { descriptionLabel.setText(labelText) } } postCrashHandlingRunnable { RulesetCache.loadRulesets() missingModsToLoad = "" loadMissingModsButton.isVisible = false - errorLabel.setText("") + errorLabel.isVisible = false + rightSideTable.pack() ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen) } } catch (ex: Exception) { @@ -182,74 +225,6 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t loadMissingModsButton.isEnabled = true descriptionLabel.setText("") } - - } - } - - private fun resetWindowState() { - updateLoadableGames(showAutosavesCheckbox.isChecked) - deleteSaveButton.disable() - copySavedGameToClipboardButton.disable() - rightSideButton.setText("Load game".tr()) - rightSideButton.disable() - descriptionLabel.setText("") - } - - private fun updateLoadableGames(showAutosaves: Boolean) { - saveTable.clear() - - val loadImage = ImageGetter.getImage("OtherIcons/Load") - loadImage.setSize(50f, 50f) // So the origin sets correctly - loadImage.setOrigin(Align.center) - loadImage.addAction(Actions.rotateBy(360f, 2f)) - saveTable.add(loadImage).size(50f) - - // Apparently, even just getting the list of saves can cause ANRs - - // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times - launchCrashHandling("GetSaves") { - // .toList() because otherwise the lastModified will only be checked inside the postRunnable - val saves = game.gameSaver.getSaves(autoSaves = showAutosaves).sortedByDescending { it.lastModified() }.toList() - - postCrashHandlingRunnable { - saveTable.clear() - for (save in saves) { - val textButton = TextButton(save.name(), skin) - textButton.onClick { onSaveSelected(save) } - saveTable.add(textButton).pad(5f).row() - } - } - } - } - - private fun onSaveSelected(save: FileHandle) { - selectedSave = save.name() - copySavedGameToClipboardButton.enable() - - rightSideButton.setText("Load [${save.name()}]".tr()) - rightSideButton.enable() - deleteSaveButton.enable() - deleteSaveButton.color = Color.RED - descriptionLabel.setText("Loading...".tr()) - - - val savedAt = Date(save.lastModified()) - var textToSet = save.name() + "\n${"Saved at".tr()}: " + savedAt.formatDate() - launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones - try { - val game = game.gameSaver.loadGamePreviewFromFile(save) - val playerCivNames = game.civilizations.filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } - textToSet += "\n" + playerCivNames + - ", " + game.difficulty.tr() + ", ${Fonts.turn}" + game.turns - textToSet += "\n${"Base ruleset:".tr()} " + game.gameParameters.baseRuleset - if (game.gameParameters.mods.isNotEmpty()) - textToSet += "\n${"Mods:".tr()} " + game.gameParameters.mods.joinToString() - } catch (ex: Exception) { - textToSet += "\n${"Could not load game".tr()}!" - } - - postCrashHandlingRunnable { - descriptionLabel.setText(textToSet) - } } } diff --git a/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt b/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt new file mode 100644 index 0000000000..5bf22a986f --- /dev/null +++ b/core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt @@ -0,0 +1,126 @@ +package com.unciv.ui.saves + +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.ui.CheckBox +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.Constants +import com.unciv.models.translations.tr +import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.postCrashHandlingRunnable +import com.unciv.ui.pickerscreens.PickerScreen +import com.unciv.ui.utils.Fonts +import com.unciv.ui.utils.KeyCharAndCode +import com.unciv.ui.utils.UncivDateFormat.formatDate +import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip +import com.unciv.ui.utils.disable +import com.unciv.ui.utils.enable +import com.unciv.ui.utils.onChange +import com.unciv.ui.utils.onClick +import com.unciv.ui.utils.pad +import com.unciv.ui.utils.toLabel +import com.unciv.ui.utils.toTextButton +import java.util.Date + + +abstract class LoadOrSaveScreen( + fileListHeaderText: String? = null +) : PickerScreen(disableScroll = true) { + + abstract fun onExistingSaveSelected(saveGameFile: FileHandle) + + protected var selectedSave = "" + private set + + private val savesScrollPane = VerticalFileListScrollPane(keyPressDispatcher) + protected val rightSideTable = Table() + protected val deleteSaveButton = "Delete save".toTextButton() + protected val showAutosavesCheckbox = CheckBox("Show autosaves".tr(), skin) + + init { + savesScrollPane.onChange(::selectExistingSave) + + rightSideTable.defaults().pad(5f, 10f) + + showAutosavesCheckbox.isChecked = false + showAutosavesCheckbox.onChange { + updateShownSaves(showAutosavesCheckbox.isChecked) + } + val ctrlA = KeyCharAndCode.ctrl('a') + keyPressDispatcher[ctrlA] = { showAutosavesCheckbox.toggle() } + showAutosavesCheckbox.addTooltip(ctrlA) + + deleteSaveButton.disable() + deleteSaveButton.onClick(::onDeleteClicked) + keyPressDispatcher[KeyCharAndCode.DEL] = ::onDeleteClicked + deleteSaveButton.addTooltip(KeyCharAndCode.DEL) + + if (fileListHeaderText != null) + topTable.add(fileListHeaderText.toLabel()).pad(10f).row() + + updateShownSaves(false) + + topTable.add(savesScrollPane) + topTable.add(rightSideTable) + topTable.pack() + } + + open fun resetWindowState() { + updateShownSaves(showAutosavesCheckbox.isChecked) + deleteSaveButton.disable() + descriptionLabel.setText("") + } + + private fun onDeleteClicked() { + if (selectedSave.isEmpty()) return + val result = try { + if (game.gameSaver.deleteSave(selectedSave)) { + resetWindowState() + "[$selectedSave] deleted successfully." + } else "Failed to delete [$selectedSave]." + } catch (ex: SecurityException) { + "Insufficient permissions to delete [$selectedSave]." + } catch (ex: Throwable) { + "Failed to delete [$selectedSave]." + } + descriptionLabel.setText(result) + } + + private fun updateShownSaves(showAutosaves: Boolean) { + savesScrollPane.updateSaveGames(game.gameSaver, showAutosaves) + } + + private fun selectExistingSave(saveGameFile: FileHandle) { + deleteSaveButton.enable() + deleteSaveButton.color = Color.RED + + selectedSave = saveGameFile.name() + showSaveInfo(saveGameFile) + onExistingSaveSelected(saveGameFile) + } + + private fun showSaveInfo(saveGameFile: FileHandle) { + descriptionLabel.setText(Constants.loading.tr()) + launchCrashHandling("LoadMetaData") { // Even loading the game to get its metadata can take a long time on older phones + val textToSet = try { + val savedAt = Date(saveGameFile.lastModified()) + val game = game.gameSaver.loadGamePreviewFromFile(saveGameFile) + val playerCivNames = game.civilizations + .filter { it.isPlayerCivilization() }.joinToString { it.civName.tr() } + val mods = if (game.gameParameters.mods.isEmpty()) "" + else "\n{Mods:} " + game.gameParameters.mods.joinToString() + + // Format result for textToSet + "${saveGameFile.name()}\n{Saved at}: ${savedAt.formatDate()}\n" + + "$playerCivNames, ${game.difficulty.tr()}, ${Fonts.turn}${game.turns}\n" + + "{Base ruleset:} ${game.gameParameters.baseRuleset}$mods" + } catch (ex: Exception) { + "\n{Could not load game}!" + } + + postCrashHandlingRunnable { + descriptionLabel.setText(textToSet.tr()) + } + } + } +} diff --git a/core/src/com/unciv/ui/saves/QuickSave.kt b/core/src/com/unciv/ui/saves/QuickSave.kt new file mode 100644 index 0000000000..8b4e6ba60c --- /dev/null +++ b/core/src/com/unciv/ui/saves/QuickSave.kt @@ -0,0 +1,97 @@ +package com.unciv.ui.saves + +import com.unciv.Constants +import com.unciv.MainMenuScreen +import com.unciv.UncivGame +import com.unciv.logic.GameInfo +import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.postCrashHandlingRunnable +import com.unciv.ui.popup.Popup +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.worldscreen.WorldScreen + + +//todo reduce code duplication + +object QuickSave { + fun save(gameInfo: GameInfo, screen: WorldScreen) { + val gameSaver = UncivGame.Current.gameSaver + val toast = ToastPopup("Quicksaving...", screen) + launchCrashHandling("QuickSaveGame", runAsDaemon = false) { + gameSaver.saveGame(gameInfo, "QuickSave") { + postCrashHandlingRunnable { + toast.close() + if (it != null) + ToastPopup("Could not save game!", screen) + else + ToastPopup("Quicksave successful.", screen) + } + } + } + } + + fun load(screen: WorldScreen) { + val gameSaver = UncivGame.Current.gameSaver + val toast = ToastPopup("Quickloading...", screen) + launchCrashHandling("QuickLoadGame") { + try { + val loadedGame = gameSaver.loadGameByName("QuickSave") + postCrashHandlingRunnable { + toast.close() + UncivGame.Current.loadGame(loadedGame) + ToastPopup("Quickload successful.", screen) + } + } catch (ex: Exception) { + postCrashHandlingRunnable { + toast.close() + ToastPopup("Could not load game!", screen) + } + } + } + } + + fun autoLoadGame(screen: MainMenuScreen) { + val loadingPopup = Popup(screen) + loadingPopup.addGoodSizedLabel(Constants.loading) + loadingPopup.open() + launchCrashHandling("autoLoadGame") { + // Load game from file to class on separate thread to avoid ANR... + fun outOfMemory() { + postCrashHandlingRunnable { + loadingPopup.close() + ToastPopup("Not enough memory on phone to load game!", screen) + } + } + + val savedGame: GameInfo + try { + savedGame = screen.game.gameSaver.loadLatestAutosave() + } catch (oom: OutOfMemoryError) { + outOfMemory() + return@launchCrashHandling + } catch (ex: Exception) { + postCrashHandlingRunnable { + loadingPopup.close() + ToastPopup("Cannot resume game!", screen) + } + return@launchCrashHandling + } + + if (savedGame.gameParameters.isOnlineMultiplayer) { + try { + screen.game.onlineMultiplayer.loadGame(savedGame) + } catch (oom: OutOfMemoryError) { + outOfMemory() + } + } else { + postCrashHandlingRunnable { /// ... and load it into the screen on main thread for GL context + try { + screen.game.loadGame(savedGame) + } catch (oom: OutOfMemoryError) { + outOfMemory() + } + } + } + } + } +} diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index 4a23bdd5a1..bae27177e4 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -1,10 +1,9 @@ package com.unciv.ui.saves import com.badlogic.gdx.Gdx +import com.badlogic.gdx.files.FileHandle 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 @@ -12,90 +11,99 @@ import com.unciv.logic.GameSaver import com.unciv.models.translations.tr import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.postCrashHandlingRunnable -import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.YesNoPopup -import com.unciv.ui.utils.* +import com.unciv.ui.utils.KeyCharAndCode +import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip +import com.unciv.ui.utils.disable +import com.unciv.ui.utils.enable +import com.unciv.ui.utils.onClick +import com.unciv.ui.utils.toLabel +import com.unciv.ui.utils.toTextButton import java.util.concurrent.CancellationException -import kotlin.concurrent.thread -import com.unciv.ui.utils.AutoScrollPane as ScrollPane -class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true) { +class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") { private val gameNameTextField = TextField("", skin) - val currentSaves = Table() init { setDefaultCloseAction() - gameNameTextField.textFieldFilter = TextField.TextFieldFilter { _, char -> char != '\\' && char != '/' } - topTable.add("Current saves".toLabel()).pad(10f).row() - updateShownSaves(false) - topTable.add(ScrollPane(currentSaves)) - - val newSave = Table() - newSave.defaults().pad(5f, 10f) - val defaultSaveName = "[${gameInfo.currentPlayer}] - [${gameInfo.turns}] turns".tr() - gameNameTextField.text = defaultSaveName - - newSave.add("Saved game name".toLabel()).row() - newSave.add(gameNameTextField).width(300f).row() - - val copyJsonButton = "Copy to clipboard".toTextButton() - copyJsonButton.onClick { - thread(name="Copy to clipboard") { // the Gzip rarely leads to ANRs - try { - Gdx.app.clipboard.contents = GameSaver.gameInfoToString(gameInfo, forceZip = true) - } catch (OOM: OutOfMemoryError) { - // you don't get a special toast, this isn't nearly common enough, this is a total edge-case - } - } - } - newSave.add(copyJsonButton).row() - - if (game.gameSaver.canLoadFromCustomSaveLocation()) { - val saveText = "Save to custom location".tr() - val saveToCustomLocation = TextButton(saveText, BaseScreen.skin) - val errorLabel = "".toLabel(Color.RED) - saveToCustomLocation.enable() - saveToCustomLocation.onClick { - errorLabel.setText("") - saveToCustomLocation.setText("Saving...".tr()) - saveToCustomLocation.disable() - launchCrashHandling("SaveGame", runAsDaemon = false) { - game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result -> - if (result.isError()) { - errorLabel.setText("Could not save game to custom location!".tr()) - result.exception?.printStackTrace() - } else if (result.isSuccessful()) { - game.resetToWorldScreen() - } - saveToCustomLocation.enable() - saveToCustomLocation.setText(saveText) - } - } - } - newSave.add(saveToCustomLocation).row() - newSave.add(errorLabel).row() - } - - val showAutosavesCheckbox = CheckBox("Show autosaves".tr(), skin) - showAutosavesCheckbox.isChecked = false - showAutosavesCheckbox.onChange { - updateShownSaves(showAutosavesCheckbox.isChecked) - } - newSave.add(showAutosavesCheckbox).row() - - topTable.add(newSave) - topTable.pack() + rightSideTable.initRightSideTable() rightSideButton.setText("Save game".tr()) - rightSideButton.onClick { + val saveAction = { if (game.gameSaver.getSave(gameNameTextField.text).exists()) YesNoPopup("Overwrite existing file?", { saveGame() }, this).open() else saveGame() } + rightSideButton.onClick(saveAction) rightSideButton.enable() + + keyPressDispatcher[KeyCharAndCode.RETURN] = saveAction + stage.keyboardFocus = gameNameTextField + } + + private fun Table.initRightSideTable() { + addGameNameField() + + val copyJsonButton = "Copy to clipboard".toTextButton() + copyJsonButton.onClick(::copyToClipboardHandler) + val ctrlC = KeyCharAndCode.ctrl('c') + keyPressDispatcher[ctrlC] = ::copyToClipboardHandler + copyJsonButton.addTooltip(ctrlC) + add(copyJsonButton).row() + + addSaveToCustomLocation() + add(deleteSaveButton).row() + add(showAutosavesCheckbox).row() + } + + private fun Table.addGameNameField() { + gameNameTextField.setTextFieldFilter { _, char -> char != '\\' && char != '/' } + val defaultSaveName = "[${gameInfo.currentPlayer}] - [${gameInfo.turns}] turns".tr() + gameNameTextField.text = defaultSaveName + gameNameTextField.setSelection(0, defaultSaveName.length) + + add("Saved game name".toLabel()).row() + add(gameNameTextField).width(300f).row() + stage.keyboardFocus = gameNameTextField + } + + private fun copyToClipboardHandler() { + launchCrashHandling("Copy game to clipboard") { + // the Gzip rarely leads to ANRs + try { + Gdx.app.clipboard.contents = GameSaver.gameInfoToString(gameInfo, forceZip = true) + } catch (ex: Throwable) { + ex.printStackTrace() + ToastPopup("Could not save game to clipboard!", this@SaveGameScreen) + } + } + } + + private fun Table.addSaveToCustomLocation() { + if (!game.gameSaver.canLoadFromCustomSaveLocation()) return + val saveToCustomLocation = "Save to custom location".toTextButton() + val errorLabel = "".toLabel(Color.RED) + saveToCustomLocation.onClick { + errorLabel.setText("") + saveToCustomLocation.setText("Saving...".tr()) + saveToCustomLocation.disable() + launchCrashHandling("Save to custom location", runAsDaemon = false) { + game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result -> + if (result.isError()) { + errorLabel.setText("Could not save game to custom location!".tr()) + result.exception?.printStackTrace() + } else if (result.isSuccessful()) { + game.resetToWorldScreen() + } + saveToCustomLocation.enable() + } + } + } + add(saveToCustomLocation).row() + add(errorLabel).row() } private fun saveGame() { @@ -110,17 +118,8 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true } } - private fun updateShownSaves(showAutosaves: Boolean) { - currentSaves.clear() - val saves = game.gameSaver.getSaves(autoSaves = showAutosaves) - .sortedByDescending { it.lastModified() } - for (saveGameFile in saves) { - val textButton = saveGameFile.name().toTextButton() - textButton.onClick { - gameNameTextField.text = saveGameFile.name() - } - currentSaves.add(textButton).pad(5f).row() - } + override fun onExistingSaveSelected(saveGameFile: FileHandle) { + gameNameTextField.text = saveGameFile.name() } } diff --git a/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt b/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt new file mode 100644 index 0000000000..9b368f836e --- /dev/null +++ b/core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt @@ -0,0 +1,151 @@ +package com.unciv.ui.saves + +import com.badlogic.gdx.Input +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.actions.Actions +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextButton +import com.badlogic.gdx.utils.Align +import com.unciv.logic.GameSaver +import com.unciv.ui.crashhandling.launchCrashHandling +import com.unciv.ui.crashhandling.postCrashHandlingRunnable +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.utils.AutoScrollPane +import com.unciv.ui.utils.BaseScreen +import com.unciv.ui.utils.KeyPressDispatcher +import com.unciv.ui.utils.onClick + +//todo key auto-repeat for navigation keys? + +/** A widget holding TextButtons vertically in a Table contained in a ScrollPane, with methods to + * hold file names and FileHandle's in those buttons. Used to display existing saves in the Load and Save game dialogs. + * + * @param keyPressDispatcher optionally pass in a [BaseScreen]'s [keyPressDispatcher][BaseScreen.keyPressDispatcher] to allow keyboard navigation. + * @param existingSavesTable exists here for coder convenience. No need to touch. + */ +class VerticalFileListScrollPane( + keyPressDispatcher: KeyPressDispatcher?, + private val existingSavesTable: Table = Table() +) : AutoScrollPane(existingSavesTable) { + + private var previousSelection: TextButton? = null + + private var onChangeListener: ((FileHandle) -> Unit)? = null + + init { + if (keyPressDispatcher != null) { + keyPressDispatcher[Input.Keys.UP] = { onArrowKey(-1) } + keyPressDispatcher[Input.Keys.DOWN] = { onArrowKey(1) } + keyPressDispatcher[Input.Keys.PAGE_UP] = { onPageKey(-1) } + keyPressDispatcher[Input.Keys.PAGE_DOWN] = { onPageKey(1) } + keyPressDispatcher[Input.Keys.HOME] = { onHomeEndKey(0) } + keyPressDispatcher[Input.Keys.END] = { onHomeEndKey(1) } + } + } + + fun onChange(action: (FileHandle) -> Unit) { + onChangeListener = action + } + + /** repopulate with existing saved games */ + fun updateSaveGames(gameSaver: GameSaver, showAutosaves: Boolean) { + update(gameSaver.getSaves(showAutosaves) + .sortedByDescending { it.lastModified() }) + } + + /** repopulate from a FileHandle Sequence - for other sources than saved games */ + fun update(files: Sequence) { + existingSavesTable.clear() + previousSelection = null + val loadImage = ImageGetter.getImage("OtherIcons/Load") + loadImage.setSize(50f, 50f) // So the origin sets correctly + loadImage.setOrigin(Align.center) + val loadAnimation = Actions.repeat(Int.MAX_VALUE, Actions.rotateBy(360f, 2f)) + loadImage.addAction(loadAnimation) + existingSavesTable.add(loadImage).size(50f).center() + + // Apparently, even just getting the list of saves can cause ANRs - + // not sure how many saves these guys had but Google Play reports this to have happened hundreds of times + launchCrashHandling("GetSaves") { + // .toList() materializes the result of the sequence + val saves = files.toList() + + postCrashHandlingRunnable { + loadAnimation.reset() + existingSavesTable.clear() + for (saveGameFile in saves) { + val textButton = TextButton(saveGameFile.name(), BaseScreen.skin) + textButton.userObject = saveGameFile + textButton.onClick { + selectExistingSave(textButton) + } + existingSavesTable.add(textButton).pad(5f).row() + } + } + } + } + + private fun selectExistingSave(textButton: TextButton) { + previousSelection?.color = Color.WHITE + textButton.color = Color.GREEN + previousSelection = textButton + + val saveGameFile = textButton.userObject as? FileHandle ?: return + onChangeListener?.invoke(saveGameFile) + } + + //region Keyboard scrolling + + // Helpers to simplify Scroll positioning - ScrollPane.scrollY goes down, normal Gdx Y goes up + // These functions all operate in the scrollY 'coordinate system' + private fun Table.getVerticalSpan(button: TextButton): ClosedFloatingPointRange { + val invertedY = height - button.y + return (invertedY - button.height)..invertedY + } + private fun getVerticalSpan() = scrollY..(scrollY + height) + private fun Table.getButtonAt(y: Float) = cells[getRow(height - y)].actor as TextButton + + private fun onArrowKey(direction: Int) { + if (existingSavesTable.rows == 0) return + val rowIndex = if (previousSelection == null) + if (direction == 1) -1 else 0 + else existingSavesTable.getCell(previousSelection).row + val newRow = (rowIndex + direction).let { + if (it < 0) existingSavesTable.rows - 1 + else if (it >= existingSavesTable.rows) 0 + else it + } + val button = existingSavesTable.cells[newRow].actor as TextButton + selectExistingSave(button) + + // Make ScrollPane follow the selection + val buttonSpan = existingSavesTable.getVerticalSpan(button) + val scrollSpan = getVerticalSpan() + if (buttonSpan.start < scrollSpan.start) + scrollY = buttonSpan.start + if (buttonSpan.endInclusive > scrollSpan.endInclusive) + scrollY = buttonSpan.endInclusive - height + } + + private fun onPageKey(direction: Int) { + scrollY += (height - 60f) * direction // ScrollPane does the clamping to 0..maxY + val buttonHeight = previousSelection?.height ?: return + val buttonSpan = existingSavesTable.getVerticalSpan(previousSelection!!) + val scrollSpan = getVerticalSpan() + val newButtonY = if (buttonSpan.start < scrollSpan.start) + scrollSpan.start + buttonHeight + else if (buttonSpan.endInclusive > scrollSpan.endInclusive) + scrollSpan.endInclusive - buttonHeight + else return + selectExistingSave(existingSavesTable.getButtonAt(newButtonY)) + } + + private fun onHomeEndKey(direction: Int) { + scrollY = direction * maxY + if (existingSavesTable.rows == 0) return + val row = (existingSavesTable.rows - 1) * direction + selectExistingSave(existingSavesTable.cells[row].actor as TextButton) + } + //endregion +} diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 1f81d8b02f..9ca2f2c1a0 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -52,6 +52,7 @@ import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.popup.hasOpenPopups import com.unciv.ui.saves.LoadGameScreen +import com.unciv.ui.saves.QuickSave import com.unciv.ui.saves.SaveGameScreen import com.unciv.ui.trade.DiplomacyScreen import com.unciv.ui.utils.BaseScreen @@ -242,43 +243,6 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas } private fun addKeyboardPresses() { - // Note these helpers might need unification with similar code e.g. in: - // GameSaver.autoSave, SaveGameScreen.saveGame, LoadGameScreen.rightSideButton.onClick,... - val quickSave = { - val toast = ToastPopup("Quicksaving...", this) - launchCrashHandling("SaveGame", runAsDaemon = false) { - game.gameSaver.saveGame(gameInfo, "QuickSave") { - postCrashHandlingRunnable { - toast.close() - if (it != null) - ToastPopup("Could not save game!", this@WorldScreen) - else { - ToastPopup("Quicksave successful.", this@WorldScreen) - } - } - } - } - Unit // change type of anonymous fun from ()->Thread to ()->Unit without unchecked cast - } - val quickLoad = { - val toast = ToastPopup("Quickloading...", this) - launchCrashHandling("LoadGame") { - try { - val loadedGame = game.gameSaver.loadGameByName("QuickSave") - postCrashHandlingRunnable { - toast.close() - UncivGame.Current.loadGame(loadedGame) - ToastPopup("Quickload successful.", this@WorldScreen) - } - } catch (ex: Exception) { - postCrashHandlingRunnable { - ToastPopup("Could not load game!", this@WorldScreen) - } - } - } - Unit // change type to ()->Unit - } - // Space and N are assigned in createNextTurnButton keyPressDispatcher[Input.Keys.F1] = { game.setScreen(CivilopediaScreen(gameInfo.ruleSet, this)) } keyPressDispatcher['E'] = { game.setScreen(EmpireOverviewScreen(selectedCiv)) } // Empire overview last used page @@ -296,8 +260,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas keyPressDispatcher[Input.Keys.F8] = { game.setScreen(VictoryScreen(this)) } // Victory Progress keyPressDispatcher[Input.Keys.F9] = { game.setScreen(EmpireOverviewScreen(selectedCiv, "Stats")) } // Demographics keyPressDispatcher[Input.Keys.F10] = { game.setScreen(EmpireOverviewScreen(selectedCiv, "Resources")) } // originally Strategic View - keyPressDispatcher[Input.Keys.F11] = quickSave // Quick Save - keyPressDispatcher[Input.Keys.F12] = quickLoad // Quick Load + keyPressDispatcher[Input.Keys.F11] = { QuickSave.save(gameInfo, this) } // Quick Save + keyPressDispatcher[Input.Keys.F12] = { QuickSave.load(this) } // Quick Load keyPressDispatcher[Input.Keys.HOME] = { // Capital City View val capital = gameInfo.currentPlayerCiv.getCapital() if (capital != null && !mapHolder.setCenterPosition(capital.location)) diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index cd33049e3b..966af89d0c 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -25,6 +25,10 @@ internal object DesktopLauncher { // Solves a rendering problem in specific GPUs and drivers. // For more info see https://github.com/yairm210/Unciv/pull/3202 and https://github.com/LWJGL/lwjgl/issues/119 System.setProperty("org.lwjgl.opengl.Display.allowSoftwareOpenGL", "true") + // This setting (default 64) limits clipboard transfers. Value in kB! + // 386 is an almost-arbitrary choice from the saves I had at the moment and their GZipped size. + // There must be a reason for lwjgl3 being so stingy, which for me meant to stay conservative. + System.setProperty("org.lwjgl.system.stackSize", "384") ImagePacker.packImages()