mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 15:29:32 +07:00
Treat remaining untyped Uniques in default rulesets (#9763)
* Treat remaining untyped Uniques in default rulesets, make unit test catch them * Change untyped filtering Uniques check to Validation by inclusion in GlobalUniques instead of UniqueType.AircraftMarker * Wiki for untyped filtering Uniques * Re-include the "Who knows" of Future Tech on the Tech picker
This commit is contained in:
@ -7,7 +7,10 @@
|
||||
"[-33]% Strength <for [All] units> <when below [-10] Happiness>",
|
||||
"Cannot build [Settler] units <when below [-10] Happiness>",
|
||||
"Rebel units may spawn <when below [-20] Happiness>",
|
||||
"[-1] Sight <for [Embarked] units>"
|
||||
"[-1] Sight <for [Embarked] units>",
|
||||
|
||||
// Filtering uniques must be listed here to tell RulesetValidator they're OK despite untyped
|
||||
"Aircraft"
|
||||
|
||||
// TODO: Implement the uniques below
|
||||
// "[+20]% [Culture] [in all cities] <during a golden age>",
|
||||
|
@ -68,7 +68,7 @@
|
||||
"Paros","Elis","Syracuse","Herakleia","Gortyn","Chalkis","Pylos","Pella","Naxos","Sicyon",
|
||||
"Larissa","Apollonia","Messene","Orchomenos","Ambracia","Kos","Knidos","Amphipolis",
|
||||
"Patras","Lamia","Nafplion","Apolyton"],
|
||||
"spyNames": ["Jason", "Helena", "Alexa", "Cletus", "Kassandra", "Andres", "Desdemona", "Anthea", "Aeneas", "Leander",]
|
||||
"spyNames": ["Jason", "Helena", "Alexa", "Cletus", "Kassandra", "Andres", "Desdemona", "Anthea", "Aeneas", "Leander"]
|
||||
},
|
||||
{
|
||||
"name": "China",
|
||||
@ -246,7 +246,7 @@
|
||||
"Satricum","Ardea","Ostia","Velitrae","Viroconium","Tarentum","Brundisium","Caesaraugusta","Caesarea","Palmyra",
|
||||
"Signia","Aquileia","Clusium","Sutrium","Cremona","Placentia","Hispalis","Artaxata","Aurelianorum","Nicopolis",
|
||||
"Agrippina","Verona","Corfinium","Treverii","Sirmium","Augustadorum","Curia","Interrama","Adria"],
|
||||
"spyNames": ["Flavius", "Regula", "Servius", "Lucia", "Cornelius", "Licina", "Canus", "Serpens", "Agrippa", "Brutus",]
|
||||
"spyNames": ["Flavius", "Regula", "Servius", "Lucia", "Cornelius", "Licina", "Canus", "Serpens", "Agrippa", "Brutus"]
|
||||
},
|
||||
{
|
||||
"name": "Arabia",
|
||||
@ -880,7 +880,7 @@
|
||||
"Amstetten", "Bad Ischl", "Wolfsberg", "Kufstein", "Leoben", "Klosterneuburg", "Leonding",
|
||||
"Kapfenberg", "Hallein", "Bischofshofen", "Waidhofen", "Saalbach", "Lienz", "Steyr"
|
||||
],
|
||||
"spyNames": ["Ferdinand", "Johanna", "Franz-Josef", "Astrid", "Anna", "Hubert", "Alois", "Natter", "Georg", "Arnold",]
|
||||
"spyNames": ["Ferdinand", "Johanna", "Franz-Josef", "Astrid", "Anna", "Hubert", "Alois", "Natter", "Georg", "Arnold"]
|
||||
},
|
||||
{
|
||||
"name": "Carthage",
|
||||
@ -903,13 +903,13 @@
|
||||
"innerColor": [81, 0, 137],
|
||||
"favoredReligion": "Islam",
|
||||
"uniqueName": "Phoenician Heritage",
|
||||
"uniques": ["Gain a free [Harbor] [in all coastal cities]","Land units may cross [Mountain] tiles after the first [Great General] is earned",
|
||||
"Units ending their turn on [Mountain] tiles take [50] damage"],
|
||||
"uniques": ["Gain a free [Harbor] [in all coastal cities]","Land units may cross [Mountain] tiles after the first [Great General] is earned"],
|
||||
"cities": ["Carthage","Utique","Hippo Regius","Gades","Saguntum","Carthago Nova","Panormus","Lilybaeum","Hadrumetum","Zama Regia",
|
||||
"Karalis","Malaca","Leptis Magna","Hippo Diarrhytus","Motya","Sulci","Leptis Parva","Tharros","Soluntum","Lixus",
|
||||
"Oea","Theveste","Ibossim","Thapsus","Aleria","Tingis","Abyla","Sabratha","Rusadir","Baecula",
|
||||
"Saldae"],
|
||||
"spyNames": ["Hamilcar", "Mago", "Baalhaan", "Sophoniba", "Yzebel", "Similce", "Kandaulo", "Zinnridi", "Gisgo", "Fierelus"]
|
||||
"spyNames": ["Hamilcar", "Mago", "Baalhaan", "Sophoniba", "Yzebel", "Similce", "Kandaulo", "Zinnridi", "Gisgo", "Fierelus"],
|
||||
"civilopediaText": [{"text": "Units ending their turn on [Mountain] tiles take [50] damage"}]
|
||||
},
|
||||
{
|
||||
"name": "Byzantium",
|
||||
|
@ -649,9 +649,10 @@
|
||||
{
|
||||
"name": "Future Tech",
|
||||
"row": 5,
|
||||
"prerequisites": ["Globalization","Particle Physics", "Nuclear Fusion", "Nanotechnology", "Stealth"],
|
||||
"uniques": ["Who knows what the future holds?", "Can be continually researched"],
|
||||
"quote": "'I think we agree, the past is over.' - George W. Bush"
|
||||
"prerequisites": ["Globalization","Particle Physics","Nuclear Fusion","Nanotechnology","Stealth"],
|
||||
"uniques": ["Can be continually researched"],
|
||||
"quote": "'I think we agree, the past is over.' - George W. Bush",
|
||||
"civilopediaText": [{"text": "Who knows what the future holds?"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -7,7 +7,10 @@
|
||||
"[-33]% Strength <for [All] units> <when below [-10] Happiness>",
|
||||
"Cannot build [Settler] units <when below [-10] Happiness>",
|
||||
"Rebel units may spawn <when below [-20] Happiness>",
|
||||
"[-1] Sight <for [Embarked] units>"
|
||||
"[-1] Sight <for [Embarked] units>",
|
||||
|
||||
// Filtering uniques must be listed here to tell RulesetValidator they're OK despite untyped
|
||||
"Aircraft"
|
||||
|
||||
// TODO: Implement the uniques below
|
||||
// "[+20]% [Culture] [in all cities] <during a golden age>",
|
||||
|
@ -615,9 +615,10 @@
|
||||
{
|
||||
"name": "Future Tech",
|
||||
"row": 5,
|
||||
"prerequisites": ["Globalization","Nuclear Fusion", "Nanotechnology"],
|
||||
"uniques": ["Who knows what the future holds?", "Can be continually researched"],
|
||||
"quote": "'I think we agree, the past is over.' - George W. Bush"
|
||||
"prerequisites": ["Globalization","Nuclear Fusion","Nanotechnology"],
|
||||
"uniques": ["Can be continually researched"],
|
||||
"quote": "'I think we agree, the past is over.' - George W. Bush",
|
||||
"civilopediaText": [{"text": "Who knows what the future holds?"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -477,46 +477,7 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
): List<RulesetError> {
|
||||
val prefix = (if (namedObj is IRulesetObject) "${namedObj.originRuleset}: " else "") +
|
||||
(if (namedObj == null) "The" else "${namedObj.name}'s")
|
||||
if (unique.type == null) {
|
||||
if (!tryFixUnknownUniques) return emptyList()
|
||||
val similarUniques = UniqueType.values().filter {
|
||||
getRelativeTextDistance(
|
||||
it.placeholderText,
|
||||
unique.placeholderText
|
||||
) <= RulesetCache.uniqueMisspellingThreshold
|
||||
}
|
||||
val equalUniques =
|
||||
similarUniques.filter { it.placeholderText == unique.placeholderText }
|
||||
return when {
|
||||
// Malformed conditional
|
||||
unique.text.count { it=='<' } != unique.text.count { it=='>' } ->listOf(
|
||||
RulesetError("$prefix unique \"${unique.text}\" contains mismatched conditional braces!",
|
||||
RulesetErrorSeverity.Warning))
|
||||
|
||||
// This should only ever happen if a bug is or has been introduced that prevents Unique.type from being set for a valid UniqueType, I think.\
|
||||
equalUniques.isNotEmpty() -> listOf(RulesetError(
|
||||
"$prefix unique \"${unique.text}\" looks like it should be fine, but for some reason isn't recognized.",
|
||||
RulesetErrorSeverity.OK))
|
||||
|
||||
similarUniques.isNotEmpty() -> {
|
||||
val text =
|
||||
"$prefix unique \"${unique.text}\" looks like it may be a misspelling of:\n" +
|
||||
similarUniques.joinToString("\n") { uniqueType ->
|
||||
var text = "\"${uniqueType.text}"
|
||||
if (unique.conditionals.isNotEmpty())
|
||||
text += " " + unique.conditionals.joinToString(" ") { "<${it.text}>" }
|
||||
text += "\""
|
||||
if (uniqueType.getDeprecationAnnotation() != null) text += " (Deprecated)"
|
||||
return@joinToString text
|
||||
}.prependIndent("\t")
|
||||
listOf(RulesetError(text, RulesetErrorSeverity.OK))
|
||||
}
|
||||
RulesetCache.modCheckerAllowUntypedUniques -> emptyList()
|
||||
else -> listOf(RulesetError(
|
||||
"$prefix unique \"${unique.text}\" not found in Unciv's unique types.",
|
||||
RulesetErrorSeverity.OK))
|
||||
}
|
||||
}
|
||||
if (unique.type == null) return checkUntypedUnique(unique, tryFixUnknownUniques, prefix)
|
||||
|
||||
val rulesetErrors = RulesetErrorList()
|
||||
|
||||
@ -581,6 +542,66 @@ class RulesetValidator(val ruleset: Ruleset) {
|
||||
|
||||
return rulesetErrors
|
||||
}
|
||||
|
||||
private fun checkUntypedUnique(unique: Unique, tryFixUnknownUniques: Boolean, prefix: String ): List<RulesetError> {
|
||||
// Malformed conditional is always bad
|
||||
if (unique.text.count { it == '<' } != unique.text.count { it == '>' })
|
||||
return listOf(RulesetError(
|
||||
"$prefix unique \"${unique.text}\" contains mismatched conditional braces!",
|
||||
RulesetErrorSeverity.Warning))
|
||||
|
||||
// Support purely filtering Uniques without actual implementation
|
||||
if (isFilteringUniqueAllowed(unique)) return emptyList()
|
||||
if (tryFixUnknownUniques) {
|
||||
val fixes = tryFixUnknownUnique(unique, prefix)
|
||||
if (fixes.isNotEmpty()) return fixes
|
||||
}
|
||||
|
||||
if (RulesetCache.modCheckerAllowUntypedUniques) return emptyList()
|
||||
|
||||
return listOf(RulesetError(
|
||||
"$prefix unique \"${unique.text}\" not found in Unciv's unique types.",
|
||||
RulesetErrorSeverity.WarningOptionsOnly))
|
||||
}
|
||||
|
||||
private fun isFilteringUniqueAllowed(unique: Unique): Boolean {
|
||||
// Isolate this decision, to allow easy change of approach
|
||||
// This says: Must have no conditionals or parameters, and is contained in GlobalUniques
|
||||
if (unique.conditionals.isNotEmpty() || unique.params.isNotEmpty()) return false
|
||||
return unique.text in ruleset.globalUniques.uniqueMap
|
||||
}
|
||||
|
||||
private fun tryFixUnknownUnique(unique: Unique, prefix: String): List<RulesetError> {
|
||||
val similarUniques = UniqueType.values().filter {
|
||||
getRelativeTextDistance(
|
||||
it.placeholderText,
|
||||
unique.placeholderText
|
||||
) <= RulesetCache.uniqueMisspellingThreshold
|
||||
}
|
||||
val equalUniques =
|
||||
similarUniques.filter { it.placeholderText == unique.placeholderText }
|
||||
return when {
|
||||
// This should only ever happen if a bug is or has been introduced that prevents Unique.type from being set for a valid UniqueType, I think.\
|
||||
equalUniques.isNotEmpty() -> listOf(RulesetError(
|
||||
"$prefix unique \"${unique.text}\" looks like it should be fine, but for some reason isn't recognized.",
|
||||
RulesetErrorSeverity.OK))
|
||||
|
||||
similarUniques.isNotEmpty() -> {
|
||||
val text =
|
||||
"$prefix unique \"${unique.text}\" looks like it may be a misspelling of:\n" +
|
||||
similarUniques.joinToString("\n") { uniqueType ->
|
||||
var text = "\"${uniqueType.text}"
|
||||
if (unique.conditionals.isNotEmpty())
|
||||
text += " " + unique.conditionals.joinToString(" ") { "<${it.text}>" }
|
||||
text += "\""
|
||||
if (uniqueType.getDeprecationAnnotation() != null) text += " (Deprecated)"
|
||||
return@joinToString text
|
||||
}.prependIndent("\t")
|
||||
listOf(RulesetError(text, RulesetErrorSeverity.OK))
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -9,7 +9,6 @@ import com.unciv.models.ruleset.unique.UniqueTarget
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.translations.squareBraceRegex
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.Fonts
|
||||
import com.unciv.ui.components.extensions.colorFromRGB
|
||||
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia
|
||||
import com.unciv.ui.screens.civilopediascreen.FormattedLine
|
||||
@ -84,8 +83,8 @@ class Nation : RulesetObject() {
|
||||
innerColorObject = if (innerColor == null) Color.BLACK
|
||||
else colorFromRGB(innerColor!!)
|
||||
|
||||
forestsAndJunglesAreRoads = uniques.contains("All units move through Forest and Jungle Tiles in friendly territory as if they have roads. These tiles can be used to establish City Connections upon researching the Wheel.")
|
||||
ignoreHillMovementCost = uniques.contains("Units ignore terrain costs when moving into any tile with Hills")
|
||||
forestsAndJunglesAreRoads = uniqueMap.containsKey(UniqueType.ForestsAndJunglesAreRoads.placeholderText)
|
||||
ignoreHillMovementCost = uniqueMap.containsKey(UniqueType.IgnoreHillMovementCost.placeholderText)
|
||||
}
|
||||
|
||||
|
||||
@ -288,11 +287,11 @@ fun getRelativeLuminance(color: Color): Double {
|
||||
if (channel < 0.03928) channel / 12.92
|
||||
else ((channel + 0.055) / 1.055).pow(2.4)
|
||||
|
||||
val R = getRelativeChannelLuminance(color.r)
|
||||
val G = getRelativeChannelLuminance(color.g)
|
||||
val B = getRelativeChannelLuminance(color.b)
|
||||
val r = getRelativeChannelLuminance(color.r)
|
||||
val g = getRelativeChannelLuminance(color.g)
|
||||
val b = getRelativeChannelLuminance(color.b)
|
||||
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
||||
/** https://www.w3.org/TR/WCAG20/#contrast-ratiodef */
|
||||
|
@ -3,6 +3,7 @@ package com.unciv.models.ruleset.unique
|
||||
import com.unciv.Constants
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetErrorSeverity
|
||||
import com.unciv.models.ruleset.RulesetValidator // Kdoc only
|
||||
import com.unciv.models.translations.getPlaceholderParameters
|
||||
import com.unciv.models.translations.getPlaceholderText
|
||||
|
||||
@ -454,6 +455,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
|
||||
CanEnterForeignTilesButLosesReligiousStrength("May enter foreign tiles without open borders, but loses [amount] religious strength each turn it ends there", UniqueTarget.Unit),
|
||||
ReducedDisembarkCost("[amount] Movement point cost to disembark", UniqueTarget.Global, UniqueTarget.Unit),
|
||||
ReducedEmbarkCost("[amount] Movement point cost to embark", UniqueTarget.Global, UniqueTarget.Unit),
|
||||
// These affect movement as Nation uniques
|
||||
ForestsAndJunglesAreRoads("All units move through Forest and Jungle Tiles in friendly territory as if they have roads. These tiles can be used to establish City Connections upon researching the Wheel.", UniqueTarget.Nation),
|
||||
IgnoreHillMovementCost("Units ignore terrain costs when moving into any tile with Hills", UniqueTarget.Nation),
|
||||
|
||||
CannotBeBarbarian("Never appears as a Barbarian unit", UniqueTarget.Unit, flags = UniqueFlag.setOfHiddenToUsers),
|
||||
|
||||
|
@ -30,6 +30,14 @@ object TechnologyDescriptions {
|
||||
fun getDescription(technology: Technology, viewingCiv: Civilization): String = technology.run {
|
||||
val ruleset = viewingCiv.gameInfo.ruleset
|
||||
val lineList = ArrayList<String>() // more readable than StringBuilder, with same performance for our use-case
|
||||
|
||||
for (pediaText in technology.civilopediaText) {
|
||||
// This is explicitly to get the "Who knows what the future holds" of Future Tech back into
|
||||
// the Tech Picker and Tech Researched Alert display, without making it an untyped Unique.
|
||||
// May need tuning for mods, in vanilla there is just the one case.
|
||||
if (pediaText.text.isEmpty() || pediaText.header != 0) continue
|
||||
lineList += pediaText.text
|
||||
}
|
||||
for (unique in uniques) lineList += unique
|
||||
|
||||
lineList.addAll(
|
||||
|
@ -82,6 +82,21 @@ The keys in this example are "science" and "culture", and both have the value "5
|
||||
|
||||
In some sense you can see from these types that JSON files themselves are actually a list of objects, each describing a single building, unit or something else.
|
||||
|
||||
## Uniques
|
||||
|
||||
"Uniques" are a label used by Unciv for extensible and customizable effects. Nearly every "ruleset object" allows a set of them, as a List with the name "uniques".
|
||||
|
||||
Every Unique follows a general structure: `Unique type defining name [placeholder] more name [another placeholder] <condition or trigger> <condition or trigger>...`
|
||||
The entire string, excluding all `<>`-delimited conditionals or triggers with their separating blanks, and excluding the placeholders but not their `[]` delimiters, are used to look up the Unique's implementation.
|
||||
The content of the optional `[placeholder]`s are implementation-dependant, they are parameters modifying the effect, and described in [Unique parameters](../Unique-parameters.md).
|
||||
All `<condition or trigger>`s are optional (but if they are used the spaces separating them are mandatory), and each in turn follows the Unique structure rules for the part between the `<>` angled brackets, including possible placeholders, but not nested conditionals.
|
||||
|
||||
Example: `"uniques":["[+1 Gold] <with a garrison>"]` on a building - does almost the same thing as the `"gold":1` attribute does, except it only applies when the city has a garrison. In this example, `[]` and `with a garrison` are the keys Unciv uses to look up two Uniques, an effect (of type `Stats`) and a condition (of type `ConditionalWhenGarrisoned`).
|
||||
|
||||
All Unique "types" that have an implementation in Unciv are automatically documented in [uniques](../uniques.md). Note that file is entirely machine-generated from source code structures. Also kindly note the separate sections for [conditionals](../uniques.md#conditional-uniques) and [trigger conditions](../uniques.md#triggercondition-uniques).
|
||||
Uniques that do not correspond to any of those entries (verbatim including upper/lower case!) are called "untyped", will have no _direct_ effect, and may result in the "Ruleset Validator" showing warnings (see the Options Tab "Locate mod errors", it also runs when starting new games).
|
||||
A legitimate use of "untyped" Uniques is their use as markers that can be recognized elsewhere in filters (example: "Aircraft" in the vanilla rulesets used as [Unit filter](../Unique-parameters.md#baseunitfilter)). To allow "Ruleset Validator" to warn about mistakes leading to untyped uniques, but still allow the "filtering Unique" use, those should be "declared" by including each in [GlobalUniques](5-Miscellaneous-JSON-files.md#globaluniquesjson), too.
|
||||
|
||||
## Information on JSON files used in the game
|
||||
|
||||
Many parts of Unciv are moddable, and for each there is a separate json file. There is a json file for buildings, for units, for promotions units can have, for technologies, etc. The different new buildings or units you define can also have lots of different attributes, though not all are required. Below are tables documenting all the different attributes everything can have. Only the attributes which are noted to be 'required' must be provided. All others have a default value that will be used when it is omitted.
|
||||
|
@ -205,11 +205,14 @@ With `civModifier` being the multiplicative aggregate of ["\[relativeAmount\]% G
|
||||
|
||||
## GlobalUniques.json
|
||||
|
||||
[link to original](https://github.com/yairm210/Unciv/tree/master/android/assets/jsons/GlobalUniques.json)
|
||||
|
||||
Defines uniques that apply globally. e.g. Vanilla rulesets define the effects of Unhappiness here.
|
||||
Only the `uniques` field is used, but a name must still be set (the Ruleset validator might display it).
|
||||
When extension rulesets define GlobalUniques, all uniques are merged. At the moment there is no way to change/remove uniques set by a base mod.
|
||||
|
||||
[link to original](https://github.com/yairm210/Unciv/tree/master/android/assets/jsons/GlobalUniques.json)
|
||||
Note: Mods can use "arbitrary" Uniques as purely filtering uniques. They are not "Typed" by Unciv code and thus have no actual effect implementation - except by being filterable elsewhere.
|
||||
In the near future, the ruleset validator will show warnings for all these, unless they are also included here, as validation that they are intentional (and - they **must** have **no** placeholders or conditionals).
|
||||
|
||||
## Tutorials.json
|
||||
|
||||
|
@ -91,6 +91,7 @@ class BasicTests {
|
||||
|
||||
@Test
|
||||
fun baseRulesetHasNoBugs() {
|
||||
RulesetCache.modCheckerAllowUntypedUniques = false
|
||||
for (baseRuleset in BaseRuleset.values()) {
|
||||
val ruleset = RulesetCache[baseRuleset.fullName]!!
|
||||
val modCheck = ruleset.checkModLinks()
|
||||
|
Reference in New Issue
Block a user