From 3da8450a2c906d86db39b690aaa789c0f4f3b580 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Mon, 2 Jan 2023 00:08:34 +0700
Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=B0=D0=B2=D0=B0=D0=B9=D1=82=D0=B5=20?=
 =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B2=D0=B0=20=D0=BF=D0=BE=D0=BF=D1=80=D0=BE?=
 =?UTF-8?q?=D0=B1=D1=83=D0=B5=D0=BC=20builder'=D0=BE=D0=B2,=20=D0=BD=D0=BE?=
 =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=8D=D1=82=D0=BE=D1=82=20=D1=80=D0=B0=D0=B7?=
 =?UTF-8?q?=20=D1=81=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?=
 =?UTF-8?q?=D1=81=D0=B0=D0=BC=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kotlin/ru/dbotthepony/kstarbound/Ext.kt   |   3 +
 .../ru/dbotthepony/kstarbound/Starbound.kt    | 122 ++++--
 .../kstarbound/defs/IThingWithDescription.kt  |  24 ++
 .../kstarbound/defs/ImmutableEnroller.kt      |   4 +-
 .../kstarbound/defs/JsonFunction.kt           |   6 +-
 .../kstarbound/defs/RawPrototype.kt           |   2 +-
 .../defs/image/AtlasConfiguration.kt          |   2 +-
 .../kstarbound/defs/image/ImageReference.kt   |   2 +-
 .../kstarbound/defs/image/SpriteReference.kt  |   2 +-
 .../defs/item/ArmorItemDefinition.kt          |  32 ++
 .../defs/item/ArmorItemPrototype.kt           |  67 ++++
 .../kstarbound/defs/item/ArmorPieceType.kt    |  26 ++
 .../defs/item/CurrencyItemDefinition.kt       |  32 ++
 .../defs/item/CurrencyItemPrototype.kt        |  65 +++
 .../defs/item/IArmorItemDefinition.kt         |  50 +++
 .../defs/item/ICurrencyItemDefinition.kt      |  38 ++
 .../defs/item/IFossilItemDefinition.kt        |  70 ++++
 .../kstarbound/defs/item/IItemDefinition.kt   | 105 +++++
 .../defs/item/ILeveledItemDefinition.kt       |  13 +
 .../defs/item/ILeveledStatusEffect.kt         |  26 ++
 .../kstarbound/defs/item/ItemDefinition.kt    | 370 +-----------------
 .../kstarbound/defs/item/ItemPrototype.kt     |  87 ++++
 .../kstarbound/defs/item/ItemRarity.kt        |  10 +-
 .../kstarbound/defs/item/ItemTooltipKind.kt   |  43 ++
 .../defs/projectile/Configurable.kt           |   6 +-
 .../defs/projectile/ProjectilePhysics.kt      |   7 +-
 .../kstarbound/defs/tile/RenderParameters.kt  |   2 +-
 .../kstarbound/defs/tile/RenderTemplate.kt    |   4 +-
 .../kstarbound/defs/world/SkyParameters.kt    |   4 +-
 .../defs/world/dungeon/Configurable.kt        |   7 +-
 .../kstarbound/io/json/BuilderAdapter.kt      |  29 +-
 .../io/json/CustomEnumTypeAdapter.kt          |  31 --
 .../kstarbound/io/json/EnumAdapter.kt         | 127 +++++-
 .../ru/dbotthepony/kstarbound/io/json/Ext.kt  |  16 +
 .../kstarbound/io/json/FactoryAdapter.kt      |  18 +-
 .../io/json/String2ObjectAdapter.kt           |   2 +-
 .../kstarbound/world/entities/ItemEntity.kt   |   3 +-
 37 files changed, 975 insertions(+), 482 deletions(-)
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorPieceType.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IArmorItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ICurrencyItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IFossilItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledItemDefinition.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemTooltipKind.kt
 delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/CustomEnumTypeAdapter.kt

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt
index 02a2ab32..2ba6913a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt
@@ -1,5 +1,6 @@
 package ru.dbotthepony.kstarbound
 
+import com.google.common.collect.ImmutableMap
 import com.google.gson.GsonBuilder
 import com.google.gson.TypeAdapter
 import java.util.Arrays
@@ -19,3 +20,5 @@ operator fun <T> ThreadLocal<T>.getValue(thisRef: Any, property: KProperty<*>):
 operator fun <T> ThreadLocal<T>.setValue(thisRef: Any, property: KProperty<*>, value: T?) {
 	set(value)
 }
+
+operator fun <K, V> ImmutableMap.Builder<K, V>.set(key: K, value: V): ImmutableMap.Builder<K, V> = put(key, value)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
index 51bf5add..9336c472 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
@@ -17,8 +17,16 @@ import ru.dbotthepony.kstarbound.api.explore
 import ru.dbotthepony.kstarbound.defs.*
 import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration
 import ru.dbotthepony.kstarbound.defs.image.SpriteReference
-import ru.dbotthepony.kstarbound.defs.item.ItemDefinition
+import ru.dbotthepony.kstarbound.defs.item.ArmorItemPrototype
+import ru.dbotthepony.kstarbound.defs.item.ArmorPieceType
+import ru.dbotthepony.kstarbound.defs.item.CurrencyItemPrototype
+import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition
+import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition
+import ru.dbotthepony.kstarbound.defs.item.IItemDefinition
+import ru.dbotthepony.kstarbound.defs.item.ItemPrototype
 import ru.dbotthepony.kstarbound.defs.item.ItemRarity
+import ru.dbotthepony.kstarbound.defs.item.ItemTooltipKind
+import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect
 import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition
 import ru.dbotthepony.kstarbound.defs.projectile.*
 import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
@@ -30,7 +38,7 @@ import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef
 import ru.dbotthepony.kstarbound.io.*
 import ru.dbotthepony.kstarbound.io.json.AABBTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter
-import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
+import ru.dbotthepony.kstarbound.io.json.EnumAdapter
 import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
