KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt

315 lines
8.6 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier
import kotlin.collections.set
import kotlin.concurrent.withLock
class Registry<T : Any>(val name: String) {
private val keysInternal = HashMap<String, Impl>()
private val idsInternal = Int2ObjectOpenHashMap<Impl>()
private val keyRefs = HashMap<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() {
lock.withLock {
var next = backlog.poll()
while (next != null) {
next.run()
next = backlog.poll()
}
}
}
private val lock = ReentrantLock()
val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(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 isEmpty: Boolean
get() = value == null
val value: T?
get() = entry?.value
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>
@Deprecated("Careful! This might be confused with 'isMeta' of some entries (such as tiles, in such case use entry.isMeta)")
abstract val isBuiltin: Boolean
abstract val ref: Ref<T>
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 val ref: Ref<T> by lazy { ref(key) }
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 to value=${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 getOrThrow(index: String): Entry<T> {
return get(index) ?: throw NoSuchElementException("No such $name: $index")
}
fun getOrThrow(index: Int): Entry<T> {
return get(index) ?: throw NoSuchElementException("No such $name: $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 valid = true
keyRefs.values.forEach {
if (!it.isPresent && it.key.left() != "") {
LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)")
valid = 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)")
valid = false
}
}
return valid
}
fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name 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> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
if (id in idsInternal) {
LOGGER.warn("Overwriting $name 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> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name 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> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
if (id in idsInternal) {
LOGGER.warn("Overwriting $name 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
}
}
val emptyRef = ref("")
companion object {
private val LOGGER = LogManager.getLogger()
}
}