diff --git a/core/src/com/unciv/models/ruleset/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/RulesetValidator.kt index 3fe007e69b..ca8f5b5d92 100644 --- a/core/src/com/unciv/models/ruleset/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/RulesetValidator.kt @@ -10,6 +10,7 @@ import com.unciv.models.ruleset.unique.IHasUniques import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.stats.INamed import com.unciv.models.stats.Stats @@ -387,6 +388,7 @@ class RulesetValidator(val ruleset: Ruleset) { if (!ruleset.unitTypes.containsKey(unitType) && (ruleset.unitTypes.isNotEmpty() || !vanillaRuleset.unitTypes.containsKey(unitType))) lines.add("${promotion.name} references unit type $unitType, which does not exist!", RulesetErrorSeverity.Warning) checkUniques(promotion, lines, rulesetSpecific, tryFixUnknownUniques) + checkPromotionCircularReferences(lines) } for (unitType in ruleset.unitTypes.values) { @@ -426,6 +428,24 @@ class RulesetValidator(val ruleset: Ruleset) { return lines } + private fun checkPromotionCircularReferences(lines: RulesetErrorList) { + fun recursiveCheck(history: LinkedHashSet, promotion: Promotion, level: Int) { + if (promotion in history) { + lines.add("Circular Reference in Promotions: ${history.joinToString("→") { it.name }}→${promotion.name}", RulesetErrorSeverity.Warning) + return + } + if (level > 99) return + history.add(promotion) + for (prerequisiteName in promotion.prerequisites) { + val prerequisite = ruleset.unitPromotions[prerequisiteName] ?: continue + recursiveCheck(history.toCollection(linkedSetOf()), prerequisite, level + 1) + } + } + for (promotion in ruleset.unitPromotions.values) { + recursiveCheck(linkedSetOf(), promotion, 0) + } + } + fun checkUniques( uniqueContainer: IHasUniques, diff --git a/core/src/com/unciv/ui/screens/pickerscreens/PromotionTree.kt b/core/src/com/unciv/ui/screens/pickerscreens/PromotionTree.kt index f4ec7816da..a8be3cc55d 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/PromotionTree.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/PromotionTree.kt @@ -6,7 +6,6 @@ import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.translations.tr -import com.unciv.utils.Log internal class PromotionTree(val unit: MapUnit) { /** Ordered set of Promotions to show - by Json column/row and translated name */ @@ -92,10 +91,7 @@ internal class PromotionTree(val unit: MapUnit) { for (node in nodes.values) { for (prerequisite in node.promotion.prerequisites) { val parent = nodes[prerequisite] ?: continue - if (node in allChildren(parent)) { - Log.debug("Ignoring circular reference: %s requires %s", node, parent) - continue - } + if (detectLoop(node, parent)) continue node.parents += parent parent.children += node if (node.level > 0 && node.baseName == parent.baseName) @@ -153,8 +149,21 @@ internal class PromotionTree(val unit: MapUnit) { fun allNodes() = nodes.values.asSequence() fun allRoots() = allNodes().filter { it.isRoot } - private fun allChildren(node: PromotionNode): Sequence { - return sequenceOf(node) + node.children.flatMap { allChildren(it) } + + private fun detectLoop(node: PromotionNode, parent: PromotionNode): Boolean { + if (parent == node) return true + val loopCheck = HashSet(nodes.size) + loopCheck.add(node) + fun detectRecursive(parent: PromotionNode, level: Int): Boolean { + if (level > 99) return true + if (parent in loopCheck) return true + loopCheck.add(parent) + for (child in parent.children) { + if (detectRecursive(child, level + 1)) return true + } + return false + } + return detectRecursive(parent, 0) } private fun getReachableNode(promotion: Promotion): PromotionNode? =