From d25b1c8c410c995f43e6c367d46d5f002458d324 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Thu, 1 Feb 2024 22:24:59 +0100 Subject: [PATCH] Fix ModOptions unique parameter types not checked and "uniquetype" ModOptionsConstants (#10930) * Kill evil ModOptionsConstants * UniqueFlag to EnumSet and add `NoConditionals` * Linting or import reorder * Fix ModOptions unique parameter types not checked * ModOptions Unique to suppress validation warnings * Silence spurious RulesetValidator complaints about Denmark * Revert "ModOptions Unique to suppress validation warnings" --- core/src/com/unciv/logic/GameInfo.kt | 5 ++-- core/src/com/unciv/logic/GameStarter.kt | 3 +-- .../civilization/NextTurnAutomation.kt | 9 ++++---- core/src/com/unciv/logic/city/City.kt | 6 ++--- core/src/com/unciv/logic/city/CityStats.kt | 3 +-- .../civilization/managers/TurnManager.kt | 5 +--- .../com/unciv/logic/trade/TradeEvaluation.kt | 23 +++++++++---------- core/src/com/unciv/logic/trade/TradeLogic.kt | 8 +++---- .../com/unciv/models/ruleset/ModOptions.kt | 10 -------- .../unciv/models/ruleset/unique/UniqueFlag.kt | 7 +++++- .../ruleset/unique/UniqueParameterType.kt | 11 ++++++--- .../unciv/models/ruleset/unique/UniqueType.kt | 23 ++++++++++++++----- .../ruleset/validation/RulesetValidator.kt | 14 ++++++----- .../ruleset/validation/UniqueValidator.kt | 15 +++++++++++- .../CityStateDiplomacyTable.kt | 3 +-- .../diplomacyscreen/MajorCivDiplomacyTable.kt | 4 ++-- .../overviewscreen/StatsOverviewTab.kt | 7 +++--- docs/Modders/uniques.md | 23 +++++++++++++++++++ 18 files changed, 107 insertions(+), 72 deletions(-) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index b37d6a69ee..3f9afe62ad 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -24,12 +24,12 @@ import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.managers.TechManager import com.unciv.logic.civilization.managers.TurnManager import com.unciv.logic.civilization.managers.VictoryManager +import com.unciv.logic.github.Github.repoNameToFolderName import com.unciv.logic.map.CityDistanceData import com.unciv.logic.map.TileMap import com.unciv.logic.map.tile.Tile import com.unciv.models.Religion import com.unciv.models.metadata.GameParameters -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.Speed @@ -39,7 +39,6 @@ import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.tr import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicTrackChooserFlags -import com.unciv.logic.github.Github.repoNameToFolderName import com.unciv.ui.screens.savescreens.Gzip import com.unciv.ui.screens.worldscreen.status.NextTurnProgress import com.unciv.utils.DebugUtils @@ -287,7 +286,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion fun isReligionEnabled(): Boolean { val religionDisabledByRuleset = (ruleset.eras[gameParameters.startingEra]!!.hasUnique(UniqueType.DisablesReligion) - || ruleset.modOptions.uniques.contains(ModOptionsConstants.disableReligion)) + || ruleset.modOptions.hasUnique(UniqueType.DisableReligion)) return !religionDisabledByRuleset } diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 5d73b76c69..6b6a88f9ec 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -14,7 +14,6 @@ import com.unciv.logic.map.tile.Tile import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.Player -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.unique.StateForConditionals @@ -483,7 +482,7 @@ object GameStarter { settlerLikeUnits: Map ) { // Adjust starting units for city states - if (civ.isCityState() && !gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.allowCityStatesSpawnUnits)) { + if (civ.isCityState() && !gameInfo.ruleset.modOptions.hasUnique(UniqueType.AllowCityStatesSpawnUnits)) { val startingSettlers = startingUnits.filter { settlerLikeUnits.contains(it) } startingUnits.clear() diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index dd413cde8b..b807cf5499 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -17,7 +17,6 @@ import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.logic.civilization.managers.EspionageManager import com.unciv.logic.map.mapunit.MapUnit import com.unciv.models.ruleset.MilestoneType -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Policy import com.unciv.models.ruleset.PolicyBranch import com.unciv.models.ruleset.Victory @@ -41,7 +40,7 @@ object NextTurnAutomation { TradeAutomation.respondToTradeRequests(civInfo) if (civInfo.isMajorCiv()) { - if (!civInfo.gameInfo.ruleset.modOptions.hasUnique(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { + if (!civInfo.gameInfo.ruleset.modOptions.hasUnique(UniqueType.DiplomaticRelationshipsCannotChange)) { DiplomacyAutomation.declareWar(civInfo) DiplomacyAutomation.offerPeaceTreaty(civInfo) DiplomacyAutomation.offerDeclarationOfFriendship(civInfo) @@ -467,10 +466,10 @@ object NextTurnAutomation { val bestCity = civInfo.cities.filterNot { it.isPuppet } // If we can build workers, then we want AT LEAST 2 improvements, OR a worker nearby. // Otherwise, AI tries to produce settlers when it can hardly sustain itself - .filter { + .filter { city -> !workersBuildableForThisCiv - || it.getCenterTile().getTilesInDistance(2).count { it.improvement!=null } > 1 - || it.getCenterTile().getTilesInDistance(3).any { it.civilianUnit?.hasUnique(UniqueType.BuildImprovements)==true } + || city.getCenterTile().getTilesInDistance(2).count { it.improvement!=null } > 1 + || city.getCenterTile().getTilesInDistance(3).any { it.civilianUnit?.hasUnique(UniqueType.BuildImprovements)==true } } .maxByOrNull { it.cityStats.currentCityStats.production } ?: return diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index 602ecc5502..72fc92b31c 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -18,7 +18,6 @@ import com.unciv.logic.map.tile.RoadStatus import com.unciv.logic.map.tile.Tile import com.unciv.models.Counter import com.unciv.models.ruleset.Building -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType @@ -35,7 +34,6 @@ enum class CityFlags { class City : IsPartOfGameInfoSerialization { - @Suppress("JoinDeclarationAndAssignment") @Transient lateinit var civ: Civilization @@ -307,8 +305,8 @@ class City : IsPartOfGameInfoSerialization { fun canBeDestroyed(justCaptured: Boolean = false): Boolean { if (civ.gameInfo.gameParameters.noCityRazing) return false - val allowRazeCapital = civ.gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.allowRazeCapital) - val allowRazeHolyCity = civ.gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.allowRazeHolyCity) + val allowRazeCapital = civ.gameInfo.ruleset.modOptions.hasUnique(UniqueType.AllowRazeCapital) + val allowRazeHolyCity = civ.gameInfo.ruleset.modOptions.hasUnique(UniqueType.AllowRazeHolyCity) if (isOriginalCapital && !allowRazeCapital) return false if (isHolyCity() && !allowRazeHolyCity) return false diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index fdafb26c66..29195eafcd 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -5,7 +5,6 @@ import com.unciv.models.Counter import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.IConstruction import com.unciv.models.ruleset.INonPerpetualConstruction -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique @@ -523,7 +522,7 @@ class CityStats(val city: City) { } // AFTER we've gotten all the gold stats figured out, only THEN do we plonk that gold into Science - if (city.getRuleset().modOptions.uniques.contains(ModOptionsConstants.convertGoldToScience)) { + if (city.getRuleset().modOptions.hasUnique(UniqueType.ConvertGoldToScience)) { val amountConverted = (newFinalStatList.values.sumOf { it.gold.toDouble() } * city.civ.tech.goldPercentConvertedToScience).toInt().toFloat() if (amountConverted > 0) // Don't want you converting negative gold to negative science yaknow diff --git a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt index 5d1f1c564d..b6df58531b 100644 --- a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt @@ -15,7 +15,6 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyTurnManager.nextTurn import com.unciv.logic.map.mapunit.UnitTurnManager import com.unciv.logic.map.tile.Tile import com.unciv.logic.trade.TradeEvaluation -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType @@ -42,7 +41,6 @@ class TurnManager(val civInfo: Civilization) { if (civInfo.cities.isNotEmpty() && civInfo.gameInfo.ruleset.technologies.isNotEmpty()) civInfo.tech.updateResearchProgress() - civInfo.cache.updateCivResources() // If you offered a trade last turn, this turn it will have been accepted/declined for (stockpiledResource in civInfo.getCivResourceSupply().filter { it.resource.isStockpiled() }) civInfo.resourceStockpiles.add(stockpiledResource.resource.name, stockpiledResource.amount) @@ -52,8 +50,7 @@ class TurnManager(val civInfo: Civilization) { civInfo.updateStatsForNextTurn() // for things that change when turn passes e.g. golden age, city state influence // Do this after updateStatsForNextTurn but before cities.startTurn - if (civInfo.playerType == PlayerType.AI && civInfo.gameInfo.ruleset.modOptions.uniques.contains( - ModOptionsConstants.convertGoldToScience)) + if (civInfo.playerType == PlayerType.AI && civInfo.gameInfo.ruleset.modOptions.hasUnique(UniqueType.ConvertGoldToScience)) NextTurnAutomation.automateGoldToSciencePercentage(civInfo) // Generate great people at the start of the turn, diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 140bdc1794..9802f2a39e 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -8,7 +8,6 @@ import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.logic.map.tile.Tile -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType @@ -64,7 +63,7 @@ class TradeEvaluation { } fun getTradeAcceptability(trade: Trade, evaluator: Civilization, tradePartner: Civilization): Int { - val citiesAskedToSurrender = trade.ourOffers.filter { it.type == TradeType.City }.count() + val citiesAskedToSurrender = trade.ourOffers.count { it.type == TradeType.City } val maxCitiesToSurrender = ceil(evaluator.cities.size.toFloat() / 5).toInt() if (citiesAskedToSurrender > maxCitiesToSurrender) { return Int.MIN_VALUE @@ -100,7 +99,7 @@ class TradeEvaluation { return evaluateBuyCost(offer, civInfo, tradePartner) } - fun evaluateBuyCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int { + private fun evaluateBuyCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int { when (offer.type) { TradeType.Gold -> return offer.amount // GPT loses 1% of value for each 'future' turn, meaning: gold now is more valuable than gold in the future @@ -151,13 +150,13 @@ class TradeEvaluation { val civToDeclareWarOn = civInfo.gameInfo.getCivilization(offer.name) val threatToThem = Automation.threatAssessment(civInfo, civToDeclareWarOn) - if (!civInfo.isAtWarWith(civToDeclareWarOn)) return 0 // why should we pay you to go fight someone...? + return if (!civInfo.isAtWarWith(civToDeclareWarOn)) 0 // why should we pay you to go fight someone...? else when (threatToThem) { - ThreatLevel.VeryLow -> return 0 - ThreatLevel.Low -> return 0 - ThreatLevel.Medium -> return 100 - ThreatLevel.High -> return 500 - ThreatLevel.VeryHigh -> return 1000 + ThreatLevel.VeryLow -> 0 + ThreatLevel.Low -> 0 + ThreatLevel.Medium -> 100 + ThreatLevel.High -> 500 + ThreatLevel.VeryHigh -> 1000 } } TradeType.City -> { @@ -209,7 +208,7 @@ class TradeEvaluation { return evaluateSellCost(offer, civInfo, tradePartner) } - fun evaluateSellCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int { + private fun evaluateSellCost(offer: TradeOffer, civInfo: Civilization, tradePartner: Civilization): Int { when (offer.type) { TradeType.Gold -> return offer.amount TradeType.Gold_Per_Turn -> return offer.amount * offer.duration @@ -307,7 +306,7 @@ class TradeEvaluation { * This returns how much one gold is worth now in comparison to starting out the game * Gold is worth less as the civilization has a higher income */ - fun getGoldInflation(civInfo: Civilization): Double { + private fun getGoldInflation(civInfo: Civilization): Double { val modifier = 1000.0 val goldPerTurn = civInfo.stats.statsForNextTurn.gold.toDouble() // To visualise the function, plug this into a 2d graphing calculator \frac{1000}{x^{1.2}+1.11*1000} @@ -371,7 +370,7 @@ class TradeEvaluation { } private fun introductionValue(ruleSet: Ruleset): Int { - val unique = ruleSet.modOptions.getMatchingUniques(ModOptionsConstants.tradeCivIntroductions).firstOrNull() + val unique = ruleSet.modOptions.getMatchingUniques(UniqueType.TradeCivIntroductions).firstOrNull() ?: return 0 return unique.params[0].toInt() } diff --git a/core/src/com/unciv/logic/trade/TradeLogic.kt b/core/src/com/unciv/logic/trade/TradeLogic.kt index 142295af5e..c763764d66 100644 --- a/core/src/com/unciv/logic/trade/TradeLogic.kt +++ b/core/src/com/unciv/logic/trade/TradeLogic.kt @@ -7,8 +7,6 @@ import com.unciv.logic.civilization.PopupAlert import com.unciv.logic.civilization.diplomacy.CityStateFunctions import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers -import com.unciv.logic.civilization.diplomacy.DiplomaticStatus -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unique.UniqueType @@ -59,7 +57,7 @@ class TradeLogic(val ourCivilization:Civilization, val otherCivilization: Civili val otherCivsWeKnow = civInfo.getKnownCivs() .filter { it.civName != otherCivilization.civName && it.isMajorCiv() && !it.isDefeated() } - if (civInfo.gameInfo.ruleset.modOptions.hasUnique(ModOptionsConstants.tradeCivIntroductions)) { + if (civInfo.gameInfo.ruleset.modOptions.hasUnique(UniqueType.TradeCivIntroductions)) { val civsWeKnowAndTheyDont = otherCivsWeKnow .filter { !otherCivilization.diplomacy.containsKey(it.civName) && !it.isDefeated() } for (thirdCiv in civsWeKnowAndTheyDont) { @@ -68,7 +66,7 @@ class TradeLogic(val ourCivilization:Civilization, val otherCivilization: Civili } if (!civInfo.isCityState() && !otherCivilization.isCityState() - && !civInfo.gameInfo.ruleset.modOptions.hasUnique(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { + && !civInfo.gameInfo.ruleset.modOptions.hasUnique(UniqueType.DiplomaticRelationshipsCannotChange)) { val civsWeBothKnow = otherCivsWeKnow .filter { otherCivilization.diplomacy.containsKey(it.civName) } val civsWeArentAtWarWith = civsWeBothKnow @@ -134,7 +132,7 @@ class TradeLogic(val ourCivilization:Civilization, val otherCivilization: Civili from.getDiplomacyManager(to) .setFlag(DiplomacyFlags.ResearchAgreement, offer.duration) } - if (offer.name == Constants.defensivePact) to.getDiplomacyManager(from).signDefensivePact(offer.duration); + if (offer.name == Constants.defensivePact) to.getDiplomacyManager(from).signDefensivePact(offer.duration) } TradeType.Introduction -> to.diplomacyFunctions.makeCivilizationsMeet(to.gameInfo.getCivilization(offer.name)) TradeType.WarDeclaration -> { diff --git a/core/src/com/unciv/models/ruleset/ModOptions.kt b/core/src/com/unciv/models/ruleset/ModOptions.kt index 0b3c1a1b9e..acba0e6790 100644 --- a/core/src/com/unciv/models/ruleset/ModOptions.kt +++ b/core/src/com/unciv/models/ruleset/ModOptions.kt @@ -6,16 +6,6 @@ import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueMap import com.unciv.models.ruleset.unique.UniqueTarget -object ModOptionsConstants { - const val diplomaticRelationshipsCannotChange = "Diplomatic relationships cannot change" - const val convertGoldToScience = "Can convert gold to science with sliders" - const val allowCityStatesSpawnUnits = "Allow City States to spawn with additional units" - const val tradeCivIntroductions = "Can trade civilization introductions for [] Gold" - const val disableReligion = "Disable religion" - const val allowRazeCapital = "Allow raze capital" - const val allowRazeHolyCity = "Allow raze holy city" -} - class ModOptions : IHasUniques { //region Modder choices var isBaseRuleset = false diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueFlag.kt b/core/src/com/unciv/models/ruleset/unique/UniqueFlag.kt index a17674ef71..39a0afbdeb 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueFlag.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueFlag.kt @@ -1,9 +1,14 @@ package com.unciv.models.ruleset.unique +import java.util.EnumSet + enum class UniqueFlag { HiddenToUsers, + NoConditionals, ; companion object { - val setOfHiddenToUsers = listOf(HiddenToUsers) + val none: EnumSet = EnumSet.noneOf(UniqueFlag::class.java) + val setOfHiddenToUsers: EnumSet = EnumSet.of(HiddenToUsers) + val setOfNoConditionals: EnumSet = EnumSet.of(NoConditionals) } } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index fde3d7c775..e258ddaf0d 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -588,9 +588,14 @@ enum class UniqueParameterType( /** Mod declarative compatibility: Define Mod relations by their name. */ ModName("modFilter", "DeCiv Redux", """A Mod name, case-sensitive _or_ a simple wildcard filter beginning and ending in an Asterisk, case-insensitive""", "Mod name filter") { override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): - UniqueType.UniqueParameterErrorSeverity? = - if ('-' !in parameterText && ('*' !in parameterText || parameterText.matches(Regex("""^\*[^*]+\*$""")))) null - else UniqueType.UniqueParameterErrorSeverity.RulesetInvariant + UniqueType.UniqueParameterErrorSeverity? = when { + BaseRuleset.values().any { it.fullName == parameterText } -> null // Only Builtin ruleset names can contain '-' + parameterText == "*Civ V -*" || parameterText == "*Civ V - *" -> null // Wildcard filter for builtin + '-' in parameterText -> UniqueType.UniqueParameterErrorSeverity.RulesetInvariant + parameterText.matches(Regex("""^\*[^*]+\*$""")) -> null + parameterText.startsWith('*') || parameterText.endsWith('*') -> UniqueType.UniqueParameterErrorSeverity.RulesetInvariant + else -> null + } override fun getTranslationWriterStringsForOutput() = scanExistingValues(this) }, diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index d599e6ef40..38938f4550 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -5,6 +5,7 @@ import com.unciv.models.ruleset.validation.RulesetErrorSeverity import com.unciv.models.ruleset.validation.RulesetValidator import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText +import java.util.EnumSet // I didn't put this in a companion object because APPARENTLY doing that means you can't use it in the init function. private val numberRegex = Regex("\\d+$") // Any number of trailing digits @@ -12,7 +13,7 @@ private val numberRegex = Regex("\\d+$") // Any number of trailing digits enum class UniqueType( val text: String, vararg targets: UniqueTarget, - val flags: List = emptyList(), + val flags: EnumSet = UniqueFlag.none, val docDescription: String? = null ) { @@ -807,17 +808,27 @@ enum class UniqueType( Comment("Comment [comment]", *UniqueTarget.Displayable, docDescription = "Allows displaying arbitrary text in a Unique listing. Only the text within the '[]' brackets will be displayed, the rest serves to allow Ruleset validation to recognize the intent."), + // Formerly `ModOptionsConstants` + DiplomaticRelationshipsCannotChange("Diplomatic relationships cannot change", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + ConvertGoldToScience("Can convert gold to science with sliders", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + AllowCityStatesSpawnUnits("Allow City States to spawn with additional units", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + TradeCivIntroductions("Can trade civilization introductions for [positiveAmount] Gold", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + DisableReligion("Disable religion", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + AllowRazeCapital("Allow raze capital", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + AllowRazeHolyCity("Allow raze holy city", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + // Declarative Mod compatibility (see [ModCompatibility]): // Note there is currently no display for these, but UniqueFlag.HiddenToUsers is not set. // That means we auto-template and ask our translators for a translation that is currently unused. //todo To think over - leave as is for future use or remove templates and translations by adding the flag? - ModIncompatibleWith("Mod is incompatible with [modFilter]", UniqueTarget.ModOptions, + + ModIncompatibleWith("Mod is incompatible with [modFilter]", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals, docDescription = "Specifies that your Mod is incompatible with another. Always treated symmetrically, and cannot be overridden by the Mod you are declaring as incompatible."), - ModRequires("Mod requires [modFilter]", UniqueTarget.ModOptions, + ModRequires("Mod requires [modFilter]", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals, docDescription = "Specifies that your Extension Mod is only available if any other Mod matching the filter is active."), - ModIsAudioVisualOnly("Should only be used as permanent audiovisual mod", UniqueTarget.ModOptions), - ModIsAudioVisual("Can be used as permanent audiovisual mod", UniqueTarget.ModOptions), - ModIsNotAudioVisual("Cannot be used as permanent audiovisual mod", UniqueTarget.ModOptions), + ModIsAudioVisualOnly("Should only be used as permanent audiovisual mod", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + ModIsAudioVisual("Can be used as permanent audiovisual mod", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), + ModIsNotAudioVisual("Cannot be used as permanent audiovisual mod", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), // endregion diff --git a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt index b2bf7bae50..7550e2f465 100644 --- a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt @@ -39,7 +39,7 @@ class RulesetValidator(val ruleset: Ruleset) { val lines = RulesetErrorList() // When not checking the entire ruleset, we can only really detect ruleset-invariant errors in uniques - addModOptionsErrors(lines) + addModOptionsErrors(lines, tryFixUnknownUniques) uniqueValidator.checkUniques(ruleset.globalUniques, lines, false, tryFixUnknownUniques) addUnitErrorsRulesetInvariant(lines, tryFixUnknownUniques) addTechErrorsRulesetInvariant(lines, tryFixUnknownUniques) @@ -63,7 +63,7 @@ class RulesetValidator(val ruleset: Ruleset) { uniqueValidator.populateFilteringUniqueHashsets() val lines = RulesetErrorList() - addModOptionsErrors(lines) + addModOptionsErrors(lines, tryFixUnknownUniques) uniqueValidator.checkUniques(ruleset.globalUniques, lines, true, tryFixUnknownUniques) addUnitErrorsBaseRuleset(lines, tryFixUnknownUniques) @@ -94,8 +94,12 @@ class RulesetValidator(val ruleset: Ruleset) { return lines } - private fun addModOptionsErrors(lines: RulesetErrorList) { - if (ruleset.name.isBlank()) return // These tests don't make sense for combined rulesets + private fun addModOptionsErrors(lines: RulesetErrorList, tryFixUnknownUniques: Boolean) { + // Basic Unique validation (type, target, parameters) should always run. + // Using reportRulesetSpecificErrors=true as ModOptions never should use Uniques depending on objects from a base ruleset anyway. + uniqueValidator.checkUniques(ruleset.modOptions, lines, reportRulesetSpecificErrors = true, tryFixUnknownUniques) + + if (ruleset.name.isBlank()) return // The rest of these tests don't make sense for combined rulesets val audioVisualUniqueTypes = setOf( UniqueType.ModIsAudioVisual, @@ -825,6 +829,4 @@ class RulesetValidator(val ruleset: Ruleset) { recursiveCheck(hashSetOf(), promotion, 0) } } - - } diff --git a/core/src/com/unciv/models/ruleset/validation/UniqueValidator.kt b/core/src/com/unciv/models/ruleset/validation/UniqueValidator.kt index d6e413cd94..d1638d7759 100644 --- a/core/src/com/unciv/models/ruleset/validation/UniqueValidator.kt +++ b/core/src/com/unciv/models/ruleset/validation/UniqueValidator.kt @@ -7,6 +7,7 @@ import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.unique.IHasUniques import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueComplianceError +import com.unciv.models.ruleset.unique.UniqueFlag import com.unciv.models.ruleset.unique.UniqueParameterType import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType @@ -84,7 +85,10 @@ class UniqueValidator(val ruleset: Ruleset) { addConditionalErrors(conditional, rulesetErrors, prefix, unique, reportRulesetSpecificErrors) } - if (unique.conditionals.any() && unique.type in MapUnitCache.UnitMovementUniques) + if (unique.type in MapUnitCache.UnitMovementUniques + && unique.conditionals.any { it.type != UniqueType.ConditionalOurUnit || it.params[0] != "All" } + ) + // (Stay silent if the only conditional is `` - as in G&K Denmark) // Not necessarily even a problem, but yes something mod maker should be aware of rulesetErrors.add("$prefix unique \"${unique.text}\" contains a conditional on a unit movement unique. " + "Due to performance considerations, this unique is cached on the unit," + @@ -105,6 +109,15 @@ class UniqueValidator(val ruleset: Ruleset) { unique: Unique, reportRulesetSpecificErrors: Boolean ) { + if (unique.hasFlag(UniqueFlag.NoConditionals)) { + rulesetErrors.add( + "$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"," + + " but the unique does not accept conditionals!", + RulesetErrorSeverity.Error + ) + return + } + if (conditional.type == null) { rulesetErrors.add( "$prefix unique \"${unique.text}\" contains the conditional \"${conditional.text}\"," + diff --git a/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt b/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt index 7cff0d19ab..728f2162d9 100644 --- a/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt +++ b/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt @@ -18,7 +18,6 @@ import com.unciv.logic.civilization.managers.AssignedQuest import com.unciv.logic.trade.TradeLogic import com.unciv.logic.trade.TradeOffer import com.unciv.logic.trade.TradeType -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Quest import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unique.UniqueType @@ -70,7 +69,7 @@ class CityStateDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) { if (diplomacyScreen.isNotPlayersTurn() || viewingCiv.isAtWarWith(otherCiv)) demandTributeButton.disable() val diplomacyManager = viewingCiv.getDiplomacyManager(otherCiv) - if (!viewingCiv.gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { + if (!viewingCiv.gameInfo.ruleset.modOptions.hasUnique(UniqueType.DiplomaticRelationshipsCannotChange)) { if (viewingCiv.isAtWarWith(otherCiv)) diplomacyTable.add(getNegotiatePeaceCityStateButton(otherCiv, diplomacyManager)).row() else diplomacyTable.add(diplomacyScreen.getDeclareWarButton(diplomacyManager, otherCiv)).row() diff --git a/core/src/com/unciv/ui/screens/diplomacyscreen/MajorCivDiplomacyTable.kt b/core/src/com/unciv/ui/screens/diplomacyscreen/MajorCivDiplomacyTable.kt index 06c5ee4348..ba9793bb9a 100644 --- a/core/src/com/unciv/ui/screens/diplomacyscreen/MajorCivDiplomacyTable.kt +++ b/core/src/com/unciv/ui/screens/diplomacyscreen/MajorCivDiplomacyTable.kt @@ -14,7 +14,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.logic.trade.TradeOffer import com.unciv.logic.trade.TradeType -import com.unciv.models.ruleset.ModOptionsConstants +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.tr import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.disable @@ -48,7 +48,7 @@ class MajorCivDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) { diplomacyTable.addSeparator() val diplomaticRelationshipsCanChange = - !viewingCiv.gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.diplomaticRelationshipsCannotChange) + !viewingCiv.gameInfo.ruleset.modOptions.hasUnique(UniqueType.DiplomaticRelationshipsCannotChange) val diplomacyManager = viewingCiv.getDiplomacyManager(otherCiv) diff --git a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt index 1deb48368d..23fac93c26 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt @@ -5,15 +5,14 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.Constants import com.unciv.GUI import com.unciv.logic.civilization.Civilization -import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.models.stats.StatMap -import com.unciv.ui.components.widgets.TabbedPager -import com.unciv.ui.components.widgets.UncivSlider import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.widgets.TabbedPager +import com.unciv.ui.components.widgets.UncivSlider import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.MarkupRenderer @@ -53,7 +52,7 @@ class StatsOverviewTab( unhappinessTable.update() goldAndSliderTable.add(goldTable).row() - if (gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.convertGoldToScience)) + if (gameInfo.ruleset.modOptions.hasUnique(UniqueType.ConvertGoldToScience)) goldAndSliderTable.addGoldSlider() update() diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index a26c35161d..b8802342d5 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -1805,6 +1805,29 @@ Due to performance considerations, this unique is cached, thus conditionals may Applicable to: CityState ## ModOptions uniques +??? example "Diplomatic relationships cannot change" + Applicable to: ModOptions + +??? example "Can convert gold to science with sliders" + Applicable to: ModOptions + +??? example "Allow City States to spawn with additional units" + Applicable to: ModOptions + +??? example "Can trade civilization introductions for [positiveAmount] Gold" + Example: "Can trade civilization introductions for [3] Gold" + + Applicable to: ModOptions + +??? example "Disable religion" + Applicable to: ModOptions + +??? example "Allow raze capital" + Applicable to: ModOptions + +??? example "Allow raze holy city" + Applicable to: ModOptions + ??? example "Mod is incompatible with [modFilter]" Specifies that your Mod is incompatible with another. Always treated symmetrically, and cannot be overridden by the Mod you are declaring as incompatible. Example: "Mod is incompatible with [DeCiv Redux]"