Deprecate all mods without an eras.json file (#4809)

* Enforce the existence of an eras.json file for mods

* Merged `getEra()` and `getEraObject()`

* Hide mods we have deemed outdated

* Fixed compile errors that I didn't notice before

* Fixed unit tests
This commit is contained in:
Xander Lenstra 2021-09-06 13:50:38 +02:00 committed by GitHub
parent 8079a8dc7b
commit 486e2a7a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 81 additions and 137 deletions

View File

@ -0,0 +1,9 @@
[
"https://github.com/k4zoo/Civ5ExpansionMod",
"https://github.com/k4zoo/Civilization-6-Mod",
"https://github.com/9kgsofrice/DeCiv",
"https://github.com/k4zoo/EmpireOfSmokySkies",
"https://github.com/shannaurelle/ph-bonifacio-unciv",
"https://github.com/ID1m0nI/PolyCiv",
"https://github.com/CrispyXYZ/Optimize-tech-mod"
]

View File

@ -143,7 +143,7 @@ object GameStarter {
civInfo.tech.addTechnology(tech.name)
for (tech in ruleset.technologies.values
.filter { ruleset.getEraNumber(it.era()) < ruleset.getEraNumber(gameSetupInfo.gameParameters.startingEra) })
.filter { ruleset.eras[it.era()]!!.eraNumber < ruleset.eras[gameSetupInfo.gameParameters.startingEra]!!.eraNumber })
if (!civInfo.tech.isResearched(tech.name))
civInfo.tech.addTechnology(tech.name)
@ -154,12 +154,7 @@ object GameStarter {
private fun addCivStats(gameInfo: GameInfo) {
val ruleSet = gameInfo.ruleSet
val startingEra = gameInfo.gameParameters.startingEra
val era =
if (startingEra in ruleSet.eras.keys) {
ruleSet.eras[startingEra]!!
} else {
Era()
}
val era = ruleSet.eras[startingEra]!!
for (civInfo in gameInfo.civilizations.filter { !it.isBarbarian() }) {
civInfo.addGold((era.startingGold * gameInfo.gameParameters.gameSpeed.modifier).toInt())
civInfo.policies.addCulture((era.startingCulture * gameInfo.gameParameters.gameSpeed.modifier).toInt())
@ -277,35 +272,9 @@ object GameStarter {
civ.placeUnitNearTile(startingLocation.position, unitName)
}
// We are using an older mod, so we only look at the difficulty file
if (ruleSet.eras.isEmpty()) {
startingUnits = (when {
civ.isPlayerCivilization() -> gameInfo.getDifficulty().startingUnits
civ.isMajorCiv() -> gameInfo.getDifficulty().aiMajorCivStartingUnits
else -> gameInfo.getDifficulty().aiCityStateStartingUnits
}).toMutableList()
val warriorEquivalent = ruleSet.units.values
.filter { it.isLandUnit() && it.isMilitary() && it.isBuildable(civ) }
.maxByOrNull {max(it.strength, it.rangedStrength)}
?.name
for (unit in startingUnits) {
val unitToAdd = if (unit == "Warrior") warriorEquivalent else unit
if (unitToAdd != null) placeNearStartingPosition(unitToAdd)
}
continue
}
// Determine starting units based on starting era
if (startingEra in ruleSet.eras.keys) {
startingUnits = ruleSet.eras[startingEra]!!.getStartingUnits().toMutableList()
eraUnitReplacement = ruleSet.eras[startingEra]!!.startingMilitaryUnit
} else {
startingUnits = Era().getStartingUnits().toMutableList()
eraUnitReplacement = Era().startingMilitaryUnit
}
// Add extra units granted by difficulty
startingUnits.addAll(when {

View File

@ -120,7 +120,6 @@ class CityInfo {
val ruleset = civInfo.gameInfo.ruleSet
workedTiles = hashSetOf() //reassign 1st working tile
if (startingEra in ruleset.eras)
population.setPopulation(ruleset.eras[startingEra]!!.settlerPopulation)
if (civInfo.religionManager.religionState == ReligionState.Pantheon) {
@ -141,14 +140,12 @@ class CityInfo {
if (civInfo.cities.size == 1) cityConstructions.addBuilding(capitalCityIndicator())
// Add buildings and pop we get from starting in this era
if (startingEra in ruleset.eras) {
for (buildingName in ruleset.eras[startingEra]!!.settlerBuildings) {
val building = ruleset.buildings[buildingName] ?: continue
val uniqueBuilding = civInfo.getEquivalentBuilding(building)
if (uniqueBuilding.isBuildable(cityConstructions))
cityConstructions.addBuilding(uniqueBuilding.name)
}
}
civInfo.policies.tryToAddPolicyBuildings()

View File

@ -113,9 +113,9 @@ class CityStats(val cityInfo: CityInfo) {
for (otherCiv in cityInfo.civInfo.getKnownCivs()) {
if (otherCiv.isCityState() && otherCiv.getDiplomacyManager(cityInfo.civInfo).relationshipLevel() >= RelationshipLevel.Friend) {
val eraInfo = cityInfo.civInfo.getEraObject()
val eraInfo = cityInfo.civInfo.getEra()
if (eraInfo == null || eraInfo.friendBonus[otherCiv.cityStateType.name] == null || eraInfo.allyBonus[otherCiv.cityStateType.name] == null) {
if (eraInfo.friendBonus[otherCiv.cityStateType.name] == null || eraInfo.allyBonus[otherCiv.cityStateType.name] == null) {
// Deprecated, assume Civ V values for compatibility
if (otherCiv.cityStateType == CityStateType.Maritime && otherCiv.getDiplomacyManager(cityInfo.civInfo).relationshipLevel() == RelationshipLevel.Ally)
stats.food += 1

View File

@ -73,7 +73,7 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
// Can be purchased with [Stat] [cityFilter]
if (getMatchingUniques("Can be purchased with [] []")
.any { it.params[0] == stat.name && cityInfo.matchesFilter(it.params[1])}
) return cityInfo.civInfo.gameInfo.ruleSet.eras[cityInfo.civInfo.getEra()]!!.baseUnitBuyCost
) return cityInfo.civInfo.getEra().baseUnitBuyCost
return null
}
}

View File

@ -316,9 +316,8 @@ class CityStateFunctions(val civInfo: CivilizationInfo) {
fun canGiveStat(statType: Stat): Boolean {
if (!civInfo.isCityState())
return false
val eraInfo = civInfo.getEraObject()
val allyBonuses = if (eraInfo == null) null
else eraInfo.allyBonus[civInfo.cityStateType.name]
val eraInfo = civInfo.getEra()
val allyBonuses = eraInfo.allyBonus[civInfo.cityStateType.name]
if (allyBonuses != null) {
// Defined city states in json
val bonuses = allyBonuses + eraInfo!!.friendBonus[civInfo.cityStateType.name]!!

View File

@ -100,16 +100,12 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
.relationshipLevel() >= RelationshipLevel.Friend
) {
val cityStateBonus = Stats()
val eraInfo = civInfo.getEraObject()
val eraInfo = civInfo.getEra()
val relevantBonuses =
when {
eraInfo == null -> null
otherCiv.getDiplomacyManager(civInfo.civName)
.relationshipLevel() == RelationshipLevel.Friend ->
if (otherCiv.getDiplomacyManager(civInfo.civName).relationshipLevel() == RelationshipLevel.Friend)
eraInfo.friendBonus[otherCiv.cityStateType.name]
else -> eraInfo.allyBonus[otherCiv.cityStateType.name]
}
else eraInfo.allyBonus[otherCiv.cityStateType.name]
if (relevantBonuses != null) {
for (bonus in relevantBonuses) {
@ -299,16 +295,11 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
if (otherCiv.isCityState() && otherCiv.getDiplomacyManager(civInfo)
.relationshipLevel() >= RelationshipLevel.Friend
) {
val eraInfo = civInfo.getEraObject()
val eraInfo = civInfo.getEra()
val relevantBonuses =
when {
eraInfo == null -> null
otherCiv.getDiplomacyManager(civInfo)
.relationshipLevel() == RelationshipLevel.Friend ->
if (otherCiv.getDiplomacyManager(civInfo).relationshipLevel() == RelationshipLevel.Friend)
eraInfo.friendBonus[otherCiv.cityStateType.name]
else ->
eraInfo.allyBonus[otherCiv.cityStateType.name]
}
else eraInfo.allyBonus[otherCiv.cityStateType.name]
if (relevantBonuses != null) {
for (bonus in relevantBonuses) {

View File

@ -428,20 +428,18 @@ class CivilizationInfo {
else -> getCivUnits().none()
}
fun getEra(): String {
if (gameInfo.ruleSet.technologies.isEmpty()) return "None"
if (tech.researchedTechnologies.isEmpty())
return gameInfo.ruleSet.getEras().first()
return tech.researchedTechnologies
fun getEra(): Era {
if (gameInfo.ruleSet.technologies.isEmpty() || tech.researchedTechnologies.isEmpty())
return Era()
val eraName = tech.researchedTechnologies
.asSequence()
.map { it.column!! }
.maxByOrNull { it.columnNumber }!!
.era
return gameInfo.ruleSet.eras[eraName]!!
}
fun getEraNumber(): Int = gameInfo.ruleSet.getEraNumber(getEra())
fun getEraObject(): Era? = gameInfo.ruleSet.eras[getEra()]
fun getEraNumber(): Int = getEra().eraNumber
fun isAtWarWith(otherCiv: CivilizationInfo): Boolean {
if (otherCiv.civName == civName) return false // never at war with itself
@ -864,8 +862,9 @@ class CivilizationInfo {
fun getResearchAgreementCost(): Int {
// https://forums.civfanatics.com/resources/research-agreements-bnw.25568/
val era = if (getEra() in gameInfo.ruleSet.eras) gameInfo.ruleSet.eras[getEra()]!! else Era()
return (era.researchAgreementCost * gameInfo.gameParameters.gameSpeed.modifier).toInt()
return (
getEra().researchAgreementCost * gameInfo.gameParameters.gameSpeed.modifier
).toInt()
}
//////////////////////// City State wrapper functions ////////////////////////

View File

@ -114,7 +114,7 @@ class PolicyManager {
if (isAdopted(policy.name)) return false
if (policy.policyBranchType == PolicyBranchType.BranchComplete) return false
if (!getAdoptedPolicies().containsAll(policy.requires!!)) return false
if (checkEra && civInfo.gameInfo.ruleSet.getEraNumber(policy.branch.era) > civInfo.getEraNumber()) return false
if (checkEra && civInfo.gameInfo.ruleSet.eras[policy.branch.era]!!.eraNumber > civInfo.getEraNumber()) return false
if (policy.uniqueObjects.any { it.placeholderText == "Incompatible with []" && adoptedPolicies.contains(it.params[0]) }) return false
return true
}

View File

@ -255,7 +255,7 @@ class TechManager {
knownCiv.addNotification("[${civInfo.civName}] has entered the [$currentEra]!", civInfo.civName, NotificationIcon.Science)
}
}
for (it in getRuleset().policyBranches.values.filter { it.era == currentEra && civInfo.policies.isAdoptable(it) }) {
for (it in getRuleset().policyBranches.values.filter { it.era == currentEra.name && civInfo.policies.isAdoptable(it) }) {
civInfo.addNotification("[" + it.name + "] policy branch unlocked!", NotificationIcon.Culture)
}
}

View File

@ -531,18 +531,15 @@ class DiplomacyManager() {
revertToZero(DiplomaticModifiers.DeclarationOfFriendship, 1 / 2f) //decreases slowly and will revert to full if it is declared later
if (otherCiv().isCityState()) {
val eraInfo = civInfo.getEraObject()
val eraInfo = civInfo.getEra()
if (relationshipLevel() < RelationshipLevel.Friend) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit)) removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
} else {
val relevantBonuses =
when {
eraInfo == null -> null
relationshipLevel() == RelationshipLevel.Friend ->
if (relationshipLevel() == RelationshipLevel.Friend)
eraInfo.friendBonus[otherCiv().cityStateType.name]
else -> eraInfo.allyBonus[otherCiv().cityStateType.name]
}
else eraInfo.allyBonus[otherCiv().cityStateType.name]
if (relevantBonuses == null && otherCiv().cityStateType == CityStateType.Militaristic) {
// Deprecated, assume Civ V values for compatibility

View File

@ -504,9 +504,8 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
ruleSet.policies.contains(filter) ->
if (!civInfo.policies.isAdopted(filter))
rejectionReasons.add(RejectionReason.RequiresPolicy.apply { errorMessage = unique.text })
// ToDo: Fix this when eras.json is required
ruleSet.getEraNumber(filter) != -1 ->
if (civInfo.getEraNumber() < ruleSet.getEraNumber(filter))
ruleSet.eras.contains(filter) ->
if (civInfo.getEraNumber() < ruleSet.eras[filter]!!.eraNumber)
rejectionReasons.add(RejectionReason.UnlockedWithEra.apply { errorMessage = unique.text })
ruleSet.buildings.contains(filter) ->
if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) })
@ -526,7 +525,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
rejectionReasons.add(RejectionReason.CityStateWonder)
val startingEra = civInfo.gameInfo.gameParameters.startingEra
if (startingEra in ruleSet.eras && name in ruleSet.eras[startingEra]!!.startingObsoleteWonders)
if (name in ruleSet.eras[startingEra]!!.startingObsoleteWonders)
rejectionReasons.add(RejectionReason.WonderDisabledEra)
}

View File

@ -7,6 +7,7 @@ import com.unciv.ui.utils.colorFromRGB
class Era : INamed {
override var name: String = ""
var eraNumber: Int = -1
var researchAgreementCost = 300
var startingSettlerCount = 1
var startingSettlerUnit = "Settler" // For mods which have differently named settlers
@ -39,30 +40,4 @@ class Era : INamed {
}
fun getHexColor() = "#" + getColor().toString().substring(0, 6)
companion object {
// User for CS bonuses in case the Eras file is missing (legacy mods)
fun getLegacyCityStateBonusEra(eraNumber: Int) = Era().apply {
val cultureBonus = if (eraNumber in 0..1) 3 else if (eraNumber in 2..3) 6 else 13
val happinessBonus = if (eraNumber in 0..1) 2 else 3
friendBonus[CityStateType.Militaristic.name] =
arrayListOf("Provides military units every [20] turns")
friendBonus[CityStateType.Cultured.name] =
arrayListOf("Provides [$cultureBonus] [Culture] per turn")
friendBonus[CityStateType.Mercantile.name] =
arrayListOf("Provides [$happinessBonus] Happiness")
friendBonus[CityStateType.Maritime.name] =
arrayListOf("Provides [2] [Food] [in capital]")
allyBonus[CityStateType.Militaristic.name] =
arrayListOf("Provides military units every [17] turns")
allyBonus[CityStateType.Cultured.name] =
arrayListOf("Provides [${cultureBonus * 2}] [Culture] per turn")
allyBonus[CityStateType.Mercantile.name] =
arrayListOf("Provides [$happinessBonus] Happiness", "Provides a unique luxury")
allyBonus[CityStateType.Maritime.name] = arrayListOf(
"Provides [2] [Food] [in capital]",
"Provides [1] [Food] [in all cities]"
)
}
}
}

View File

@ -248,7 +248,7 @@ class Nation : INamed, ICivilopediaText, IHasUniques {
textList += FormattedLine("Type: [$cityStateType]", header = 4, color = cityStateType!!.color)
val viewingCiv = UncivGame.Current.gameInfo.currentPlayerCiv
val era = viewingCiv.getEraObject() ?: Era.getLegacyCityStateBonusEra(viewingCiv.getEraNumber())
val era = viewingCiv.getEra()
var showResources = false
val friendBonus = era.friendBonus[cityStateType!!.name]

View File

@ -54,7 +54,7 @@ open class Policy : INamed, IHasUniques, ICivilopediaText {
override fun replacesCivilopediaDescription() = true
override fun hasCivilopediaTextLines() = true
override fun getSortGroup(ruleset: Ruleset) =
ruleset.getEraNumber(branch.era) * 10000 +
ruleset.eras[branch.era]!!.eraNumber * 10000 +
ruleset.policyBranches.keys.indexOf(branch.name) * 100 +
policyBranchType.ordinal

View File

@ -179,6 +179,10 @@ class Ruleset {
val erasFile = folderHandle.child("Eras.json")
if (erasFile.exists()) eras += createHashmap(jsonParser.getFromJson(Array<Era>::class.java, erasFile))
// While `eras.values.toList()` might seem more logical, eras.values is a MutableCollection and
// therefore does not guarantee keeping the order of elements like a LinkedHashMap does.
// Using a map sidesteps this problem
eras.map { it.value }.withIndex().forEach { it.value.eraNumber = it.index }
val unitTypesFile = folderHandle.child("UnitTypes.json")
if (unitTypesFile.exists()) unitTypes += createHashmap(jsonParser.getFromJson(Array<UnitType>::class.java, unitTypesFile))
@ -251,10 +255,8 @@ class Ruleset {
}
}
fun getEras(): List<String> = technologies.values.map { it.column!!.era }.distinct()
fun hasReligion() = beliefs.any() && modWithReligionLoaded
fun getEraNumber(era: String) = getEras().indexOf(era)
fun getSummary(): String {
val stringList = ArrayList<String>()
if (modOptions.isBaseRuleset) stringList += "Base Ruleset"
@ -428,15 +430,15 @@ class Ruleset {
warningCount++
}
}
// eras.isNotEmpty() is only for mod compatibility, it should be removed at some point.
if (eras.isNotEmpty() && tech.era() !in eras)
if (tech.era() !in eras)
lines += "Unknown era ${tech.era()} referenced in column of tech ${tech.name}"
}
if(eras.isEmpty()){
lines += "Eras file is empty! This mod will be unusable in the near future!"
if (eras.isEmpty()) {
lines += "Eras file is empty! This will likely lead to crashes. Ask the mod maker to update this mod!"
warningCount++
}
for (era in eras) {
for (wonder in era.value.startingObsoleteWonders)
if (wonder !in buildings)

View File

@ -220,7 +220,7 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
.any {
matchesFilter(it.params[0])
&& cityInfo.matchesFilter(it.params[3])
&& cityInfo.civInfo.getEraNumber() >= ruleset.getEraNumber(it.params[4])
&& cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber
&& it.params[2] == stat.name
}
) return true
@ -241,7 +241,7 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
.filter {
matchesFilter(it.params[0])
&& cityInfo.matchesFilter(it.params[3])
&& cityInfo.civInfo.getEraNumber() >= ruleset.getEraNumber(it.params[4])
&& cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber
&& it.params[2] == stat.name
}.map {
getCostForConstructionsIncreasingInPrice(
@ -334,9 +334,8 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
ruleSet.policies.contains(filter) ->
if (!civInfo.policies.isAdopted(filter))
rejectionReasons.add(RejectionReason.RequiresPolicy.apply { errorMessage = unique.text })
// ToDo: Fix this when eras.json is required
ruleSet.getEraNumber(filter) != -1 ->
if (civInfo.getEraNumber() < ruleSet.getEraNumber(filter))
ruleSet.eras.contains(filter) ->
if (civInfo.getEraNumber() < ruleSet.eras[filter]!!.eraNumber)
rejectionReasons.add(RejectionReason.UnlockedWithEra.apply { errorMessage = unique.text })
ruleSet.buildings.contains(filter) ->
if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) })
@ -392,7 +391,7 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
.any {
matchesFilter(it.params[0])
&& cityConstructions.cityInfo.matchesFilter(it.params[3])
&& cityConstructions.cityInfo.civInfo.getEraNumber() >= ruleset.getEraNumber(it.params[4])
&& cityConstructions.cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber
&& it.params[2] == boughtWith.name
}
) {

View File

@ -178,6 +178,11 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
if (stopBackgroundTasks) return@postRunnable
repo.name = repo.name.replace('-', ' ')
// Mods we have manually decided to remove for instability are removed here
// If at some later point these mods are updated, we should definitely remove
// this piece of code. This is a band-aid, not a full solution.
if (repo.html_url in modsToHide) continue
modDescriptionsOnline[repo.name] =
(repo.description ?: "-{No description provided}-".tr()) +
"\n" + "[${repo.stargazers_count}]✯".tr()
@ -503,4 +508,9 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
game.setScreen(ModManagementScreen())
}
}
companion object {
private val blockedModsFile = FileHandle("jsons/ManuallyBlockedMods.json")
val modsToHide = JsonParser().getFromJson(Array<String>::class.java, blockedModsFile)
}
}

View File

@ -239,7 +239,7 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
var locationToCheck = baseLocation
if (tileInfo.owningCity != null) {
val ownersEra = tileInfo.getOwner()!!.getEra()
val eraSpecificLocation = tileSetStrings.getString(locationToCheck, tileSetStrings.tag, ownersEra)
val eraSpecificLocation = tileSetStrings.getString(locationToCheck, tileSetStrings.tag, ownersEra.name)
if (ImageGetter.imageExists(eraSpecificLocation))
locationToCheck = eraSpecificLocation
}

View File

@ -177,7 +177,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
diplomacyTable.add(nextLevelString.toLabel()).row()
}
val eraInfo = viewingCiv.getEraObject() ?: Era.getLegacyCityStateBonusEra(viewingCiv.getEraNumber())
val eraInfo = viewingCiv.getEra()
var friendBonusText = "{When Friends:} ".tr()
val friendBonuses = eraInfo.friendBonus[otherCiv.cityStateType.name]

View File

@ -347,10 +347,7 @@ object ImageGetter {
fun getTechIconGroup(techName: String, circleSize: Float) = getTechIcon(techName).surroundWithCircle(circleSize)
fun getTechIcon(techName: String): Image {
val era = ruleset.technologies[techName]!!.era()
val techIconColor =
if (era !in ruleset.eras) Era().getColor()
else ruleset.eras[ruleset.technologies[techName]!!.era()]!!.getColor()
val techIconColor = ruleset.eras[ruleset.technologies[techName]!!.era()]!!.getColor()
return getImage("TechIcons/$techName").apply { color = techIconColor.lerp(Color.BLACK, 0.6f) }
}

View File

@ -49,6 +49,7 @@ class SerializationTests {
players.add(Player("Rome").apply { playerType = PlayerType.Human })
players.add(Player("Greece"))
religionEnabled = true
startingEra = "Ancient era"
}
val mapParameters = MapParameters().apply {
mapSize = MapSizeNew(MapSize.Tiny)