KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt
2024-03-17 11:10:18 +07:00

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
}
}
}