mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
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:
@ -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. =
|
||||
|
@ -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"
|
||||
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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}") {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
126
core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt
Normal file
126
core/src/com/unciv/ui/saves/LoadOrSaveScreen.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
core/src/com/unciv/ui/saves/QuickSave.kt
Normal file
97
core/src/com/unciv/ui/saves/QuickSave.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
151
core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt
Normal file
151
core/src/com/unciv/ui/saves/VerticalFileListScrollPane.kt
Normal 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
|
||||
}
|
@ -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))
|
||||
|
@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user