Refactored the way cities determine what uniques should apply when (#4393)

* I'm so sorry for the size of this PR

* Uniques are better generalized

* Code now compiles

* Missed a line

* Some more cleaning up
This commit is contained in:
Xander Lenstra
2021-07-07 21:01:14 +02:00
committed by GitHub
parent ecd5ae5128
commit fbd64b9520
11 changed files with 209 additions and 114 deletions

View File

@ -79,13 +79,40 @@ class CityConstructions {
val stats = Stats()
for (building in getBuiltBuildings())
stats.add(building.getStats(cityInfo))
for (unique in builtBuildingUniqueMap.getAllUniques()) when (unique.placeholderText) {
"[] per [] population []" -> if (cityInfo.matchesFilter(unique.params[2]))
stats.add(unique.stats.times(cityInfo.population.population / unique.params[1].toFloat()))
"[] once [] is discovered" -> if (cityInfo.civInfo.tech.isResearched(unique.params[1])) stats.add(unique.stats)
// Why only the local matching uniques you ask? Well, the non-local uniques are evaluated in
// CityStats.getStatsFromUniques(uniques). This function gets a list of uniques and one of
// the placeholderTexts it filters for is "[] per [] population []". Why doesn't that function
// then not also handle the local (i.e. cityFilter == "in this city") versions?
// This is because of what it is used for. The only time time a unique with this placeholderText
// is actually contained in `uniques`, is when `getStatsFromUniques` is called for determining
// the stats a city receives from wonders. It is then called with `unique` being the list
// of all specifically non-local uniques of all cities.
//
// This averts the problem, albeit it barely, and it might change in the future without
// anyone noticing, which might lead to further bugs. So why can't these two unique checks
// just be merged then? Because of another problem.
//
// As noted earlier, `getStatsFromUniques` is called in that case to calculate the stats
// this city received from wonders, both local and non-local. This function, `getStats`,
// is only called to calculate the stats the city receives from local buildings.
// In the current codebase with the current JSON objects it just so happens to be that
// all non-local uniques with this placeholderText from other cities belong to wonders,
// while the local uniques with this placeholderText are from buildings, but this is in no
// way a given. In reality, there should be functions getBuildingStats and getWonderStats,
// to solve this, with getStats merely adding these two together. Implementing this is on
// my ToDoList, but this PR is already large enough as it is.
for (unique in cityInfo.getLocalMatchingUniques("[] per [] population []")
.filter { cityInfo.matchesFilter(it.params[2])}
) {
stats.add(unique.stats.times(cityInfo.population.population / unique.params[1].toFloat()))
}
for (unique in cityInfo.getLocalMatchingUniques("[] once [] is discovered")) {
if (cityInfo.civInfo.tech.isResearched(unique.params[1]))
stats.add(unique.stats)
}
return stats
}

View File

@ -6,6 +6,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Unique
import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unit.BaseUnit
@ -253,7 +254,7 @@ class CityInfo {
cityResources.add(resource, -amount, "Buildings")
}
}
for (unique in cityConstructions.builtBuildingUniqueMap.getUniques("Provides [] []")) { // E.G "Provides [1] [Iron]"
for (unique in getLocalMatchingUniques("Provides [] []")) { // E.G "Provides [1] [Iron]"
val resource = getRuleset().tileResources[unique.params[1]]
if (resource != null) {
cityResources.add(resource, unique.params[0].toInt() * civInfo.getResourceModifier(resource), "Tiles")
@ -337,10 +338,20 @@ class CityInfo {
// this is not very efficient, and if it causes problems we can try and think of a way of improving it
if (stat != null) entry.value.add(stat, entry.value.get(stat) * unique.params[1].toFloat() / 100)
}
for (unique in civInfo.getMatchingUniques("+[]% great person generation in all cities")
+ cityConstructions.builtBuildingUniqueMap.getUniques("+[]% great person generation in this city"))
stats[entry.key] = stats[entry.key]!!.times(1 + (unique.params[0].toFloat() / 100))
for (unique in getMatchingUniques("[]% great person generation []")) {
if (!matchesFilter(unique.params[1])) continue
stats[entry.key]!!.timesInPlace(1 + unique.params[0].toFloat() / 100f)
}
// Deprecated since 3.15.9
for (unique in getMatchingUniques("+[]% great person generation in this city")
+ getMatchingUniques("+[]% great person generation in all cities")
) {
stats[entry.key]!!.timesInPlace(1 + unique.params[0].toFloat() / 100f)
}
//
}
return stats
@ -532,6 +543,54 @@ class CityInfo {
else -> false
}
}
// So everywhere in the codebase there were continuous calls to either
// `cityConstructions.builtBuildingUniqueMap.getUniques()` or `cityConstructions.builtBuildingMap.getAllUniques()`,
// which was fine as long as those were the only uniques that cities could provide.
// However, with the introduction of religion, cities might also get uniques from the religion the city follows.
// Adding both calls to `builtBuildingsUniqueMap` and `Religion` every time is not really modular and also ugly, so something had to be done.
// Looking at all the use cases, the following functions were written to handle all findMatchingUniques() problems.
// Sadly, due to the large disparity between use cases, there needed to be lots of functions.
// Finds matching uniques provided from both local and non-local sources.
fun getMatchingUniques(
placeholderText: String,
// We might have this cached to avoid concurrency problems. If we don't, just get it directly
localUniques: Sequence<Unique> = getLocalMatchingUniques(placeholderText),
): Sequence<Unique> {
// The localUniques might not be filtered when passed as a parameter, so we filter it anyway
// The time loss shouldn't be that large I don't think
return civInfo.getMatchingUniques(placeholderText, this) +
localUniques.filter { it.placeholderText == placeholderText }
}
// Matching uniques provided by sources in the city itself
fun getLocalMatchingUniques(placeholderText: String): Sequence<Unique> {
return (
cityConstructions.builtBuildingUniqueMap.getUniques(placeholderText) +
religion.getMatchingUniques(placeholderText)
).asSequence()
}
// Get all uniques that originate from this city
fun getAllLocalUniques(): Sequence<Unique> {
return cityConstructions.builtBuildingUniqueMap.getAllUniques() + religion.getUniques()
}
// Get all matching uniques that don't apply to only this city
fun getMatchingUniquesWithNonLocalEffects(placeholderText: String): Sequence<Unique> {
return cityConstructions.builtBuildingUniqueMap.getUniques(placeholderText).asSequence()
.filter { it.params.none { param -> param == "in this city" } }
// Note that we don't query religion here, as those only have local effects (for now at least)
}
// Get all uniques that don't apply to only this city
fun getAllUniquesWithNonLocalEffects(): Sequence<Unique> {
return cityConstructions.builtBuildingUniqueMap.getAllUniques()
.filter { it.params.none { param -> param == "in this city" } }
// Note that we don't query religion here, as those only have local effects (for now at least)
}
//endregion
}

View File

@ -8,14 +8,14 @@ class CityInfoReligionManager: Counter<String>() {
@Transient
lateinit var cityInfo: CityInfo
fun getUniques(): List<Unique> {
fun getUniques(): Sequence<Unique> {
val majorityReligion = getMajorityReligion()
if (majorityReligion == null) return listOf()
if (majorityReligion == null) return sequenceOf()
// This should later be changed when religions can have multiple beliefs
return cityInfo.civInfo.gameInfo.ruleSet.beliefs[majorityReligion]!!.uniqueObjects
return cityInfo.civInfo.gameInfo.ruleSet.beliefs[majorityReligion]!!.uniqueObjects.asSequence()
}
fun getMatchingUniques(unique: String): List<Unique> {
fun getMatchingUniques(unique: String): Sequence<Unique> {
return getUniques().filter { it.placeholderText == unique }
}

View File

@ -373,22 +373,24 @@ class CityStats {
newBaseStatList["National ability"] = getStatsFromNationUnique()
newBaseStatList["Wonders"] = getStatsFromUniques(civInfo.getCivWideBuildingUniques())
newBaseStatList["City-States"] = getStatsFromCityStates()
newBaseStatList["Religion"] = getStatsFromUniques(cityInfo.religion.getUniques())
baseStatList = newBaseStatList
}
private fun updateStatPercentBonusList(currentConstruction: IConstruction, citySpecificUniques: Sequence<Unique>) {
private fun updateStatPercentBonusList(currentConstruction: IConstruction, localBuildingUniques: Sequence<Unique>) {
val newStatPercentBonusList = LinkedHashMap<String, Stats>()
newStatPercentBonusList["Golden Age"] = getStatPercentBonusesFromGoldenAge(cityInfo.civInfo.goldenAges.isGoldenAge())
newStatPercentBonusList["Policies"] = getStatPercentBonusesFromUniques(currentConstruction, cityInfo.civInfo.policies.policyUniques.getAllUniques())
newStatPercentBonusList["Buildings"] = getStatPercentBonusesFromUniques(currentConstruction, citySpecificUniques)
newStatPercentBonusList["Buildings"] = getStatPercentBonusesFromUniques(currentConstruction, localBuildingUniques)
.plus(cityInfo.cityConstructions.getStatPercentBonuses()) // This function is to be deprecated but it'll take a while.
newStatPercentBonusList["Wonders"] = getStatPercentBonusesFromUniques(currentConstruction, cityInfo.civInfo.getCivWideBuildingUniques())
newStatPercentBonusList["Railroad"] = getStatPercentBonusesFromRailroad()
newStatPercentBonusList["Resources"] = getStatPercentBonusesFromResources(currentConstruction)
newStatPercentBonusList["National ability"] = getStatPercentBonusesFromNationUnique(currentConstruction)
newStatPercentBonusList["Puppet City"] = getStatPercentBonusesFromPuppetCity()
newStatPercentBonusList["Religion"] = getStatPercentBonusesFromUniques(currentConstruction, cityInfo.religion.getUniques())
if (UncivGame.Current.superchargedForDebug) {
val stats = Stats()
@ -402,12 +404,13 @@ class CityStats {
fun update(currentConstruction: IConstruction = cityInfo.cityConstructions.getCurrentConstruction()) {
// We calculate this here for concurrency reasons
// If something needs this, we pass this through as a parameter
val citySpecificUniques = getCitySpecificUniques()
val localBuildingUniques = cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques()
val citySpecificUniques = cityInfo.getAllLocalUniques()
// We need to compute Tile yields before happiness
updateBaseStatList()
updateCityHappiness()
updateStatPercentBonusList(currentConstruction, citySpecificUniques)
updateStatPercentBonusList(currentConstruction, localBuildingUniques)
updateFinalStatList(currentConstruction, citySpecificUniques) // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions
@ -498,21 +501,6 @@ class CityStats {
finalStatList = newFinalStatList
}
private fun getCitySpecificUniques(): Sequence<Unique> {
return cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques()
.filter { it.params.isNotEmpty() && it.params.last() == "in this city" }
}
private fun getUniquesForThisCity(
unique: String,
// We might have to cached to avoid concurrency problems, so if we don't, just get it directly
citySpecificUniques: Sequence<Unique> = getCitySpecificUniques()
): Sequence<Unique> {
return citySpecificUniques.filter { it.placeholderText == unique } +
cityInfo.civInfo.getMatchingUniques(unique).filter { cityInfo.matchesFilter(it.params[1]) } +
cityInfo.religion.getMatchingUniques(unique)
}
private fun getBuildingMaintenanceCosts(citySpecificUniques: Sequence<Unique>): Float {
// Same here - will have a different UI display.
var buildingsMaintenance = cityInfo.cityConstructions.getMaintenanceCosts().toFloat() // this is AFTER the bonus calculation!
@ -521,12 +509,12 @@ class CityStats {
}
// e.g. "-[50]% maintenance costs for buildings [in this city]"
for (unique in getUniquesForThisCity("-[]% maintenance cost for buildings []", citySpecificUniques)) {
for (unique in cityInfo.getMatchingUniques("-[]% maintenance cost for buildings []", citySpecificUniques)) {
buildingsMaintenance *= (1f - unique.params[0].toFloat() / 100)
}
// Deprecated since 3.15
for (unique in getUniquesForThisCity("-[]% building maintenance costs []", citySpecificUniques)) {
for (unique in cityInfo.getMatchingUniques("-[]% building maintenance costs []", citySpecificUniques)) {
buildingsMaintenance *= (1f - unique.params[0].toFloat() / 100)
}
//

View File

@ -59,9 +59,11 @@ class PopulationManager {
}
if (foodStored >= getFoodToNextPopulation()) { // growth!
foodStored -= getFoodToNextPopulation()
val percentOfFoodCarriedOver = cityInfo.cityConstructions.builtBuildingUniqueMap
.getUniques("[]% of food is carried over after population increases")
var percentOfFoodCarriedOver = cityInfo
.getMatchingUniques("[]% of food is carried over after population increases")
.sumBy { it.params[0].toInt() }
// Try to avoid runaway food gain in mods, just in case mod makes don't notice it
if (percentOfFoodCarriedOver > 95) percentOfFoodCarriedOver = 95
foodStored += (getFoodToNextPopulation() * percentOfFoodCarriedOver / 100f).toInt()
population++
autoAssignPopulation()

View File

@ -242,18 +242,16 @@ class CivilizationInfo {
fun hasResource(resourceName: String): Boolean = getCivResourcesByName()[resourceName]!! > 0
fun getCivWideBuildingUniques(): Sequence<Unique> = cities.asSequence().flatMap {
city -> city.cityConstructions.builtBuildingUniqueMap.getAllUniques()
.filter { it.params.isEmpty() || it.params.last() != "in this city" }
city -> city.getAllUniquesWithNonLocalEffects()
}
fun hasUnique(unique: String) = getMatchingUniques(unique).any()
// Does not return local uniques, only global ones.
fun getMatchingUniques(uniqueTemplate: String): Sequence<Unique> {
fun getMatchingUniques(uniqueTemplate: String, cityToIgnore: CityInfo? = null): Sequence<Unique> {
return nation.uniqueObjects.asSequence().filter { it.placeholderText == uniqueTemplate } +
cities.asSequence().flatMap {
city -> city.cityConstructions.builtBuildingUniqueMap.getUniques(uniqueTemplate).asSequence()
.filter { it.params.isEmpty() || it.params.last() != "in this city" }
cities.filter { it != cityToIgnore}.flatMap {
city -> city.getMatchingUniquesWithNonLocalEffects(uniqueTemplate)
} +
policies.policyUniques.getUniques(uniqueTemplate) +
tech.getTechUniques().filter { it.placeholderText == uniqueTemplate } +

View File

@ -225,11 +225,13 @@ open class TileInfo {
}
if (city != null) {
val cityWideUniques = city.cityConstructions.builtBuildingUniqueMap.getUniques("[] from [] tiles in this city")
val civWideUniques = city.civInfo.getMatchingUniques("[] from every []")
val religionUniques = city.religion.getMatchingUniques( "[] from every []")
// Should be refactored to use city.getUniquesForThisCity(), probably
for (unique in cityWideUniques + civWideUniques + religionUniques) {
var tileUniques = city.getMatchingUniques("[] from [] tiles []")
.filter { city.matchesFilter(it.params[2]) }
// Deprecated since 3.15.9
tileUniques += city.getLocalMatchingUniques("[] from [] tiles in this city")
//
tileUniques += city.getMatchingUniques("[] from every []")
for (unique in tileUniques) {
val tileType = unique.params[1]
if (tileType == improvement) continue // This is added to the calculation in getImprovementStats. we don't want to add it twice
if (matchesTerrainFilter(tileType, observingCiv))
@ -278,12 +280,16 @@ open class TileInfo {
stats.add(unique.stats)
if (city != null) {
val cityWideUniques = city.cityConstructions.builtBuildingUniqueMap.getUniques("[] from [] tiles in this city")
var tileUniques = city.getMatchingUniques("[] from [] tiles []")
.filter { city.matchesFilter(it.params[2]) }
// Deprecated since 3.15.9
tileUniques += city.getLocalMatchingUniques("[] from [] tiles in this city")
//
val improvementUniques = improvement.uniqueObjects.filter {
it.placeholderText == "[] on [] tiles once [] is discovered"
&& observingCiv.tech.isResearched(it.params[2])
}
for (unique in cityWideUniques + improvementUniques) {
for (unique in tileUniques + improvementUniques) {
if (improvement.matchesFilter(unique.params[1])
// Freshwater and non-freshwater cannot be moved to matchesUniqueFilter since that creates an endless feedback.
// If you're attempting that, check that it works!
@ -292,7 +298,7 @@ open class TileInfo {
stats.add(unique.stats)
}
for (unique in city.civInfo.getMatchingUniques("[] from every []") + city.religion.getMatchingUniques("[] from every []")) {
for (unique in city.getMatchingUniques("[] from every []")) {
if (improvement.matchesFilter(unique.params[1])) {
stats.add(unique.stats)
}

View File

@ -88,7 +88,7 @@ class Building : NamedStats(), IConstruction {
val tileBonusHashmap = HashMap<String, ArrayList<String>>()
val finalUniques = ArrayList<String>()
for (unique in uniqueObjects)
if (unique.placeholderText == "[] from [] tiles in this city") {
if (unique.placeholderText == "[] from [] tiles []" && unique.params[2] == "in this city") {
val stats = unique.params[0]
if (!tileBonusHashmap.containsKey(stats)) tileBonusHashmap[stats] = ArrayList()
tileBonusHashmap[stats]!!.add(unique.params[1])
@ -155,30 +155,27 @@ class Building : NamedStats(), IConstruction {
fun getStats(city: CityInfo?): Stats {
val stats = this.clone()
val civInfo = city?.civInfo
if (civInfo != null) {
val baseBuildingName = getBaseBuilding(civInfo.gameInfo.ruleSet).name
if (city == null) return stats
val civInfo = city.civInfo
// We don't have to check whether 'city' is null, as if it was, cityInfo would also be null, and we wouldn't be here.
for (unique in civInfo.getMatchingUniques("[] from every []") + city.religion.getMatchingUniques("[] from every []")) {
if (unique.params[1] != baseBuildingName) continue
stats.add(unique.stats)
}
for (unique in uniqueObjects)
if (unique.placeholderText == "[] with []" && civInfo.hasResource(unique.params[1])
&& Stats.isStats(unique.params[0]))
stats.add(unique.stats)
if (!isWonder)
for (unique in civInfo.getMatchingUniques("[] from all [] buildings")) {
if (isStatRelated(Stat.valueOf(unique.params[1])))
stats.add(unique.stats)
}
else
for (unique in civInfo.getMatchingUniques("[] from every Wonder"))
stats.add(unique.stats)
for (unique in city.getMatchingUniques("[] from every []")) {
if (!matchesFilter(unique.params[1])) continue
stats.add(unique.stats)
}
for (unique in uniqueObjects)
if (unique.placeholderText == "[] with []" && civInfo.hasResource(unique.params[1])
&& Stats.isStats(unique.params[0]))
stats.add(unique.stats)
if (!isWonder)
for (unique in city.getMatchingUniques("[] from all [] buildings")) {
if (isStatRelated(Stat.valueOf(unique.params[1])))
stats.add(unique.stats)
}
else
for (unique in city.getMatchingUniques("[] from every Wonder"))
stats.add(unique.stats)
return stats
}

View File

@ -286,22 +286,40 @@ class BaseUnit : INamed, IConstruction, CivilopediaText() {
var XP = cityConstructions.getBuiltBuildings().sumBy { it.xpForNewUnits }
for (unique in cityConstructions.builtBuildingUniqueMap
.getUniques("New [] units start with [] Experience in this city")
+ civInfo.getMatchingUniques("New [] units start with [] Experience")) {
for (unique in
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience") +
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience []")
.filter { cityConstructions.cityInfo.matchesFilter(it.params[2]) } +
// Deprecated since 3.15.9
cityConstructions.cityInfo.getLocalMatchingUniques("New [] units start with [] Experience in this city")
//
) {
if (unit.matchesFilter(unique.params[0]))
XP += unique.params[1].toInt()
}
unit.promotions.XP = XP
for (unique in cityConstructions.builtBuildingUniqueMap
.getUniques("All newly-trained [] units in this city receive the [] promotion")) {
for (unique in
cityConstructions.cityInfo.getMatchingUniques("All newly-trained [] units [] receive the [] promotion")
.filter { cityConstructions.cityInfo.matchesFilter(it.params[1]) } +
// Deprecated since 3.15.9
cityConstructions.cityInfo.getLocalMatchingUniques("All newly-trained [] units in this city receive the [] promotion")
//
) {
val filter = unique.params[0]
val promotion = unique.params[1]
val promotion = unique.params.last()
if (unit.matchesFilter(filter) || (filter == "relevant" && civInfo.gameInfo.ruleSet.unitPromotions.values
.any { unit.type.name in it.unitTypes && it.name == promotion }))
if (unit.matchesFilter(filter) ||
(
filter == "relevant" &&
civInfo.gameInfo.ruleSet.unitPromotions.values
.any {
it.name == promotion && unit.type.name in it.unitTypes
}
)
) {
unit.promotions.addPromotion(promotion, isFree = true)
}
}
return true