From a3d56845f9521f360a9e48ef59284e32dac9ac07 Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Tue, 23 Apr 2024 15:59:02 -0500 Subject: [PATCH] AutoPlayEndTurn can run on a different thread (#11329) * AutoPlay now builds military units more * AutoPlayEndTurn now launches in a new thread if there are more than 30 units/cities * Moved AutoPlay to WorldScreen and added isAIOrAutoPlaying() to Civilization * Fixed AI not wanting to pass through city-state tiles * Added black space to the end of AutoPlay * Partially fixed some NextTurnButton AutoPlay Behaviour * AutoPlay now persists across next turn WorldScreens * Made player's turn using AutoPlay run on a different thread * Remove the extra isAutoPlaying variable * AutoPlay class now manages all AutoPlay threads * Made AutoPlayMilitary and AutoPlayCivilian both able to run on a new thread. * Added more comments to AutoPlay * Maybe finally fixed the problems? --- core/src/com/unciv/UncivGame.kt | 8 +- core/src/com/unciv/logic/GameInfo.kt | 7 +- .../automation/city/ConstructionAutomation.kt | 2 +- .../unit/RoadBetweenCitiesAutomation.kt | 2 +- .../logic/automation/unit/UnitAutomation.kt | 2 +- .../logic/automation/unit/WorkerAutomation.kt | 2 +- core/src/com/unciv/logic/battle/Battle.kt | 2 +- .../com/unciv/logic/city/CityConstructions.kt | 2 +- .../unciv/logic/civilization/Civilization.kt | 10 ++- .../diplomacy/DiplomacyFunctions.kt | 2 +- .../logic/multiplayer/OnlineMultiplayer.kt | 1 - .../com/unciv/models/metadata/GameSettings.kt | 16 ---- .../ruleset/unique/UniqueTriggerActivation.kt | 2 +- .../com/unciv/ui/components/MayaCalendar.kt | 2 +- .../unciv/ui/popups/options/AutoPlayTab.kt | 8 +- .../com/unciv/ui/popups/options/DebugTab.kt | 2 +- .../pickerscreens/GreatPersonPickerScreen.kt | 5 +- .../ui/screens/savescreens/LoadGameScreen.kt | 8 +- .../unciv/ui/screens/savescreens/QuickSave.kt | 3 +- .../ui/screens/victoryscreen/VictoryScreen.kt | 4 +- .../ui/screens/worldscreen/WorldScreen.kt | 24 +++--- .../mainmenu/WorldScreenMenuPopup.kt | 2 +- .../worldscreen/status/AutoPlayMenu.kt | 68 ++++++++++++----- .../status/AutoPlayStatusButton.kt | 14 ++-- .../worldscreen/status/NextTurnAction.kt | 4 +- .../worldscreen/status/NextTurnButton.kt | 21 +++--- .../worldscreen/status/NextTurnProgress.kt | 2 +- .../ui/screens/worldscreen/unit/AutoPlay.kt | 73 +++++++++++++++++++ .../unit/actions/UnitActionsPillage.kt | 2 +- 29 files changed, 197 insertions(+), 103 deletions(-) create mode 100644 core/src/com/unciv/ui/screens/worldscreen/unit/AutoPlay.kt diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 0af1844783..a33083bf09 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -37,6 +37,7 @@ import com.unciv.ui.screens.mainmenuscreen.MainMenuScreen import com.unciv.ui.screens.savescreens.LoadGameScreen import com.unciv.ui.screens.worldscreen.PlayerReadyScreen import com.unciv.ui.screens.worldscreen.WorldScreen +import com.unciv.ui.screens.worldscreen.unit.AutoPlay import com.unciv.utils.Concurrency import com.unciv.utils.DebugUtils import com.unciv.utils.Display @@ -173,8 +174,9 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci * Automatically runs on the appropriate thread. * * Sets the returned `WorldScreen` as the only active screen. + * @param autoPlay pass in the old WorldScreen AutoPlay to retain the state throughout turns. Otherwise leave it is the default. */ - suspend fun loadGame(newGameInfo: GameInfo, callFromLoadScreen: Boolean = false): WorldScreen = withThreadPoolContext toplevel@{ + suspend fun loadGame(newGameInfo: GameInfo, autoPlay: AutoPlay = AutoPlay(settings.autoPlay), callFromLoadScreen: Boolean = false): WorldScreen = withThreadPoolContext toplevel@{ val prevGameInfo = gameInfo gameInfo = newGameInfo @@ -204,7 +206,7 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci screenStack.clear() worldScreen = null // This allows the GC to collect our old WorldScreen, otherwise we keep two WorldScreens in memory. - val newWorldScreen = WorldScreen(newGameInfo, newGameInfo.getPlayerToViewAs(), worldScreenRestoreState) + val newWorldScreen = WorldScreen(newGameInfo, autoPlay, newGameInfo.getPlayerToViewAs(), worldScreenRestoreState) worldScreen = newWorldScreen val moreThanOnePlayer = newGameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1 @@ -290,7 +292,7 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci fun popScreen(): BaseScreen? { if (screenStack.size == 1) { musicController.pause() - settings.autoPlay.stopAutoPlay() + worldScreen?.autoPlay?.stopAutoPlay() ConfirmPopup( screen = screenStack.last(), question = "Do you want to exit the game?", diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 9c2742ee92..bf34407927 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -389,16 +389,17 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion // Automation done here TurnManager(player).automateTurn() + val worldScreen = UncivGame.Current.worldScreen // Do we need to break if player won? if (simulateUntilWin && player.victoryManager.hasWon()) { simulateUntilWin = false - UncivGame.Current.settings.autoPlay.stopAutoPlay() + worldScreen?.autoPlay?.stopAutoPlay() break } // Do we need to stop AutoPlay? - if (UncivGame.Current.settings.autoPlay.isAutoPlaying() && player.victoryManager.hasWon() && !oneMoreTurnMode) - UncivGame.Current.settings.autoPlay.stopAutoPlay() + if (worldScreen != null && worldScreen.autoPlay.isAutoPlaying() && player.victoryManager.hasWon() && !oneMoreTurnMode) + worldScreen.autoPlay.stopAutoPlay() // Clean up TurnManager(player).endTurn(progressBar) diff --git a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt index 11382f5453..1fb9a5ffbd 100644 --- a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt @@ -164,7 +164,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { && city.getCenterTile().getTilesInDistance(5).none { it.militaryUnit?.civ == civInfo }) modifier = 5f // there's a settler just sitting here, doing nothing - BAD - if (civInfo.playerType == PlayerType.Human) modifier /= 2 // Players prefer to make their own unit choices usually + if (!civInfo.isAIOrAutoPlaying()) modifier /= 2 // Players prefer to make their own unit choices usually modifier *= personality.scaledFocus(PersonalityValue.Military) addChoice(relativeCostEffectiveness, militaryUnit, modifier) } diff --git a/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt b/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt index 856a4dadc3..7a1086ea46 100644 --- a/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt @@ -36,7 +36,7 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int, cloningSource?.bestRoadAvailable ?: //Player can choose not to auto-build roads & railroads. if (civInfo.isHuman() && (!UncivGame.Current.settings.autoBuildingRoads - || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI())) + || UncivGame.Current.settings.autoPlay.fullAutoPlayAI)) RoadStatus.None else civInfo.tech.getBestRoadAvailable() diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 8f06561efa..0c16ec8ecb 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -128,7 +128,7 @@ object UnitAutomation { internal fun tryUpgradeUnit(unit: MapUnit): Boolean { if (unit.civ.isHuman() && (!UncivGame.Current.settings.automatedUnitsCanUpgrade - || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI())) return false + || UncivGame.Current.settings.autoPlay.fullAutoPlayAI)) return false val upgradeUnits = getUnitsToUpgradeTo(unit) if (upgradeUnits.none()) return false // for resource reasons, usually diff --git a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt index 14d398325a..4369bbf1e5 100644 --- a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt @@ -325,7 +325,7 @@ class WorkerAutomation( .maxByOrNull { it.second }?.first if (tile.improvement != null && civInfo.isHuman() && (!UncivGame.Current.settings.automatedWorkersReplaceImprovements - || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI())) { + || UncivGame.Current.settings.autoPlay.fullAutoPlayAI)) { // Note that we might still want to build roads or remove fallout, so we can't exit the function immedietly bestBuildableImprovement = null } diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 9627b10062..ab4aef8cb8 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -560,7 +560,7 @@ object Battle { city.puppetCity(attackerCiv) //Although in Civ5 Venice is unable to re-annex their capital, that seems a bit silly. No check for May not annex cities here. city.annexCity() - } else if (attackerCiv.isHuman() && !(UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI())) { + } else if (attackerCiv.isHuman() && !(UncivGame.Current.settings.autoPlay.fullAutoPlayAI)) { // we're not taking our former capital attackerCiv.popupAlerts.add(PopupAlert(AlertType.CityConquered, city.id)) } else automateCityConquer(attackerCiv, city) diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index d10064919a..6696dc2a64 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -709,7 +709,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { val isCurrentPlayersTurn = city.civ.gameInfo.isUsersTurn() || !city.civ.gameInfo.gameParameters.isOnlineMultiplayer if ((isCurrentPlayersTurn && (UncivGame.Current.settings.autoAssignCityProduction - || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI())) // only automate if the active human player has the setting to automate production + || UncivGame.Current.settings.autoPlay.fullAutoPlayAI)) // only automate if the active human player has the setting to automate production || !city.civ.isHuman() || city.isPuppet) { ConstructionAutomation(this).chooseNextConstruction() } diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 4a03c92d02..0753a6b763 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -333,6 +333,11 @@ class Civilization : IsPartOfGameInfoSerialization { if (firstCityIfNoCapital) cities.firstOrNull() else null fun isHuman() = playerType == PlayerType.Human fun isAI() = playerType == PlayerType.AI + fun isAIOrAutoPlaying(): Boolean { + if (playerType == PlayerType.AI) return true + val worldScreen = UncivGame.Current.worldScreen ?: return false + return worldScreen.viewingCiv == this && worldScreen.autoPlay.isAutoPlaying() + } fun isOneCityChallenger() = playerType == PlayerType.Human && gameInfo.gameParameters.oneCityChallenge fun isCurrentPlayer() = gameInfo.currentPlayerCiv == this @@ -384,12 +389,11 @@ class Civilization : IsPartOfGameInfoSerialization { } fun wantsToFocusOn(focus: Victory.Focus): Boolean { - return thingsToFocusOnForVictory.contains(focus) && - (isAI() || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()) + return thingsToFocusOnForVictory.contains(focus) && isAIOrAutoPlaying() } fun getPersonality(): Personality { - return if (isAI() || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()) gameInfo.ruleset.personalities[nation.personality] ?: Personality.neutralPersonality + return if (isAIOrAutoPlaying()) gameInfo.ruleset.personalities[nation.personality] ?: Personality.neutralPersonality else Personality.neutralPersonality } diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt index 071f868e83..650ec9ba0f 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyFunctions.kt @@ -156,7 +156,7 @@ class DiplomacyFunctions(val civInfo: Civilization) { if (diplomacyManager != null && (diplomacyManager.hasOpenBorders || diplomacyManager.diplomaticStatus == DiplomaticStatus.War)) return true // Players can always pass through city-state tiles - if ((civInfo.isHuman() && !UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()) && otherCiv.isCityState()) return true + if (!civInfo.isAIOrAutoPlaying() && otherCiv.isCityState()) return true return false } diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt index e178e49a9c..ac343251f4 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt @@ -241,7 +241,6 @@ class OnlineMultiplayer { } else if (onlinePreview != null && hasNewerGameState(preview, onlinePreview)) { onlineGame.doManualUpdate(preview) } - UncivGame.Current.settings.autoPlay.stopAutoPlay() UncivGame.Current.loadGame(gameInfo) } diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index c154b6abe1..d99933c936 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -339,22 +339,6 @@ class GameSettings { var autoPlayPolicies: Boolean = true var autoPlayReligion: Boolean = true var autoPlayDiplomacy: Boolean = true - - var turnsToAutoPlay: Int = 0 - var autoPlayTurnInProgress: Boolean = false - - fun startAutoPlay() { - turnsToAutoPlay = autoPlayMaxTurns - } - - fun stopAutoPlay() { - turnsToAutoPlay = 0 - autoPlayTurnInProgress = false - } - - fun isAutoPlaying(): Boolean = turnsToAutoPlay > 0 - - fun isAutoPlayingAndFullAI(): Boolean = isAutoPlaying() && fullAutoPlayAI } @Suppress("SuspiciousCallableReferenceInLambda") // By @Azzurite, safe as long as that warning below is followed diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index 0b6ca018bd..6b80d8927c 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -339,7 +339,7 @@ object UniqueTriggerActivation { if (notification != null) civInfo.addNotification(notification, NotificationCategory.General) - if (civInfo.isAI() || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()) { + if (civInfo.isAI() || UncivGame.Current.settings.autoPlay.fullAutoPlayAI) { NextTurnAutomation.chooseGreatPerson(civInfo) } true diff --git a/core/src/com/unciv/ui/components/MayaCalendar.kt b/core/src/com/unciv/ui/components/MayaCalendar.kt index 03c6a77575..0e64fedf54 100644 --- a/core/src/com/unciv/ui/components/MayaCalendar.kt +++ b/core/src/com/unciv/ui/components/MayaCalendar.kt @@ -76,7 +76,7 @@ object MayaCalendar { val year = game.getYear() if (!isNewCycle(year, game.getYear(-1))) return civInfo.greatPeople.triggerMayanGreatPerson() - if (civInfo.isAI() || UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()) + if (civInfo.isAI() || UncivGame.Current.settings.autoPlay.fullAutoPlayAI) NextTurnAutomation.chooseGreatPerson(civInfo) } diff --git a/core/src/com/unciv/ui/popups/options/AutoPlayTab.kt b/core/src/com/unciv/ui/popups/options/AutoPlayTab.kt index a1f8e2d3d7..5485ec9705 100644 --- a/core/src/com/unciv/ui/popups/options/AutoPlayTab.kt +++ b/core/src/com/unciv/ui/popups/options/AutoPlayTab.kt @@ -1,13 +1,14 @@ package com.unciv.ui.popups.options import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.GUI import com.unciv.models.metadata.GameSettings import com.unciv.ui.components.widgets.UncivSlider import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.ui.screens.worldscreen.WorldScreen -fun autoPlayTab( - optionsPopup: OptionsPopup +fun autoPlayTab(optionsPopup: OptionsPopup ): Table = Table(BaseScreen.skin).apply { pad(10f) defaults().pad(5f) @@ -56,7 +57,8 @@ fun autoPlayTab( "Show AutoPlay button", settings.autoPlay.showAutoPlayButton, true ) { settings.autoPlay.showAutoPlayButton = it - settings.autoPlay.stopAutoPlay() } + GUI.getWorldScreenIfActive()?.autoPlay?.stopAutoPlay() + } optionsPopup.addCheckbox( diff --git a/core/src/com/unciv/ui/popups/options/DebugTab.kt b/core/src/com/unciv/ui/popups/options/DebugTab.kt index f350b3175a..bb2e642853 100644 --- a/core/src/com/unciv/ui/popups/options/DebugTab.kt +++ b/core/src/com/unciv/ui/popups/options/DebugTab.kt @@ -135,7 +135,7 @@ fun debugTab( val clipboardContentsString = Gdx.app.clipboard.contents.trim() val loadedGame = UncivFiles.gameInfoFromString(clipboardContentsString) loadedGame.gameParameters.isOnlineMultiplayer = false - optionsPopup.game.loadGame(loadedGame, true) + optionsPopup.game.loadGame(loadedGame, callFromLoadScreen = true) optionsPopup.close() } catch (ex: Exception) { ToastPopup(ex.message ?: ex::class.java.simpleName, optionsPopup.stageToShowOn).open(true) diff --git a/core/src/com/unciv/ui/screens/pickerscreens/GreatPersonPickerScreen.kt b/core/src/com/unciv/ui/screens/pickerscreens/GreatPersonPickerScreen.kt index 4481c175ef..3ac745e38c 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/GreatPersonPickerScreen.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/GreatPersonPickerScreen.kt @@ -9,12 +9,13 @@ import com.unciv.ui.images.ImageGetter import com.unciv.ui.components.extensions.isEnabled import com.unciv.ui.components.input.onClick import com.unciv.ui.components.input.onDoubleClick +import com.unciv.ui.screens.worldscreen.WorldScreen -class GreatPersonPickerScreen(val civInfo: Civilization) : PickerScreen() { +class GreatPersonPickerScreen(val worldScreen: WorldScreen, val civInfo: Civilization) : PickerScreen() { private var theChosenOne: BaseUnit? = null init { - UncivGame.Current.settings.autoPlay.stopAutoPlay() + worldScreen.autoPlay.stopAutoPlay() closeButton.isVisible = false rightSideButton.setText("Choose a free great person".tr()) diff --git a/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt b/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt index 5405a85830..718442098d 100644 --- a/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt @@ -29,6 +29,7 @@ import com.unciv.ui.popups.Popup import com.unciv.ui.popups.ToastPopup import com.unciv.logic.github.Github import com.unciv.logic.github.Github.folderNameToRepoName +import com.unciv.ui.popups.closeAllPopups import com.unciv.utils.Concurrency import com.unciv.utils.Log import com.unciv.utils.launchOnGLThread @@ -122,14 +123,13 @@ class LoadGameScreen : LoadOrSaveScreen() { } private fun onLoadGame() { - UncivGame.Current.settings.autoPlay.stopAutoPlay() if (selectedSave == null) return val loadingPopup = LoadingPopup(this) Concurrency.run(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.files.loadGameFromFile(selectedSave!!) - game.loadGame(loadedGame, true) + game.loadGame(loadedGame, callFromLoadScreen = true) } catch (notAPlayer: UncivShowableException) { launchOnGLThread { val (message) = getLoadExceptionMessage(notAPlayer) @@ -155,7 +155,7 @@ class LoadGameScreen : LoadOrSaveScreen() { try { val clipboardContentsString = Gdx.app.clipboard.contents.trim() val loadedGame = UncivFiles.gameInfoFromString(clipboardContentsString) - game.loadGame(loadedGame, true) + game.loadGame(loadedGame, callFromLoadScreen = true) } catch (ex: Exception) { launchOnGLThread { handleLoadGameException(ex, "Could not load game from clipboard!") } } finally { @@ -181,7 +181,7 @@ class LoadGameScreen : LoadOrSaveScreen() { Concurrency.run(Companion.loadFromCustomLocation) { game.files.loadGameFromCustomLocation( { - Concurrency.run { game.loadGame(it, true) } + Concurrency.run { game.loadGame(it, callFromLoadScreen = true) } }, { if (it !is PlatformSaverLoader.Cancelled) diff --git a/core/src/com/unciv/ui/screens/savescreens/QuickSave.kt b/core/src/com/unciv/ui/screens/savescreens/QuickSave.kt index 17372326c3..3aad459172 100644 --- a/core/src/com/unciv/ui/screens/savescreens/QuickSave.kt +++ b/core/src/com/unciv/ui/screens/savescreens/QuickSave.kt @@ -35,7 +35,7 @@ object QuickSave { } fun load(screen: WorldScreen) { - UncivGame.Current.settings.autoPlay.stopAutoPlay() + screen.autoPlay.stopAutoPlay() val files = UncivGame.Current.files val toast = ToastPopup("Quickloading...", screen) Concurrency.run("QuickLoadGame") { @@ -58,7 +58,6 @@ object QuickSave { } fun autoLoadGame(screen: MainMenuScreen) { - UncivGame.Current.settings.autoPlay.stopAutoPlay() val loadingPopup = LoadingPopup(screen) Concurrency.run("autoLoadGame") { // Load game from file to class on separate thread to avoid ANR... diff --git a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt index 39a9536a65..f8e2a52907 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/VictoryScreen.kt @@ -85,7 +85,7 @@ class VictoryScreen( } init { - UncivGame.Current.settings.autoPlay.stopAutoPlay() + worldScreen.autoPlay.stopAutoPlay() //**************** Set up the tabs **************** splitPane.setFirstWidget(tabs) val iconSize = Constants.headingFontSize.toFloat() @@ -161,7 +161,7 @@ class VictoryScreen( displayWonOrLost("[$winningCiv] has won a [$victoryType] Victory!", victory.defeatString) music.chooseTrack(playerCiv.civName, MusicMood.Defeat, EnumSet.of(MusicTrackChooserFlags.SuffixMustMatch)) } - UncivGame.Current.settings.autoPlay.stopAutoPlay() + worldScreen.autoPlay.stopAutoPlay() } private fun displayWonOrLost(vararg descriptions: String) { diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index 33c86700f7..2a22eedfc7 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -59,6 +59,7 @@ import com.unciv.ui.screens.worldscreen.status.NextTurnButton import com.unciv.ui.screens.worldscreen.status.NextTurnProgress import com.unciv.ui.screens.worldscreen.status.StatusButtons import com.unciv.ui.screens.worldscreen.topbar.WorldScreenTopBar +import com.unciv.ui.screens.worldscreen.unit.AutoPlay import com.unciv.ui.screens.worldscreen.unit.UnitTable import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsTable import com.unciv.utils.Concurrency @@ -81,6 +82,7 @@ import kotlin.concurrent.timer */ class WorldScreen( val gameInfo: GameInfo, + val autoPlay: AutoPlay, val viewingCiv: Civilization, restoreState: RestoreState? = null ) : BaseScreen() { @@ -130,6 +132,7 @@ class WorldScreen( internal val undoHandler = UndoHandler(this) + init { // notifications are right-aligned, they take up only as much space as necessary. notificationsScroll.width = stage.width / 2 @@ -328,7 +331,7 @@ class WorldScreen( launchOnGLThread { loadingGamePopup.close() } - startNewScreenJob(latestGame) + startNewScreenJob(latestGame, autoPlay) } catch (ex: Throwable) { launchOnGLThread { val (message) = LoadGameScreen.getLoadExceptionMessage(ex, "Couldn't download the latest game state!") @@ -406,19 +409,18 @@ class WorldScreen( } // If the game has ended, lets stop AutoPlay - if (game.settings.autoPlay.isAutoPlaying() - && !gameInfo.oneMoreTurnMode && (viewingCiv.isDefeated() || gameInfo.checkForVictory())) { - game.settings.autoPlay.stopAutoPlay() + if (autoPlay.isAutoPlaying() && !gameInfo.oneMoreTurnMode && (viewingCiv.isDefeated() || gameInfo.checkForVictory())) { + autoPlay.stopAutoPlay() } - if (!hasOpenPopups() && !game.settings.autoPlay.isAutoPlaying() && isPlayersTurn) { + if (!hasOpenPopups() && !autoPlay.isAutoPlaying() && isPlayersTurn) { when { viewingCiv.shouldShowDiplomaticVotingResults() -> UncivGame.Current.pushScreen(DiplomaticVoteResultScreen(gameInfo.diplomaticVictoryVotesCast, viewingCiv)) !gameInfo.oneMoreTurnMode && (viewingCiv.isDefeated() || gameInfo.checkForVictory()) -> game.pushScreen(VictoryScreen(this)) viewingCiv.greatPeople.freeGreatPeople > 0 -> - game.pushScreen(GreatPersonPickerScreen(viewingCiv)) + game.pushScreen(GreatPersonPickerScreen(this, viewingCiv)) viewingCiv.popupAlerts.any() -> AlertPopup(this, viewingCiv.popupAlerts.first()) viewingCiv.tradeRequests.isNotEmpty() -> { // In the meantime this became invalid, perhaps because we accepted previous trades @@ -648,7 +650,7 @@ class WorldScreen( progressBar.increment() - startNewScreenJob(gameInfoClone) + startNewScreenJob(gameInfoClone, autoPlay) } } @@ -701,7 +703,7 @@ class WorldScreen( } else { if (!game.settings.autoPlay.showAutoPlayButton) { statusButtons.autoPlayStatusButton = null - game.settings.autoPlay.stopAutoPlay() + autoPlay.stopAutoPlay() } } } @@ -725,7 +727,7 @@ class WorldScreen( resizeDeferTimer = timer("Resize", daemon = true, 500L, Long.MAX_VALUE) { resizeDeferTimer?.cancel() resizeDeferTimer = null - startNewScreenJob(gameInfo, true) // start over + startNewScreenJob(gameInfo, autoPlay, true) // start over } } @@ -805,10 +807,10 @@ class WorldScreen( } /** This exists so that no reference to the current world screen remains, so the old world screen can get garbage collected during [UncivGame.loadGame]. */ -private fun startNewScreenJob(gameInfo: GameInfo, autosaveDisabled: Boolean = false) { +private fun startNewScreenJob(gameInfo: GameInfo, autoPlay: AutoPlay, autosaveDisabled: Boolean = false) { Concurrency.run { val newWorldScreen = try { - UncivGame.Current.loadGame(gameInfo) + UncivGame.Current.loadGame(gameInfo, autoPlay) } catch (notAPlayer: UncivShowableException) { withGLContext { val (message) = LoadGameScreen.getLoadExceptionMessage(notAPlayer) diff --git a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt index 0a8f62319b..b130132907 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt @@ -11,7 +11,7 @@ import com.unciv.ui.screens.worldscreen.WorldScreen class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen, scrollable = Scrollability.All) { init { - UncivGame.Current.settings.autoPlay.stopAutoPlay() + worldScreen.autoPlay.stopAutoPlay() defaults().fillX() addButton("Main menu") { diff --git a/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayMenu.kt b/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayMenu.kt index 5a4646b949..bef89cb0e4 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayMenu.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayMenu.kt @@ -3,13 +3,14 @@ package com.unciv.ui.screens.worldscreen.status import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.unciv.GUI import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.civilization.managers.TurnManager import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.popups.AnimatedMenuPopup import com.unciv.ui.screens.worldscreen.WorldScreen +import com.unciv.ui.screens.worldscreen.unit.AutoPlay +import com.unciv.utils.Concurrency /** * The "context" menu for the AutoPlay button @@ -20,18 +21,19 @@ class AutoPlayMenu( private val nextTurnButton: NextTurnButton, private val worldScreen: WorldScreen ) : AnimatedMenuPopup(stage, getActorTopRight(positionNextTo)) { - private val settings = GUI.getSettings() + + private val autoPlay: AutoPlay = worldScreen.autoPlay init { // We need to activate the end turn button again after the menu closes afterCloseCallback = { worldScreen.shouldUpdate = true } } - + override fun createContentTable(): Table { val table = super.createContentTable()!! // Using the same keyboard binding for bypassing this menu and the default option if (!worldScreen.gameInfo.gameParameters.isOnlineMultiplayer) - table.add(getButton("Start AutoPlay", KeyboardBinding.AutoPlay, ::autoPlay)).row() + table.add(getButton("Start AutoPlay", KeyboardBinding.AutoPlay, ::multiturnAutoPlay)).row() table.add(getButton("AutoPlay End Turn", KeyboardBinding.AutoPlayMenuEndTurn, ::autoPlayEndTurn)).row() table.add(getButton("AutoPlay Military Once", KeyboardBinding.AutoPlayMenuMilitary, ::autoPlayMilitary)).row() table.add(getButton("AutoPlay Civilians Once", KeyboardBinding.AutoPlayMenuCivilians, ::autoPlayCivilian)).row() @@ -41,33 +43,61 @@ class AutoPlayMenu( } private fun autoPlayEndTurn() { - TurnManager(worldScreen.viewingCiv).automateTurn() - worldScreen.nextTurn() + val endTurnFunction = { + nextTurnButton.update() + TurnManager(worldScreen.viewingCiv).automateTurn() + worldScreen.autoPlay.stopAutoPlay() + worldScreen.nextTurn() + } + + if (worldScreen.viewingCiv.units.getCivUnitsSize() + worldScreen.viewingCiv.cities.size >= 30) { + autoPlay.runAutoPlayJobInNewThread("AutoPlayEndTurn", worldScreen, false, endTurnFunction) + } else { + autoPlay.autoPlayTurnInProgress = true + endTurnFunction() + } } - private fun autoPlay() { - settings.autoPlay.startAutoPlay() + private fun multiturnAutoPlay() { + worldScreen.autoPlay.startMultiturnAutoPlay() nextTurnButton.update() } private fun autoPlayMilitary() { val civInfo = worldScreen.viewingCiv - val isAtWar = civInfo.isAtWar() - val sortedUnits = civInfo.units.getCivUnits().filter { it.isMilitary() }.sortedBy { unit -> NextTurnAutomation.getUnitPriority(unit, isAtWar) } - for (unit in sortedUnits) UnitAutomation.automateUnitMoves(unit) + val autoPlayMilitaryFunction = { + val isAtWar = civInfo.isAtWar() + val sortedUnits = civInfo.units.getCivUnits().filter { it.isMilitary() }.sortedBy { unit -> NextTurnAutomation.getUnitPriority(unit, isAtWar) } + for (unit in sortedUnits) UnitAutomation.automateUnitMoves(unit) - for (city in civInfo.cities) UnitAutomation.tryBombardEnemy(city) - worldScreen.shouldUpdate = true - worldScreen.render(0f) + for (city in civInfo.cities) UnitAutomation.tryBombardEnemy(city) + worldScreen.shouldUpdate = true + } + if (civInfo.units.getCivUnitsSize() > 30) { + autoPlay.runAutoPlayJobInNewThread("AutoPlayMilitary", worldScreen, true, autoPlayMilitaryFunction) + } else { + autoPlay.autoPlayTurnInProgress = true + autoPlayMilitaryFunction() + autoPlay.autoPlayTurnInProgress = false + } } private fun autoPlayCivilian() { val civInfo = worldScreen.viewingCiv - val isAtWar = civInfo.isAtWar() - val sortedUnits = civInfo.units.getCivUnits().filter { it.isCivilian() }.sortedBy { unit -> NextTurnAutomation.getUnitPriority(unit, isAtWar) } - for (unit in sortedUnits) UnitAutomation.automateUnitMoves(unit) - worldScreen.shouldUpdate = true - worldScreen.render(0f) + val autoPlayCivilainFunction = { + val isAtWar = civInfo.isAtWar() + val sortedUnits = civInfo.units.getCivUnits().filter { it.isCivilian() } + .sortedBy { unit -> NextTurnAutomation.getUnitPriority(unit, isAtWar) } + for (unit in sortedUnits) UnitAutomation.automateUnitMoves(unit) + worldScreen.shouldUpdate = true + } + if (civInfo.units.getCivUnitsSize() > 50) { + autoPlay.runAutoPlayJobInNewThread("AutoPlayCivilian", worldScreen, true, autoPlayCivilainFunction) + } else { + autoPlay.autoPlayTurnInProgress = true + autoPlayCivilainFunction() + autoPlay.autoPlayTurnInProgress = false + } } private fun autoPlayEconomy() { diff --git a/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayStatusButton.kt b/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayStatusButton.kt index ee61b8b423..2eec0019b4 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayStatusButton.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/status/AutoPlayStatusButton.kt @@ -23,17 +23,16 @@ class AutoPlayStatusButton( init { add(Stack(autoPlayImage)).pad(5f) - val settings = GUI.getSettings() onActivation(binding = KeyboardBinding.AutoPlayMenu) { - if (settings.autoPlay.isAutoPlaying()) - settings.autoPlay.stopAutoPlay() - else if (worldScreen.viewingCiv == worldScreen.gameInfo.currentPlayerCiv) + if (worldScreen.autoPlay.isAutoPlaying()) + worldScreen.autoPlay.stopAutoPlay() + else if (worldScreen.isPlayersTurn) AutoPlayMenu(stage,this, nextTurnButton, worldScreen) } val directAutoPlay = { if (!worldScreen.gameInfo.gameParameters.isOnlineMultiplayer && worldScreen.viewingCiv == worldScreen.gameInfo.currentPlayerCiv) { - settings.autoPlay.startAutoPlay() + worldScreen.autoPlay.startMultiturnAutoPlay() nextTurnButton.update() } } @@ -48,9 +47,8 @@ class AutoPlayStatusButton( } override fun dispose() { - val settings = GUI.getSettings() - if (isPressed && settings.autoPlay.isAutoPlaying()) { - settings.autoPlay.stopAutoPlay() + if (isPressed && worldScreen.autoPlay.isAutoPlaying()) { + worldScreen.autoPlay.stopAutoPlay() } } } diff --git a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnAction.kt b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnAction.kt index 1b058851b5..c552a8f06b 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnAction.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnAction.kt @@ -27,9 +27,9 @@ enum class NextTurnAction(protected val text: String, val color: Color) { }, AutoPlay("AutoPlay", Color.WHITE) { override fun isChoice(worldScreen: WorldScreen) = - UncivGame.Current.settings.autoPlay.isAutoPlaying() + worldScreen.autoPlay.isAutoPlaying() override fun action(worldScreen: WorldScreen) = - UncivGame.Current.settings.autoPlay.stopAutoPlay() + worldScreen.autoPlay.stopAutoPlay() }, Working(Constants.working, Color.GRAY) { override fun isChoice(worldScreen: WorldScreen) = diff --git a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt index a60f8385cc..e87021fbdc 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnButton.kt @@ -14,6 +14,7 @@ import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.hasOpenPopups import com.unciv.ui.screens.worldscreen.WorldScreen +import com.unciv.utils.Concurrency class NextTurnButton( private val worldScreen: WorldScreen @@ -33,19 +34,17 @@ class NextTurnButton( fun update() { nextTurnAction = getNextTurnAction(worldScreen) updateButton(nextTurnAction) - val settings = GUI.getSettings() - if (!settings.autoPlay.autoPlayTurnInProgress && settings.autoPlay.isAutoPlaying() - && worldScreen.isPlayersTurn && !worldScreen.waitingForAutosave && !worldScreen.isNextTurnUpdateRunning()) { - settings.autoPlay.autoPlayTurnInProgress = true - if (!worldScreen.viewingCiv.isSpectator()) + val autoPlay = worldScreen.autoPlay + if (autoPlay.shouldContinueAutoPlaying() && worldScreen.isPlayersTurn + && !worldScreen.waitingForAutosave && !worldScreen.isNextTurnUpdateRunning()) { + autoPlay.runAutoPlayJobInNewThread("MultiturnAutoPlay", worldScreen, false) { TurnManager(worldScreen.viewingCiv).automateTurn() - worldScreen.nextTurn() - if (!settings.autoPlay.autoPlayUntilEnd) - settings.autoPlay.turnsToAutoPlay-- - settings.autoPlay.autoPlayTurnInProgress = false + worldScreen.nextTurn() + autoPlay.endTurnMultiturnAutoPlay() + } } - - isEnabled = nextTurnAction.getText (worldScreen) == "AutoPlay" + + isEnabled = nextTurnAction.getText (worldScreen) == "AutoPlay" || (!worldScreen.hasOpenPopups() && worldScreen.isPlayersTurn && !worldScreen.waitingForAutosave && !worldScreen.isNextTurnUpdateRunning()) if (isEnabled) addTooltip(KeyboardBinding.NextTurn) else addTooltip("") diff --git a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnProgress.kt b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnProgress.kt index 862e4364a9..5fcbfc09a1 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnProgress.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/status/NextTurnProgress.kt @@ -98,7 +98,7 @@ class NextTurnProgress( // On first update the button text is not yet updated. To stabilize geometry, do it now if (progress == 0) nextTurnButton?.apply { disable() - if (UncivGame.Current.settings.autoPlay.isAutoPlaying()) + if (GUI.getWorldScreenIfActive()?.autoPlay?.isAutoPlaying() == true) updateButton(NextTurnAction.AutoPlay) else updateButton(NextTurnAction.Working) barWidth = width - removeHorizontalPad - diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/AutoPlay.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/AutoPlay.kt new file mode 100644 index 0000000000..93a000961c --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/AutoPlay.kt @@ -0,0 +1,73 @@ +package com.unciv.ui.screens.worldscreen.unit + +import com.unciv.models.metadata.GameSettings +import com.unciv.ui.screens.worldscreen.WorldScreen +import com.unciv.utils.Concurrency +import kotlinx.coroutines.Job + +class AutoPlay(private var autoPlaySettings: GameSettings.GameSettingsAutoPlay) { + /** + * How many turns we should multiturn AutoPlay for. + * In the case that [autoPlaySettings].autoPlayUntilEnd is true, the value should not be decremented after each turn. + */ + var turnsToAutoPlay: Int = 0 + + /** + * Determines whether or not we are currently processing the viewing player's turn. + * This can be on the main thread or on a different thread. + */ + var autoPlayTurnInProgress: Boolean = false + var autoPlayJob: Job? = null + + fun startMultiturnAutoPlay() { + autoPlayTurnInProgress = false + turnsToAutoPlay = autoPlaySettings.autoPlayMaxTurns + } + + /** + * Processes the end of the user's turn being AutoPlayed. + * Only decrements [turnsToAutoPlay] if [autoPlaySettings].autoPlayUntilEnd is false. + */ + fun endTurnMultiturnAutoPlay() { + if (!autoPlaySettings.autoPlayUntilEnd && turnsToAutoPlay > 0) + turnsToAutoPlay-- + } + + /** + * Stops multiturn AutoPlay and sets [autoPlayTurnInProgress] to false + */ + fun stopAutoPlay() { + turnsToAutoPlay = 0 + autoPlayTurnInProgress = false + } + + /** + * Does the provided job on a new thread if there isn't already an AutoPlay thread running. + * Will set autoPlayTurnInProgress to true for the duration of the job. + * + * @param setPlayerTurnAfterEnd keep this as the default (true) if it will still be the viewing player's turn after the job is finished. + * Set it to false if the turn will end. + * @throws IllegalStateException if an AutoPlay job is currently running as this is called. + */ + fun runAutoPlayJobInNewThread(jobName: String, worldScreen: WorldScreen, setPlayerTurnAfterEnd: Boolean = true, job: () -> Unit) { + if (autoPlayTurnInProgress) throw IllegalStateException("Trying to start an AutoPlay job while a job is currently running") + autoPlayTurnInProgress = true + worldScreen.isPlayersTurn = false + autoPlayJob = Concurrency.runOnNonDaemonThreadPool(jobName) { + job() + autoPlayTurnInProgress = false + if (setPlayerTurnAfterEnd) + worldScreen.isPlayersTurn = true + } + } + + fun isAutoPlaying(): Boolean = turnsToAutoPlay > 0 || autoPlayTurnInProgress + + fun fullAutoPlayAI(): Boolean = isAutoPlaying() && autoPlaySettings.fullAutoPlayAI + + /** + * @return true if we should play at least 1 more turn and we are not currenlty processing any AutoPlay + */ + fun shouldContinueAutoPlaying(): Boolean = !autoPlayTurnInProgress && turnsToAutoPlay > 0 +} + diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt index cdd9c5e7a9..bdd6f80a7c 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt @@ -20,7 +20,7 @@ object UnitActionsPillage { internal fun getPillageActions(unit: MapUnit, tile: Tile): Sequence { val pillageAction = getPillageAction(unit, tile) ?: return emptySequence() - if (pillageAction.action == null || unit.civ.isAI() || (unit.civ.isHuman() && UncivGame.Current.settings.autoPlay.isAutoPlaying())) + if (pillageAction.action == null || unit.civ.isAIOrAutoPlaying()) return sequenceOf(pillageAction) else return sequenceOf(UnitAction(UnitActionType.Pillage, 65f, pillageAction.title) { val pillageText = "Are you sure you want to pillage this [${tile.getImprovementToPillageName()!!}]?"