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
|
package at.hannibal2.skyhanni.config
import at.hannibal2.skyhanni.events.LorenzEvent
import at.hannibal2.skyhanni.features.misc.limbo.LimboTimeTracker
import at.hannibal2.skyhanni.utils.LorenzLogger
import at.hannibal2.skyhanni.utils.LorenzUtils.asIntOrNull
import at.hannibal2.skyhanni.utils.json.shDeepCopy
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
object ConfigUpdaterMigrator {
val logger = LorenzLogger("ConfigMigration")
const val CONFIG_VERSION = 53
fun JsonElement.at(chain: List<String>, init: Boolean): JsonElement? {
if (chain.isEmpty()) return this
if (this !is JsonObject) return null
var obj = this[chain.first()]
if (obj == null && init) {
obj = JsonObject()
this.add(chain.first(), obj)
}
return obj?.at(chain.drop(1), init)
}
data class ConfigFixEvent(
val old: JsonObject,
val new: JsonObject,
val oldVersion: Int,
var movesPerformed: Int,
val dynamicPrefix: Map<String, List<String>>,
) : LorenzEvent() {
init {
dynamicPrefix.entries
.filter { it.value.isEmpty() }
.forEach {
logger.log("Dynamic prefix ${it.key} does not resolve to anything.")
}
}
fun transform(since: Int, path: String, transform: (JsonElement) -> JsonElement = { it }) {
move(since, path, path, transform)
}
fun move(since: Int, oldPath: String, newPath: String, transform: (JsonElement) -> JsonElement = { it }) {
if (since <= oldVersion) {
logger.log("Skipping move from $oldPath to $newPath ($since <= $oldVersion)")
return
}
if (since > CONFIG_VERSION) {
error("Illegally new version $since > $CONFIG_VERSION")
}
if (since > oldVersion + 1) {
logger.log("Skipping move from $oldPath to $newPath (will be done in another pass)")
return
}
val op = oldPath.split(".")
val np = newPath.split(".")
if (op.first().startsWith("#")) {
require(np.first() == op.first())
val realPrefixes = dynamicPrefix[op.first()]
if (realPrefixes == null) {
logger.log("Could not resolve dynamic prefix $oldPath")
return
}
for (realPrefix in realPrefixes) {
move(
since,
"$realPrefix.${oldPath.substringAfter('.')}",
"$realPrefix.${newPath.substringAfter('.')}", transform
)
return
}
}
val oldElem = old.at(op, false)
if (oldElem == null) {
logger.log("Skipping move from $oldPath to $newPath ($oldPath not present)")
return
}
val newParentElement = new.at(np.dropLast(1), true)
if (newParentElement !is JsonObject) {
logger.log("Catastrophic: element at path $old could not be relocated to $new, since another element already inhabits that path")
return
}
movesPerformed++
if (np == listOf("#player", "personalBest")) LimboTimeTracker.workaroundMigration(oldElem.asInt)
newParentElement.add(np.last(), transform(oldElem.shDeepCopy()))
logger.log("Moved element from $oldPath to $newPath")
}
}
private fun merge(originalObject: JsonObject, overrideObject: JsonObject): Int {
var count = 0
for ((key, newElement) in overrideObject.entrySet()) {
val element = originalObject[key]
if (element is JsonObject && newElement is JsonObject) {
count += merge(element, newElement)
} else {
if (element != null) {
logger.log("Encountered destructive merge. Erasing $element in favour of $newElement.")
count++
}
originalObject.add(key, newElement)
}
}
return count
}
fun fixConfig(config: JsonObject): JsonObject {
val lastVersion = (config["lastVersion"] as? JsonPrimitive)?.asIntOrNull ?: -1
if (lastVersion > CONFIG_VERSION) {
logger.log("Attempted to downgrade config version")
config.add("lastVersion", JsonPrimitive(CONFIG_VERSION))
return config
}
if (lastVersion == CONFIG_VERSION) return config
return (lastVersion until CONFIG_VERSION).fold(config) { accumulator, i ->
logger.log("Starting config transformation from $i to ${i + 1}")
val storage = accumulator["storage"]?.asJsonObject
val dynamicPrefix: Map<String, List<String>> = mapOf(
"#profile" to
(storage?.get("players")?.asJsonObject?.entrySet()
?.flatMap { player ->
player.value.asJsonObject["profiles"]?.asJsonObject?.entrySet()?.map {
"storage.players.${player.key}.profiles.${it.key}"
} ?: listOf()
}
?: listOf()),
"#player" to
(storage?.get("players")?.asJsonObject?.entrySet()?.map { "storage.players.${it.key}" }
?: listOf()),
)
val migration = ConfigFixEvent(accumulator, JsonObject().also {
it.add("lastVersion", JsonPrimitive(i + 1))
}, i, 0, dynamicPrefix).also { it.postAndCatch() }
logger.log("Transformations scheduled: ${migration.new}")
val mergesPerformed = merge(migration.old, migration.new)
logger.log("Migration done with $mergesPerformed merges and ${migration.movesPerformed} moves performed")
migration.old
}.also {
logger.log("Final config: $it")
}
}
}
|