Save and load reorg and keyboard handling (#6929)

* Load and Save Game Screens rework - Linting

* Load and Save Game Screens rework - Modularize and Keyboard

* Load and Save Game Screens rework - error handling

* Load and Save Game Screens rework - Move other save/load code

* Load and Save Game Screens rework - More Keyboard

* Load and Save Game Screens rework - Increase clipboard limit

* Load and Save Game Screens rework - Post-merge patch

* Load and Save Game Screens rework - Home, End, harden

* Load and Save Game Screens rework - Post-merge patch again

* Load and Save Game Screens rework - reviews
This commit is contained in:
SomeTroglodyte 2022-05-31 15:31:19 +02:00 committed by GitHub
parent 983a9b705e
commit 1b008905f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 627 additions and 341 deletions

View File

@ -598,6 +598,9 @@ Copy saved game to clipboard =
Could not load game = Could not load game =
Load [saveFileName] = Load [saveFileName] =
Delete save = Delete save =
[saveFileName] deleted successfully. =
Insufficient permissions to delete [saveFileName]. =
Failed to delete [saveFileName]. =
Saved at = Saved at =
Saving... = Saving... =
Overwrite existing file? = Overwrite existing file? =
@ -610,6 +613,7 @@ Load from custom location =
Could not load game from custom location! = Could not load game from custom location! =
Save to custom location = Save to custom location =
Could not save game to custom location! = Could not save game to custom location! =
Download missing mods =
Missing mods are downloaded successfully. = Missing mods are downloaded successfully. =
Could not load the missing mods! = Could not load the missing mods! =
Could not download mod list. = Could not download mod list. =

View File

@ -66,6 +66,7 @@ object Constants {
const val close = "Close" const val close = "Close"
const val yes = "Yes" const val yes = "Yes"
const val no = "No" const val no = "No"
const val loading = "Loading..."
const val barbarians = "Barbarians" const val barbarians = "Barbarians"
const val spectator = "Spectator" const val spectator = "Spectator"

View File

@ -27,6 +27,7 @@ import com.unciv.ui.newgamescreen.NewGameScreen
import com.unciv.ui.pickerscreens.ModManagementScreen import com.unciv.ui.pickerscreens.ModManagementScreen
import com.unciv.ui.popup.* import com.unciv.ui.popup.*
import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.saves.LoadGameScreen
import com.unciv.ui.saves.QuickSave
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import com.unciv.ui.worldscreen.mainmenu.WorldScreenMenuPopup import com.unciv.ui.worldscreen.mainmenu.WorldScreenMenuPopup
@ -175,49 +176,7 @@ class MainMenuScreen: BaseScreen() {
curWorldScreen.popups.filterIsInstance(WorldScreenMenuPopup::class.java).forEach(Popup::close) curWorldScreen.popups.filterIsInstance(WorldScreenMenuPopup::class.java).forEach(Popup::close)
return return
} }
QuickSave.autoLoadGame(this)
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()
}
}
}
}
} }
private fun quickstartNewGame() { private fun quickstartNewGame() {

View File

@ -73,8 +73,12 @@ class GameSaver(
fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null
fun deleteSave(gameName: String) { /** Deletes a save.
getSave(gameName).delete() * @return `true` if successful.
* @throws SecurityException when delete access was denied
*/
fun deleteSave(gameName: String): Boolean {
return getSave(gameName).delete()
} }
fun deleteMultiplayerSave(gameName: String) { fun deleteMultiplayerSave(gameName: String) {

View File

@ -204,7 +204,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
val constructionsScrollY = availableConstructionsScrollPane.scrollY val constructionsScrollY = availableConstructionsScrollPane.scrollY
if (!availableConstructionsTable.hasChildren()) { // if (!availableConstructionsTable.hasChildren()) { //
availableConstructionsTable.add("Loading...".toLabel()).pad(10f) availableConstructionsTable.add(Constants.loading.toLabel()).pad(10f)
} }
launchCrashHandling("Construction info gathering - ${cityScreen.city.name}") { launchCrashHandling("Construction info gathering - ${cityScreen.city.name}") {

View File

@ -5,6 +5,7 @@ import com.badlogic.gdx.Input
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants
import com.unciv.logic.MapSaver import com.unciv.logic.MapSaver
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
@ -89,7 +90,7 @@ class MapEditorLoadTab(
Gdx.app.postRunnable { Gdx.app.postRunnable {
if (!needPopup) return@postRunnable if (!needPopup) return@postRunnable
popup = Popup(editorScreen).apply { popup = Popup(editorScreen).apply {
addGoodSizedLabel("Loading...") addGoodSizedLabel(Constants.loading)
open() open()
} }
} }

View File

@ -1,13 +1,14 @@
package com.unciv.ui.multiplayer package com.unciv.ui.multiplayer
import com.unciv.Constants
import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.center import com.unciv.ui.utils.center
import com.unciv.ui.utils.toLabel import com.unciv.ui.utils.toLabel
class LoadDeepLinkScreen : BaseScreen() { class LoadDeepLinkScreen : BaseScreen() {
init { init {
val loadingLabel = "Loading...".toLabel() val loadingLabel = Constants.loading.toLabel()
stage.addActor(loadingLabel) stage.addActor(loadingLabel)
loadingLabel.center(stage) loadingLabel.center(stage)
} }
} }

View File

@ -26,7 +26,7 @@ open class PickerScreen(disableScroll: Boolean = false) : BaseScreen() {
init { init {
pickerPane.setFillParent(true) pickerPane.setFillParent(true)
stage.addActor(pickerPane) stage.addActor(pickerPane)
ensureLayout() ensureLayout()
} }
/** Make sure that anyone relying on sizes of the tables within this class during construction gets correct size readings. /** 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) * 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. * 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 = { val closeAction = {
if (previousScreen != null) game.setScreen(previousScreen) if (previousScreen != null) game.setScreen(previousScreen)
else game.resetToWorldScreen() else game.resetToWorldScreen()

View File

@ -3,12 +3,10 @@ package com.unciv.ui.saves
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.Color 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.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.SerializationException
import com.unciv.UncivGame import com.unciv.Constants
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.MissingModsException import com.unciv.logic.MissingModsException
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
@ -16,90 +14,126 @@ import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.pickerscreens.Github import com.unciv.ui.pickerscreens.Github
import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.popup.Popup import com.unciv.ui.popup.Popup
import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.ui.utils.UncivDateFormat.formatDate import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import java.util.* import java.io.FileNotFoundException
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = true) { class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() {
lateinit var selectedSave: String private val copySavedGameToClipboardButton = getCopyExistingSaveToClipboardButton()
private val copySavedGameToClipboardButton = "Copy saved game to clipboard".toTextButton() private val errorLabel = "".toLabel(Color.RED).apply { isVisible = false }
private val saveTable = Table() private val loadMissingModsButton = getLoadMissingModsButton()
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)
private var missingModsToLoad = "" 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 { init {
setDefaultCloseAction(previousScreen) setDefaultCloseAction(previousScreen)
rightSideTable.initRightSideTable()
rightSideButton.onClick(::onLoadGame)
keyPressDispatcher[KeyCharAndCode.RETURN] = ::onLoadGame
}
resetWindowState() override fun resetWindowState() {
topTable.add(ScrollPane(saveTable)) 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 { private fun onLoadGame() {
val loadingPopup = Popup( this) if (selectedSave.isEmpty()) return
loadingPopup.addGoodSizedLabel("Loading...") val loadingPopup = Popup( this)
loadingPopup.open() loadingPopup.addGoodSizedLabel(Constants.loading)
launchCrashHandling("Load Game") { loadingPopup.open()
try { launchCrashHandling(loadGame) {
// This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread try {
val loadedGame = game.gameSaver.loadGameByName(selectedSave) // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread
postCrashHandlingRunnable { UncivGame.Current.loadGame(loadedGame) } val loadedGame = game.gameSaver.loadGameByName(selectedSave)
} catch (ex: Exception) { postCrashHandlingRunnable { game.loadGame(loadedGame) }
postCrashHandlingRunnable { } catch (ex: Exception) {
loadingPopup.close() postCrashHandlingRunnable {
val cantLoadGamePopup = Popup(this@LoadGameScreen) loadingPopup.close()
cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() if (ex is MissingModsException) {
if (ex is UncivShowableException && ex.localizedMessage != null) { handleLoadGameException("Could not load game", ex)
// thrown exceptions are our own tests and can be shown to the user return@postCrashHandlingRunnable
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()
}
} }
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 { private fun getLoadFromClipboardButton(): TextButton {
val rightSideTable = Table() val pasteButton = loadFromClipboard.toTextButton()
rightSideTable.defaults().pad(10f) val pasteHandler: ()->Unit = {
launchCrashHandling(loadFromClipboard) {
val loadFromClipboardButton = "Load copied data".toTextButton() try {
loadFromClipboardButton.onClick { val clipboardContentsString = Gdx.app.clipboard.contents.trim()
try { val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString)
val clipboardContentsString = Gdx.app.clipboard.contents.trim() postCrashHandlingRunnable { game.loadGame(loadedGame) }
val loadedGame = GameSaver.gameInfoFromString(clipboardContentsString) } catch (ex: Exception) {
UncivGame.Current.loadGame(loadedGame) postCrashHandlingRunnable { handleLoadGameException("Could not load game from clipboard!", ex) }
} catch (ex: Exception) { }
handleLoadGameException("Could not load game from clipboard!", ex)
} }
} }
rightSideTable.add(loadFromClipboardButton).row() pasteButton.onClick(pasteHandler)
if (game.gameSaver.canLoadFromCustomSaveLocation()) { val ctrlV = KeyCharAndCode.ctrl('v')
val loadFromCustomLocation = "Load from custom location".toTextButton() keyPressDispatcher[ctrlV] = pasteHandler
loadFromCustomLocation.onClick { pasteButton.addTooltip(ctrlV)
game.gameSaver.loadGameFromCustomLocation { result -> 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()) { if (result.isError()) {
handleLoadGameException("Could not load game from custom location!", result.exception) handleLoadGameException("Could not load game from custom location!", result.exception)
} else if (result.isSuccessful()) { } 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() loadMissingMods()
} }
loadMissingModsButton.isVisible = false button.isVisible = false
rightSideTable.add(loadMissingModsButton).row() return button
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
} }
private fun handleLoadGameException(primaryText: String, ex: Exception?) { private fun handleLoadGameException(primaryText: String, ex: Exception?) {
var errorText = primaryText.tr() var errorText = primaryText.tr()
if (ex is UncivShowableException) errorText += "\n${ex.message}" if (ex is UncivShowableException) errorText += "\n${ex.localizedMessage}"
errorLabel.setText(errorText)
ex?.printStackTrace() ex?.printStackTrace()
if (ex is MissingModsException) { postCrashHandlingRunnable {
loadMissingModsButton.isVisible = true errorLabel.setText(errorText)
missingModsToLoad = ex.missingMods errorLabel.isVisible = true
if (ex is MissingModsException) {
loadMissingModsButton.isVisible = true
missingModsToLoad = ex.missingMods
}
} }
} }
private fun loadMissingMods() { private fun loadMissingMods() {
loadMissingModsButton.isEnabled = false loadMissingModsButton.isEnabled = false
descriptionLabel.setText("Loading...".tr()) descriptionLabel.setText(Constants.loading.tr())
launchCrashHandling("DownloadMods", runAsDaemon = false) { launchCrashHandling(downloadMissingMods, runAsDaemon = false) {
try { try {
val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-") val mods = missingModsToLoad.replace(' ', '-').lowercase().splitToSequence(",-")
for (modName in mods) { for (modName in mods) {
val repos = Github.tryGetGithubReposWithTopic(10, 1, modName) 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 } 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( val modFolder = Github.downloadAndExtract(
repo.html_url, repo.default_branch, repo.html_url, repo.default_branch,
Gdx.files.local("mods") Gdx.files.local("mods")
) )
?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something! ?: throw Exception() // downloadAndExtract returns null for 404 errors and the like -> display something!
Github.rewriteModOptions(repo, modFolder) Github.rewriteModOptions(repo, modFolder)
val labelText = descriptionLabel.text // Surprise - a StringBuilder
labelText.appendLine()
labelText.append("[${repo.name}] Downloaded!".tr())
postCrashHandlingRunnable { descriptionLabel.setText(labelText) }
} }
postCrashHandlingRunnable { postCrashHandlingRunnable {
RulesetCache.loadRulesets() RulesetCache.loadRulesets()
missingModsToLoad = "" missingModsToLoad = ""
loadMissingModsButton.isVisible = false loadMissingModsButton.isVisible = false
errorLabel.setText("") errorLabel.isVisible = false
rightSideTable.pack()
ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen) ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen)
} }
} catch (ex: Exception) { } catch (ex: Exception) {
@ -182,74 +225,6 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t
loadMissingModsButton.isEnabled = true loadMissingModsButton.isEnabled = true
descriptionLabel.setText("") 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)
}
} }
} }

View File

@ -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())
}
}
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -1,10 +1,9 @@
package com.unciv.ui.saves package com.unciv.ui.saves
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
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.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
@ -12,90 +11,99 @@ import com.unciv.logic.GameSaver
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.crashhandling.launchCrashHandling import com.unciv.ui.crashhandling.launchCrashHandling
import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.crashhandling.postCrashHandlingRunnable
import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.popup.YesNoPopup 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 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) private val gameNameTextField = TextField("", skin)
val currentSaves = Table()
init { init {
setDefaultCloseAction() setDefaultCloseAction()
gameNameTextField.textFieldFilter = TextField.TextFieldFilter { _, char -> char != '\\' && char != '/' } rightSideTable.initRightSideTable()
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()
rightSideButton.setText("Save game".tr()) rightSideButton.setText("Save game".tr())
rightSideButton.onClick { val saveAction = {
if (game.gameSaver.getSave(gameNameTextField.text).exists()) if (game.gameSaver.getSave(gameNameTextField.text).exists())
YesNoPopup("Overwrite existing file?", { saveGame() }, this).open() YesNoPopup("Overwrite existing file?", { saveGame() }, this).open()
else saveGame() else saveGame()
} }
rightSideButton.onClick(saveAction)
rightSideButton.enable() 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() { private fun saveGame() {
@ -110,17 +118,8 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true
} }
} }
private fun updateShownSaves(showAutosaves: Boolean) { override fun onExistingSaveSelected(saveGameFile: FileHandle) {
currentSaves.clear() gameNameTextField.text = saveGameFile.name()
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()
}
} }
} }

View File

@ -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<FileHandle>) {
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<Float> {
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
}

View File

@ -52,6 +52,7 @@ import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.popup.YesNoPopup
import com.unciv.ui.popup.hasOpenPopups import com.unciv.ui.popup.hasOpenPopups
import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.saves.LoadGameScreen
import com.unciv.ui.saves.QuickSave
import com.unciv.ui.saves.SaveGameScreen import com.unciv.ui.saves.SaveGameScreen
import com.unciv.ui.trade.DiplomacyScreen import com.unciv.ui.trade.DiplomacyScreen
import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.BaseScreen
@ -242,43 +243,6 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
} }
private fun addKeyboardPresses() { 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 // Space and N are assigned in createNextTurnButton
keyPressDispatcher[Input.Keys.F1] = { game.setScreen(CivilopediaScreen(gameInfo.ruleSet, this)) } keyPressDispatcher[Input.Keys.F1] = { game.setScreen(CivilopediaScreen(gameInfo.ruleSet, this)) }
keyPressDispatcher['E'] = { game.setScreen(EmpireOverviewScreen(selectedCiv)) } // Empire overview last used page 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.F8] = { game.setScreen(VictoryScreen(this)) } // Victory Progress
keyPressDispatcher[Input.Keys.F9] = { game.setScreen(EmpireOverviewScreen(selectedCiv, "Stats")) } // Demographics 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.F10] = { game.setScreen(EmpireOverviewScreen(selectedCiv, "Resources")) } // originally Strategic View
keyPressDispatcher[Input.Keys.F11] = quickSave // Quick Save keyPressDispatcher[Input.Keys.F11] = { QuickSave.save(gameInfo, this) } // Quick Save
keyPressDispatcher[Input.Keys.F12] = quickLoad // Quick Load keyPressDispatcher[Input.Keys.F12] = { QuickSave.load(this) } // Quick Load
keyPressDispatcher[Input.Keys.HOME] = { // Capital City View keyPressDispatcher[Input.Keys.HOME] = { // Capital City View
val capital = gameInfo.currentPlayerCiv.getCapital() val capital = gameInfo.currentPlayerCiv.getCapital()
if (capital != null && !mapHolder.setCenterPosition(capital.location)) if (capital != null && !mapHolder.setCenterPosition(capital.location))

View File

@ -25,6 +25,10 @@ internal object DesktopLauncher {
// Solves a rendering problem in specific GPUs and drivers. // 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 // 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") 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() ImagePacker.packImages()