KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt
DBotThePony f95bc9762f
Minimally working Monster entities
PathController, PathFinder

Actor movement controller Lua bindings

Game loading no longer block Universe thread, more efficient registry population synchronization

Environmental status effects now can be stat modifiers
2024-06-28 22:44:13 +07:00

336 lines
8.7 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.longs.LongOpenHashSet
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.XXHash64
import ru.dbotthepony.kstarbound.util.limit
import java.util.Collections
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.function.Supplier
import kotlin.collections.set
import kotlin.concurrent.read
import kotlin.concurrent.write
class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
private val keysInternal = HashMap<String, Impl>()
private val idsInternal = Int2ObjectOpenHashMap<Impl>()
private val keyRefs = HashMap<String, RefImpl>()
private val idRefs = Int2ObjectOpenHashMap<RefImpl>()
private val lock = ReentrantReadWriteLock()
private var hasBeenValidated = false
// idiot-proof miss lookup. Surely, it will cause some entries to be never logged
// if they are missing, but at least if malicious actor spams with long-ass invalid data
// it won't explode memory usage of server
private val loggedMisses = LongOpenHashSet()
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
}
private val hash: Int
init {
var x = key.hashCode()
// avalanche bits using murmur3 hash
x = x xor (x ushr 16)
x *= -0x7a143595
x = x xor (x ushr 13)
x *= -0x3d4d51cb
x = x xor (x ushr 16)
hash = x
}
override fun hashCode(): Int {
return hash
}
override fun toString(): String {
return "Entry of $name at $key/${id ?: "-"}"
}
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
}
private val hash: Int
init {
var x = key.hashCode()
// avalanche bits using murmur3 hash
x = x xor (x ushr 16)
x *= -0x7a143595
x = x xor (x ushr 13)
x *= -0x3d4d51cb
x = x xor (x ushr 16)
hash = x
}
override fun hashCode(): Int {
return hash
}
override fun toString(): String {
return "Ref of $name at $key/${if (entry != null) "bound" else "missing"}"
}
override val registry: Registry<T>
get() = this@Registry
}
operator fun get(index: String): Entry<T>? {
val result = lock.read { keysInternal[index] }
if (result == null && hasBeenValidated) {
val hasher = XXHash64()
hasher.update(index.toByteArray())
val missIndex = hasher.digestAsLong()
lock.write {
if (loggedMisses.add(missIndex)) {
LOGGER.warn("No such $name: ${index.limit()}")
}
}
}
return result
}
operator fun get(index: Int): Entry<T>? {
val result = lock.read { idsInternal[index] }
if (result == null && hasBeenValidated) {
val hasher = XXHash64()
hasher.update(index.toByte())
hasher.update((index ushr 8).toByte())
hasher.update((index ushr 16).toByte())
hasher.update((index ushr 24).toByte())
val missIndex = hasher.digestAsLong()
lock.write {
if (loggedMisses.add(missIndex)) {
LOGGER.warn("No such $name: ID $index")
}
}
}
return result
}
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")
}
/**
* To be used inside network readers, so clients sending invalid data will get kicked
*/
fun refOrThrow(index: String): Ref<T> = get(index)?.ref ?: throw NoSuchElementException("No such $name: ${index.limit()}")
fun ref(index: String): Ref<T> = lock.write {
keyRefs.computeIfAbsent(index, Object2ObjectFunction {
val ref = RefImpl(Either.left(it as String))
ref.entry = keysInternal[it]
if (hasBeenValidated && ref.entry == null) {
val hasher = XXHash64()
hasher.update(index.toByteArray())
if (loggedMisses.add(hasher.digestAsLong())) {
LOGGER.warn("No such $name: ${it.limit()}")
}
}
ref
}).also {
it.references++
}
}
/**
* To be used inside network readers, so clients sending invalid data will get kicked
*/
fun refOrThrow(index: Int): Ref<T> = get(index)?.ref ?: throw NoSuchElementException("No such $name: ID $index")
fun ref(index: Int): Ref<T> = lock.write {
idRefs.computeIfAbsent(index, Int2ObjectFunction {
val ref = RefImpl(Either.right(it))
ref.entry = idsInternal[it]
if (hasBeenValidated && ref.entry == null) {
val hasher = XXHash64()
hasher.update(index.toByte())
hasher.update((index ushr 8).toByte())
hasher.update((index ushr 16).toByte())
hasher.update((index ushr 24).toByte())
if (loggedMisses.add(hasher.digestAsLong())) {
LOGGER.warn("No such $name: ID $it")
}
}
ref
}).also {
it.references++
}
}
operator fun contains(index: String) = lock.read { index in keysInternal }
operator fun contains(index: Int) = lock.read { index in idsInternal }
fun validate(): Boolean {
hasBeenValidated = true
lock.read {
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 = JsonNull.INSTANCE,
file: IStarboundFile? = null,
id: KOptional<Int?> = KOptional(),
isBuiltin: Boolean = false
): Entry<T> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.write {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
id.ifPresent { id ->
if (id != null && 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 || isBuiltin) { "Trying to redefine builtin entry at $key" }
id.ifPresent {
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = it
}
entry.value = value
if (storeJson)
entry.json = json
entry.file = file
entry.isBuiltin = isBuiltin
keyRefs[key]?.entry = entry
id.ifPresent { id ->
if (id != null) {
idRefs[id]?.entry = entry
idsInternal[id] = entry
}
}
return entry
}
}
val emptyRef = ref("")
companion object {
private val LOGGER = LogManager.getLogger()
}
}