-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathAbstractSetting.kt
More file actions
260 lines (236 loc) · 9.45 KB
/
AbstractSetting.kt
File metadata and controls
260 lines (236 loc) · 9.45 KB
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/*
* Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lambda.config
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import com.lambda.Lambda.LOG
import com.lambda.Lambda.gson
import com.lambda.brigadier.CommandResult.Companion.failure
import com.lambda.brigadier.CommandResult.Companion.success
import com.lambda.brigadier.argument.string
import com.lambda.brigadier.argument.value
import com.lambda.brigadier.executeWithResult
import com.lambda.brigadier.required
import com.lambda.command.CommandRegistry
import com.lambda.command.commands.ConfigCommand
import com.lambda.context.SafeContext
import com.lambda.gui.Layout
import com.lambda.threading.runSafe
import com.lambda.util.Communication.info
import com.lambda.util.Describable
import com.lambda.util.Nameable
import com.lambda.util.NamedEnum
import com.lambda.util.extension.CommandBuilder
import com.lambda.util.text.ClickEvents
import com.lambda.util.text.HoverEvents
import com.lambda.util.text.TextBuilder
import com.lambda.util.text.buildText
import com.lambda.util.text.clickEvent
import com.lambda.util.text.highlighted
import com.lambda.util.text.hoverEvent
import com.lambda.util.text.literal
import net.minecraft.command.CommandRegistryAccess
import java.lang.reflect.Type
import kotlin.properties.Delegates
import kotlin.reflect.KProperty
/**
* Represents a setting with a [defaultValue], [visibility] condition, and [description].
* This setting is serializable ([Jsonable]) and has a [name].
*
* When the [value] is modified, all registered [listeners] are notified.
* The [visibility] of the setting can be checked with the [isVisible] property.
* The setting can be [reset] to its [defaultValue].
*
* Simple Usage:
* ```kotlin
* // this uses the delegate (by) association to access the setting value in the code directly.
* val mode by setting("Mode", Modes.FREEZE, { page == Page.CUSTOM }, "The mode of the module.")
*
* init {
* listener<TickEvent.Pre> {
* LOG.info("Mode: $mode") // direct access of the value
* }
* }
* ```
*
* Advanced usage with listeners:
* ```kotlin
* // notice how this does not use the delegate (by) association, to access the setting object to register listeners.
* val mode = setting("Mode", Modes.FREEZE, { page == Page.CUSTOM }, "The mode of the module.")
*
* init {
* mode.listener { from, to ->
* // Do something when the mode changes in a safe context
* }
* mode.unsafeListener { from, to ->
* // Do something when the mode changes in an unsafe context
* }
*
* listener<TickEvent.Pre> {
* LOG.info("Mode: ${mode.value}") // indirect access of the value
* }
* }
* ```
*
* @property defaultValue The default value of the setting.
* @property description A description of the setting.
* @property type The type reflection of the setting.
* @property visibility A function that determines whether the setting is visible.
*/
abstract class AbstractSetting<T : Any>(
override var name: String,
internal var defaultValue: T,
val type: Type,
override var description: String,
var visibility: () -> Boolean,
) : Jsonable, Nameable, Describable, Layout {
private val listeners = mutableListOf<ValueListener<T>>()
var groups: MutableList<List<NamedEnum>> = mutableListOf()
var value by Delegates.observable(defaultValue) { _, from, to ->
listeners.forEach {
if (it.requiresValueChange && from == to) return@forEach
it.execute(from, to)
}
}
val isModified get() = value != defaultValue
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
open operator fun setValue(thisRef: Any?, property: KProperty<*>, valueIn: T) {
value = valueIn
}
override fun toJson(): JsonElement =
gson.toJsonTree(value, type)
override fun loadFromJson(serialized: JsonElement) {
runCatching {
value = gson.fromJson(serialized, type)
}.onFailure {
LOG.warn("Failed to load setting ${this.name} with value $serialized. Resetting to default value $defaultValue")
value = defaultValue
}
}
class ValueListener<T>(val requiresValueChange: Boolean, val execute: (from: T, to: T) -> Unit)
/**
* Will only register changes of the variable, not the content of the variable!
* E.g., if the variable is a list, it will only register if the list reference changes, not if the content of the list changes.
*/
fun onValueChange(block: SafeContext.(from: T, to: T) -> Unit) = apply {
listeners.add(ValueListener(true) { from, to ->
runSafe {
block(from, to)
}
})
}
fun onValueChangeUnsafe(block: (from: T, to: T) -> Unit) = apply {
listeners.add(ValueListener(true, block))
}
fun onValueSet(block: (from: T, to: T) -> Unit) = apply {
listeners.add(ValueListener(false, block))
}
fun group(path: List<NamedEnum>, vararg continuation: NamedEnum) = apply {
groups.add(path + continuation)
}
fun group(vararg path: NamedEnum) = apply {
groups.add(path.toList())
}
fun group(path: NamedEnum?) = apply {
path?.let { groups.add(listOf(it)) }
}
fun reset(silent: Boolean = false) {
if (!silent && value == defaultValue) {
ConfigCommand.info(notChangedMessage())
return
}
if (!silent) ConfigCommand.info(resetMessage(value, defaultValue))
value = defaultValue
}
open fun CommandBuilder.buildCommand(registry: CommandRegistryAccess) {
required(string("value as JSON")) { value ->
executeWithResult {
val valueString = value().value()
val parsed = try {
JsonParser.parseString("\"$valueString\"")
} catch (_: Exception) {
return@executeWithResult failure("$valueString is not a valid JSON string.")
}
val config = Configuration.configurableBySetting(this@AbstractSetting)
?: return@executeWithResult failure("No config found for $name.")
val previous = this@AbstractSetting.value
try {
loadFromJson(parsed)
} catch (_: Exception) {
return@executeWithResult failure("Failed to load $valueString as a ${type::class.simpleName} for $name in ${config.name}.")
}
ConfigCommand.info(setMessage(previous, this@AbstractSetting.value))
return@executeWithResult success()
}
}
}
fun trySetValue(newValue: T) {
if (newValue == value) {
ConfigCommand.info(notChangedMessage())
} else {
val previous = value
value = newValue
ConfigCommand.info(setMessage(previous, newValue))
}
}
private fun setMessage(previousValue: T, newValue: T) = buildText {
literal("Set ")
changedMessage(previousValue, newValue)
val config = Configuration.configurableBySetting(this@AbstractSetting) ?: return@buildText
clickEvent(ClickEvents.suggestCommand("${CommandRegistry.prefix}${ConfigCommand.name} reset ${config.commandName} $commandName")) {
hoverEvent(HoverEvents.showText(buildText {
literal("Click to reset to default value ")
highlighted(defaultValue.toString())
})) {
highlighted(" [Reset]")
}
}
}
private fun resetMessage(previousValue: T, newValue: T) = buildText {
literal("Reset ")
changedMessage(previousValue, newValue)
}
private fun notChangedMessage() = buildText {
literal("No changes made to ")
highlighted(name)
literal(" as it is already set to ")
highlighted(value.toString())
literal(".")
}
private fun TextBuilder.changedMessage(previousValue: T, newValue: T) {
val config = Configuration.configurableBySetting(this@AbstractSetting) ?: return
highlighted(config.name)
literal(" > ")
highlighted(name)
literal(" from ")
highlighted(previousValue.toString())
literal(" to ")
highlighted(newValue.toString())
literal(".")
clickEvent(ClickEvents.suggestCommand("${CommandRegistry.prefix}${ConfigCommand.name} set ${config.commandName} $commandName $previousValue")) {
hoverEvent(HoverEvents.showText(buildText {
literal("Click to undo to previous value ")
highlighted(previousValue.toString())
})) {
highlighted(" [Undo]")
}
}
}
override fun toString() = "Setting $name: $value of type ${type.typeName}"
override fun equals(other: Any?) = other is AbstractSetting<*> && name == other.name
override fun hashCode() = name.hashCode()
}