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?
This commit is contained in:
Oskar Niesen 2024-04-23 15:59:02 -05:00 committed by GitHub
parent 6b2fe87887
commit a3d56845f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 197 additions and 103 deletions

View File

@ -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?",

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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...

View File

@ -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) {

View File

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

View File

@ -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") {

View File

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

View File

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

View File

@ -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) =

View File

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

View File

@ -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 -

View File

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

View File

@ -20,7 +20,7 @@ object UnitActionsPillage {
internal fun getPillageActions(unit: MapUnit, tile: Tile): Sequence<UnitAction> {
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()!!}]?"