From 949ed802ad8f268a4de04c3f5180ce3e90e10224 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Sun, 22 Oct 2023 00:45:22 +0700
Subject: [PATCH] Redesigned registry

---
 .../kotlin/ru/dbotthepony/kstarbound/Ext.kt   |   9 +-
 .../kotlin/ru/dbotthepony/kstarbound/Main.kt  |   3 +-
 .../dbotthepony/kstarbound/ObjectRegistry.kt  | 185 ---------
 .../dbotthepony/kstarbound/RecipeRegistry.kt  |  83 +++--
 .../ru/dbotthepony/kstarbound/Registries.kt   | 145 +++++---
 .../ru/dbotthepony/kstarbound/Registry.kt     | 350 ++++++++++++++++++
 .../ru/dbotthepony/kstarbound/Starbound.kt    |  62 ++--
 .../kstarbound/client/render/RenderLayer.kt   |   4 +-
 .../kstarbound/client/render/TileRenderer.kt  |   4 +-
 .../kstarbound/client/world/ClientWorld.kt    |  15 +-
 .../kstarbound/defs/ItemReference.kt          |  11 +-
 .../kstarbound/defs/RegistryReference.kt      | 100 -----
 .../ru/dbotthepony/kstarbound/defs/Species.kt |   7 +-
 .../defs/item/TreasurePoolDefinition.kt       |  14 +-
 .../defs/item/api/IItemDefinition.kt          |   4 +-
 .../defs/item/impl/ItemDefinition.kt          |   5 +-
 .../defs/monster/MonsterTypeDefinition.kt     |   4 +-
 .../defs/object/ObjectDefinition.kt           |  13 +-
 .../defs/object/ObjectOrientation.kt          |   4 +-
 .../defs/particle/ParticleCreator.kt          |   4 +-
 .../defs/player/BlueprintLearnList.kt         |   4 +-
 .../defs/player/DeploymentConfig.kt           |   6 +-
 .../defs/player/PlayerDefinition.kt           |   8 +-
 .../defs/tile/BuiltinMetaMaterials.kt         |   8 +-
 .../kstarbound/defs/tile/LiquidDefinition.kt  |   4 +-
 .../ru/dbotthepony/kstarbound/lua/LuaState.kt |  19 +-
 .../dbotthepony/kstarbound/player/Avatar.kt   |   8 +-
 .../ru/dbotthepony/kstarbound/util/Either.kt  |  16 +
 .../dbotthepony/kstarbound/util/ItemStack.kt  |   9 +-
 .../dbotthepony/kstarbound/util/KOptional.kt  |   2 +-
 .../kstarbound/world/LightCalculator.kt       |   2 +-
 .../kstarbound/world/Raycasting.kt            |   2 +-
 .../ru/dbotthepony/kstarbound/world/World.kt  |   2 +-
 .../world/api/AbstractLiquidState.kt          |   5 +-
 .../kstarbound/world/api/AbstractTileState.kt |   5 +-
 .../world/api/ImmutableLiquidState.kt         |   3 +-
 .../world/api/ImmutableTileState.kt           |   5 +-
 .../world/api/MutableLiquidState.kt           |   5 +-
 .../kstarbound/world/api/MutableTileState.kt  |   9 +-
 .../kstarbound/world/entities/WorldObject.kt  |   6 +-
 40 files changed, 641 insertions(+), 513 deletions(-)
 delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt
 delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt
index 83dfb253..9260c45d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt
@@ -50,11 +50,14 @@ fun String.sintern(): String = Starbound.STRINGS.intern(this)
 
 inline fun <reified T> Gson.fromJson(reader: JsonReader): T? = fromJson<T>(reader, T::class.java)
 
