234 lines
6.1 KiB
Kotlin
234 lines
6.1 KiB
Kotlin
package ru.dbotthepony.kstarbound.defs
|
|
|
|
import com.google.gson.JsonArray
|
|
import com.google.gson.JsonElement
|
|
import com.google.gson.JsonObject
|
|
import com.google.gson.TypeAdapter
|
|
import com.google.gson.reflect.TypeToken
|
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
|
|
import ru.dbotthepony.kommons.util.Either
|
|
import ru.dbotthepony.kstarbound.Starbound
|
|
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
|
import ru.dbotthepony.kommons.gson.set
|
|
import java.util.function.Consumer
|
|
import java.util.function.Function
|
|
import java.util.function.Supplier
|
|
import kotlin.properties.ReadWriteProperty
|
|
import kotlin.reflect.KProperty
|
|
import kotlin.reflect.javaType
|
|
|
|
/**
|
|
* Base class for instances which can be mutated by altering underlying JSON
|
|
*/
|
|
abstract class JsonDriven(val path: String) {
|
|
private val delegates = ArrayList<Property<*>>()
|
|
private val delegatesMap = Object2ObjectOpenHashMap<String, ArrayList<Property<*>>>()
|
|
|
|
private val lazies = ArrayList<LazyData<*>>()
|
|
private val namedLazies = Object2ObjectOpenHashMap<String, ArrayList<LazyData<*>>>()
|
|
|
|
protected val properties = JsonObject()
|
|
|
|
/**
|
|
* [JsonObject]s which define behavior of properties
|
|
*/
|
|
protected abstract fun defs(): Collection<JsonObject>
|
|
|
|
protected open fun invalidate() {
|
|
delegates.forEach { it.invalidate() }
|
|
lazies.forEach { it.invalidate() }
|
|
}
|
|
|
|
protected open fun invalidate(name: String) {
|
|
delegatesMap[name]?.forEach { it.invalidate() }
|
|
namedLazies[name]?.forEach { it.invalidate() }
|
|
lazies.forEach { it.invalidate() }
|
|
}
|
|
|
|
inner class LazyData<T>(names: Iterable<String> = listOf(), private val initializer: () -> T) : Lazy<T> {
|
|
constructor(initializer: () -> T) : this(listOf(), initializer)
|
|
|
|
init {
|
|
for (name in names) {
|
|
namedLazies.computeIfAbsent(name, Function { ArrayList() }).add(this)
|
|
}
|
|
}
|
|
|
|
private var _value: Any? = mark
|
|
|
|
override val value: T get() {
|
|
var value = _value
|
|
|
|
if (value !== mark) {
|
|
return value as T
|
|
}
|
|
|
|
value = initializer.invoke()
|
|
_value = value
|
|
return value
|
|
}
|
|
|
|
override fun isInitialized(): Boolean {
|
|
return _value !== mark
|
|
}
|
|
|
|
fun invalidate() {
|
|
_value = mark
|
|
}
|
|
}
|
|
|
|
inner class Property<T>(
|
|
name: String? = null,
|
|
val default: Either<Supplier<T>, JsonElement>? = null,
|
|
private var adapter: TypeAdapter<T>? = null,
|
|
) : Supplier<T>, Consumer<T>, ReadWriteProperty<Any?, T> {
|
|
constructor(name: String, default: T, adapter: TypeAdapter<T>? = null) : this(name, Either.left(Supplier { default }), adapter)
|
|
constructor(name: String, default: Supplier<T>, adapter: TypeAdapter<T>? = null) : this(name, Either.left(default), adapter)
|
|
constructor(name: String, default: JsonElement, adapter: TypeAdapter<T>? = null) : this(name, Either.right(default), adapter)
|
|
constructor(default: T, adapter: TypeAdapter<T>? = null) : this(null, Either.left(Supplier { default }), adapter)
|
|
constructor(default: Supplier<T>, adapter: TypeAdapter<T>? = null) : this(null, Either.left(default), adapter)
|
|
constructor(default: JsonElement, adapter: TypeAdapter<T>? = null) : this(null, Either.right(default), adapter)
|
|
|
|
var name: String? = name
|
|
private set(value) {
|
|
if (field != null)
|
|
throw IllegalStateException()
|
|
|
|
field = value
|
|
delegatesMap.computeIfAbsent(value, Function { ArrayList() }).add(this)
|
|
}
|
|
|
|
init {
|
|
delegates.add(this)
|
|
|
|
if (name != null)
|
|
delegatesMap.computeIfAbsent(name, Function { ArrayList() }).add(this)
|
|
}
|
|
|
|
private var value: Supplier<T> = never as Supplier<T>
|
|
|
|
private fun compute(): T {
|
|
val value = dataValue(checkNotNull(name))
|
|
|
|
if (value == null) {
|
|
if (default == null) {
|
|
throw NoSuchElementException("No json value present at '$name', and no default value was provided")
|
|
} else if (default.isLeft) {
|
|
return default.left().get()
|
|
} else {
|
|
AssetPathStack.block(path) {
|
|
return adapter!!.fromJsonTree(default.right())
|
|
}
|
|
}
|
|
} else {
|
|
AssetPathStack.block(path) {
|
|
return adapter!!.fromJsonTree(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun invalidate() {
|
|
value = Supplier {
|
|
val new = compute()
|
|
this.value = Supplier { new }
|
|
new
|
|
}
|
|
}
|
|
|
|
init {
|
|
invalidate()
|
|
}
|
|
|
|
override fun get(): T {
|
|
return value.get()
|
|
}
|
|
|
|
override fun accept(t: T) {
|
|
AssetPathStack.block(path) {
|
|
properties[checkNotNull(name)] = adapter!!.toJsonTree(t)
|
|
}
|
|
|
|
// value = Supplier { t }
|
|
invalidate(name!!)
|
|
}
|
|
|
|
@OptIn(ExperimentalStdlibApi::class)
|
|
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
|
if (adapter == null) {
|
|
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
|
|
}
|
|
|
|
if (name == null) {
|
|
name = property.name
|
|
}
|
|
|
|
return value.get()
|
|
}
|
|
|
|
@OptIn(ExperimentalStdlibApi::class)
|
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
|
if (adapter == null) {
|
|
adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter<T>
|
|
}
|
|
|
|
if (name == null) {
|
|
name = property.name
|
|
}
|
|
|
|
return accept(value)
|
|
}
|
|
}
|
|
|
|
fun dataValue(name: String, alwaysCopy: Boolean = false): JsonElement? {
|
|
val defs = defs()
|
|
var value: JsonElement?
|
|
|
|
if (defs.isEmpty()) {
|
|
value = properties[name]?.let { if (alwaysCopy) it.deepCopy() else it }
|
|
} else {
|
|
val itr = defs.iterator()
|
|
var isCopy = false
|
|
value = properties[name]
|
|
|
|
while ((value == null || value is JsonObject) && itr.hasNext()) {
|
|
val next = itr.next()[name]
|
|
|
|
if (value is JsonObject) {
|
|
if (next !is JsonObject) continue
|
|
value = mergeNoCopy(if (isCopy) value else value.deepCopy(), next)
|
|
isCopy = true
|
|
} else {
|
|
value = next
|
|
}
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
fun hasDataValue(name: String): Boolean {
|
|
if (properties[name] != null) return true
|
|
return defs().any { it[name] != null }
|
|
}
|
|
|
|
companion object {
|
|
private val mark = Any()
|
|
private val never = Supplier { throw NoSuchElementException() }
|
|
|
|
@JvmStatic
|
|
fun mergeNoCopy(a: JsonObject, b: JsonObject): JsonObject {
|
|
for ((k, v) in b.entrySet()) {
|
|
val existing = a[k]
|
|
|
|
if (existing is JsonObject && v is JsonObject) {
|
|
a[k] = mergeNoCopy(existing, v)
|
|
} else if (existing !is JsonObject) {
|
|
a[k] = v.deepCopy()
|
|
}
|
|
}
|
|
|
|
return a
|
|
}
|
|
}
|
|
}
|