KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt
2024-02-08 20:23:40 +07:00

371 lines
10 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectMap
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.json.consumeNull
import java.lang.reflect.ParameterizedType
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier
import kotlin.collections.contains
import kotlin.collections.set
import kotlin.concurrent.withLock
import ru.dbotthepony.kstarbound.util.traverseJsonPath
import java.util.concurrent.ConcurrentLinkedQueue
inline fun <reified S : Any> Registry<S>.adapter(): TypeAdapterFactory {
return object : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val subtype = type.type as? ParameterizedType ?: return null
if (subtype.actualTypeArguments.size != 1 || subtype.actualTypeArguments[0] != S::class.java) return null
return when (type.rawType) {
Registry.Entry::class.java -> {
object : TypeAdapter<Registry.Entry<S>>() {
override fun write(out: JsonWriter, value: Registry.Entry<S>?) {
if (value != null) {
out.value(value.key)
} else {
out.nullValue()
}
}
override fun read(`in`: JsonReader): Registry.Entry<S>? {
if (`in`.consumeNull()) {
return null
} else if (`in`.peek() == JsonToken.STRING) {
return this@adapter[`in`.nextString()]
} else if (`in`.peek() == JsonToken.NUMBER) {
return this@adapter[`in`.nextInt()]
} else {
throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}")
}
}
}
}
Registry.Ref::class.java -> {
object : TypeAdapter<Registry.Ref<S>>() {
override fun write(out: JsonWriter, value: Registry.Ref<S>?) {
if (value != null) {
value.key.map(out::value, out::value)
} else {
out.nullValue()
}
}
override fun read(`in`: JsonReader): Registry.Ref<S>? {
if (`in`.consumeNull()) {
return null
} else if (`in`.peek() == JsonToken.STRING) {
return this@adapter.ref(`in`.nextString())
} else if (`in`.peek() == JsonToken.NUMBER) {
return this@adapter.ref(`in`.nextInt())
} else {
throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}")
}
}
}
}
else -> null
} as TypeAdapter<T>?
}
}
}
class Registry<T : Any>(val name: String) {
private val keysInternal = Object2ObjectOpenHashMap<String, Impl>()
private val idsInternal = Int2ObjectOpenHashMap<Impl>()
private val keyRefs = Object2ObjectOpenHashMap<String, RefImpl>()
private val idRefs = Int2ObjectOpenHashMap<RefImpl>()
private val backlog = ConcurrentLinkedQueue<Runnable>()
// it is much cheaper to queue registry additions rather than locking during high congestion
fun add(task: Runnable) {
backlog.add(task)
}
fun finishLoad() {
var next = backlog.poll()
while (next != null) {
next.run()
next = backlog.poll()
}
}
private val lock = ReentrantLock()
val keys: Object2ObjectMap<String, out Entry<T>> = Object2ObjectMaps.unmodifiable(keysInternal)
val ids: Int2ObjectMap<out Entry<T>> = Int2ObjectMaps.unmodifiable(idsInternal)
sealed class Ref<T : Any> : Supplier<Entry<T>?> {
abstract val key: Either<String, Int>
abstract val entry: Entry<T>?
abstract val registry: Registry<T>
val isPresent: Boolean
get() = value != null
val value: T?
get() = entry?.value
fun traverseJsonPath(path: String): JsonElement? {
return traverseJsonPath(path, entry?.json ?: return null)
}
final override fun get(): Entry<T>? {
return entry
}
}
sealed class Entry<T : Any> : Supplier<T> {
abstract val key: String
abstract val id: Int?
abstract val value: T
abstract val json: JsonElement
abstract val file: IStarboundFile?
abstract val registry: Registry<T>
abstract val isBuiltin: Boolean
fun traverseJsonPath(path: String): JsonElement? {
return traverseJsonPath(path, json)
}
final override fun get(): T {
return value
}
val jsonObject: JsonObject
get() = json as JsonObject
}
private inner class Impl(override val key: String, override var value: T, override var id: Int? = null) : Entry<T>() {
override var json: JsonElement = JsonNull.INSTANCE
override var file: IStarboundFile? = null
override var isBuiltin: Boolean = false
override fun equals(other: Any?): Boolean {
return this === other
}
override fun hashCode(): Int {
return key.hashCode()
}
override fun toString(): String {
return "Registry.Entry[key=$key, id=$id, registry=$name]"
}
override val registry: Registry<T>
get() = this@Registry
}
private inner class RefImpl(override val key: Either<String, Int>) : Ref<T>() {
override var entry: Entry<T>? = null
var references = 0
override fun equals(other: Any?): Boolean {
return this === other || other is Registry<*>.RefImpl && other.key == key && other.registry == registry
}
override fun hashCode(): Int {
return key.hashCode()
}
override fun toString(): String {
return "Registry.Ref[key=$key, bound=${entry != null}, registry=$name]"
}
override val registry: Registry<T>
get() = this@Registry
}
operator fun get(index: String): Entry<T>? = lock.withLock { keysInternal[index] }
operator fun get(index: Int): Entry<T>? = lock.withLock { idsInternal[index] }
fun ref(index: String): Ref<T> = lock.withLock {
keyRefs.computeIfAbsent(index, Object2ObjectFunction {
val ref = RefImpl(Either.left(it as String))
ref.entry = keysInternal[it]
ref
}).also {
it.references++
}
}
fun ref(index: Int): Ref<T> = lock.withLock {
idRefs.computeIfAbsent(index, Int2ObjectFunction {
val ref = RefImpl(Either.right(it))
ref.entry = idsInternal[it]
ref
}).also {
it.references++
}
}
operator fun contains(index: String) = lock.withLock { index in keysInternal }
operator fun contains(index: Int) = lock.withLock { index in idsInternal }
fun validate(): Boolean {
var any = true
keyRefs.values.forEach {
if (!it.isPresent) {
LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)")
any = false
}
}
idRefs.values.forEach {
if (!it.isPresent) {
LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)")
any = false
}
}
return any
}
fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = null
entry.value = value
entry.json = json
entry.file = file
keyRefs[key]?.entry = entry
return entry
}
}
fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
if (id in idsInternal) {
LOGGER.warn("Overwriting Registry entry with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = id
entry.value = value
entry.json = json
entry.file = file
keyRefs[key]?.entry = entry
idRefs[id]?.entry = entry
idsInternal[id] = entry
return entry
}
}
fun add(key: String, value: T, isBuiltin: Boolean = false): Entry<T> {
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = null
entry.value = value
entry.json = JsonNull.INSTANCE
entry.file = null
entry.isBuiltin = isBuiltin
keyRefs[key]?.entry = entry
return entry
}
}
fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry<T> {
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
if (id in idsInternal) {
LOGGER.warn("Overwriting Registry entry with ID '$id' (new def originate from <code>; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = id
entry.value = value
entry.json = JsonNull.INSTANCE
entry.file = null
entry.isBuiltin = isBuiltin
keyRefs[key]?.entry = entry
idRefs[id]?.entry = entry
idsInternal[id] = entry
return entry
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}