Fix a Nation Picker crash (#5311)

This commit is contained in:
SomeTroglodyte 2021-09-24 14:49:56 +02:00 committed by GitHub
parent 12302deef6
commit c05bf0a2a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 160 deletions

View File

@ -22,7 +22,7 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
fun getStatBuyCost(cityInfo: CityInfo, stat: Stat): Int?
fun getRejectionReasons(cityConstructions: CityConstructions): RejectionReasons
fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat? = null): Boolean // Yes I'm hilarious.
fun canBePurchasedWithStat(cityInfo: CityInfo?, stat: Stat): Boolean {
if (stat in listOf(Stat.Production, Stat.Happiness)) return false
if ("Cannot be purchased" in uniques) return false
@ -47,12 +47,12 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
fun canBePurchasedWithAnyStat(cityInfo: CityInfo): Boolean {
return Stat.values().any { canBePurchasedWithStat(cityInfo, it) }
}
fun getBaseGoldCost(civInfo: CivilizationInfo): Double {
// https://forums.civfanatics.com/threads/rush-buying-formula.393892/
return (30.0 * getProductionCost(civInfo)).pow(0.75) * hurryCostModifier.toPercent()
}
fun getBaseBuyCost(cityInfo: CityInfo, stat: Stat): Int? {
if (stat == Stat.Gold) return getBaseGoldCost(cityInfo.civInfo).toInt()
@ -73,20 +73,20 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
class RejectionReasons(): HashSet<RejectionReason>() {
class RejectionReasons: HashSet<RejectionReason>() {
fun filterTechPolicyEraWonderRequirements(): HashSet<RejectionReason> {
return filterNot { it in techPolicyEraWonderRequirements }.toHashSet()
}
fun hasAReasonToBeRemovedFromQueue(): Boolean {
return any { it in reasonsToDefinitivelyRemoveFromQueue }
}
fun getMostImportantRejectionReason(): String? {
return orderOfErrorMessages.firstOrNull { it in this }?.errorMessage
}
// Used for constant variables in the functions above
companion object {
private val techPolicyEraWonderRequirements = hashSetOf(
@ -122,35 +122,35 @@ enum class RejectionReason(val shouldShow: Boolean, var errorMessage: String) {
Unbuildable(false, "Unbuildable"),
CanOnlyBePurchased(true, "Can only be purchased"),
ShouldNotBeDisplayed(false, "Should not be displayed"),
DisabledBySetting(false, "Disabled by setting"),
HiddenWithoutVictory(false, "Hidden because a victory type has been disabled"),
MustBeOnTile(false, "Must be on a specific tile"),
MustNotBeOnTile(false, "Must not be on a specific tile"),
MustBeNextToTile(false, "Must be next to a specific tile"),
MustNotBeNextToTile(false, "Must not be next to a specific tile"),
MustOwnTile(false, "Must own a specific tile closeby"),
MustOwnTile(false, "Must own a specific tile close by"),
WaterUnitsInCoastalCities(false, "May only built water units in coastal cities"),
CanOnlyBeBuiltInSpecificCities(false, "Can only be built in specific cities"),
UniqueToOtherNation(false, "Unique to another nation"),
ReplacedByOurUnique(false, "Our unique replaces this"),
Obsoleted(false, "Obsolete"),
RequiresTech(false, "Required tech not researched"),
RequiresPolicy(false, "Requires a specific policy!"),
UnlockedWithEra(false, "Unlocked when reacing a specific era"),
UnlockedWithEra(false, "Unlocked when reaching a specific era"),
MorePolicyBranches(false, "Hidden until more policy branches are fully adopted"),
RequiresNearbyResource(false, "Requires a certain resource being exploited nearby"),
InvalidRequiredBuilding(false, "Required building does not exist in ruleSet!"),
CannotBeBuiltWith(false, "Cannot be built at the same time as another building already built"),
RequiresBuildingInThisCity(true, "Requires a specific building in this city!"),
RequiresBuildingInAllCities(true, "Requires a specific building in all cities!"),
RequiresBuildingInSomeCity(true, "Requires a specific building anywhere in your empire!"),
WonderAlreadyBuilt(false, "Wonder already built"),
NationalWonderAlreadyBuilt(false, "National Wonder already built"),
WonderBeingBuiltElsewhere(true, "Wonder is being built elsewhere"),
@ -158,21 +158,19 @@ enum class RejectionReason(val shouldShow: Boolean, var errorMessage: String) {
CityStateWonder(false, "No Wonders for city-states"),
CityStateNationalWonder(false, "No National Wonders for city-states"),
WonderDisabledEra(false, "This Wonder is disabled when starting in this era"),
ReachedBuildCap(false, "Don't need to build any more of these!"),
ConsumesResources(true, "Consumes resources which you are lacking"),
PopulationRequirement(true, "Requires more population"),
NoSettlerForOneCityPlayers(false, "No settlers for city-states or one-city challangers");
NoSettlerForOneCityPlayers(false, "No settlers for city-states or one-city challengers");
}
open class PerpetualConstruction(override var name: String, val description: String) : IConstruction {
override fun shouldBeDisplayed(cityConstructions: CityConstructions) = isBuildable(cityConstructions)
open fun getProductionTooltip(cityInfo: CityInfo) : String
= "\r\n${(cityInfo.cityStats.currentCityStats.production / CONVERSION_RATE).roundToInt()}/${Fonts.turn}"
@ -207,7 +205,7 @@ open class PerpetualConstruction(override var name: String, val description: Str
override fun isBuildable(cityConstructions: CityConstructions): Boolean =
throw Exception("Impossible!")
override fun getResourceRequirements(): HashMap<String, Int> = hashMapOf()
}
}

View File

@ -1,7 +1,6 @@
package com.unciv.models.ruleset
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.*
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.Counter
@ -22,6 +21,7 @@ import com.unciv.ui.utils.Fonts
import com.unciv.ui.utils.toPercent
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.pow
class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
@ -229,7 +229,9 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
if (cost > 0) {
val stats = mutableListOf("$cost${Fonts.production}")
if (canBePurchasedWithStat(null, Stat.Gold)) {
stats += "${getBaseGoldCost(UncivGame.Current.gameInfo.currentPlayerCiv).toInt() / 10 * 10}${Fonts.gold}"
// We need what INonPerpetualConstruction.getBaseGoldCost calculates but without any game- or civ-specific modifiers
val buyCost = 30.0 * cost.toFloat().pow(0.75f) * hurryCostModifier.toPercent() / 10 * 10
stats += "$buyCost${Fonts.gold}"
}
textList += FormattedLine(stats.joinToString(", ", "{Cost}: "))
}
@ -403,7 +405,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
val cityCenter = cityConstructions.cityInfo.getCenterTile()
val civInfo = cityConstructions.cityInfo.civInfo
val ruleSet = civInfo.gameInfo.ruleSet
if (cityConstructions.isBuilt(name))
rejectionReasons.add(RejectionReason.AlreadyBuilt)
// for buildings that are created as side effects of other things, and not directly built,
@ -418,7 +420,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
if (!cityConstructions.containsBuildingOrEquivalent(unique.params[0]))
rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed)
//
"Not displayed as an available construction without []" ->
if (unique.params[0] in ruleSet.tileResources && !civInfo.hasResource(unique.params[0])
|| unique.params[0] in ruleSet.buildings && !cityConstructions.containsBuildingOrEquivalent(unique.params[0])
@ -426,35 +428,35 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
|| unique.params[0] in ruleSet.policies && !civInfo.policies.isAdopted(unique.params[0])
)
rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed)
"Enables nuclear weapon" -> if (!cityConstructions.cityInfo.civInfo.gameInfo.gameParameters.nuclearWeaponsEnabled)
rejectionReasons.add(RejectionReason.DisabledBySetting)
"Must be on []" ->
if (!cityCenter.matchesTerrainFilter(unique.params[0], civInfo))
rejectionReasons.add(RejectionReason.MustBeOnTile.apply { errorMessage = unique.text })
"Must not be on []" ->
if (cityCenter.matchesTerrainFilter(unique.params[0], civInfo))
rejectionReasons.add(RejectionReason.MustNotBeOnTile.apply { errorMessage = unique.text })
"Must be next to []" ->
if (// Fresh water is special, in that rivers are not tiles themselves but also fit the filter.
!(unique.params[0] == "Fresh water" && cityCenter.isAdjacentToRiver())
&& cityCenter.getTilesInDistance(1).none { it.matchesFilter(unique.params[0], civInfo) }
)
rejectionReasons.add(RejectionReason.MustBeNextToTile.apply { errorMessage = unique.text })
"Must not be next to []" ->
if (cityCenter.getTilesInDistance(1).any { it.matchesFilter(unique.params[0], civInfo) })
rejectionReasons.add(RejectionReason.MustNotBeNextToTile.apply { errorMessage = unique.text })
"Must have an owned [] within [] tiles" ->
if (cityCenter.getTilesInDistance(unique.params[1].toInt())
.none { it.matchesFilter(unique.params[0], civInfo) && it.getOwner() == cityConstructions.cityInfo.civInfo }
)
rejectionReasons.add(RejectionReason.MustOwnTile.apply { errorMessage = unique.text })
// Deprecated since 3.16.11
"Can only be built in annexed cities" ->
if (
@ -463,15 +465,15 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
)
rejectionReasons.add(RejectionReason.CanOnlyBeBuiltInSpecificCities.apply { errorMessage = unique.text })
//
"Can only be built []" ->
if (!cityConstructions.cityInfo.matchesFilter(unique.params[0]))
rejectionReasons.add(RejectionReason.CanOnlyBeBuiltInSpecificCities.apply { errorMessage = unique.text })
"Obsolete with []" ->
if (civInfo.tech.isResearched(unique.params[0]))
rejectionReasons.add(RejectionReason.Obsoleted.apply { errorMessage = unique.text })
Constants.hiddenWithoutReligionUnique ->
if (!civInfo.gameInfo.isReligionEnabled())
rejectionReasons.add(RejectionReason.DisabledBySetting)
@ -480,10 +482,10 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
if (uniqueTo != null && uniqueTo != civInfo.civName)
rejectionReasons.add(RejectionReason.UniqueToOtherNation.apply { errorMessage = "Unique to $uniqueTo"})
if (civInfo.gameInfo.ruleSet.buildings.values.any { it.uniqueTo == civInfo.civName && it.replaces == name })
rejectionReasons.add(RejectionReason.ReplacedByOurUnique)
if (requiredTech != null && !civInfo.tech.isResearched(requiredTech!!))
rejectionReasons.add(RejectionReason.RequiresTech.apply { "$requiredTech not researched!"})
@ -527,7 +529,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
if (isNationalWonder) {
if (civInfo.cities.any { it.cityConstructions.isBuilt(name) })
rejectionReasons.add(RejectionReason.NationalWonderAlreadyBuilt)
if (requiredBuildingInAllCities != null && civInfo.gameInfo.ruleSet.buildings[requiredBuildingInAllCities!!] == null) {
rejectionReasons.add(RejectionReason.InvalidRequiredBuilding)
} else {
@ -540,10 +542,10 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
rejectionReasons.add(RejectionReason.RequiresBuildingInAllCities
.apply { errorMessage = "Requires a [${civInfo.getEquivalentBuilding(requiredBuildingInAllCities!!)}] in all cities"})
}
if (civInfo.cities.any { it != cityConstructions.cityInfo && it.cityConstructions.isBeingConstructedOrEnqueued(name) })
rejectionReasons.add(RejectionReason.NationalWonderBeingBuiltElsewhere)
if (civInfo.isCityState())
rejectionReasons.add(RejectionReason.CityStateNationalWonder)
}
@ -554,7 +556,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
rejectionReasons.add(
RejectionReason.RequiresBuildingInSomeCity.apply { errorMessage = "Apollo project not built!" }
)
if (civInfo.victoryManager.unconstructedSpaceshipParts()[name] == 0)
rejectionReasons.add(RejectionReason.ReachedBuildCap)
}
@ -584,7 +586,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
)
}
}
"Hidden until [] social policy branches have been completed" -> {
if (cityConstructions.cityInfo.civInfo.getCompletedPolicyBranchesCount() < unique.params[0].toInt())
rejectionReasons.add(RejectionReason.MorePolicyBranches.apply { errorMessage = unique.text })
@ -633,7 +635,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
if (!containsResourceWithImprovement)
rejectionReasons.add(RejectionReason.RequiresNearbyResource.apply { errorMessage = "Nearby $requiredNearbyImprovedResources required" })
}
return rejectionReasons
}

