371 lines
10 KiB
Kotlin
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()
|
|
}
|
|
}
|