-fun <T : Any> Collection<IStarboundFile>.batch(executor: ForkJoinPool, batchSize: Int = 16, mapper: (IStarboundFile) -> KOptional<RegistryObject<T>>): Stream<RegistryObject<T>> {
+/**
+ * guarantees even distribution of tasks while also preserving encountered order of elements
+ */
+fun <T> Collection<IStarboundFile>.batch(executor: ForkJoinPool, batchSize: Int = 16, mapper: (IStarboundFile) -> KOptional<T>): Stream<T> {
 	require(batchSize >= 1) { "Invalid batch size: $batchSize" }
 
 	if (batchSize == 1 || size <= batchSize) {
-		val tasks = ArrayList<ForkJoinTask<KOptional<RegistryObject<T>>>>()
+		val tasks = ArrayList<ForkJoinTask<KOptional<T>>>()
 
 		for (listedFile in this) {
 			tasks.add(executor.submit(Callable { mapper.invoke(listedFile) }))
@@ -63,7 +66,7 @@ fun <T : Any> Collection<IStarboundFile>.batch(executor: ForkJoinPool, batchSize
 		return tasks.stream().map { it.join() }.filter { it.isPresent }.map { it.value }
 	}
 
-	val batches = ArrayList<ForkJoinTask<List<KOptional<RegistryObject<T>>>>>()
+	val batches = ArrayList<ForkJoinTask<List<KOptional<T>>>>()
 	var batch = ArrayList<IStarboundFile>(batchSize)
 
 	for (listedFile in this) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index 9a9ce81c..7d379e4e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -11,7 +11,6 @@ import ru.dbotthepony.kstarbound.player.Avatar
 import ru.dbotthepony.kstarbound.player.QuestDescriptor
 import ru.dbotthepony.kstarbound.player.QuestInstance
 import ru.dbotthepony.kstarbound.util.JVMTimeSource
-import ru.dbotthepony.kstarbound.world.api.AbstractCell
 import ru.dbotthepony.kstarbound.world.entities.ItemEntity
 import ru.dbotthepony.kstarbound.io.json.VersionedJson
 import ru.dbotthepony.kstarbound.io.readVarInt
@@ -118,7 +117,7 @@ fun main() {
 		val rand = Random()
 
 		for (i in 0 .. 128) {
-			val item = ItemEntity(client.world!!, Registries.items.values.random().value)
+			val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value)
 
 			item.position = Vector2d(225.0 - i, 785.0)
 			item.spawn()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt
deleted file mode 100644
index 8d4810d2..00000000
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-package ru.dbotthepony.kstarbound
-
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.JsonPrimitive
-import com.google.gson.internal.bind.JsonTreeReader
-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
-import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
-import org.apache.logging.log4j.LogManager
-import ru.dbotthepony.kstarbound.api.IStarboundFile
-import ru.dbotthepony.kstarbound.lua.LuaState
-import ru.dbotthepony.kstarbound.util.AssetPathStack
-import ru.dbotthepony.kstarbound.util.set
-import ru.dbotthepony.kstarbound.util.traverseJsonPath
-import java.util.*
-import kotlin.reflect.KClass
-
-inline fun <reified T : Any> ObjectRegistry(name: String, noinline key: ((T) -> String)? = null, noinline intKey: ((T) -> Int)? = null): ObjectRegistry<T> {
-	return ObjectRegistry(T::class, name, key, intKey)
-}
-
-fun mergeJsonElements(source: JsonObject, destination: JsonObject): JsonObject {
-	for ((k, v) in source.entrySet()) {
-		if (!destination.has(k)) {
-			destination[k] = v.deepCopy()
-		} else {
-			mergeJsonElements(v, destination[k])
-		}
-	}
-
-	return destination
-}
-
-fun mergeJsonElements(source: JsonArray, destination: JsonArray): JsonArray {
-	for ((i, v) in source.withIndex()) {
-		if (i >= destination.size()) {
-			destination.add(v.deepCopy())
-		} else {
-			destination[i] = mergeJsonElements(v, destination[i])
-		}
-	}
-
-	return destination
-}
-
-fun mergeJsonElements(source: JsonElement, destination: JsonElement): JsonElement {
-	if (destination is JsonPrimitive) {
-		return destination
-	}
-
-	if (destination is JsonObject && source is JsonObject) {
-		return mergeJsonElements(source, destination)
-	}
-
-	if (destination is JsonArray && source is JsonArray) {
-		return mergeJsonElements(source, destination)
-	}
-
-	return destination
-}
-
-class RegistryObject<T : Any>(
-	/**
-	 * Объект реестра
-	 */
-	val value: T,
-	/**
-	 * Оригинальный JSON объекта без каких либо изменений
-	 */
-	val json: JsonElement,
-	/**
-	 * Файл, откуда данный объект был загружен
-	 */
-	val file: IStarboundFile,
-) {
-	val jsonObject get() = json as JsonObject
-
-	fun push(lua: LuaState) {
-		lua.push(toJson())
-	}
-
-	fun push(lua: LuaState.ArgStack) {
-		lua.push(toJson())
-	}
-
-	/**
-	 * Возвращает полную (обработанную) структуру [JsonObject] объекта [value]
-	 *
-	 * Полнота определяется тем, что [value] может иметь свойства по умолчанию, которые не указаны
-	 * в оригинальной JSON структуре. [copy] не вернёт данные свойства по умолчанию, а [toJson] вернёт.
-	 */
-	fun toJson(): JsonElement {
-		return mergeJsonElements(json, Starbound.gson.toJsonTree(value))
-	}
-
-	fun traverseJsonPath(path: String): JsonElement? {
-		return traverseJsonPath(path, mergeJsonElements(json, Starbound.gson.toJsonTree(value)))
-	}
-
-	override fun equals(other: Any?): Boolean {
-		return other === this || other is RegistryObject<*> && other.value == value && other.json == json
-	}
-
-	private var computedHash = false
-	private var hash = 0
-
-	override fun hashCode(): Int {
-		if (!computedHash) {
-			hash = value.hashCode().rotateRight(13) xor json.hashCode()
-			computedHash = true
-		}
-
-		return hash
-	}
-
-	override fun toString(): String {
-		return "RegistryObject[$value from $file]"
-	}
-}
-
-class ObjectRegistry<T : Any>(val clazz: KClass<T>, val name: String, val key: ((T) -> String)? = null, val intKey: ((T) -> Int)? = null) {
-	val objects = Object2ObjectOpenHashMap<String, RegistryObject<T>>()
-	val intObjects = Int2ObjectOpenHashMap<RegistryObject<T>>()
-
-	val values get() = objects.values
-
-	operator fun get(index: String) = objects[index]
-	operator fun get(index: Int): RegistryObject<T>? = intObjects[index]
-	operator fun contains(index: String) = index in objects
-	operator fun contains(index: Int) = index in intObjects
-
-	fun clear() {
-		objects.clear()
-		intObjects.clear()
-	}
-
-	fun add(file: IStarboundFile): Boolean {
-		return AssetPathStack(file.computeDirectory()) {
-			val elem = Starbound.gson.fromJson(file.reader(), JsonElement::class.java)
-			val value = Starbound.gson.fromJson<T>(JsonTreeReader(elem), clazz.java)
-			add(RegistryObject(value, elem, file), this.key?.invoke(value) ?: throw UnsupportedOperationException("No key mapper"))
-		}
-	}
-
-	fun add(value: RegistryObject<T>): Boolean {
-		return add(value, this.key?.invoke(value.value) ?: throw UnsupportedOperationException("No key mapper"))
-	}
-
-	fun add(value: T, json: JsonElement, file: IStarboundFile): Boolean {
-		return add(RegistryObject(value, json, file), this.key?.invoke(value) ?: throw UnsupportedOperationException("No key mapper"))
-	}
-
-	fun add(value: T, json: JsonElement, file: IStarboundFile, key: String): Boolean {
-		return add(RegistryObject(value, json, file), key)
-	}
-
-	private val lock = Any()
-
-	private fun add(value: RegistryObject<T>, key: String): Boolean {
-		synchronized(lock) {
-			val existing = objects.put(key, value)
-
-			if (existing != null) {
-				LOGGER.warn("Registry $name already has object with key $key! Overwriting. (old originated from ${existing.file}, new originate from ${value.file}).")
-			}
-
-			if (this.intKey == null)
-				return existing != null
-
-			val intKey = this.intKey.invoke(value.value)
-			val intExisting = intObjects.put(intKey, value)
-
-			if (intExisting != null) {
-				LOGGER.warn("Registry $name already has object with ID $intKey (new $key, old ${ objects.entries.firstOrNull { it.value === intExisting }?.key })! Overwriting. (old originated from ${intExisting.file}, new originate from ${value.file}).")
-			}
-
-			return existing != null || intExisting != null
-		}
-	}
-
-	companion object {
-		private val LOGGER = LogManager.getLogger()
-	}
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt
index 565cbaf3..43f1a16a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt
@@ -1,63 +1,66 @@
 package ru.dbotthepony.kstarbound
 
-import com.google.gson.Gson
 import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.internal.bind.JsonTreeReader
 import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kstarbound.api.IStarboundFile
 import ru.dbotthepony.kstarbound.defs.player.RecipeDefinition
 import ru.dbotthepony.kstarbound.util.KOptional
-import java.util.Collections
-import java.util.LinkedList
-import java.util.concurrent.Callable
-import java.util.concurrent.ExecutorService
+import ru.dbotthepony.kstarbound.util.ParallelPerform
+import java.util.*
 import java.util.concurrent.ForkJoinPool
 import java.util.concurrent.ForkJoinTask
-import java.util.concurrent.Future
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.collections.ArrayList
+import kotlin.concurrent.withLock
 
 object RecipeRegistry {
 	private val LOGGER = LogManager.getLogger()
 
-	private val recipesInternal = ArrayList<RegistryObject<RecipeDefinition>>()
-	private val group2recipesInternal = Object2ObjectOpenHashMap<String, LinkedList<RegistryObject<RecipeDefinition>>>()
-	private val group2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>()
-	private val output2recipesInternal = Object2ObjectOpenHashMap<String, LinkedList<RegistryObject<RecipeDefinition>>>()
-	private val output2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>()
-	private val input2recipesInternal = Object2ObjectOpenHashMap<String, LinkedList<RegistryObject<RecipeDefinition>>>()
-	private val input2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>()
+	data class Entry(val value: RecipeDefinition, val json: JsonElement, val file: IStarboundFile)
 
-	val recipes: List<RegistryObject<RecipeDefinition>> = Collections.unmodifiableList(recipesInternal)
-	val group2recipes: Map<String, List<RegistryObject<RecipeDefinition>>> = Collections.unmodifiableMap(group2recipesBacking)
-	val output2recipes: Map<String, List<RegistryObject<RecipeDefinition>>> = Collections.unmodifiableMap(output2recipesBacking)
-	val input2recipes: Map<String, List<RegistryObject<RecipeDefinition>>> = Collections.unmodifiableMap(input2recipesBacking)
+	private val recipesInternal = ArrayList<Entry>()
+	private val group2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<Entry>>()
+	private val group2recipesBacking = Object2ObjectOpenHashMap<String, List<Entry>>()
+	private val output2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<Entry>>()
+	private val output2recipesBacking = Object2ObjectOpenHashMap<String, List<Entry>>()
+	private val input2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<Entry>>()
+	private val input2recipesBacking = Object2ObjectOpenHashMap<String, List<Entry>>()
 
-	fun add(recipe: RegistryObject<RecipeDefinition>) {
-		val value = recipe.value
-		recipesInternal.add(recipe)
+	val recipes: List<Entry> = Collections.unmodifiableList(recipesInternal)
+	val group2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(group2recipesBacking)
+	val output2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(output2recipesBacking)
+	val input2recipes: Map<String, List<Entry>> = Collections.unmodifiableMap(input2recipesBacking)
 
-		for (group in value.groups) {
-			group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p ->
-				LinkedList<RegistryObject<RecipeDefinition>>().also {
-					group2recipesBacking[p as String] = Collections.unmodifiableList(it)
-				}
-			}).add(recipe)
-		}
+	private val lock = ReentrantLock()
 
-		output2recipesInternal.computeIfAbsent(value.output.item.name, Object2ObjectFunction { p ->
-			LinkedList<RegistryObject<RecipeDefinition>>().also {
-				output2recipesBacking[p as String] = Collections.unmodifiableList(it)
+	fun add(recipe: Entry) {
+		lock.withLock {
+			val value = recipe.value
+			recipesInternal.add(recipe)
+
+			for (group in value.groups) {
+				group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p ->
+					ArrayList<Entry>(1).also {
+						group2recipesBacking[p as String] = Collections.unmodifiableList(it)
+					}
+				}).add(recipe)
 			}
-		}).add(recipe)
 
-		for (input in value.input) {
-			input2recipesInternal.computeIfAbsent(input.item.name, Object2ObjectFunction { p ->
-				LinkedList<RegistryObject<RecipeDefinition>>().also {
-					input2recipesBacking[p as String] = Collections.unmodifiableList(it)
+			output2recipesInternal.computeIfAbsent(value.output.item.key.left(), Object2ObjectFunction { p ->
+				ArrayList<Entry>(1).also {
+					output2recipesBacking[p as String] = Collections.unmodifiableList(it)
 				}
 			}).add(recipe)
+
+			for (input in value.input) {
+				input2recipesInternal.computeIfAbsent(input.item.key.left(), Object2ObjectFunction { p ->
+					ArrayList<Entry>(1).also {
+						input2recipesBacking[p as String] = Collections.unmodifiableList(it)
+					}
+				}).add(recipe)
+			}
 		}
 	}
 
@@ -77,12 +80,12 @@ object RecipeRegistry {
 					line.text = ("Loading $listedFile")
 					val json = elements.read(listedFile.jsonReader())
 					val value = recipes.fromJsonTree(json)
-					line.elements.incrementAndGet()
-					KOptional(RegistryObject(value, json, listedFile))
+					KOptional.of(Entry(value, json, listedFile))
 				} catch (err: Throwable) {
 					LOGGER.error("Loading recipe definition file $listedFile", err)
-					line.elements.incrementAndGet()
 					KOptional.empty()
+				} finally {
+					line.elements.incrementAndGet()
 				}
 			}.forEach { add(it) }
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt
index 2243044b..2788eeb3 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt
@@ -36,33 +36,64 @@ import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
 import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
 import ru.dbotthepony.kstarbound.util.AssetPathStack
 import ru.dbotthepony.kstarbound.util.KOptional
-import java.util.concurrent.Callable
-import java.util.concurrent.ExecutorService
+import ru.dbotthepony.kstarbound.util.ParallelPerform
 import java.util.concurrent.ForkJoinPool
 import java.util.concurrent.ForkJoinTask
-import java.util.concurrent.Future
 
 object Registries {
 	private val LOGGER = LogManager.getLogger()
 
-	val tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId)
-	val tileModifiers = ObjectRegistry("tile modifiers", MaterialModifier::modName, MaterialModifier::modId)
-	val liquid = ObjectRegistry("liquid", LiquidDefinition::name, LiquidDefinition::liquidId)
-	val species = ObjectRegistry("species", Species::kind)
-	val statusEffects = ObjectRegistry("status effects", StatusEffectDefinition::name)
-	val particles = ObjectRegistry("particles", ParticleDefinition::kind)
-	val items = ObjectRegistry("items", IItemDefinition::itemName)
-	val questTemplates = ObjectRegistry("quest templates", QuestTemplate::id)
-	val techs = ObjectRegistry("techs", TechDefinition::name)
-	val jsonFunctions = ObjectRegistry<JsonFunction>("json functions")
-	val json2Functions = ObjectRegistry<Json2Function>("json 2functions")
-	val npcTypes = ObjectRegistry("npc types", NpcTypeDefinition::type)
-	val projectiles = ObjectRegistry("projectiles", ProjectileDefinition::projectileName)
-	val tenants = ObjectRegistry("tenants", TenantDefinition::name)
-	val treasurePools = ObjectRegistry("treasure pools", TreasurePoolDefinition::name)
-	val monsterSkills = ObjectRegistry("monster skills", MonsterSkillDefinition::name)
-	val monsterTypes = ObjectRegistry("monster types", MonsterTypeDefinition::type)
-	val worldObjects = ObjectRegistry("objects", ObjectDefinition::objectName)
+	val tiles = Registry<TileDefinition>("tiles")
+	val tileModifiers = Registry<MaterialModifier>("tile modifiers")
+	val liquid = Registry<LiquidDefinition>("liquid")
+	val species = Registry<Species>("species")
+	val statusEffects = Registry<StatusEffectDefinition>("status effects")
+	val particles = Registry<ParticleDefinition>("particles")
+	val items = Registry<IItemDefinition>("items")
+	val questTemplates = Registry<QuestTemplate>("quest templates")
+	val techs = Registry<TechDefinition>("techs")
+	val jsonFunctions = Registry<JsonFunction>("json functions")
+	val json2Functions = Registry<Json2Function>("json 2functions")
+	val npcTypes = Registry<NpcTypeDefinition>("npc types")
+	val projectiles = Registry<ProjectileDefinition>("projectiles")
+	val tenants = Registry<TenantDefinition>("tenants")
+	val treasurePools = Registry<TreasurePoolDefinition>("treasure pools")
+	val monsterSkills = Registry<MonsterSkillDefinition>("monster skills")
+	val monsterTypes = Registry<MonsterTypeDefinition>("monster types")
+	val worldObjects = Registry<ObjectDefinition>("objects")
+
+	private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> {
+		return { mapper.invoke(it) to null }
+	}
+
+	private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int): (T) -> Pair<String, Int> {
+		return { mapper.invoke(it) to mapperInt.invoke(it) }
+	}
+
+	fun validate(): Boolean {
+		var any = false
+
+		any = !tiles.validate() || any
+		any = !tileModifiers.validate() || any
+		any = !liquid.validate() || any
+		any = !species.validate() || any
+		any = !statusEffects.validate() || any
+		any = !particles.validate() || any
+		any = !items.validate() || any
+		any = !questTemplates.validate() || any
+		any = !techs.validate() || any
+		any = !jsonFunctions.validate() || any
+		any = !json2Functions.validate() || any
+		any = !npcTypes.validate() || any
+		any = !projectiles.validate() || any
+		any = !tenants.validate() || any
+		any = !treasurePools.validate() || any
+		any = !monsterSkills.validate() || any
+		any = !monsterTypes.validate() || any
+		any = !worldObjects.validate() || any
+
+		return !any
+	}
 
 	private fun loadStage(
 		log: ILoadingLog,
@@ -81,8 +112,9 @@ object Registries {
 	private inline fun <reified T : Any> loadStage(
 		log: ILoadingLog,
 		executor: ForkJoinPool,
-		registry: ObjectRegistry<T>,
+		registry: Registry<T>,
 		files: List<IStarboundFile>,
+		noinline keyProvider: (T) -> Pair<String, Int?>,
 		name: String = registry.name
 	) {
 		val adapter = Starbound.gson.getAdapter(T::class.java)
@@ -95,19 +127,29 @@ object Registries {
 				try {
 					it.text = "Loading $listedFile"
 
-					val result = AssetPathStack(listedFile.computeDirectory()) {
+					AssetPathStack(listedFile.computeDirectory()) {
 						val elem = elementAdapter.read(listedFile.jsonReader())
-						RegistryObject(adapter.fromJsonTree(elem), elem, listedFile)
-					}
+						val read = adapter.fromJsonTree(elem)
+						val keys = keyProvider(read);
 
-					it.elements.incrementAndGet()
-					KOptional(result)
+						KOptional.of {
+							try {
+								if (keys.second != null)
+									registry.add(keys.first, keys.second!!, read, elem, listedFile)
+								else
+									registry.add(keys.first, read, elem, listedFile)
+							} catch (err: Throwable) {
+								LOGGER.error("Loading ${registry.name} definition file $listedFile", err);
+							}
+						}
+					}
 				} catch (err: Throwable) {
-					LOGGER.error("Loading ${registry.name} definition file $listedFile", err)
-					it.elements.incrementAndGet()
+					LOGGER.error("Loading ${registry.name} definition file $listedFile", err);
 					KOptional.empty()
+				} finally {
+					it.elements.incrementAndGet()
 				}
-			}.forEach { registry.add(it) }
+			}.forEach { it.invoke() }
 		}, name)
 	}
 
@@ -120,20 +162,20 @@ object Registries {
 		tasks.add(executor.submit { loadStage(log, { loadJson2Functions(it, fileTree["2functions"] ?: listOf()) }, "json 2functions") })
 		tasks.add(executor.submit { loadStage(log, { loadTreasurePools(it, fileTree["treasurepools"] ?: listOf()) }, "treasure pools") })
 
-		tasks.add(executor.submit { loadStage(log, executor, tiles, fileTree["material"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, tileModifiers, fileTree["matmod"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, liquid, fileTree["liquid"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, worldObjects, fileTree["object"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, statusEffects, fileTree["statuseffect"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, species, fileTree["species"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, particles, fileTree["particle"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, questTemplates, fileTree["questtemplate"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, techs, fileTree["tech"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, npcTypes, fileTree["npctype"] ?: listOf()) })
-		// tasks.add(executor.submit { loadStage(log, executor, projectiles, ext2files["projectile"] ?: listOf()) })
-		// tasks.add(executor.submit { loadStage(log, executor, tenants, ext2files["tenant"] ?: listOf()) })
-		tasks.add(executor.submit { loadStage(log, executor, monsterSkills, fileTree["monsterskill"] ?: listOf()) })
-		// tasks.add(executor.submit { loadStage(log, _monsterTypes, ext2files["monstertype"] ?: listOf()) })
+		tasks.add(executor.submit { loadStage(log, executor, tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId)) })
+		tasks.add(executor.submit { loadStage(log, executor, tileModifiers, fileTree["matmod"] ?: listOf(), key(MaterialModifier::modName, MaterialModifier::modId)) })
+		tasks.add(executor.submit { loadStage(log, executor, liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId)) })
+
+		tasks.add(executor.submit { loadStage(log, executor, worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)) })
+		tasks.add(executor.submit { loadStage(log, executor, statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)) })
+		tasks.add(executor.submit { loadStage(log, executor, species, fileTree["species"] ?: listOf(), key(Species::kind)) })
+		tasks.add(executor.submit { loadStage(log, executor, particles, fileTree["particle"] ?: listOf(), key(ParticleDefinition::kind)) })
+		tasks.add(executor.submit { loadStage(log, executor, questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)) })
+		tasks.add(executor.submit { loadStage(log, executor, techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name)) })
+		tasks.add(executor.submit { loadStage(log, executor, npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)) })
+		// tasks.add(executor.submit { loadStage(log, executor, projectiles, ext2files["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName)) })
+		// tasks.add(executor.submit { loadStage(log, executor, tenants, ext2files["tenant"] ?: listOf(), key(TenantDefinition::name)) })
+		tasks.add(executor.submit { loadStage(log, executor, monsterSkills, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)) })
 
 		return tasks
 	}
@@ -164,19 +206,18 @@ object Registries {
 				line.maxElements = fileList.size
 				val time = System.nanoTime()
 
-				fileList.batch(executor) { listedFile ->
+				ParallelPerform(fileList.spliterator(), { listedFile ->
 					try {
 						line.text = "Loading $listedFile"
 						val json = objects.read(listedFile.jsonReader())
 						val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) }
-						line.elements.incrementAndGet()
-						KOptional(RegistryObject(def, json, listedFile))
+						items.add(def.itemName, def, json, listedFile)
 					} catch (err: Throwable) {
 						LOGGER.error("Loading item definition file $listedFile", err)
+					} finally {
 						line.elements.incrementAndGet()
-						KOptional.empty()
 					}
-				}.forEach { items.add(it) }
+				}).fork().join()
 
 				line.text = "Loaded items '$ext' in ${((System.nanoTime() - time) / 1_000_000.0).toLong()}ms"
 			})
@@ -196,7 +237,7 @@ object Registries {
 					try {
 						line.text = ("Loading $k from $listedFile")
 						val fn = Starbound.gson.fromJson<JsonFunction>(JsonTreeReader(v), JsonFunction::class.java)
-						jsonFunctions.add(fn, v, listedFile, k)
+						jsonFunctions.add(k, fn, v, listedFile)
 					} catch (err: Exception) {
 						LOGGER.error("Loading json function definition $k from file $listedFile", err)
 					}
@@ -222,7 +263,7 @@ object Registries {
 					try {
 						line.text = ("Loading $k from $listedFile")
 						val fn = Starbound.gson.fromJson<Json2Function>(JsonTreeReader(v), Json2Function::class.java)
-						json2Functions.add(fn, v, listedFile, k)
+						json2Functions.add(k, fn, v, listedFile)
 					} catch (err: Throwable) {
 						LOGGER.error("Loading json 2function definition $k from file $listedFile", err)
 					}
@@ -249,7 +290,7 @@ object Registries {
 						line.text = ("Loading $k from $listedFile")
 						val result = Starbound.gson.fromJson<TreasurePoolDefinition>(JsonTreeReader(v), TreasurePoolDefinition::class.java)
 						result.name = k
-						treasurePools.add(result, v, listedFile)
+						treasurePools.add(result.name, result, v, listedFile)
 					} catch (err: Throwable) {
 						LOGGER.error("Loading treasure pool definition $k from file $listedFile", err)
 					}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt
new file mode 100644
index 00000000..dd2f14dd
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt
@@ -0,0 +1,350 @@
+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.kstarbound.api.IStarboundFile
+import ru.dbotthepony.kstarbound.io.json.consumeNull
+import ru.dbotthepony.kstarbound.util.Either
+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
+
+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 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
+
+		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
+		})
+	}
+
+	fun ref(index: Int): Ref<T> = lock.withLock {
+		idRefs.computeIfAbsent(index, Int2ObjectFunction {
+			val ref = RefImpl(Either.right(it))
+			ref.entry = idsInternal[it]
+			ref
+		})
+	}
+
+	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")
+				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")
+				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()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
index 483c8418..0968550d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
@@ -194,40 +194,30 @@ object Starbound : ISBFileLocator {
 
 		registerTypeAdapterFactory(Poly.Companion)
 
-		registerTypeAdapterFactory(with(RegistryReferenceFactory()) {
-			add(Registries.tiles)
-			add(Registries.tileModifiers)
-			add(Registries.liquid)
-			add(Registries.items)
-			add(Registries.species)
-			add(Registries.statusEffects)
-			add(Registries.particles)
-			add(Registries.questTemplates)
-			add(Registries.techs)
-			add(Registries.jsonFunctions)
-			add(Registries.json2Functions)
-			add(Registries.npcTypes)
-			add(Registries.projectiles)
-			add(Registries.tenants)
-			add(Registries.treasurePools)
-			add(Registries.monsterSkills)
-			add(Registries.monsterTypes)
-			add(Registries.worldObjects)
-		})
+		registerTypeAdapterFactory(Registries.tiles.adapter())
+		registerTypeAdapterFactory(Registries.tileModifiers.adapter())
+		registerTypeAdapterFactory(Registries.liquid.adapter())
+		registerTypeAdapterFactory(Registries.items.adapter())
+		registerTypeAdapterFactory(Registries.species.adapter())
+		registerTypeAdapterFactory(Registries.statusEffects.adapter())
+		registerTypeAdapterFactory(Registries.particles.adapter())
+		registerTypeAdapterFactory(Registries.questTemplates.adapter())
+		registerTypeAdapterFactory(Registries.techs.adapter())
+		registerTypeAdapterFactory(Registries.jsonFunctions.adapter())
+		registerTypeAdapterFactory(Registries.json2Functions.adapter())
+		registerTypeAdapterFactory(Registries.npcTypes.adapter())
+		registerTypeAdapterFactory(Registries.projectiles.adapter())
+		registerTypeAdapterFactory(Registries.tenants.adapter())
+		registerTypeAdapterFactory(Registries.treasurePools.adapter())
+		registerTypeAdapterFactory(Registries.monsterSkills.adapter())
+		registerTypeAdapterFactory(Registries.monsterTypes.adapter())
+		registerTypeAdapterFactory(Registries.worldObjects.adapter())
 
 		registerTypeAdapter(LongRangeAdapter)
 
 		create()
 	}
 
-	init {
-		val f = NonExistingFile("/metamaterials.config")
-
-		for (material in BuiltinMetaMaterials.MATERIALS) {
-			Registries.tiles.add(material, JsonNull.INSTANCE, f)
-		}
-	}
-
 	fun item(name: String): ItemStack {
 		return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY)
 	}
@@ -411,7 +401,7 @@ object Starbound : ISBFileLocator {
 
 		state.setTableFunction("recipesForItem", this) { args ->
 			args.lua.push(JsonArray().also { a ->
-				RecipeRegistry.output2recipes[args.getString()]?.stream()?.map { it.toJson() }?.forEach {
+				RecipeRegistry.output2recipes[args.getString()]?.stream()?.map { it.json }?.forEach {
 					a.add(it)
 				}
 			})
@@ -449,7 +439,7 @@ object Starbound : ISBFileLocator {
 				args.push()
 			} else {
 				args.push(JsonObject().also {
-					it["directory"] = item.item!!.file.computeDirectory()
+					it["directory"] = item.item?.file?.computeDirectory()?.let(::JsonPrimitive) ?: JsonNull.INSTANCE
 					it["config"] = item.item!!.json
 					it["parameters"] = item.parameters
 				})
@@ -481,7 +471,7 @@ object Starbound : ISBFileLocator {
 		state.setTableFunction("tenantConfig", this) { args ->
 			// Json root.tenantConfig(String tenantName)
 			val name = args.getString()
-			Registries.tenants[name]?.push(args) ?: throw NoSuchElementException("No such tenant $name")
+			args.push(Registries.tenants[name] ?: throw NoSuchElementException("No such tenant $name"))
 			1
 		}
 
@@ -496,11 +486,11 @@ object Starbound : ISBFileLocator {
 				}
 			}
 
-			args.push(Registries.tenants.values
+			args.push(Registries.tenants.keys.values
 						  .stream()
 						  .filter { it.value.test(actualTags) }
 						  .sorted { a, b -> b.value.compareTo(a.value) }
-						  .map { it.toJson() }
+						  .map { it.json }
 						  .collect(JsonArrayCollector))
 
 			1
@@ -517,7 +507,7 @@ object Starbound : ISBFileLocator {
 				liquid = Registries.liquid[id]?.value ?: throw NoSuchElementException("No such liquid with ID $id")
 			}
 
-			args.lua.pushStrings(liquid.statusEffects.stream().map { it.value?.value?.name }.filterNotNull().toList())
+			args.lua.pushStrings(liquid.statusEffects.stream().map { it.value?.name }.filterNotNull().toList())
 
 			1
 		}
@@ -711,7 +701,7 @@ object Starbound : ISBFileLocator {
 		state.setTableFunction("techConfig", this) { args ->
 			val name = args.getString()
 			val tech = Registries.techs[name] ?: throw NoSuchElementException("No such tech $name")
-			tech.push(args)
+			args.push(tech)
 			1
 		}
 
@@ -898,6 +888,8 @@ object Starbound : ISBFileLocator {
 		if (!parallel)
 			pool.shutdown()
 
+		Registries.validate()
+
 		initializing = false
 		initialized = true
 		log.line("Finished loading in ${System.currentTimeMillis() - time}ms")
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt
index 5f766386..537cb8d2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt
@@ -71,9 +71,9 @@ enum class RenderLayer {
 
 		fun tileLayer(isBackground: Boolean, isModifier: Boolean, tile: AbstractTileState): Point {
 			if (isModifier) {
-				return tileLayer(isBackground, true, tile.modifier?.renderParameters?.zLevel ?: 0L, tile.modifier?.modId?.toLong() ?: 0L, tile.modifierHueShift)
+				return tileLayer(isBackground, true, tile.modifier?.value?.renderParameters?.zLevel ?: 0L, tile.modifier?.value?.modId?.toLong() ?: 0L, tile.modifierHueShift)
 			} else {
-				return tileLayer(isBackground, false, tile.material.renderParameters.zLevel, tile.material.materialId.toLong(), tile.hueShift)
+				return tileLayer(isBackground, false, tile.material.value.renderParameters.zLevel, tile.material.value.materialId.toLong(), tile.hueShift)
 			}
 		}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
index 90ba78e7..124b9977 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
@@ -95,13 +95,13 @@ class TileRenderers(val client: StarboundClient) {
 
 private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester {
 	override fun test(thisTile: AbstractTileState?, otherTile: AbstractTileState?): Boolean {
-		return otherTile?.material == definition && thisTile?.hueShift == otherTile.hueShift
+		return otherTile?.material?.value == definition && thisTile?.hueShift == otherTile.hueShift
 	}
 }
 
 private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester {
 	override fun test(thisTile: AbstractTileState?, otherTile: AbstractTileState?): Boolean {
-		return otherTile?.modifier == definition
+		return otherTile?.modifier?.value == definition
 	}
 }
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
index a39ae0ee..6f3ae8c6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt
@@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
 import it.unimi.dsi.fastutil.longs.LongArraySet
 import it.unimi.dsi.fastutil.objects.ObjectArrayList
 import it.unimi.dsi.fastutil.objects.ReferenceArraySet
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
 import ru.dbotthepony.kstarbound.client.StarboundClient
 import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh
@@ -97,9 +98,9 @@ class ClientWorld(
 							val tile = view.getTile(x, y) ?: continue
 							val material = tile.material
 
-							if (!material.isMeta) {
+							if (!material.value.isMeta) {
 								client.tileRenderers
-									.getMaterialRenderer(material.materialName)
+									.getMaterialRenderer(material.key)
 									.tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground)
 							}
 
@@ -107,7 +108,7 @@ class ClientWorld(
 
 							if (modifier != null) {
 								client.tileRenderers
-									.getModifierRenderer(modifier.modName)
+									.getModifierRenderer(modifier.key)
 									.tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground, isModifier = true)
 							}
 						}
@@ -134,11 +135,11 @@ class ClientWorld(
 				liquidIsDirty = false
 				liquidMesh.clear()
 
-				val liquidTypes = ReferenceArraySet<LiquidDefinition>()
+				val liquidTypes = ReferenceArraySet<Registry.Entry<LiquidDefinition>>()
 
 				for (x in 0 until renderRegionWidth) {
 					for (y in 0 until renderRegionHeight) {
-						view.getCell(x, y)?.liquid?.def?.let { liquidTypes.add(it) }
+						view.getCell(x, y).liquid.def?.let { liquidTypes.add(it) }
 					}
 				}
 
@@ -151,7 +152,7 @@ class ClientWorld(
 						for (y in 0 until renderRegionHeight) {
 							val state = view.getCell(x, y)
 
-							if (state?.liquid?.def === type) {
+							if (state.liquid.def == type) {
 								builder.vertex(x.toFloat(), y.toFloat())
 								builder.vertex(x.toFloat() + 1f, y.toFloat())
 								builder.vertex(x.toFloat() + 1f, y.toFloat() + 1f)
@@ -160,7 +161,7 @@ class ClientWorld(
 						}
 					}
 
-					liquidMesh.add(Mesh(builder) to type.color)
+					liquidMesh.add(Mesh(builder) to type.value.color)
 				}
 			}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt
index 1700aa6c..3e368ce1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt
@@ -9,6 +9,7 @@ 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 ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
 import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@@ -19,12 +20,16 @@ import ru.dbotthepony.kstarbound.util.ItemStack
  * Прототип [ItemStack] в JSON файлах
  */
 data class ItemReference(
-	val item: RegistryReference<IItemDefinition>,
+	val item: Registry.Ref<IItemDefinition>,
 	val count: Long = 1,
 	val parameters: JsonObject = JsonObject()
 ) {
+	init {
+		require(item.key.isLeft) { "Can't reference item by ID" }
+	}
+
 	fun makeStack(): ItemStack {
-		return ItemStack(item.value ?: return ItemStack.EMPTY, count, parameters)
+		return ItemStack(item.entry ?: return ItemStack.EMPTY, count, parameters)
 	}
 
 	class Factory(val stringInterner: Interner<String> = Interner { it }) : TypeAdapterFactory {
@@ -33,7 +38,7 @@ data class ItemReference(
 				return object : TypeAdapter<ItemReference>() {
 					private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = false), gson, stringInterner)
 					private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = true), gson, stringInterner)
-					private val references = gson.getAdapter(TypeToken.getParameterized(RegistryReference::class.java, IItemDefinition::class.java)) as TypeAdapter<RegistryReference<IItemDefinition>>
+					private val references = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, IItemDefinition::class.java)) as TypeAdapter<Registry.Ref<IItemDefinition>>
 
 					override fun write(out: JsonWriter, value: ItemReference?) {
 						if (value == null)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt
deleted file mode 100644
index 5736a269..00000000
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package ru.dbotthepony.kstarbound.defs
-
-import com.google.gson.Gson
-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.objects.Reference2ObjectArrayMap
-import org.apache.logging.log4j.LogManager
-import ru.dbotthepony.kstarbound.ObjectRegistry
-import ru.dbotthepony.kstarbound.RegistryObject
-import ru.dbotthepony.kstarbound.io.json.consumeNull
-import java.lang.reflect.ParameterizedType
-import java.util.function.Supplier
-
-class RegistryReferenceFactory : TypeAdapterFactory {
-	private val types = Reference2ObjectArrayMap<Class<*>, Pair<(String) -> RegistryObject<Nothing>?, String>>()
-	private var isLenient = false
-
-	fun lenient(): RegistryReferenceFactory {
-		isLenient = true
-		return this
-	}
-
-	fun <T : Any> add(clazz: Class<T>, resolver: (String) -> RegistryObject<T>?, name: String): RegistryReferenceFactory {
-		check(types.put(clazz, (resolver as (String) -> RegistryObject<Nothing>?) to name) == null) { "Already has resolver for class $clazz!" }
-		return this
-	}
-
-	fun <T : Any> add(registry: ObjectRegistry<T>): RegistryReferenceFactory {
-		return add(registry.clazz.java, registry::get, registry.name)
-	}
-
-	inline fun <reified T: Any> add(noinline resolver: (String) -> RegistryObject<T>?, name: String) = add(T::class.java, resolver, name)
-
-	override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
-		if (type.rawType == RegistryReference::class.java) {
-			val ptype = type.type as? ParameterizedType ?: return null
-			val registryType = ptype.actualTypeArguments[0]
-			val resolver = types[registryType] ?: return if (isLenient) null else throw NoSuchElementException("Can't deserialize registry reference with type $registryType!")
-			return RegistryReferenceTypeAdapter(resolver.first, gson.getAdapter(String::class.java), resolver.second) as TypeAdapter<T>
-		}
-
-		return null
-	}
-}
-
-class RegistryReferenceTypeAdapter<T : Any>(val resolver: (String) -> RegistryObject<T>?, val strings: TypeAdapter<String>, val name: String) : TypeAdapter<RegistryReference<T>>() {
-	override fun write(out: JsonWriter, value: RegistryReference<T>?) {
-		if (value == null)
-			out.nullValue()
-		else
-			strings.write(out, value.name)
-	}
-
-	override fun read(`in`: JsonReader): RegistryReference<T>? {
-		if (`in`.consumeNull())
-			return null
-
-		if (`in`.peek() == JsonToken.STRING) {
-			return RegistryReference(strings.read(`in`)!!, resolver, name)
-		}
-
-		throw JsonSyntaxException("Expecting string for registry reference, ${`in`.peek()} given, near ${`in`.path}")
-	}
-}
-
-data class RegistryReference<T : Any>(val name: String, val resolver: (String) -> RegistryObject<T>?, val registryName: String) : Supplier<RegistryObject<T>?>, () -> RegistryObject<T>?, Lazy<RegistryObject<T>?> {
-	private val lazy = lazy {
-		val result = resolver.invoke(name)
-
-		if (result == null) {
-			LOGGER.error("No such object '$name' in registry '$registryName'! Expect stuff being broken!")
-		}
-
-		result
-	}
-
-	override fun get(): RegistryObject<T>? {
-		return lazy.value
-	}
-
-	override val value: RegistryObject<T>?
-		get() = lazy.value
-
-	override fun isInitialized(): Boolean {
-		return lazy.isInitialized()
-	}
-
-	override fun invoke(): RegistryObject<T>? {
-		return lazy.value
-	}
-
-	companion object {
-		private val LOGGER = LogManager.getLogger()
-	}
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt
index 8b968eeb..61f0d7cf 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt
@@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs
 
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableSet
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.image.SpriteReference
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
 import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList
@@ -25,7 +26,7 @@ data class Species(
 	val undyColor: ImmutableList<ColorReplacements>,
 	val hairColor: ImmutableList<ColorReplacements>,
 	val genders: ImmutableList<Gender>,
-	val statusEffects: ImmutableSet<RegistryReference<StatusEffectDefinition>> = ImmutableSet.of(),
+	val statusEffects: ImmutableSet<Registry.Ref<StatusEffectDefinition>> = ImmutableSet.of(),
 ) {
 	@JsonFactory
 	data class Tooltip(val title: String, val subTitle: String, val description: String)
@@ -37,8 +38,8 @@ data class Species(
 		val characterImage: SpriteReference,
 		val hairGroup: String? = null,
 		val hair: ImmutableSet<String>,
-		val shirt: ImmutableSet<RegistryReference<IItemDefinition>>,
-		val pants: ImmutableSet<RegistryReference<IItemDefinition>>,
+		val shirt: ImmutableSet<Registry.Ref<IItemDefinition>>,
+		val pants: ImmutableSet<Registry.Ref<IItemDefinition>>,
 		val facialHairGroup: String? = null,
 		val facialHair: ImmutableSet<String> = ImmutableSet.of(),
 		val facialMaskGroup: String? = null,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt
index 2dfbd7e0..0b952276 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt
@@ -12,8 +12,8 @@ import com.google.gson.internal.bind.JsonTreeReader
 import com.google.gson.reflect.TypeToken
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.ItemReference
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.io.json.consumeNull
 import ru.dbotthepony.kstarbound.io.json.stream
 import ru.dbotthepony.kstarbound.util.Either
@@ -49,7 +49,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
 	data class Piece(
 		val level: Double,
 		val pool: ImmutableList<PoolEntry> = ImmutableList.of(),
-		val fill: ImmutableList<Either<ItemReference, RegistryReference<TreasurePoolDefinition>>> = ImmutableList.of(),
+		val fill: ImmutableList<Either<ItemReference, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(),
 		val poolRounds: IPoolRounds = OneRound,
 		// TODO: что оно делает?
 		// оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool
@@ -69,7 +69,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
 						val stack = it.makeStack()
 						if (stack.isNotEmpty) result.add(stack)
 					}, {
-						it.value?.value?.evaluate(random, actualLevel)
+						it.value?.evaluate(random, actualLevel)
 					})
 				}
 
@@ -82,7 +82,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
 								val stack = it.makeStack()
 								if (stack.isNotEmpty) result.add(stack)
 							}, {
-								it.value?.value?.evaluate(random, actualLevel)
+								it.value?.evaluate(random, actualLevel)
 							})
 
 							break
@@ -145,7 +145,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
 
 	data class PoolEntry(
 		val weight: Double,
-		val treasure: Either<ItemReference, RegistryReference<TreasurePoolDefinition>>
+		val treasure: Either<ItemReference, Registry.Ref<TreasurePoolDefinition>>
 	) {
 		init {
 			require(weight > 0.0) { "Invalid pool entry weight: $weight" }
@@ -157,7 +157,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
 			if (type.rawType === TreasurePoolDefinition::class.java) {
 				return object : TypeAdapter<TreasurePoolDefinition>() {
 					private val itemAdapter = gson.getAdapter(ItemReference::class.java)
-					private val poolAdapter = gson.getAdapter(TypeToken.getParameterized(RegistryReference::class.java, TreasurePoolDefinition::class.java)) as TypeAdapter<RegistryReference<TreasurePoolDefinition>>
+					private val poolAdapter = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, TreasurePoolDefinition::class.java)) as TypeAdapter<Registry.Ref<TreasurePoolDefinition>>
 					private val objReader = gson.getAdapter(JsonObject::class.java)
 
 					override fun write(out: JsonWriter, value: TreasurePoolDefinition?) {
@@ -184,7 +184,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
 							val things = objReader.read(`in`)
 
 							val pool = ImmutableList.Builder<PoolEntry>()
-							val fill = ImmutableList.Builder<Either<ItemReference, RegistryReference<TreasurePoolDefinition>>>()
+							val fill = ImmutableList.Builder<Either<ItemReference, Registry.Ref<TreasurePoolDefinition>>>()
 							var poolRounds: IPoolRounds = OneRound
 							val allowDuplication = things["allowDuplication"]?.asBoolean ?: false
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt
index 1c293b3a..ed181369 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt
@@ -1,7 +1,7 @@
 package ru.dbotthepony.kstarbound.defs.item.api
 
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.IThingWithDescription
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
 import ru.dbotthepony.kstarbound.defs.item.ItemRarity
 import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition
@@ -46,7 +46,7 @@ interface IItemDefinition : IThingWithDescription {
 	/**
 	 * При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта
 	 */
-	val learnBlueprintsOnPickup: List<RegistryReference<IItemDefinition>>
+	val learnBlueprintsOnPickup: List<Registry.Ref<IItemDefinition>>
 
 	/**
 	 * Максимальное количество предмета в стопке
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt
index fb9d7198..9368cc78 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt
@@ -1,12 +1,11 @@
 package ru.dbotthepony.kstarbound.defs.item.impl
 
 import com.google.common.collect.ImmutableList
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.IThingWithDescription
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.defs.ThingDescription
 import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
-import ru.dbotthepony.kstarbound.defs.item.InventoryIcon
 import ru.dbotthepony.kstarbound.defs.item.ItemRarity
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFlat
@@ -22,7 +21,7 @@ data class ItemDefinition(
 	override val category: String? = null,
 	override val inventoryIcon: ImmutableList<IInventoryIcon>? = null,
 	override val itemTags: ImmutableList<String> = ImmutableList.of(),
-	override val learnBlueprintsOnPickup: ImmutableList<RegistryReference<IItemDefinition>> = ImmutableList.of(),
+	override val learnBlueprintsOnPickup: ImmutableList<Registry.Ref<IItemDefinition>> = ImmutableList.of(),
 	override val maxStack: Long = 9999L,
 	override val eventCategory: String? = null,
 	override val consumeOnPickup: Boolean = false,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt
index 7429dba8..7cef7bb0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt
@@ -3,11 +3,11 @@ package ru.dbotthepony.kstarbound.defs.monster
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableSet
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.AssetReference
 import ru.dbotthepony.kstarbound.defs.IScriptable
 import ru.dbotthepony.kstarbound.defs.IThingWithDescription
 import ru.dbotthepony.kstarbound.defs.player.PlayerMovementParameters
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
 import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@@ -24,7 +24,7 @@ data class MonsterTypeDefinition(
 	val animation: AssetReference<AnimationDefinition>,
 	// [ { "default" : "poptopTreasure", "bow" : "poptopHunting" } ],
 	// "dropPools" : [ "smallRobotTreasure" ],
-	val dropPools: Either<ImmutableList<ImmutableMap<String, RegistryReference<TreasurePoolDefinition>>>, ImmutableList<RegistryReference<TreasurePoolDefinition>>>,
+	val dropPools: Either<ImmutableList<ImmutableMap<String, Registry.Ref<TreasurePoolDefinition>>>, ImmutableList<Registry.Ref<TreasurePoolDefinition>>>,
 	val baseParameters: BaseParameters
 ) : IThingWithDescription by desc {
 	@JsonFactory
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt
index 172d22b3..3ae0defa 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt
@@ -11,14 +11,11 @@ import com.google.gson.JsonSyntaxException
 import com.google.gson.TypeAdapter
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.AssetPath
-import ru.dbotthepony.kstarbound.defs.AssetReference
 import ru.dbotthepony.kstarbound.defs.ItemReference
 import ru.dbotthepony.kstarbound.defs.JsonReference
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.defs.StatModifier
-import ru.dbotthepony.kstarbound.defs.TouchDamage
-import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
 import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
 import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
@@ -44,10 +41,10 @@ data class ObjectDefinition(
 	val hasObjectItem: Boolean = true,
 	val scannable: Boolean = true,
 	val retainObjectParametersInItem: Boolean = false,
-	val breakDropPool: RegistryReference<TreasurePoolDefinition>? = null,
+	val breakDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
 	// null - not specified, empty list - always drop nothing
 	val breakDropOptions: ImmutableList<ImmutableList<ItemReference>>? = null,
-	val smashDropPool: RegistryReference<TreasurePoolDefinition>? = null,
+	val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
 	val smashDropOptions: ImmutableList<ImmutableList<ItemReference>> = ImmutableList.of(),
 	//val animation: AssetReference<AnimationDefinition>? = null,
 	val animation: AssetPath? = null,
@@ -97,10 +94,10 @@ data class ObjectDefinition(
 			val hasObjectItem: Boolean = true,
 			val scannable: Boolean = true,
 			val retainObjectParametersInItem: Boolean = false,
-			val breakDropPool: RegistryReference<TreasurePoolDefinition>? = null,
+			val breakDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
 			// null - not specified, empty list - always drop nothing
 			val breakDropOptions: ImmutableList<ImmutableList<ItemReference>>? = null,
-			val smashDropPool: RegistryReference<TreasurePoolDefinition>? = null,
+			val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
 			val smashDropOptions: ImmutableList<ImmutableList<ItemReference>> = ImmutableList.of(),
 			//val animation: AssetReference<AnimationDefinition>? = null,
 			val animation: AssetPath? = null,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt
index 601fb53d..22af88c8 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt
@@ -210,8 +210,8 @@ data class ObjectOrientation(
 				val builder = ImmutableList.Builder<Pair<Vector2i, String>>()
 
 				when (val collisionType = obj.get("collisionType", "none").lowercase()) {
-					"solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.materialName) }
-					"platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.materialName) }
+					"solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.key) }
+					"platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) }
 					"none" -> {}
 					else -> throw JsonSyntaxException("Unknown collision type $collisionType")
 				}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt
index be01fd50..516abf61 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt
@@ -1,13 +1,13 @@
 package ru.dbotthepony.kstarbound.defs.particle
 
-import ru.dbotthepony.kstarbound.defs.RegistryReference
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
 import ru.dbotthepony.kstarbound.util.Either
 
 @JsonFactory
 data class ParticleCreator(
 	val count: Int = 1,
-	val particle: Either<RegistryReference<ParticleDefinition>, IParticleConfig>,
+	val particle: Either<Registry.Ref<ParticleDefinition>, IParticleConfig>,
 
 	//override val offset: Vector2d? = null,
 	//override val position: Vector2d? = null,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt
index 8e141d5e..c3fbb882 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt
@@ -10,7 +10,7 @@ import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonToken
 import com.google.gson.stream.JsonWriter
 import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
-import ru.dbotthepony.kstarbound.defs.RegistryReference
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
 
@@ -18,7 +18,7 @@ class BlueprintLearnList private constructor(private val tiers: Int2ObjectArrayM
 	constructor(tiers: Map<Int, List<Entry>>) : this(Int2ObjectArrayMap<ImmutableList<Entry>>().also { for ((k, v) in tiers.entries) it.put(k, ImmutableList.copyOf(v)) })
 
 	@JsonFactory
-	data class Entry(val item: RegistryReference<IItemDefinition>)
+	data class Entry(val item: Registry.Ref<IItemDefinition>)
 
 	operator fun get(tier: Int): List<Entry> {
 		return tiers.getOrDefault(tier, ImmutableList.of())
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/DeploymentConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/DeploymentConfig.kt
index d6fdd86d..769ea0c6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/DeploymentConfig.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/DeploymentConfig.kt
@@ -2,9 +2,9 @@ package ru.dbotthepony.kstarbound.defs.player
 
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.AssetPath
 import ru.dbotthepony.kstarbound.defs.IScriptable
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
 
@@ -13,8 +13,8 @@ data class DeploymentConfig(
 	override val scripts: ImmutableList<AssetPath>,
 	override val scriptDelta: Int,
 
-	val starterMechSet: ImmutableMap<String, RegistryReference<IItemDefinition>>,
-	val speciesStarterMechBody: ImmutableMap<String, RegistryReference<IItemDefinition>>,
+	val starterMechSet: ImmutableMap<String, Registry.Ref<IItemDefinition>>,
+	val speciesStarterMechBody: ImmutableMap<String, Registry.Ref<IItemDefinition>>,
 
 	val enemyDetectRadius: Double,
 	val enemyDetectTypeNames: ImmutableList<String>,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt
index 9490f255..bab74ab6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt
@@ -4,8 +4,8 @@ import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableSet
 import com.google.gson.JsonObject
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.AssetReference
-import ru.dbotthepony.kstarbound.defs.RegistryReference
 import ru.dbotthepony.kstarbound.defs.Species
 import ru.dbotthepony.kstarbound.util.SBPattern
 import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
@@ -22,14 +22,14 @@ data class PlayerDefinition(
 	val blueprintAlreadyKnown: SBPattern,
 	val collectableUnlock: SBPattern,
 
-	val species: ImmutableSet<RegistryReference<Species>>,
+	val species: ImmutableSet<Registry.Ref<Species>>,
 	val nametagColor: RGBAColor,
 	val ageItemsEvery: Int,
-	val defaultItems: ImmutableSet<RegistryReference<IItemDefinition>>,
+	val defaultItems: ImmutableSet<Registry.Ref<IItemDefinition>>,
 
 	val defaultBlueprints: BlueprintLearnList,
 
-	val defaultCodexes: ImmutableMap<String, ImmutableList<RegistryReference<IItemDefinition>>>,
+	val defaultCodexes: ImmutableMap<String, ImmutableList<Registry.Ref<IItemDefinition>>>,
 	val metaBoundBox: AABB,
 	val movementParameters: PlayerMovementParameters,
 	val zeroGMovementParameters: PlayerMovementParameters,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt
index a07ca8aa..ceec4a6e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt
@@ -1,12 +1,14 @@
 package ru.dbotthepony.kstarbound.defs.tile
 
 import com.google.common.collect.ImmutableList
+import ru.dbotthepony.kstarbound.Registries
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.AssetReference
 import ru.dbotthepony.kstarbound.world.physics.CollisionType
 import ru.dbotthepony.kstarbound.defs.ThingDescription
 
 object BuiltinMetaMaterials {
-	private fun make(id: Int, name: String, collisionType: CollisionType) = TileDefinition(
+	private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition(
 		materialId = id,
 		materialName = "metamaterial:$name",
 		descriptionData = ThingDescription.EMPTY,
@@ -15,7 +17,7 @@ object BuiltinMetaMaterials {
 		renderParameters = RenderParameters.META,
 		isMeta = true,
 		collisionKind = collisionType
-	)
+	))
 
 	/**
 	 * air
@@ -38,7 +40,7 @@ object BuiltinMetaMaterials {
 	val OBJECT_SOLID = make(65500, "objectsolid", CollisionType.BLOCK)
 	val OBJECT_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM)
 
-	val MATERIALS: ImmutableList<TileDefinition> = ImmutableList.of(
+	val MATERIALS: ImmutableList<Registry.Entry<TileDefinition>> = ImmutableList.of(
 		EMPTY,
 		NULL,
 		STRUCTURE,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt
index 49c8bf6b..b883deb9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt
@@ -1,7 +1,7 @@
 package ru.dbotthepony.kstarbound.defs.tile
 
 import com.google.common.collect.ImmutableList
-import ru.dbotthepony.kstarbound.defs.RegistryReference
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
 import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
 import ru.dbotthepony.kvector.vector.RGBAColor
@@ -14,7 +14,7 @@ data class LiquidDefinition(
 	val tickDelta: Int = 1,
 	val color: RGBAColor,
 	val itemDrop: String? = null,
-	val statusEffects: ImmutableList<RegistryReference<StatusEffectDefinition>> = ImmutableList.of(),
+	val statusEffects: ImmutableList<Registry.Ref<StatusEffectDefinition>> = ImmutableList.of(),
 	val interactions: ImmutableList<Interaction> = ImmutableList.of(),
 	val texture: String,
 	val bottomLightMix: RGBAColor,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt
index 5a55188d..28c374b6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt
@@ -17,7 +17,7 @@ import jnr.ffi.Pointer
 import org.apache.logging.log4j.LogManager
 import org.lwjgl.system.MemoryStack
 import org.lwjgl.system.MemoryUtil
-import ru.dbotthepony.kstarbound.RegistryObject
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
 import ru.dbotthepony.kvector.api.IStruct2i
@@ -595,8 +595,8 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter
 		fun push(value: Boolean) = this@LuaState.push(value)
 		fun push(value: String?) = this@LuaState.push(value)
 		fun push(value: JsonElement?) = this@LuaState.push(value)
-		fun push(value: RegistryObject<*>?) = this@LuaState.push(value)
-		fun pushFull(value: RegistryObject<*>?) = this@LuaState.pushFull(value)
+		fun push(value: Registry.Entry<*>?) = this@LuaState.push(value)
+		fun pushFull(value: Registry.Entry<*>?) = this@LuaState.pushFull(value)
 	}
 
 	/**
@@ -902,7 +902,8 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter
 		this.setTableValue(table)
 	}
 
-	fun setTableValue(key: String, value: String) {
+	fun setTableValue(key: String, value: String?) {
+		value ?: return
 		val table = this.stackTop
 		this.push(key)
 		this.push(value)
@@ -1058,20 +1059,20 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter
 		}
 	}
 
-	fun push(value: RegistryObject<*>?) {
+	fun push(value: Registry.Entry<*>?) {
 		if (value == null)
 			push()
 		else
-			push(value.toJson())
+			push(value.json)
 	}
 
-	fun pushFull(value: RegistryObject<*>?) {
+	fun pushFull(value: Registry.Entry<*>?) {
 		if (value == null)
 			push()
 		else {
 			pushTable(hashSize = 2)
-			setTableValue("path", value.file.computeFullPath())
-			setTableValue("config", value.toJson())
+			setTableValue("path", value.file?.computeFullPath())
+			setTableValue("config", value.json)
 		}
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt
index b53398c2..c636697d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt
@@ -7,7 +7,7 @@ import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
 import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
 import ru.dbotthepony.kstarbound.Registries
-import ru.dbotthepony.kstarbound.RegistryObject
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.player.TechDefinition
 import ru.dbotthepony.kstarbound.lua.LuaState
@@ -48,9 +48,9 @@ class Avatar(val uniqueId: UUID) {
 
 	var cursorItem = ItemStack.EMPTY
 
-	private val availableTechs = ObjectOpenHashSet<RegistryObject<TechDefinition>>()
-	private val enabledTechs = ObjectOpenHashSet<RegistryObject<TechDefinition>>()
-	private val equippedTechs = Object2ObjectOpenHashMap<String, RegistryObject<TechDefinition>>()
+	private val availableTechs = ObjectOpenHashSet<Registry.Entry<TechDefinition>>()
+	private val enabledTechs = ObjectOpenHashSet<Registry.Entry<TechDefinition>>()
+	private val equippedTechs = Object2ObjectOpenHashMap<String, Registry.Entry<TechDefinition>>()
 
 	private val knownBlueprints = ObjectOpenHashSet<ItemStack>()
 	// С подписью NEW
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt
index 2c5b3435..f2be38b0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt
@@ -51,6 +51,22 @@ class Either<L, R> private constructor(val left: KOptional<L>, val right: KOptio
 			return orElse.invoke()
 	}
 
+	override fun equals(other: Any?): Boolean {
+		return other === this || other is Either<*, *> && other.left == left && other.right == right
+	}
+
+	override fun hashCode(): Int {
+		return left.hashCode() * 31 + right.hashCode()
+	}
+
+	override fun toString(): String {
+		if (isLeft) {
+			return "Either.left[${left.value}]"
+		} else {
+			return "Either.right[${right.value}]"
+		}
+	}
+
 	companion object {
 		@JvmStatic
 		fun <L, R> left(value: L): Either<L, R> {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt
index d4a4e3a4..141e9851 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt
@@ -5,17 +5,16 @@ import com.google.gson.JsonPrimitive
 import com.google.gson.TypeAdapter
 import com.google.gson.internal.bind.TypeAdapters
 import com.google.gson.stream.JsonReader
-import com.google.gson.stream.JsonToken
 import com.google.gson.stream.JsonWriter
-import ru.dbotthepony.kstarbound.RegistryObject
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
 import ru.dbotthepony.kstarbound.io.json.consumeNull
 
-class ItemStack private constructor(item: RegistryObject<IItemDefinition>?, count: Long, val parameters: JsonObject, marker: Unit) {
-	constructor(item: RegistryObject<IItemDefinition>, count: Long = 1L, parameters: JsonObject = JsonObject()) : this(item, count, parameters, Unit)
+class ItemStack private constructor(item: Registry.Entry<IItemDefinition>?, count: Long, val parameters: JsonObject, marker: Unit) {
+	constructor(item: Registry.Entry<IItemDefinition>, count: Long = 1L, parameters: JsonObject = JsonObject()) : this(item, count, parameters, Unit)
 
-	var item: RegistryObject<IItemDefinition>? = item
+	var item: Registry.Entry<IItemDefinition>? = item
 		private set
 
 	var size = count
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/KOptional.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/KOptional.kt
index 3df464e6..9c42ae6e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/KOptional.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/KOptional.kt
@@ -67,7 +67,7 @@ class KOptional<T> private constructor(private val _value: T, val isPresent: Boo
 	}
 
 	override fun hashCode(): Int {
-		return _value.hashCode()
+		return _value.hashCode() + 43839429
 	}
 
 	override fun toString(): String {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt
index 5228934f..31ac6433 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt
@@ -64,7 +64,7 @@ class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int)
 				val parent = this@LightCalculator.parent.getCell(x, y) ?: return@lazy 0f
 				val lightBlockStrength: Float
 
-				if (parent.foreground.material.renderParameters.lightTransparent) {
+				if (parent.foreground.material.value.renderParameters.lightTransparent) {
 					lightBlockStrength = 0f
 				} else {
 					lightBlockStrength = 1f
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt
index 6e4b050e..f6094e80 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt
@@ -47,7 +47,7 @@ fun interface TileRayFilter {
 }
 
 val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE }
-val NonEmptyFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.of(!state.foreground.material.collisionKind.isEmpty) }
+val NonEmptyFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.of(!state.foreground.material.value.collisionKind.isEmpty) }
 
 fun ICellAccess.castRay(startPos: Vector2d, direction: Vector2d, length: Double, filter: TileRayFilter) = castRay(startPos, startPos + direction * length, filter)
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
index 5490c17a..4c7f38cb 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
@@ -226,7 +226,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 
 				result.add(CollisionPoly(
 					BLOCK_POLY + Vector2d(x.toDouble(), y.toDouble()),
-					cell.foreground.material.collisionKind,
+					cell.foreground.material.value.collisionKind,
 					//velocity = Vector2d(EARTH_FREEFALL_ACCELERATION, 0.0)
 				))
 			}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt
index dc8b6a79..89eabdd0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt
@@ -1,12 +1,11 @@
 package ru.dbotthepony.kstarbound.world.api
 
-import ru.dbotthepony.kstarbound.Registries
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
-import ru.dbotthepony.kstarbound.util.HashTableInterner
 import java.io.DataInputStream
 
 sealed class AbstractLiquidState {
-	abstract val def: LiquidDefinition?
+	abstract val def: Registry.Entry<LiquidDefinition>?
 	abstract val level: Float
 	abstract val pressure: Float
 	abstract val isInfinite: Boolean
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt
index e2114c44..85ba8f17 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt
@@ -1,13 +1,14 @@
 package ru.dbotthepony.kstarbound.world.api
 
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
 import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
 import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
 import java.io.DataInputStream
 
 sealed class AbstractTileState {
-	abstract val material: TileDefinition
-	abstract val modifier: MaterialModifier?
+	abstract val material: Registry.Entry<TileDefinition>
+	abstract val modifier: Registry.Entry<MaterialModifier>?
 	abstract val color: TileColor
 	abstract val hueShift: Float
 	abstract val modifierHueShift: Float
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt
index 0a73cfb7..d7a1f3c1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt
@@ -1,9 +1,10 @@
 package ru.dbotthepony.kstarbound.world.api
 
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
 
 data class ImmutableLiquidState(
-	override val def: LiquidDefinition? = null,
+	override val def: Registry.Entry<LiquidDefinition>? = null,
 	override val level: Float = 0f,
 	override val pressure: Float = 0f,
 	override val isInfinite: Boolean = false,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt
index d5328f77..c7b2d302 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt
@@ -1,12 +1,13 @@
 package ru.dbotthepony.kstarbound.world.api
 
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
 import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
 import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
 
 data class ImmutableTileState(
-	override var material: TileDefinition = BuiltinMetaMaterials.NULL,
-	override var modifier: MaterialModifier? = null,
+	override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL,
+	override var modifier: Registry.Entry<MaterialModifier>? = null,
 	override var color: TileColor = TileColor.DEFAULT,
 	override var hueShift: Float = 0f,
 	override var modifierHueShift: Float = 0f,
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt
index f6add68e..0158b666 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt
@@ -1,17 +1,18 @@
 package ru.dbotthepony.kstarbound.world.api
 
 import ru.dbotthepony.kstarbound.Registries
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
 import java.io.DataInputStream
 
 data class MutableLiquidState(
-	override var def: LiquidDefinition? = null,
+	override var def: Registry.Entry<LiquidDefinition>? = null,
 	override var level: Float = 0f,
 	override var pressure: Float = 0f,
 	override var isInfinite: Boolean = false,
 ) : AbstractLiquidState() {
 	fun read(stream: DataInputStream): MutableLiquidState {
-		def = Registries.liquid[stream.readUnsignedByte()]?.value
+		def = Registries.liquid[stream.readUnsignedByte()]
 		level = stream.readFloat()
 		pressure = stream.readFloat()
 		isInfinite = stream.readBoolean()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt
index 7fdcbe3a..c65a47cc 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt
@@ -1,14 +1,15 @@
 package ru.dbotthepony.kstarbound.world.api
 
 import ru.dbotthepony.kstarbound.Registries
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
 import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
 import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
 import java.io.DataInputStream
 
 data class MutableTileState(
-	override var material: TileDefinition = BuiltinMetaMaterials.NULL,
-	override var modifier: MaterialModifier? = null,
+	override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL,
+	override var modifier: Registry.Entry<MaterialModifier>? = null,
 	override var color: TileColor = TileColor.DEFAULT,
 	override var hueShift: Float = 0f,
 	override var modifierHueShift: Float = 0f,
@@ -49,10 +50,10 @@ data class MutableTileState(
 	}
 
 	fun read(stream: DataInputStream): MutableTileState {
-		material = Registries.tiles[stream.readUnsignedShort()]?.value ?: BuiltinMetaMaterials.EMPTY
+		material = Registries.tiles[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY
 		setHueShift(stream.read())
 		color = TileColor.of(stream.read())
-		modifier = Registries.tileModifiers[stream.readUnsignedShort()]?.value
+		modifier = Registries.tileModifiers[stream.readUnsignedShort()]
 		setModHueShift(stream.read())
 		return this
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt
index 5eb76346..4bc6abe0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt
@@ -6,7 +6,7 @@ import com.google.gson.TypeAdapter
 import com.google.gson.reflect.TypeToken
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
 import ru.dbotthepony.kstarbound.Registries
-import ru.dbotthepony.kstarbound.RegistryObject
+import ru.dbotthepony.kstarbound.Registry
 import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.client.world.ClientWorld
 import ru.dbotthepony.kstarbound.defs.Drawable
@@ -26,9 +26,9 @@ import ru.dbotthepony.kvector.vector.Vector2i
 
 open class WorldObject(
 	val world: World<*, *>,
-	val prototype: RegistryObject<ObjectDefinition>,
+	val prototype: Registry.Entry<ObjectDefinition>,
 	val pos: Vector2i,
-) : JsonDriven(prototype.file.computeDirectory()) {
+) : JsonDriven(prototype.file?.computeDirectory() ?: "/") {
 	constructor(world: World<*, *>, data: JsonObject) : this(
 		world,
 		Registries.worldObjects[data["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${data["name"]}'"),