@@ -57,24 +65,24 @@ object Starbound {
 	/**
 	 * Служит переменной для указания из какой папки происходит чтение asset'а в данном потоке
 	 */
-	var readingFolder by ThreadLocal<String>()
+	var assetFolder by ThreadLocal<String>()
 		private set
 
-	fun readingFolderTransformer(input: String): String {
-		val readingFolder = readingFolder
-		require(readingFolder != null) { "Not reading an asset on current thread" }
+	fun assetFolder(input: String): String {
+		val assetFolder = assetFolder
+		require(assetFolder != null) { "Not reading an asset on current thread" }
 
 		if (input[0] == '/')
 			return input
 
-		return assetStringInterner.intern("$readingFolder/$input")
+		return STRING_INTERNER.intern("$assetFolder/$input")
 	}
 
-	fun readingFolderTransformerNullable(input: String?): String? {
-		require(readingFolder != null) { "Not reading an asset on current thread" }
+	fun assetFolderNullable(input: String?): String? {
+		require(assetFolder != null) { "Not reading an asset on current thread" }
 
 		if (input != null)
-			return readingFolderTransformer(input)
+			return assetFolder(input)
 
 		return null
 	}
@@ -83,7 +91,7 @@ object Starbound {
 		if (input == null)
 			return null
 
-		return input.stream().map { readingFolderTransformer(it) }.collect(ImmutableList.toImmutableList())
+		return input.stream().map { assetFolder(it) }.collect(ImmutableList.toImmutableList())
 	}
 
 	private val tiles = Object2ObjectOpenHashMap<String, TileDefinition>()
@@ -99,7 +107,7 @@ object Starbound {
 	private val parallax = Object2ObjectOpenHashMap<String, ParallaxPrototype>()
 	private val functions = Object2ObjectOpenHashMap<String, JsonFunction>()
 
-	private val items = Object2ObjectOpenHashMap<String, ItemDefinition>()
+	private val items = Object2ObjectOpenHashMap<String, IItemDefinition>()
 
 	val liquidAccess: Map<String, LiquidDefinition> = Collections.unmodifiableMap(liquid)
 	val liquidByIDAccess: Map<Int, LiquidDefinition> = Collections.unmodifiableMap(liquidByID)
@@ -110,21 +118,21 @@ object Starbound {
 	val projectilesAccess: Map<String, ConfiguredProjectile> = Collections.unmodifiableMap(projectiles)
 	val parallaxAccess: Map<String, ParallaxPrototype> = Collections.unmodifiableMap(parallax)
 	val functionsAccess: Map<String, JsonFunction> = Collections.unmodifiableMap(functions)
-	val itemAccess: Map<String, ItemDefinition> = Collections.unmodifiableMap(items)
+	val itemAccess: Map<String, IItemDefinition> = Collections.unmodifiableMap(items)
 
-	val assetStringInterner: Interner<String> = Interners.newStrongInterner()
+	val STRING_INTERNER: Interner<String> = Interners.newStrongInterner()
 
-	val nonnullStringTypeAdapter: TypeAdapter<String> = object : TypeAdapter<String>() {
+	val STRING_ADAPTER: TypeAdapter<String> = object : TypeAdapter<String>() {
 		override fun write(out: JsonWriter, value: String) {
 			out.value(value)
 		}
 
 		override fun read(`in`: JsonReader): String {
-			return assetStringInterner.intern(TypeAdapters.STRING.read(`in`))
+			return STRING_INTERNER.intern(TypeAdapters.STRING.read(`in`))
 		}
 	}
 
-	val stringTypeAdapter: TypeAdapter<String?> = nonnullStringTypeAdapter.nullSafe()
+	val NULLABLE_STRING_ADAPTER: TypeAdapter<String?> = STRING_ADAPTER.nullSafe()
 
 	val gson: Gson = GsonBuilder()
 		.enableComplexMapKeySerialization()
@@ -135,7 +143,7 @@ object Starbound {
 		.registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe())
 
 		// чтоб строки всегда intern'ились
-		.registerTypeAdapter(stringTypeAdapter)
+		.registerTypeAdapter(NULLABLE_STRING_ADAPTER)
 
 		// math
 		.registerTypeAdapter(AABBTypeAdapter)
@@ -156,12 +164,22 @@ object Starbound {
 		.also(RenderTemplate::registerGson)
 		.also(TileDefinition::registerGson)
 		.also(LiquidDefinition::registerGson)
-		.also(ItemDefinition::registerGson)
-		.also(ItemRarity::registerGson)
 		.also(SpriteReference::registerGson)
 		.also(AtlasConfiguration::registerGson)
 
-		.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe())
+		.registerTypeAdapter(LeveledStatusEffect.ADAPTER)
+
+		.registerTypeAdapter(ItemPrototype.ADAPTER)
+		.registerTypeAdapter(CurrencyItemPrototype.ADAPTER)
+		.registerTypeAdapter(ArmorItemPrototype.ADAPTER)
+
+		.registerTypeAdapter(IItemDefinition.InventoryIcon.ADAPTER)
+		.registerTypeAdapter(IFossilItemDefinition.FossilSetDescription.ADAPTER)
+		.registerTypeAdapter(IArmorItemDefinition.ArmorFrames.ADAPTER)
+
+		.registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL).neverNull())
+		.registerTypeAdapter(EnumAdapter(ItemRarity::class).neverNull())
+		.registerTypeAdapter(EnumAdapter(ItemTooltipKind::class).neverNull())
 
 		.create()
 
@@ -170,7 +188,7 @@ object Starbound {
 		return when (type) {
 			Float::class.java -> TypeAdapters.FLOAT as TypeAdapter<T>
 			Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter<T>
-			String::class.java -> stringTypeAdapter as TypeAdapter<T>
+			String::class.java -> NULLABLE_STRING_ADAPTER as TypeAdapter<T>
 			Int::class.java -> TypeAdapters.INTEGER as TypeAdapter<T>
 			Long::class.java -> TypeAdapters.LONG as TypeAdapter<T>
 			Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter<T>
@@ -339,14 +357,14 @@ object Starbound {
 	}
 
 	private fun loadTileMaterials(callback: (String) -> Unit) {
-		readingFolder = "/tiles/materials"
+		assetFolder = "/tiles/materials"
 
 		for (fs in fileSystems) {
 			for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".material") }) {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
+					assetFolder = listedFile.computeDirectory()
 					val tileDef = gson.fromJson(listedFile.reader(), TileDefinition::class.java)
 
 					check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" }
@@ -364,7 +382,7 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 
 	private fun loadProjectiles(callback: (String) -> Unit) {
@@ -373,7 +391,7 @@ object Starbound {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
+					assetFolder = listedFile.computeDirectory()
 					val def = gson.fromJson(listedFile.reader(), ConfigurableProjectile::class.java).assemble(listedFile.computeDirectory())
 					check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" }
 					projectiles[def.projectileName] = def
@@ -388,7 +406,7 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 
 	private fun loadFunctions(callback: (String) -> Unit) {
@@ -397,7 +415,7 @@ object Starbound {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
+					assetFolder = listedFile.computeDirectory()
 					val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject
 
 					for (key in readObject.keySet()) {
@@ -414,7 +432,7 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 
 	private fun loadParallax(callback: (String) -> Unit) {
@@ -423,7 +441,7 @@ object Starbound {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
+					assetFolder = listedFile.computeDirectory()
 					val def = gson.fromJson(listedFile.reader(), ParallaxPrototype::class.java)
 					parallax[listedFile.name.substringBefore('.')] = def
 				} catch(err: Throwable) {
@@ -436,18 +454,18 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 
 	private fun loadMaterialModifiers(callback: (String) -> Unit) {
-		readingFolder = "/tiles/materials"
+		assetFolder = "/tiles/materials"
 
 		for (fs in fileSystems) {
 			for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".matmod") }) {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
+					assetFolder = listedFile.computeDirectory()
 					val tileDef = gson.fromJson(listedFile.reader(), MaterialModifier::class.java)
 
 					check(tileModifiers[tileDef.modName] == null) { "Already has material with name ${tileDef.modName} loaded!" }
@@ -465,7 +483,7 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 
 	private fun loadLiquidDefinitions(callback: (String) -> Unit) {
@@ -474,7 +492,7 @@ object Starbound {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
+					assetFolder = listedFile.computeDirectory()
 					val liquidDef = gson.fromJson(listedFile.reader(), LiquidDefinition::class.java)
 
 					check(liquid.put(liquidDef.name, liquidDef) == null) { "Already has liquid with name ${liquidDef.name} loaded!" }
@@ -490,22 +508,42 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 
 	private fun loadItemDefinitions(callback: (String) -> Unit) {
-		val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".activeitem")
+		val files = listOf(".item", ".currency", ".head", ".chest", ".legs", ".back", ".activeitem")
 
 		for (fs in fileSystems) {
 			for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.any { f.name.endsWith(it) } }) {
 				try {
 					callback("Loading $listedFile")
 
-					readingFolder = listedFile.computeDirectory()
-					ItemDefinition.ADAPTER.currentSymbolicName = listedFile.computeFullPath()
-					val def = gson.fromJson(listedFile.reader(), ItemDefinition::class.java)
+					assetFolder = listedFile.computeDirectory()
 
-					check(items.put(def.itemName, def) == null) { "Already has item with name ${def.itemName} loaded!" }
+					if (listedFile.name.endsWith(".item")) {
+						val def = gson.fromJson(listedFile.reader(), ItemPrototype::class.java)
+						check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
+					} else if (listedFile.name.endsWith(".currency")) {
+						val def = gson.fromJson(listedFile.reader(), CurrencyItemPrototype::class.java)
+						check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
+					} else if (listedFile.name.endsWith(".head")) {
+						val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
+						def.armorType = ArmorPieceType.HEAD
+						check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
+					} else if (listedFile.name.endsWith(".chest")) {
+						val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
+						def.armorType = ArmorPieceType.CHEST
+						check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
+					} else if (listedFile.name.endsWith(".legs")) {
+						val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
+						def.armorType = ArmorPieceType.LEGS
+						check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
+					} else if (listedFile.name.endsWith(".back")) {
+						val def = gson.fromJson(listedFile.reader(), ArmorItemPrototype::class.java)
+						def.armorType = ArmorPieceType.BACK
+						check(items.put(def.itemName, def.assemble()) == null) { "Already has item with name ${def.itemName} loaded!" }
+					}
 				} catch (err: Throwable) {
 					LOGGER.error("Loading item definition file $listedFile", err)
 				}
@@ -516,6 +554,6 @@ object Starbound {
 			}
 		}
 
-		readingFolder = null
+		assetFolder = null
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt
new file mode 100644
index 00000000..8c26f34b
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt
@@ -0,0 +1,24 @@
+package ru.dbotthepony.kstarbound.defs
+
+interface IThingWithDescription {
+	/**
+	 * Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание",
+	 * на самом деле данное поле отвечает за название штуки.
+	 *
+	 * Примеры:
+	 * * Microwave Oven
+	 * * Copper Ore
+	 * * Poptop
+	 */
+	val shortdescription: String
+
+	/**
+	 * Полное описание штуки. Оно отображается игроку, когда последний наводит курсор на штуку.
+	 *
+	 * Примеры:
+	 * * A microwave. For when you're hungry enough to nuke your food.
+	 * * Copper ore. Can be used for smelting.
+	 * * The Poptop hums beautifully to confuse its prey.
+	 */
+	val description: String
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt
index d2fa8f5a..153022a0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt
@@ -4,7 +4,7 @@ import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 
 /**
- * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
+ * Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
  */
 fun enrollList(input: List<Any>, interner: (String) -> String = String::intern): ImmutableList<Any> {
 	val builder = ImmutableList.builder<Any>()
@@ -21,7 +21,7 @@ fun enrollList(input: List<Any>, interner: (String) -> String = String::intern):
 }
 
 /**
- * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
+ * Возвращает глубокую, неизменяемую копию [input] примитивов/List'ов/Map'ов
  */
 fun enrollMap(input: Map<String, Any>, interner: (String) -> String = String::intern): ImmutableMap<String, Any> {
 	val builder = ImmutableMap.builder<String, Any>()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt
index 1d240bd3..7e5a6d86 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt
@@ -6,7 +6,7 @@ import com.google.gson.TypeAdapter
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonToken
 import com.google.gson.stream.JsonWriter
-import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
+import ru.dbotthepony.kstarbound.io.json.EnumAdapter
 import ru.dbotthepony.kstarbound.io.json.IStringSerializable
 import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
 import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@@ -29,7 +29,7 @@ enum class JsonFunctionInterpolation(vararg aliases: String) : IStringSerializab
 	}
 
 	companion object {
-		val ADAPTER: TypeAdapter<JsonFunctionInterpolation> = CustomEnumTypeAdapter(values()).nullSafe()
+		val ADAPTER: TypeAdapter<JsonFunctionInterpolation> = EnumAdapter(JsonFunctionInterpolation::class).neverNull()
 	}
 }
 
@@ -51,7 +51,7 @@ enum class JsonFunctionConstraint(vararg aliases: String) : IStringSerializable
 	}
 
 	companion object {
-		val ADAPTER: TypeAdapter<JsonFunctionConstraint> = CustomEnumTypeAdapter(values()).nullSafe()
+		val ADAPTER: TypeAdapter<JsonFunctionConstraint> = EnumAdapter(JsonFunctionConstraint::class).neverNull()
 	}
 }
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt
index 9ae9ed9f..46a72306 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt
@@ -16,7 +16,7 @@ import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder
  */
 abstract class RawPrototype<RAW : RawPrototype<RAW, ASSEMBLED>, ASSEMBLED : AssembledPrototype<ASSEMBLED, RAW>> : INativeJsonHolder {
 	val json = Object2ObjectArrayMap<String, Any>()
-	fun enroll() = enrollMap(json, Starbound.assetStringInterner::intern)
+	fun enroll() = enrollMap(json, Starbound.STRING_INTERNER::intern)
 	abstract fun assemble(directory: String = ""): ASSEMBLED
 
 	override fun acceptJson(json: MutableMap<String, Any>) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt
index 00ad6daf..2105916c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/AtlasConfiguration.kt
@@ -304,7 +304,7 @@ class AtlasConfiguration private constructor(
 			return EMPTY
 		}
 
-		val ADAPTER: TypeAdapter<AtlasConfiguration?> = Starbound.stringTypeAdapter.transform(read = read@{ get(it ?: return@read it as AtlasConfiguration?) }, write = write@{ it?.name })
+		val ADAPTER: TypeAdapter<AtlasConfiguration?> = Starbound.NULLABLE_STRING_ADAPTER.transform(read = read@{ get(it ?: return@read it as AtlasConfiguration?) }, write = write@{ it?.name })
 
 		fun registerGson(gsonBuilder: GsonBuilder) {
 			gsonBuilder.registerTypeAdapter(ADAPTER)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt
index 03d086da..6fd2079c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/ImageReference.kt
@@ -30,7 +30,7 @@ data class ImageReference(
 
 		override fun read(`in`: JsonReader): ImageReference {
 			if (`in`.peek() == JsonToken.STRING) {
-				val image = Starbound.readingFolderTransformer(`in`.nextString())
+				val image = Starbound.assetFolder(`in`.nextString())
 
 				if (image.contains(':')) {
 					throw JsonSyntaxException("Expected atlas/image reference, but got sprite reference: $image")
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt
index fed0737e..9e55ec30 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt
@@ -30,7 +30,7 @@ data class SpriteReference(
 		}
 
 		override fun read(`in`: JsonReader): SpriteReference {
-			return parse(Starbound.readingFolderTransformer(`in`.nextString()))
+			return parse(Starbound.assetFolder(`in`.nextString()))
 		}
 
 		fun registerGson(gsonBuilder: GsonBuilder) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt
new file mode 100644
index 00000000..ed8e5c6b
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt
@@ -0,0 +1,32 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+data class ArmorItemDefinition(
+	override val shortdescription: String,
+	override val description: String,
+	override val itemName: String,
+	override val price: Long,
+	override val rarity: ItemRarity,
+	override val category: String?,
+	override val inventoryIcon: List<IItemDefinition.IInventoryIcon>?,
+	override val itemTags: List<String>,
+	override val learnBlueprintsOnPickup: List<String>,
+	override val maxStack: Long,
+	override val eventCategory: String?,
+	override val consumeOnPickup: Boolean,
+	override val pickupQuestTemplates: List<String>,
+	override val scripts: List<String>,
+	override val tooltipKind: ItemTooltipKind,
+	override val twoHanded: Boolean,
+	override val radioMessagesOnPickup: List<String>,
+	override val fuelAmount: Long?,
+
+	override val colorOptions: List<Map<String, String>>,
+	override val maleFrames: IArmorItemDefinition.IArmorFrames,
+	override val femaleFrames: IArmorItemDefinition.IArmorFrames,
+	override val level: Double,
+	override val leveledStatusEffects: List<ILeveledStatusEffect>,
+
+	override val armorType: ArmorPieceType,
+
+	val json: Map<String, Any>,
+) : IArmorItemDefinition
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt
new file mode 100644
index 00000000..0240250b
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt
@@ -0,0 +1,67 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.Starbound
+import ru.dbotthepony.kstarbound.defs.enrollMap
+import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
+import ru.dbotthepony.kstarbound.io.json.asJsonObject
+import ru.dbotthepony.kstarbound.io.json.asList
+import ru.dbotthepony.kstarbound.io.json.neverNull
+import ru.dbotthepony.kstarbound.util.NotNullVar
+
+class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition {
+	override var colorOptions: List<Map<String, String>> = listOf()
+	override var maleFrames: IArmorItemDefinition.ArmorFrames by NotNullVar()
+	override var femaleFrames: IArmorItemDefinition.ArmorFrames by NotNullVar()
+	override var level: Double = 1.0
+	override var leveledStatusEffects: List<LeveledStatusEffect> = listOf()
+
+	override var armorType: ArmorPieceType by NotNullVar()
+
+	init {
+		maxStack = 1L
+	}
+
+	override fun assemble(): IItemDefinition {
+		return ArmorItemDefinition(
+			shortdescription = shortdescription,
+			description = description,
+			itemName = itemName,
+			price = price,
+			rarity = rarity,
+			category = category,
+			inventoryIcon = inventoryIcon,
+			itemTags = itemTags,
+			learnBlueprintsOnPickup = learnBlueprintsOnPickup,
+			maxStack = maxStack,
+			eventCategory = eventCategory,
+			consumeOnPickup = consumeOnPickup,
+			pickupQuestTemplates = pickupQuestTemplates,
+			scripts = scripts,
+			tooltipKind = tooltipKind,
+			twoHanded = twoHanded,
+			radioMessagesOnPickup = radioMessagesOnPickup,
+			fuelAmount = fuelAmount,
+
+			json = enrollMap(json),
+
+			colorOptions = colorOptions,
+			maleFrames = maleFrames,
+			femaleFrames = femaleFrames,
+			level = level,
+			leveledStatusEffects = leveledStatusEffects,
+
+			armorType = armorType,
+		)
+	}
+
+	companion object {
+		val ADAPTER = BuilderAdapter.Builder(::ArmorItemPrototype)
+			.also { addFields(it as BuilderAdapter.Builder<ItemPrototype>) } // безопасность: свойства родительского класса объявлены как final
+			.add(ArmorItemPrototype::colorOptions, Starbound.NULLABLE_STRING_ADAPTER.neverNull().asJsonObject().asList())
+			.auto(ArmorItemPrototype::maleFrames)
+			.auto(ArmorItemPrototype::femaleFrames)
+			.auto(ArmorItemPrototype::level)
+			.autoList(ArmorItemPrototype::leveledStatusEffects)
+			.build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorPieceType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorPieceType.kt
new file mode 100644
index 00000000..e942eb10
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorPieceType.kt
@@ -0,0 +1,26 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+/**
+ * Тип брони. Более формально, в какой слот надевается данный предмет
+ */
+enum class ArmorPieceType {
+	/**
+	 * Шлем
+	 */
+	HEAD,
+
+	/**
+	 * Нагрудник
+	 */
+	CHEST,
+
+	/**
+	 * Поножи
+	 */
+	LEGS,
+
+	/**
+	 * Плащ/рюкзак/прочее
+	 */
+	BACK
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt
new file mode 100644
index 00000000..52704e1b
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt
@@ -0,0 +1,32 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+data class CurrencyItemDefinition(
+	override val shortdescription: String,
+	override val description: String,
+	override val itemName: String,
+	override val price: Long,
+	override val rarity: ItemRarity,
+	override val category: String?,
+	override val inventoryIcon: List<IItemDefinition.IInventoryIcon>?,
+	override val itemTags: List<String>,
+	override val learnBlueprintsOnPickup: List<String>,
+	override val maxStack: Long,
+	override val eventCategory: String?,
+	override val consumeOnPickup: Boolean,
+	override val pickupQuestTemplates: List<String>,
+	override val scripts: List<String>,
+	override val tooltipKind: ItemTooltipKind,
+	override val twoHanded: Boolean,
+	override val radioMessagesOnPickup: List<String>,
+	override val fuelAmount: Long?,
+
+	override val pickupSoundsSmall: List<String>,
+	override val pickupSoundsMedium: List<String>,
+	override val pickupSoundsLarge: List<String>,
+	override val smallStackLimit: Long,
+	override val mediumStackLimit: Long,
+	override val currency: String,
+	override val value: Long,
+
+	val json: Map<String, Any>,
+) : ICurrencyItemDefinition
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt
new file mode 100644
index 00000000..22cc14e8
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt
@@ -0,0 +1,65 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.defs.enrollMap
+import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
+import ru.dbotthepony.kstarbound.util.NotNullVar
+
+class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition {
+	override var pickupSoundsSmall: List<String> = listOf()
+	override var pickupSoundsMedium: List<String> = listOf()
+	override var pickupSoundsLarge: List<String> = listOf()
+	override var smallStackLimit: Long by NotNullVar()
+	override var mediumStackLimit: Long by NotNullVar()
+	override var currency: String by NotNullVar()
+	override var value: Long by NotNullVar()
+
+	init {
+		maxStack = 16777216L
+	}
+
+	override fun assemble(): IItemDefinition {
+		return CurrencyItemDefinition(
+			shortdescription = shortdescription,
+			description = description,
+			itemName = itemName,
+			price = price,
+			rarity = rarity,
+			category = category,
+			inventoryIcon = inventoryIcon,
+			itemTags = itemTags,
+			learnBlueprintsOnPickup = learnBlueprintsOnPickup,
+			maxStack = maxStack,
+			eventCategory = eventCategory,
+			consumeOnPickup = consumeOnPickup,
+			pickupQuestTemplates = pickupQuestTemplates,
+			scripts = scripts,
+			tooltipKind = tooltipKind,
+			twoHanded = twoHanded,
+			radioMessagesOnPickup = radioMessagesOnPickup,
+			fuelAmount = fuelAmount,
+
+			json = enrollMap(json),
+
+			pickupSoundsSmall = pickupSoundsSmall,
+			pickupSoundsMedium = pickupSoundsMedium,
+			pickupSoundsLarge = pickupSoundsLarge,
+			smallStackLimit = smallStackLimit,
+			mediumStackLimit = mediumStackLimit,
+			currency = currency,
+			value = value,
+		)
+	}
+
+	companion object {
+		val ADAPTER = BuilderAdapter.Builder(::CurrencyItemPrototype)
+			.also { addFields(it as BuilderAdapter.Builder<ItemPrototype>) } // безопасность: свойства родительского класса объявлены как final
+			.autoList(CurrencyItemPrototype::pickupSoundsSmall)
+			.autoList(CurrencyItemPrototype::pickupSoundsMedium)
+			.autoList(CurrencyItemPrototype::pickupSoundsLarge)
+			.auto(CurrencyItemPrototype::smallStackLimit)
+			.auto(CurrencyItemPrototype::mediumStackLimit)
+			.auto(CurrencyItemPrototype::currency)
+			.auto(CurrencyItemPrototype::value)
+			.build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IArmorItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IArmorItemDefinition.kt
new file mode 100644
index 00000000..08b4df32
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IArmorItemDefinition.kt
@@ -0,0 +1,50 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.Starbound
+import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
+import ru.dbotthepony.kstarbound.io.json.ifString
+
+interface IArmorItemDefinition : ILeveledItemDefinition {
+	/**
+	 * @see ArmorPieceType
+	 */
+	val armorType: ArmorPieceType
+
+	/**
+	 * Варианты покраски (???)
+	 */
+	val colorOptions: List<Map<String, String>>
+
+	/**
+	 * Визуальные кадры анимации, когда надето на гуманоида мужского пола
+	 */
+	val maleFrames: IArmorFrames
+
+	/**
+	 * Визуальные кадры анимации, когда надето на гуманоида женского пола
+	 */
+	val femaleFrames: IArmorFrames
+
+	interface IArmorFrames {
+		val body: String
+		val backSleeve: String?
+		val frontSleeve: String?
+	}
+
+	data class ArmorFrames(
+		override val body: String,
+		override val backSleeve: String?,
+		override val frontSleeve: String?,
+	) : IArmorFrames {
+		companion object {
+			val ADAPTER = FactoryAdapter.Builder(
+				ArmorFrames::class,
+				ArmorFrames::body,
+				ArmorFrames::backSleeve,
+				ArmorFrames::frontSleeve,
+			)
+				.build()
+				.ifString { ArmorFrames(Starbound.assetFolder(it), null, null) }
+		}
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ICurrencyItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ICurrencyItemDefinition.kt
new file mode 100644
index 00000000..ebe991ca
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ICurrencyItemDefinition.kt
@@ -0,0 +1,38 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+interface ICurrencyItemDefinition : IItemDefinition {
+	/**
+	 * Звуки при поднятии "малого" количества предметов. Не имеет никакого смысла без [smallStackLimit]
+	 */
+	val pickupSoundsSmall: List<String>
+
+	/**
+	 * Звуки при поднятии "среднего" количества предметов. Не имеет никакого смысла без [mediumStackLimit]
+	 */
+	val pickupSoundsMedium: List<String>
+
+	/**
+	 * Звуки при поднятии "большого" количества предметов. Не имеет никакого смысла без [smallStackLimit] и без [mediumStackLimit]
+	 */
+	val pickupSoundsLarge: List<String>
+
+	/**
+	 * Количество предметов ниже или равному данному значению проиграет звук [pickupSoundsSmall]
+	 */
+	val smallStackLimit: Long
+
+	/**
+	 * Количество предметов ниже или равному данному значению (но не меньше [smallStackLimit]) проиграет звук [pickupSoundsMedium]
+	 */
+	val mediumStackLimit: Long
+
+	/**
+	 * ID валюты
+	 */
+	val currency: String
+
+	/**
+	 * Ценность одного предмета в [currency]
+	 */
+	val value: Long
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IFossilItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IFossilItemDefinition.kt
new file mode 100644
index 00000000..c1212780
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IFossilItemDefinition.kt
@@ -0,0 +1,70 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.defs.IThingWithDescription
+import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
+import ru.dbotthepony.kvector.vector.ndouble.Vector2d
+
+interface IFossilItemDefinition : IItemDefinition {
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val race: String
+	val displayImage: String
+	val displayoffset: Vector2d
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val fossilSetName: String
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val setIndex: Int
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val setCount: Int
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val setCollectables: Map<String, String>
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val completeFossilIcon: String?
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val completeFossilObject: String?
+
+	/**
+	 * Используется в костях-ископаемых
+	 */
+	val completeSetDescriptions: IFossilSetDescription?
+
+	interface IFossilSetDescription : IThingWithDescription {
+		/**
+		 * Цена в пикселях
+		 */
+		val price: Long
+	}
+
+	data class FossilSetDescription(
+		override val price: Long = 0L,
+		override val shortdescription: String = "...",
+		override val description: String = "..."
+	) : IFossilSetDescription {
+		companion object {
+			val ADAPTER = FactoryAdapter.Builder(
+					FossilSetDescription::class,
+					FossilSetDescription::price,
+					FossilSetDescription::shortdescription,
+					FossilSetDescription::description).build()
+		}
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt
new file mode 100644
index 00000000..4f645076
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt
@@ -0,0 +1,105 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.Starbound
+import ru.dbotthepony.kstarbound.defs.IThingWithDescription
+import ru.dbotthepony.kstarbound.defs.image.SpriteReference
+import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
+import ru.dbotthepony.kstarbound.io.json.ifString
+
+interface IItemDefinition : IThingWithDescription {
+	/**
+	 * Внутреннее имя предмета (ID).
+	 * Не путать с именем предмета!
+	 *
+	 * @see shortdescription
+	 * @see description
+	 */
+	val itemName: String
+
+	/**
+	 * Цена в пикселях
+	 */
+	val price: Long
+
+	/**
+	 * Редкость как [ItemRarity]
+	 */
+	val rarity: ItemRarity
+
+	/**
+	 * Категория предмета, определяет, в какую вкладку инвентаря оно попадает
+	 */
+	val category: String?
+
+	/**
+	 * Иконка в инвентаре, относительный и абсолютный пути
+	 */
+	val inventoryIcon: List<IInventoryIcon>?
+
+	interface IInventoryIcon {
+		val image: SpriteReference
+	}
+
+	data class InventoryIcon(
+		override val image: SpriteReference
+	) : IInventoryIcon {
+		companion object {
+			val ADAPTER = FactoryAdapter.Builder(InventoryIcon::class, InventoryIcon::image).build().ifString { InventoryIcon(SpriteReference.parse(Starbound.assetFolder(it))) }
+		}
+	}
+
+	/**
+	 * Теги предмета
+	 */
+	val itemTags: List<String>
+
+	/**
+	 * При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта
+	 */
+	val learnBlueprintsOnPickup: List<String>
+
+	/**
+	 * Максимальное количество предмета в стопке
+	 */
+	val maxStack: Long
+
+	/**
+	 * snip
+	 */
+	val eventCategory: String?
+
+	/**
+	 * Заставляет предмет "использовать" сразу же при подборе
+	 */
+	val consumeOnPickup: Boolean
+
+	/**
+	 * Запускает следующие квест(ы) при подборе
+	 */
+	val pickupQuestTemplates: List<String>
+
+	/**
+	 * Lua скрипты для выполнения
+	 */
+	val scripts: List<String>
+
+	/**
+	 * это где либо ещё применяется кроме брони?
+	 */
+	val tooltipKind: ItemTooltipKind
+
+	/**
+	 * Занимает ли предмет обе руки
+	 */
+	val twoHanded: Boolean
+
+	/**
+	 * Заставляет SAIL/прочих болтать при подборе предмета в первый раз
+	 */
+	val radioMessagesOnPickup: List<String>
+
+	/**
+	 * Топливо корабля
+	 */
+	val fuelAmount: Long?
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledItemDefinition.kt
new file mode 100644
index 00000000..0e6ed158
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledItemDefinition.kt
@@ -0,0 +1,13 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+interface ILeveledItemDefinition : IItemDefinition {
+	/**
+	 * Изначальный уровень предмета, может быть изменён позднее чем угодно
+	 */
+	val level: Double
+
+	/**
+	 * Эффекты предмета, растущие с уровнем
+	 */
+	val leveledStatusEffects: List<ILeveledStatusEffect>
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt
new file mode 100644
index 00000000..795a646b
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt
@@ -0,0 +1,26 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
+
+interface ILeveledStatusEffect {
+	val levelFunction: String
+	val stat: String
+	val baseMultiplier: Double
+	val amount: Double
+}
+
+data class LeveledStatusEffect(
+	override val levelFunction: String,
+	override val stat: String,
+	override val baseMultiplier: Double = 1.0,
+	override val amount: Double = 0.0,
+) : ILeveledStatusEffect {
+	companion object {
+		val ADAPTER = FactoryAdapter.Builder(LeveledStatusEffect::class,
+			LeveledStatusEffect::levelFunction,
+			LeveledStatusEffect::stat,
+			LeveledStatusEffect::baseMultiplier,
+			LeveledStatusEffect::amount,
+		).build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt
index 05035001..56acab8a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt
@@ -1,354 +1,24 @@
 package ru.dbotthepony.kstarbound.defs.item
 
-import com.google.gson.GsonBuilder
-import ru.dbotthepony.kstarbound.Starbound
-import ru.dbotthepony.kstarbound.defs.image.SpriteReference
-import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
-import ru.dbotthepony.kstarbound.io.json.ListAdapter
-import ru.dbotthepony.kstarbound.io.json.asJsonObject
-import ru.dbotthepony.kstarbound.io.json.asList
-import ru.dbotthepony.kstarbound.io.json.ifString
-import ru.dbotthepony.kstarbound.registerTypeAdapter
-import ru.dbotthepony.kvector.vector.ndouble.Vector2d
-
 data class ItemDefinition(
-	/**
-	 * Внутреннее имя предмета, как строка
-	 */
-	val itemName: String,
+	override val shortdescription: String,
+	override val description: String,
+	override val itemName: String,
+	override val price: Long,
+	override val rarity: ItemRarity,
+	override val category: String?,
+	override val inventoryIcon: List<IItemDefinition.IInventoryIcon>?,
+	override val itemTags: List<String>,
+	override val learnBlueprintsOnPickup: List<String>,
+	override val maxStack: Long,
+	override val eventCategory: String?,
+	override val consumeOnPickup: Boolean,
+	override val pickupQuestTemplates: List<String>,
+	override val scripts: List<String>,
+	override val tooltipKind: ItemTooltipKind,
+	override val twoHanded: Boolean,
+	override val radioMessagesOnPickup: List<String>,
+	override val fuelAmount: Long?,
 
-	/**
-	 * Цена в пикселях
-	 */
-	val price: Long = 0L,
-
-	/**
-	 * Редкость как [ItemRarity]
-	 */
-	val rarity: ItemRarity = ItemRarity.COMMON,
-
-	/**
-	 * Категория предмета, определяет, в какую вкладку инвентаря оно попадает
-	 */
-	val category: String? = null,
-
-	/**
-	 * Иконка в инвентаре, относительный и абсолютный пути
-	 */
-	val inventoryIcon: List<InventoryIcon>? = null,
-
-	/**
-	 * Описание предмета
-	 */
-	val description: String = "...",
-
-	/**
-	 * Название предмета
-	 */
-	val shortdescription: String = "...",
-
-	/**
-	 * Теги предмета
-	 */
-	val itemTags: List<String> = listOf(),
-
-	/**
-	 * При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта
-	 */
-	val learnBlueprintsOnPickup: List<String> = listOf(),
-
-	/**
-	 * Максимальное количество предмета в стопке, по умолчанию 9999
-	 */
-	val maxStack: Long = 9999L,
-
-	/**
-	 * snip
-	 */
-	val eventCategory: String? = null,
-
-	/**
-	 * Заставляет предмет "использовать" сразу же при подборе
-	 */
-	val consumeOnPickup: Boolean = false,
-
-	/**
-	 * Запускает следующие квест(ы) при подборе
-	 */
-	val pickupQuestTemplates: List<String> = listOf(),
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val race: String? = null,
-	val displayImage: String? = null,
-	val displayoffset: Vector2d? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val fossilSetName: String? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val setIndex: Int? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val setCount: Int? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val setCollectables: Map<String, String>? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val completeFossilIcon: String? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val completeFossilObject: String? = null,
-
-	/**
-	 * Используется в костях-ископаемых
-	 */
-	val completeSetDescriptions: FossilSetDescription? = null,
-
-	/**
-	 * Заставляет SAIL/прочих болтать при подборе предмета в первый раз
-	 */
-	val radioMessagesOnPickup: List<String> = listOf(),
-
-	/**
-	 * Топливо корабля
-	 */
-	val fuelAmount: Long? = null,
-
-	// ----------------
-	// Поля ниже были видны только в файлах валюты
-	// ----------------
-
-	/**
-	 * Звуки при поднятии "малого" количества предметов. Не имеет никакого смысла без [smallStackLimit]
-	 */
-	val pickupSoundsSmall: List<String> = listOf(),
-
-	/**
-	 * Звуки при поднятии "среднего" количества предметов. Не имеет никакого смысла без [mediumStackLimit]
-	 */
-	val pickupSoundsMedium: List<String> = listOf(),
-
-	/**
-	 * Звуки при поднятии "большого" количества предметов. Не имеет никакого смысла без [smallStackLimit] и без [mediumStackLimit]
-	 */
-	val pickupSoundsLarge: List<String> = listOf(),
-
-	/**
-	 * Количество предметов ниже или равному данному значению проиграет звук [pickupSoundsSmall]
-	 */
-	val smallStackLimit: Long? = null,
-
-	/**
-	 * Количество предметов ниже или равному данному значению (но не меньше [smallStackLimit]) проиграет звук [pickupSoundsMedium]
-	 */
-	val mediumStackLimit: Long? = null,
-
-	/**
-	 * Превращает предмет в валюту
-	 */
-	val currency: String? = null,
-
-	/**
-	 * Ценность в [currency]
-	 */
-	val value: Long? = null,
-
-	// ----------------
-	// /Валюта
-	// ----------------
-
-	/**
-	 * Lua скрипты для выполнения
-	 */
-	val scripts: List<String> = listOf(),
-	val animationScripts: List<String> = listOf(),
-
-	// ----------------
-	// Броня
-	// ----------------
-
-	/**
-	 * это где либо ещё применяется кроме брони?
-	 */
-	val tooltipKind: String? = null,
-
-	/**
-	 * Изначальный уровень, может быть изменён позднее чем угодно
-	 */
-	val level: Int? = null,
-
-	/**
-	 * Эффекты предмета, растущие с уровнем
-	 */
-	val leveledStatusEffects: List<StatusEffect> = listOf(),
-
-	/**
-	 * Варианты покраски (???)
-	 */
-	val colorOptions: List<Map<String, String>> = listOf(),
-
-	/**
-	 * Визуальные кадры анимации, когда надето на гуманоида мужского пола
-	 */
-	val maleFrames: ArmorFrames? = null,
-
-	/**
-	 * Визуальные кадры анимации, когда надето на гуманоида женского пола
-	 */
-	val femaleFrames: ArmorFrames? = null,
-
-	// ----------------
-	// /Броня
-	// ----------------
-
-	// ----------------
-	// activeitem
-	// ----------------
-
-	// TODO: это указатель на структуру
-	val animation: String? = null,
-
-	/**
-	 * Занимает ли предмет обе руки
-	 */
-	val twoHanded: Boolean = false,
-
-	// ----------------
-	// /activeitem
-	// ----------------
-
-	/**
-	 * Прототип данного предмета, как JSON структура
-	 *
-	 * Имеет смысл только для Lua скриптов
-	 */
-	val json: Map<String, Any>,
-) {
-	data class FossilSetDescription(
-		val price: Long = 0L,
-		val shortdescription: String = "...",
-		val description: String = "..."
-	)
-
-	data class ArmorFrames(
-		val body: String,
-		val backSleeve: String?,
-		val frontSleeve: String?,
-	)
-
-	data class StatusEffect(
-		val levelFunction: String,
-		val stat: String,
-		val baseMultiplier: Double = 1.0,
-		val amount: Double = 0.0,
-	)
-
-	data class InventoryIcon(
-		val image: SpriteReference
-	)
-
-	companion object {
-		val INVENTORY_ICON_ADAPTER = FactoryAdapter.Builder(InventoryIcon::class)
-			.auto(InventoryIcon::image)
-			.build()
-
-		val ADAPTER = FactoryAdapter.Builder(ItemDefinition::class)
-			.auto(ItemDefinition::itemName)
-			.auto(ItemDefinition::price)
-			.auto(ItemDefinition::rarity)
-			.auto(ItemDefinition::category)
-			.add(ItemDefinition::inventoryIcon, ListAdapter(INVENTORY_ICON_ADAPTER).ifString { listOf(InventoryIcon(SpriteReference.parse(Starbound.readingFolderTransformer(it)))) }.nullSafe())
-			.auto(ItemDefinition::description)
-			.auto(ItemDefinition::shortdescription)
-
-			.autoList(ItemDefinition::itemTags)
-			.autoList(ItemDefinition::learnBlueprintsOnPickup)
-
-			.auto(ItemDefinition::maxStack)
-			.auto(ItemDefinition::eventCategory)
-			.auto(ItemDefinition::consumeOnPickup)
-			.autoList(ItemDefinition::pickupQuestTemplates)
-
-			.auto(ItemDefinition::race)
-			.auto(ItemDefinition::displayImage, transformer = Starbound::readingFolderTransformerNullable)
-			.auto(ItemDefinition::displayoffset)
-			.auto(ItemDefinition::fossilSetName)
-			.auto(ItemDefinition::setIndex)
-			.auto(ItemDefinition::setCount)
-			.mapAsObject(ItemDefinition::setCollectables, String::class)
-			.auto(ItemDefinition::completeFossilIcon)
-			.auto(ItemDefinition::completeFossilObject)
-
-			.auto(ItemDefinition::completeSetDescriptions)
-			.autoList(ItemDefinition::radioMessagesOnPickup)
-			.auto(ItemDefinition::fuelAmount)
-
-			.autoList(ItemDefinition::pickupSoundsSmall)
-			.autoList(ItemDefinition::pickupSoundsMedium)
-			.autoList(ItemDefinition::pickupSoundsLarge)
-			.auto(ItemDefinition::smallStackLimit)
-			.auto(ItemDefinition::mediumStackLimit)
-			.auto(ItemDefinition::currency)
-			.auto(ItemDefinition::value)
-
-			.autoList(ItemDefinition::scripts, transformer = Starbound::readingFolderListTransformer)
-			.autoList(ItemDefinition::animationScripts, transformer = Starbound::readingFolderListTransformer)
-
-			.auto(ItemDefinition::tooltipKind)
-			.auto(ItemDefinition::level)
-			.autoList(ItemDefinition::leveledStatusEffects)
-			.add(ItemDefinition::colorOptions, Starbound.nonnullStringTypeAdapter.asJsonObject().asList())
-			.auto(ItemDefinition::maleFrames)
-			.auto(ItemDefinition::femaleFrames)
-
-			.auto(ItemDefinition::animation, transformer = Starbound::readingFolderTransformerNullable)
-			.auto(ItemDefinition::twoHanded)
-
-			.storesJson()
-
-			.build()
-
-		val FOSSIL_ADAPTER = FactoryAdapter.Builder(FossilSetDescription::class)
-			.auto(FossilSetDescription::price)
-			.auto(FossilSetDescription::shortdescription)
-			.auto(FossilSetDescription::description)
-			.build()
-
-		val ARMOR_FRAMES_ADAPTER = FactoryAdapter.Builder(ArmorFrames::class)
-			.auto(ArmorFrames::body, transformer = Starbound::readingFolderTransformer)
-			.auto(ArmorFrames::backSleeve, transformer = Starbound::readingFolderTransformerNullable)
-			.auto(ArmorFrames::frontSleeve, transformer = Starbound::readingFolderTransformerNullable)
-			.build()
-			.ifString { ArmorFrames(Starbound.readingFolderTransformer(it), null, null) }
-
-		val STATUS_EFFECT_ADAPTER = FactoryAdapter.Builder(StatusEffect::class)
-			.auto(StatusEffect::levelFunction)
-			.auto(StatusEffect::stat)
-			.auto(StatusEffect::baseMultiplier)
-			.auto(StatusEffect::amount)
-			.build()
-
-		fun registerGson(gsonBuilder: GsonBuilder) {
-			gsonBuilder.registerTypeAdapter(ADAPTER)
-			gsonBuilder.registerTypeAdapter(FOSSIL_ADAPTER)
-			gsonBuilder.registerTypeAdapter(ARMOR_FRAMES_ADAPTER)
-			gsonBuilder.registerTypeAdapter(STATUS_EFFECT_ADAPTER)
-			gsonBuilder.registerTypeAdapter(INVENTORY_ICON_ADAPTER)
-		}
-	}
-}
+	val json: Map<String, Any>
+) : IItemDefinition
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt
new file mode 100644
index 00000000..0311ace8
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt
@@ -0,0 +1,87 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+import ru.dbotthepony.kstarbound.defs.enrollMap
+import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
+import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder
+import ru.dbotthepony.kstarbound.util.NotNullVar
+
+open class ItemPrototype : IItemDefinition, INativeJsonHolder {
+	final override var shortdescription: String = "..."
+	final override var description: String = "..."
+	final override var itemName: String by NotNullVar()
+	final override var price: Long = 0L
+	final override var rarity: ItemRarity = ItemRarity.COMMON
+	final override var category: String? = null
+	final override var inventoryIcon: List<IItemDefinition.InventoryIcon>? = null
+	final override var itemTags: List<String> = listOf()
+	final override var learnBlueprintsOnPickup: List<String> = listOf()
+	final override var maxStack: Long = 9999L
+	final override var eventCategory: String? = null
+	final override var consumeOnPickup: Boolean = false
+	final override var pickupQuestTemplates: List<String> = listOf()
+	final override var scripts: List<String> = listOf()
+	final override var tooltipKind: ItemTooltipKind = ItemTooltipKind.NORMAL
+	final override var twoHanded: Boolean = false
+	final override var radioMessagesOnPickup: List<String> = listOf()
+	final override var fuelAmount: Long? = null
+
+	var json: Map<String, Any> = mapOf()
+
+	final override fun acceptJson(json: MutableMap<String, Any>) {
+		this.json = json
+	}
+
+	open fun assemble(): IItemDefinition {
+		return ItemDefinition(
+			shortdescription = shortdescription,
+			description = description,
+			itemName = itemName,
+			price = price,
+			rarity = rarity,
+			category = category,
+			inventoryIcon = inventoryIcon,
+			itemTags = itemTags,
+			learnBlueprintsOnPickup = learnBlueprintsOnPickup,
+			maxStack = maxStack,
+			eventCategory = eventCategory,
+			consumeOnPickup = consumeOnPickup,
+			pickupQuestTemplates = pickupQuestTemplates,
+			scripts = scripts,
+			tooltipKind = tooltipKind,
+			twoHanded = twoHanded,
+			radioMessagesOnPickup = radioMessagesOnPickup,
+			fuelAmount = fuelAmount,
+
+			json = enrollMap(json),
+		)
+	}
+
+	companion object {
+		val ADAPTER = BuilderAdapter.Builder(::ItemPrototype)
+			.also(::addFields)
+			.build()
+
+		fun addFields(builder: BuilderAdapter.Builder<ItemPrototype>) {
+			with(builder) {
+				auto(ItemPrototype::shortdescription)
+				auto(ItemPrototype::description)
+				auto(ItemPrototype::itemName)
+				auto(ItemPrototype::price)
+				auto(ItemPrototype::rarity)
+				auto(ItemPrototype::category)
+				autoNullableList(ItemPrototype::inventoryIcon)
+				autoList(ItemPrototype::itemTags)
+				autoList(ItemPrototype::learnBlueprintsOnPickup)
+				auto(ItemPrototype::maxStack)
+				auto(ItemPrototype::eventCategory)
+				auto(ItemPrototype::consumeOnPickup)
+				autoList(ItemPrototype::pickupQuestTemplates)
+				autoList(ItemPrototype::scripts)
+				auto(ItemPrototype::tooltipKind)
+				auto(ItemPrototype::twoHanded)
+				autoList(ItemPrototype::radioMessagesOnPickup)
+				auto(ItemPrototype::fuelAmount)
+			}
+		}
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemRarity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemRarity.kt
index 1703cf97..5090251c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemRarity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemRarity.kt
@@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.item
 import com.google.gson.GsonBuilder
 import com.google.gson.TypeAdapter
 import com.google.gson.stream.JsonWriter
-import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
+import ru.dbotthepony.kstarbound.io.json.EnumAdapter
 import ru.dbotthepony.kstarbound.io.json.IStringSerializable
 import ru.dbotthepony.kstarbound.registerTypeAdapter
 
@@ -21,12 +21,4 @@ enum class ItemRarity(val canonical: String) : IStringSerializable {
 	override fun write(out: JsonWriter) {
 		out.value(canonical)
 	}
-
-	companion object {
-		val ADAPTER: TypeAdapter<ItemRarity> = CustomEnumTypeAdapter(values()).nullSafe()
-
-		fun registerGson(gsonBuilder: GsonBuilder) {
-			gsonBuilder.registerTypeAdapter(ADAPTER)
-		}
-	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemTooltipKind.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemTooltipKind.kt
new file mode 100644
index 00000000..a2df17fa
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemTooltipKind.kt
@@ -0,0 +1,43 @@
+package ru.dbotthepony.kstarbound.defs.item
+
+enum class ItemTooltipKind {
+	/**
+	 * Обычные предметы
+	 */
+	NORMAL,
+
+	/**
+	 * Улучшение для рюкзака
+	 */
+	BASE_AUGMENT,
+
+	/**
+	 * Рюкзаки
+	 */
+	BACK,
+
+	/**
+	 * Броня
+	 */
+	ARMOR,
+
+	/**
+	 * "Руки" меха
+	 */
+	MECH_ARM,
+
+	/**
+	 * "Ноги" меха
+	 */
+	MECH_LEGS,
+
+	/**
+	 * Ускорители меха
+	 */
+	MECH_BOOSTER,
+
+	/**
+	 * Тело меха
+	 */
+	MECH_BODY,
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt
index c92f5a3f..2e769615 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt
@@ -9,7 +9,7 @@ import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.defs.*
 import ru.dbotthepony.kstarbound.defs.image.ImageReference
 import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
-import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
+import ru.dbotthepony.kstarbound.io.json.EnumAdapter
 import ru.dbotthepony.kstarbound.registerTypeAdapter
 import ru.dbotthepony.kstarbound.util.NotNullVar
 import ru.dbotthepony.kvector.vector.Color
@@ -80,7 +80,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
 			lightColor = lightColor,
 			onlyHitTerrain = onlyHitTerrain,
 			orientationLocked = orientationLocked,
-			image = ImageReference(Starbound.readingFolderTransformer(requireNotNull(image) { "image is null" })),
+			image = ImageReference(Starbound.assetFolder(requireNotNull(image) { "image is null" })),
 			timeToLive = timeToLive,
 			animationCycle = animationCycle,
 			bounces = bounces,
@@ -123,7 +123,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
 
 		fun registerGson(gson: GsonBuilder) {
 			gson.registerTypeAdapter(ADAPTER)
-			gson.registerTypeAdapter(CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
+			gson.registerTypeAdapter(EnumAdapter(ProjectilePhysics::class).neverNull())
 			gson.registerTypeAdapter(ActionConfig.ADAPTER)
 			gson.registerTypeAdapter(ActionProjectile.ADAPTER)
 			gson.registerTypeAdapter(ActionSound.ADAPTER)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt
index 8341122e..fd8b014a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt
@@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.projectile
 import com.google.gson.stream.JsonWriter
 import ru.dbotthepony.kstarbound.io.json.IStringSerializable
 
-enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerializable {
+enum class ProjectilePhysics(vararg aliases: String) : IStringSerializable {
 	GAS,
 	LASER,
 	BOOMERANG,
@@ -99,7 +99,12 @@ enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerial
 	GRENADE_LOW_BOUNCE("GRENADELOWBOUNCE"),
 	GRENADE_NO_BOUNCE("GRENADENOBOUNCE");
 
+	private val aliases = Array(aliases.size) { aliases[it].lowercase() }
+
 	override fun match(name: String): Boolean {
+		@Suppress("name_shadowing")
+		val name = name.lowercase()
+
 		for (alias in aliases)
 			if (name == alias)
 				return true
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt
index f5cea217..514c2ec6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt
@@ -17,7 +17,7 @@ data class RenderParameters(
 	val absoluteTexturePath: String
 
 	init {
-		val dir = Starbound.readingFolder
+		val dir = Starbound.assetFolder
 
 		if (dir == null || texture[0] == '/') {
 			absoluteTexturePath = texture
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt
index 11daed35..e0f88da9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt
@@ -280,7 +280,7 @@ data class RenderTemplate(
 
 			gsonBuilder.registerTypeAdapter(ADAPTER)
 
-			gsonBuilder.registerTypeAdapter(RenderRuleList.Combination::class.java, EnumAdapter(RenderRuleList.Combination::class.java))
+			gsonBuilder.registerTypeAdapter(EnumAdapter(RenderRuleList.Combination::class.java))
 		}
 
 		private val cache = ConcurrentHashMap<String, RenderTemplate>()
@@ -300,7 +300,7 @@ data class RenderTemplate(
 				if (path[0] != '/') {
 					// относительный путь
 
-					val readingFolder = Starbound.readingFolder ?: throw NullPointerException("Currently read folder is not specified")
+					val readingFolder = Starbound.assetFolder ?: throw NullPointerException("Currently read folder is not specified")
 					path = "$readingFolder/$path"
 				}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SkyParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SkyParameters.kt
index 33ae1ea3..c73e5bb0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SkyParameters.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/SkyParameters.kt
@@ -6,8 +6,8 @@ import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kstarbound.io.ColorTypeAdapter
-import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
+import ru.dbotthepony.kstarbound.io.json.EnumAdapter
 import ru.dbotthepony.kvector.vector.Color
 import ru.dbotthepony.kvector.vector.ndouble.Vector2d
 import kotlin.properties.ReadWriteProperty
@@ -43,7 +43,7 @@ class SkyParameters {
 			gsonBuilder.registerTypeAdapter(SkyParameters::class.java, ADAPTER)
 			gsonBuilder.registerTypeAdapter(SkyColoringManifold::class.java, SkyColoringManifold.ADAPTER)
 			gsonBuilder.registerTypeAdapter(SkyColoring::class.java, SkyColoring.ADAPTER)
-			gsonBuilder.registerTypeAdapter(SkyType::class.java, CustomEnumTypeAdapter(SkyType.values()).nullSafe())
+			gsonBuilder.registerTypeAdapter(SkyType::class.java, EnumAdapter(SkyType::class))
 			SkySatellite.registerGson(gsonBuilder)
 		}
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/dungeon/Configurable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/dungeon/Configurable.kt
index 95fe1252..5d003eb2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/dungeon/Configurable.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/dungeon/Configurable.kt
@@ -3,8 +3,9 @@ package ru.dbotthepony.kstarbound.defs.world.dungeon
 import com.google.gson.GsonBuilder
 import ru.dbotthepony.kstarbound.defs.world.SkyParameters
 import ru.dbotthepony.kstarbound.defs.world.WorldProperties
-import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
+import ru.dbotthepony.kstarbound.io.json.EnumAdapter
+import ru.dbotthepony.kstarbound.registerTypeAdapter
 import kotlin.properties.Delegates
 
 class DungeonWorldDef {
@@ -50,8 +51,8 @@ class DungeonWorldDef {
 
 		fun registerGson(gsonBuilder: GsonBuilder) {
 			gsonBuilder.registerTypeAdapter(DungeonWorldDef::class.java, ADAPTER)
-			gsonBuilder.registerTypeAdapter(BeamUpRule::class.java, CustomEnumTypeAdapter(BeamUpRule.values()).nullSafe())
-			gsonBuilder.registerTypeAdapter(DungeonType::class.java, CustomEnumTypeAdapter(DungeonType.values()).nullSafe())
+			gsonBuilder.registerTypeAdapter(EnumAdapter(BeamUpRule::class))
+			gsonBuilder.registerTypeAdapter(EnumAdapter(DungeonType::class))
 		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt
index dbcd4b52..6bd8aba9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt
@@ -98,7 +98,7 @@ class BuilderAdapter<T : Any> private constructor(
 			reader = JsonTreeReader(obj)
 
 			if (instance is INativeJsonHolder) {
-				instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.assetStringInterner::intern))
+				instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.STRING_INTERNER::intern))
 			} else {
 				instance.acceptJson(obj.asJsonObject)
 			}
@@ -121,12 +121,16 @@ class BuilderAdapter<T : Any> private constructor(
 					val peek = reader.peek()
 
 					if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) {
-						throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls")
+						throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (JSON contains null)")
 					} else if (peek == JsonToken.NULL) {
 						property.set(instance, null)
 						reader.nextNull()
 					} else {
 						val readValue = property.adapter.read(reader)
+
+						if (!property.returnType.isMarkedNullable && readValue == null)
+							throw JsonSyntaxException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (Type provider returned null)")
+
 						property.set(instance, readValue)
 						check(missing.remove(property))
 					}
@@ -248,22 +252,39 @@ class BuilderAdapter<T : Any> private constructor(
 			return this
 		}
 
+		/**
+		 * Автоматически определяет тип свойства и необходимый [TypeAdapter]
+		 */
 		fun <V> auto(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
 			val returnType = property.returnType
 			val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!")
 
 			if (classifier.isSubclassOf(List::class)) {
-				throw IllegalArgumentException("${property.name} is a List, please use autoList() method instead")
+				throw IllegalArgumentException("${property.name} is a List, please use autoList() or directly specify type adapter method instead")
 			}
 
 			if (classifier.isSubclassOf(Map::class)) {
-				throw IllegalArgumentException("${property.name} is a Map, please use autoMap() method instead")
+				throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead")
 			}
 
 			@Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа
 			return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
 		}
 
+		/**
+		 * Автоматически создаёт [ListAdapter] для заданного свойства
+		 */
+		inline fun <reified V : Any> autoList(property: KMutableProperty1<T, List<V>>, noinline configurator: PropertyConfigurator<T, List<V>>.() -> Unit = {}): Builder<T> {
+			return add(property, ListAdapter(V::class.java), configurator)
+		}
+
+		/**
+		 * Автоматически создаёт [ListAdapter] для заданного свойства, но в данном случае само свойство может принимать значение null
+		 */
+		inline fun <reified V : Any> autoNullableList(property: KMutableProperty1<T, List<V>?>, noinline configurator: PropertyConfigurator<T, List<V>?>.() -> Unit = {}): Builder<T> {
+			return add(property, ListAdapter(V::class.java).nullSafe(), configurator)
+		}
+
 		fun ignoreKey(name: String): Builder<T> {
 			if (properties.any { it.property.name == name }) {
 				throw IllegalArgumentException("Can not ignore key $name because we have property with this name!")
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/CustomEnumTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/CustomEnumTypeAdapter.kt
deleted file mode 100644
index 19d433a6..00000000
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/CustomEnumTypeAdapter.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package ru.dbotthepony.kstarbound.io.json
-
-import com.google.gson.TypeAdapter
-import com.google.gson.stream.JsonReader
-import com.google.gson.stream.JsonWriter
-
-interface IStringSerializable {
-	fun match(name: String): Boolean
-	fun write(out: JsonWriter)
-}
-
-class CustomEnumTypeAdapter<T : Enum<T>>(private val clazz: Array<T>) : TypeAdapter<T>() {
-	override fun write(out: JsonWriter, value: T) {
-		if (value is IStringSerializable)
-			value.write(out)
-		else
-			out.value(value.name)
-	}
-
-	override fun read(`in`: JsonReader): T {
-		val str = `in`.nextString().uppercase()
-
-		for (value in clazz) {
-			if (value is IStringSerializable && value.match(str) || value.name == str) {
-				return value
-			}
-		}
-
-		throw IllegalArgumentException("${clazz[0]::class.qualifiedName} does not have value for $str")
-	}
-}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EnumAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EnumAdapter.kt
index 752d47ce..70f479d8 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EnumAdapter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EnumAdapter.kt
@@ -1,40 +1,133 @@
 package ru.dbotthepony.kstarbound.io.json
 
+import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
+import com.google.common.collect.Streams
 import com.google.gson.JsonSyntaxException
 import com.google.gson.TypeAdapter
 import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonToken
 import com.google.gson.stream.JsonWriter
 import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
+import ru.dbotthepony.kstarbound.set
+import java.util.Arrays
+import java.util.stream.Stream
+import kotlin.reflect.KClass
+import kotlin.reflect.full.isSuperclassOf
 
-class EnumAdapter<T : Enum<T>>(private val enum: Class<T>) : TypeAdapter<T>() {
-	private val mapping: ImmutableMap<String, T> = Object2ObjectArrayMap<String, T>().let {
-		for (value in enum.enumConstants) {
-			it[value.name] = value
-			it[value.name.uppercase()] = value
-			it[value.name.lowercase()] = value
+interface IStringSerializable {
+	fun match(name: String): Boolean
+	fun write(out: JsonWriter)
+}
+
+@Suppress("FunctionName")
+inline fun <reified T : Enum<T>>EnumAdapter(values: Stream<T> = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter<T> {
+	return EnumAdapter(T::class, values, default)
+}
+
+@Suppress("FunctionName")
+inline fun <reified T : Enum<T>>EnumAdapter(values: Iterator<T>, default: T? = null): EnumAdapter<T> {
+	return EnumAdapter(T::class, Streams.stream(values), default)
+}
+
+@Suppress("FunctionName")
+inline fun <reified T : Enum<T>>EnumAdapter(values: Array<out T>, default: T? = null): EnumAdapter<T> {
+	return EnumAdapter(T::class, Arrays.stream(values), default)
+}
+
+@Suppress("FunctionName")
+inline fun <reified T : Enum<T>>EnumAdapter(values: Collection<T>, default: T? = null): EnumAdapter<T> {
+	return EnumAdapter(T::class, values.stream(), default)
+}
+
+@Suppress("name_shadowing")
+class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> = Arrays.stream(enum.java.enumConstants), val default: T? = null) : TypeAdapter<T?>() {
+	constructor(clazz: Class<T>, values: Stream<T> = Arrays.stream(clazz.enumConstants), default: T? = null) : this(clazz.kotlin, values, default)
+
+	constructor(clazz: Class<T>, values: Iterator<T>, default: T? = null) : this(clazz.kotlin, Streams.stream(values), default)
+	constructor(clazz: Class<T>, values: Array<out T>, default: T? = null) : this(clazz.kotlin, Arrays.stream(values), default)
+	constructor(clazz: Class<T>, values: Collection<T>, default: T? = null) : this(clazz.kotlin, values.stream(), default)
+
+	constructor(clazz: KClass<T>, values: Iterator<T>, default: T? = null) : this(clazz, Streams.stream(values), default)
+	constructor(clazz: KClass<T>, values: Array<out T>, default: T? = null) : this(clazz, Arrays.stream(values), default)
+	constructor(clazz: KClass<T>, values: Collection<T>, default: T? = null) : this(clazz, values.stream(), default)
+
+	private val values = values.collect(ImmutableList.toImmutableList())
+	private val mapping: ImmutableMap<String, T>
+	private val areCustom = IStringSerializable::class.isSuperclassOf(enum)
+
+	init {
+		val builder = Object2ObjectArrayMap<String, T>()
+
+		for (value in this.values) {
+			builder[value.name] = value
+			builder[value.name.uppercase()] = value
+			builder[value.name.lowercase()] = value
 
 			val spaced = value.name.replace('_', ' ')
 			val stitched = value.name.replace("_", "")
 
-			it[spaced] = value
-			it[spaced.uppercase()] = value
-			it[spaced.lowercase()] = value
+			builder[spaced] = value
+			builder[spaced.uppercase()] = value
+			builder[spaced.lowercase()] = value
 
-			it[stitched] = value
-			it[stitched.uppercase()] = value
-			it[stitched.lowercase()] = value
+			builder[stitched] = value
+			builder[stitched.uppercase()] = value
+			builder[stitched.lowercase()] = value
 		}
 
-		ImmutableMap.copyOf(it)
+		mapping = ImmutableMap.copyOf(builder)
 	}
 
-	override fun write(out: JsonWriter, value: T) {
-		out.value(value.name)
+	override fun write(out: JsonWriter, value: T?) {
+		if (value == null) {
+			out.nullValue()
+		} else {
+			out.value(value.name)
+		}
 	}
 
-	override fun read(`in`: JsonReader): T {
+	@Suppress("unchecked_cast")
+	override fun read(`in`: JsonReader): T? {
+		if (`in`.peek() == JsonToken.NULL) {
+			return null
+		}
+
 		val key = `in`.nextString()
-		return mapping[key] ?: throw JsonSyntaxException("Unable to match '$key' against ${enum.canonicalName}")
+
+		if (areCustom) {
+			for (value in values) {
+				if ((value as IStringSerializable).match(key)) {
+					return value
+				}
+			}
+		}
+
+		return mapping[key] ?: default
+	}
+
+	/**
+	 * Возвращает [EnumAdapter] с тем же типом и набором [T], которому запрещено возвращать или принимать null'ы
+	 */
+	fun neverNull(): TypeAdapter<T> {
+		return object : TypeAdapter<T>() {
+			override fun write(out: JsonWriter, value: T) {
+				return this@EnumAdapter.write(out, value)
+			}
+
+			override fun read(`in`: JsonReader): T {
+				val key = `in`.nextString()
+
+				if (areCustom) {
+					for (value in values) {
+						if ((value as IStringSerializable).match(key)) {
+							return value
+						}
+					}
+				}
+
+				return mapping[key] ?: default ?: throw JsonSyntaxException("$key is not a valid ${enum.qualifiedName} value")
+			}
+		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/Ext.kt
index 7506e7b9..371166f0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/Ext.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/Ext.kt
@@ -1,5 +1,6 @@
 package ru.dbotthepony.kstarbound.io.json
 
+import com.google.gson.JsonSyntaxException
 import com.google.gson.TypeAdapter
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonToken
@@ -56,3 +57,18 @@ fun <T> TypeAdapter<T>.ifString(reader: (String) -> T): TypeAdapter<T> {
 		}
 	}
 }
+
+fun <T> TypeAdapter<T?>.neverNull(): TypeAdapter<T> {
+	return object : TypeAdapter<T>() {
+		override fun write(out: JsonWriter, value: T) {
+			this@neverNull.write(out, value)
+		}
+
+		override fun read(`in`: JsonReader): T {
+			val path = `in`.path
+			return this@neverNull.read(`in`) ?: throw JsonSyntaxException("Value was null near $path")
+		}
+	}
+}
+
+fun <T> TypeAdapter<T>.allowNull(): TypeAdapter<T?> = nullSafe()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt
index 061ca52a..9375fb67 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt
@@ -188,7 +188,7 @@ class FactoryAdapter<T : Any> private constructor(
 				}
 
 				reader = JsonTreeReader(readArray)
-				readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray) as List<Any>, Starbound.assetStringInterner::intern)
+				readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray) as List<Any>, Starbound.STRING_INTERNER::intern)
 			}
 
 			reader.beginArray()
@@ -233,7 +233,7 @@ class FactoryAdapter<T : Any> private constructor(
 				}
 
 				reader = JsonTreeReader(readMap)
-				readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, Starbound.assetStringInterner::intern)
+				readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, Starbound.STRING_INTERNER::intern)
 			}
 
 			reader.beginObject()
@@ -357,6 +357,12 @@ class FactoryAdapter<T : Any> private constructor(
 	 * Позволяет построить класс [FactoryAdapter] на основе заданных параметров
 	 */
 	class Builder<T : Any>(val clazz: KClass<T>) {
+		constructor(clazz: KClass<T>, vararg fields: KProperty1<T, *>) : this(clazz) {
+			for (field in fields) {
+				auto(field)
+			}
+		}
+
 		private val types = ArrayList<PackedProperty<T, *>>()
 
 		/**
@@ -418,8 +424,8 @@ class FactoryAdapter<T : Any> private constructor(
 		 *
 		 * Список неизменяем (создаётся объект [ImmutableList])
 		 */
-		fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (List<V>?) -> List<V>? = { it }): Builder<T> {
-			types.add(PackedProperty(field, ListAdapter(type).nullSafe(), transformer = transformer))
+		fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (V) -> V = { it }): Builder<T> {
+			types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe()))
 			return this
 		}
 
@@ -428,8 +434,8 @@ class FactoryAdapter<T : Any> private constructor(
 		 *
 		 * Список неизменяем (создаётся объект [ImmutableList])
 		 */
-		inline fun <reified V : Any> autoList(field: KProperty1<T, List<V>?>, noinline transformer: (List<V>?) -> List<V>? = { it }): Builder<T> {
-			return add(field, ListAdapter(V::class.java).nullSafe())
+		inline fun <reified V : Any> autoList(field: KProperty1<T, List<V>?>, noinline transformer: (V) -> V = { it }): Builder<T> {
+			return add(field, ListAdapter(V::class.java, transformer).nullSafe())
 		}
 
 		/**
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/String2ObjectAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/String2ObjectAdapter.kt
index def7f538..f662349e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/String2ObjectAdapter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/String2ObjectAdapter.kt
@@ -31,7 +31,7 @@ class String2ObjectAdapter<T>(val adapter: TypeAdapter<T>, val valueTransformer:
 		reader.beginObject()
 
 		while (reader.peek() != JsonToken.END_OBJECT) {
-			builder.put(Starbound.assetStringInterner.intern(reader.nextName()), valueTransformer(adapter.read(reader)))
+			builder.put(Starbound.STRING_INTERNER.intern(reader.nextName()), valueTransformer(adapter.read(reader)))
 		}
 
 		reader.endObject()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt
index e337e4c3..021d4e15 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt
@@ -5,10 +5,11 @@ import ru.dbotthepony.kbox2d.api.FixtureDef
 import ru.dbotthepony.kbox2d.api.Manifold
 import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
 import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
+import ru.dbotthepony.kstarbound.defs.item.IItemDefinition
 import ru.dbotthepony.kstarbound.defs.item.ItemDefinition
 import ru.dbotthepony.kstarbound.world.World
 
-class ItemEntity(world: World<*, *>, val def: ItemDefinition) : Entity(world) {
+class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) {
 	override val movement = object : MovementController<ItemEntity>(this) {
 		override fun beginContact(contact: AbstractContact) {
 			// тут надо код подбора предмета игроком, если мы начинаем коллизию с окружностью подбора