View File

@ -99,104 +99,6 @@ class Nation : INamed, ICivilopediaText, IHasUniques {
var cities: ArrayList<String> = arrayListOf()
/** Used only by NewGame Nation picker */
fun getUniqueString(ruleset: Ruleset): String {
val textList = ArrayList<String>()
if (uniqueName != "") textList += uniqueName.tr() + ":"
if (uniqueText != "") {
textList += " " + uniqueText.tr()
} else {
textList += " " + uniques.joinToString(", ") { it.tr() }
textList += ""
}
if (startBias.isNotEmpty()) {
textList += "Start bias:".tr() + startBias.joinToString(", ", " ") { it.tr() }
textList += ""
}
addUniqueBuildingsText(textList, ruleset)
addUniqueUnitsText(textList, ruleset)
addUniqueImprovementsText(textList, ruleset)
return textList.joinToString("\n")
}
private fun addUniqueBuildingsText(textList: ArrayList<String>, ruleset: Ruleset) {
for (building in ruleset.buildings.values
.filter { it.uniqueTo == name && Constants.hideFromCivilopediaUnique !in it.uniques }) {
if (building.replaces != null && ruleset.buildings.containsKey(building.replaces!!)) {
val originalBuilding = ruleset.buildings[building.replaces!!]!!
textList += building.name.tr() + " - " + "Replaces [${originalBuilding.name}]".tr()
for ((key, value) in building)
if (value != originalBuilding[key])
textList += " " + key.name.tr() + " " + "[${value.toInt()}] vs [${originalBuilding[key].toInt()}]".tr()
for (unique in building.uniques.filter { it !in originalBuilding.uniques })
textList += " " + unique.tr()
if (building.maintenance != originalBuilding.maintenance)
textList += " {Maintenance} " + "[${building.maintenance}] vs [${originalBuilding.maintenance}]".tr()
if (building.cost != originalBuilding.cost)
textList += " {Cost} " + "[${building.cost}] vs [${originalBuilding.cost}]".tr()
if (building.cityStrength != originalBuilding.cityStrength)
textList += " {City strength} " + "[${building.cityStrength}] vs [${originalBuilding.cityStrength}]".tr()
if (building.cityHealth != originalBuilding.cityHealth)
textList += " {City health} " + "[${building.cityHealth}] vs [${originalBuilding.cityHealth}]".tr()
textList += ""
} else if (building.replaces != null) {
textList += building.name.tr() + " - " + "Replaces [${building.replaces}], which is not found in the ruleset!".tr()
} else textList += building.getShortDescription(ruleset)
}
}
private fun addUniqueUnitsText(textList: ArrayList<String>, ruleset: Ruleset) {
for (unit in ruleset.units.values
.filter { it.uniqueTo == name && Constants.hideFromCivilopediaUnique !in it.uniques }) {
if (unit.replaces != null && ruleset.units.containsKey(unit.replaces!!)) {
val originalUnit = ruleset.units[unit.replaces!!]!!
textList += unit.name.tr() + " - " + "Replaces [${originalUnit.name}]".tr()
if (unit.cost != originalUnit.cost)
textList += " {Cost} " + "[${unit.cost}] vs [${originalUnit.cost}]".tr()
if (unit.strength != originalUnit.strength)
textList += " ${Fonts.strength} " + "[${unit.strength}] vs [${originalUnit.strength}]".tr()
if (unit.rangedStrength != originalUnit.rangedStrength)
textList += " ${Fonts.rangedStrength} " + "[${unit.rangedStrength}] vs [${originalUnit.rangedStrength}]".tr()
if (unit.range != originalUnit.range)
textList += " ${Fonts.range} " + "[${unit.range}] vs [${originalUnit.range}]".tr()
if (unit.movement != originalUnit.movement)
textList += " ${Fonts.movement} " + "[${unit.movement}] vs [${originalUnit.movement}]".tr()
for (resource in originalUnit.getResourceRequirements().keys)
if (!unit.getResourceRequirements().containsKey(resource))
textList += " " + "[$resource] not required".tr()
for (unique in unit.uniques.filterNot { it in originalUnit.uniques })
textList += " " + unique.tr()
for (unique in originalUnit.uniques.filterNot { it in unit.uniques })
textList += " " + "Lost ability".tr() + "(" + "vs [${originalUnit.name}]".tr() + "): " + unique.tr()
for (promotion in unit.promotions.filter { it !in originalUnit.promotions })
textList += " " + promotion.tr() + " (" + ruleset.unitPromotions[promotion]!!.uniquesWithEffect().joinToString(",") { it.tr() } + ")"
} else if (unit.replaces != null) {
textList += unit.name.tr() + " - " + "Replaces [${unit.replaces}], which is not found in the ruleset!".tr()
} else {
textList += unit.name.tr()
textList += " " + unit.getDescription().split("\n").joinToString("\n ")
}
textList += ""
}
}
private fun addUniqueImprovementsText(textList: ArrayList<String>, ruleset: Ruleset) {
for (improvement in ruleset.tileImprovements.values
.filter { it.uniqueTo == name }) {
textList += improvement.name.tr()
textList += " " + improvement.clone().toString()
for (unique in improvement.uniques)
textList += " " + unique.tr()
}
}
override fun makeLink() = "Nation/$name"
override fun getSortGroup(ruleset: Ruleset) = when {
isCityState() -> 1
@ -250,8 +152,11 @@ class Nation : INamed, ICivilopediaText, IHasUniques {
val textList = ArrayList<FormattedLine>()
textList += FormattedLine("Type: [$cityStateType]", header = 4, color = cityStateType!!.color)
val viewingCiv = UncivGame.Current.gameInfo.currentPlayerCiv
val era = viewingCiv.getEra()
val era = if (UncivGame.isCurrentInitialized() && UncivGame.Current.isGameInfoInitialized())
UncivGame.Current.gameInfo.currentPlayerCiv.getEra()
else
ruleset.eras.values.first()
var showResources = false
val friendBonus = era.friendBonus[cityStateType!!.name]
@ -292,7 +197,6 @@ class Nation : INamed, ICivilopediaText, IHasUniques {
return textList
}
@JvmName("addUniqueBuildingsText1") // These overloads are too similar - but I hope to remove the other one soon
private fun addUniqueBuildingsText(textList: ArrayList<FormattedLine>, ruleset: Ruleset) {
for (building in ruleset.buildings.values) {
if (building.uniqueTo != name || Constants.hideFromCivilopediaUnique in building.uniques) continue
@ -324,7 +228,6 @@ class Nation : INamed, ICivilopediaText, IHasUniques {
}
}
@JvmName("addUniqueUnitsText1")
private fun addUniqueUnitsText(textList: ArrayList<FormattedLine>, ruleset: Ruleset) {
for (unit in ruleset.units.values) {
if (unit.uniqueTo != name || Constants.hideFromCivilopediaUnique in unit.uniques) continue
@ -373,7 +276,6 @@ class Nation : INamed, ICivilopediaText, IHasUniques {
}
}
@JvmName("addUniqueImprovementsText1")
private fun addUniqueImprovementsText(textList: ArrayList<FormattedLine>, ruleset: Ruleset) {
for (improvement in ruleset.tileImprovements.values) {
if (improvement.uniqueTo != name ) continue

View File

@ -165,8 +165,15 @@ class TileImprovement : NamedStats(), ICivilopediaText, IHasUniques {
}
if (isAncientRuinsEquivalent() && ruleset.ruinRewards.isNotEmpty()) {
val difficulty = UncivGame.Current.gameInfo.gameParameters.difficulty
val religionEnabled = UncivGame.Current.gameInfo.isReligionEnabled()
val difficulty: String
val religionEnabled: Boolean
if (UncivGame.isCurrentInitialized() && UncivGame.Current.isGameInfoInitialized()) {
difficulty = UncivGame.Current.gameInfo.gameParameters.difficulty
religionEnabled = UncivGame.Current.gameInfo.isReligionEnabled()
} else {
difficulty = "Prince" // most factors == 1
religionEnabled = true
}
textList += FormattedLine()
textList += FormattedLine("The possible rewards are:")
ruleset.ruinRewards.values.asSequence()

View File

@ -1,7 +1,6 @@
package com.unciv.models.ruleset.unit
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.*
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.MapUnit
@ -127,8 +126,11 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
if (cost > 0) {
stats.clear()
stats += "$cost${Fonts.production}"
if (canBePurchasedWithStat(null, Stat.Gold))
stats += "${getBaseGoldCost(UncivGame.Current.gameInfo.currentPlayerCiv).toInt() / 10 * 10}${Fonts.gold}"
if (canBePurchasedWithStat(null, Stat.Gold)) {
// We need what INonPerpetualConstruction.getBaseGoldCost calculates but without any game- or civ-specific modifiers
val buyCost = 30.0 * cost.toFloat().pow(0.75f) * hurryCostModifier.toPercent() / 10 * 10
stats += "$buyCost${Fonts.gold}"
}
textList += FormattedLine(stats.joinToString(", ", "{Cost}: "))
}
@ -272,10 +274,10 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
&& it.params[2] == stat.name
}
) return true
return super.canBePurchasedWithStat(cityInfo, stat)
}
private fun getCostForConstructionsIncreasingInPrice(baseCost: Int, increaseCost: Int, previouslyBought: Int): Int {
return (baseCost + increaseCost / 2f * ( previouslyBought * previouslyBought + previouslyBought )).toInt()
}
@ -314,7 +316,7 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
)
).minOrNull()
}
override fun getStatBuyCost(cityInfo: CityInfo, stat: Stat): Int? {
var cost = getBaseBuyCost(cityInfo, stat)?.toDouble()
if (cost == null) return null

View File

@ -434,6 +434,9 @@ interface ICivilopediaText {
/** Generate automatic lines from object metadata.
*
* Please do not rely on a UncivGame.Current.gameInfo being initialized, this should be able to run from the main menu.
* (And the info displayed should be about the **ruleset**, not the player situation)
*
* Default implementation is empty - no need to call super in overrides.
*
* @param ruleset The current ruleset for the Civilopedia viewer