-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathRulesetCache.kt
188 lines (165 loc) · 8.71 KB
/
RulesetCache.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package com.unciv.models.ruleset
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame
import com.unciv.logic.UncivShowableException
import com.unciv.logic.map.MapParameters
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.metadata.GameParameters
import com.unciv.models.ruleset.validation.RulesetError
import com.unciv.models.ruleset.validation.RulesetErrorList
import com.unciv.models.ruleset.validation.RulesetErrorSeverity
import com.unciv.models.ruleset.validation.UniqueValidator
import com.unciv.models.ruleset.validation.getRelativeTextDistance
import com.unciv.utils.Log
import com.unciv.utils.debug
/** Loading mods is expensive, so let's only do it once and
* save all of the loaded rulesets somewhere for later use
* */
object RulesetCache : HashMap<String, Ruleset>() {
/** Similarity below which an untyped unique can be considered a potential misspelling.
* Roughly corresponds to the fraction of the Unique placeholder text that can be different/misspelled, but with some extra room for [getRelativeTextDistance] idiosyncrasies. */
var uniqueMisspellingThreshold = 0.15 // Tweak as needed. Simple misspellings seem to be around 0.025, so would mostly be caught by 0.05. IMO 0.1 would be good, but raising to 0.15 also seemed to catch what may be an outdated Unique.
/** Returns error lines from loading the rulesets, so we can display the errors to users */
fun loadRulesets(consoleMode: Boolean = false, noMods: Boolean = false): List<String> {
val newRulesets = HashMap<String, Ruleset>()
for (ruleset in BaseRuleset.entries) {
val fileName = "jsons/${ruleset.fullName}"
val fileHandle =
if (consoleMode) FileHandle(fileName)
else Gdx.files.internal(fileName)
newRulesets[ruleset.fullName] = Ruleset().apply {
name = ruleset.fullName
load(fileHandle)
}
}
this.putAll(newRulesets)
val errorLines = ArrayList<String>()
if (!noMods) {
val modsHandles = if (consoleMode) FileHandle("mods").list()
else UncivGame.Current.files.getModsFolder().list()
for (modFolder in modsHandles) {
if (modFolder.name().startsWith('.')) continue
if (!modFolder.isDirectory) continue
try {
val modRuleset = Ruleset()
modRuleset.name = modFolder.name()
modRuleset.load(modFolder.child("jsons"))
modRuleset.folderLocation = modFolder
newRulesets[modRuleset.name] = modRuleset
debug("Mod loaded successfully: %s", modRuleset.name)
if (Log.shouldLog()) {
val modLinksErrors = modRuleset.getErrorList()
// For extension mods which use references to base ruleset objects, the parameter type
// errors are irrelevant - the checker ran without a base ruleset
val logFilter: (RulesetError) -> Boolean =
if (modRuleset.modOptions.isBaseRuleset) {
{ it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly }
} else {
{ it.errorSeverityToReport > RulesetErrorSeverity.WarningOptionsOnly &&
!it.text.contains(UniqueValidator.whichDoesNotFitParameterType) }
}
if (modLinksErrors.any(logFilter)) {
debug(
"checkModLinks errors: %s",
modLinksErrors.getErrorText(logFilter)
)
}
}
} catch (ex: Exception) {
errorLines += "Exception loading mod '${modFolder.name()}':"
errorLines += " ${ex.localizedMessage}"
errorLines += " ${ex.cause?.localizedMessage}"
}
}
if (Log.shouldLog()) for (line in errorLines) debug(line)
}
// We save the 'old' cache values until we're ready to replace everything, so that the cache isn't empty while we try to load ruleset files
// - this previously lead to "can't find Vanilla ruleset" if the user had a lot of mods and downloaded a new one
this.clear()
this.putAll(newRulesets)
return errorLines
}
fun getVanillaRuleset() = this[BaseRuleset.Civ_V_Vanilla.fullName]!!.clone() // safeguard, so no-one edits the base ruleset by mistake
fun getSortedBaseRulesets(): List<String> {
val baseRulesets = values
.filter { it.modOptions.isBaseRuleset }
.map { it.name }
.distinct()
if (baseRulesets.size < 2) return baseRulesets
// We sort the base rulesets such that the ones unciv provides are on the top,
// and the rest is alphabetically ordered.
return baseRulesets.sortedWith(
compareBy(
{ ruleset ->
BaseRuleset.entries
.firstOrNull { br -> br.fullName == ruleset }?.ordinal
?: BaseRuleset.entries.size
},
{ it }
)
)
}
/** Creates a combined [Ruleset] from a list of mods contained in [parameters]. */
fun getComplexRuleset(parameters: MapParameters) =
getComplexRuleset(parameters.mods, parameters.baseRuleset)
/** Creates a combined [Ruleset] from a list of mods contained in [parameters]. */
fun getComplexRuleset(parameters: GameParameters) =
getComplexRuleset(parameters.mods, parameters.baseRuleset)
/**
* Creates a combined [Ruleset] from a list of mods.
* If no baseRuleset is passed in [optionalBaseRuleset] (or a non-existing one), then the vanilla Ruleset is included automatically.
* Any mods in the [mods] parameter marked as base ruleset (or not loaded in [RulesetCache]) are ignored.
*/
fun getComplexRuleset(mods: LinkedHashSet<String>, optionalBaseRuleset: String? = null): Ruleset {
val baseRuleset =
if (containsKey(optionalBaseRuleset) && this[optionalBaseRuleset]!!.modOptions.isBaseRuleset)
this[optionalBaseRuleset]!!
else getVanillaRuleset()
val loadedMods = mods.asSequence()
.filter { containsKey(it) }
.map { this[it]!! }
.filter { !it.modOptions.isBaseRuleset }
return getComplexRuleset(baseRuleset, loadedMods.asIterable())
}
/**
* Creates a combined [Ruleset] from [baseRuleset] and [extensionRulesets] which must only contain non-base rulesets.
*/
fun getComplexRuleset(baseRuleset: Ruleset, extensionRulesets: Iterable<Ruleset>): Ruleset {
val newRuleset = Ruleset()
val loadedMods = extensionRulesets.asSequence() + baseRuleset
for (mod in loadedMods.sortedByDescending { it.modOptions.isBaseRuleset }) {
if (mod.modOptions.isBaseRuleset) {
// This is so we don't keep using the base ruleset's uniques *by reference* and add to in ad infinitum
newRuleset.modOptions.uniques = ArrayList()
newRuleset.modOptions.isBaseRuleset = true
// Default tileset and unitset are according to base ruleset
newRuleset.modOptions.tileset = mod.modOptions.tileset
newRuleset.modOptions.unitset = mod.modOptions.unitset
}
newRuleset.add(mod)
newRuleset.mods += mod.name
}
newRuleset.updateBuildingCosts() // only after we've added all the mods can we calculate the building costs
newRuleset.updateResourceTransients()
return newRuleset
}
/**
* Runs [Ruleset.getErrorList] on a temporary [combined Ruleset][getComplexRuleset] for a list of [mods]
*/
fun checkCombinedModLinks(
mods: LinkedHashSet<String>,
baseRuleset: String? = null,
tryFixUnknownUniques: Boolean = false
): RulesetErrorList {
return try {
val newRuleset = getComplexRuleset(mods, baseRuleset)
newRuleset.modOptions.isBaseRuleset = true // This is so the checkModLinks finds all connections
newRuleset.getErrorList(tryFixUnknownUniques)
} catch (ex: UncivShowableException) {
// This happens if a building is dependent on a tech not in the base ruleset
// because newRuleset.updateBuildingCosts() in getComplexRuleset() throws an error
RulesetErrorList.of(ex.message, RulesetErrorSeverity.Error)
}
}
}