diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt
index 1035b3ed..4810f546 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt
@@ -4,6 +4,9 @@ import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableSet
 import com.google.gson.TypeAdapter
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.future.await
+import kotlinx.coroutines.launch
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
 import ru.dbotthepony.kstarbound.defs.ClientConfig
@@ -135,32 +138,31 @@ object Globals {
 		words.build()
 	}
 
-	private fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: Lazy<TypeAdapter<T>>): Future<*> {
-		val file = Starbound.loadJsonAsset(path)
+	val onLoadedFuture = CompletableFuture<Unit>()
+
+	private suspend fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: Lazy<TypeAdapter<T>>) {
+		val file = Starbound.loadJsonAsset(path).await()
 
 		if (file == null) {
 			LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!")
-			return CompletableFuture.completedFuture(Unit)
 		} else {
-			return Starbound.EXECUTOR.submit {
-				try {
-					AssetPathStack("/") {
-						accept.set(adapter.value.fromJsonTree(file))
-					}
-				} catch (err: Throwable) {
-					LOGGER.fatal("Error while reading $path, expect bad things to happen!", err)
-					throw err
+			try {
+				AssetPathStack("/") {
+					accept.set(adapter.value.fromJsonTree(file))
 				}
+			} catch (err: Throwable) {
+				LOGGER.fatal("Error while reading $path, expect bad things to happen!", err)
+				throw err
 			}
 		}
 	}
 
-	private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>): Future<*> {
-		return load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) })
+	private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>): CompletableFuture<*> {
+		return Starbound.GLOBAL_SCOPE.launch { load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) }) }.asCompletableFuture()
 	}
 
 	fun load(): List<Future<*>> {
-		val tasks = ArrayList<Future<*>>()
+		val tasks = ArrayList<CompletableFuture<*>>()
 
 		tasks.add(load("/default_actor_movement.config", ::actorMovementParameters))
 		tasks.add(load("/default_movement.config", ::movementParameters))
@@ -184,12 +186,18 @@ object Globals {
 		tasks.add(load("/plants/bushDamage.config", ::bushDamage))
 		tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
 
-		tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, lazy { Starbound.gson.mapAdapter() }))
-		tasks.add(load("/currencies.config", ::currencies, lazy { Starbound.gson.mapAdapter() }))
-		tasks.add(load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() }))
-		tasks.add(load("/instance_worlds.config", ::instanceWorlds, lazy { Starbound.gson.mapAdapter() }))
+		tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
+		tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
+		tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
+		tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/instance_worlds.config", ::instanceWorlds, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
 
-		tasks.add(load("/names/profanityfilter.config", ::profanityFilterInternal, lazy { Starbound.gson.listAdapter() }))
+		tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/names/profanityfilter.config", ::profanityFilterInternal, lazy { Starbound.gson.listAdapter() }) }.asCompletableFuture())
+
+		CompletableFuture.allOf(*tasks.toTypedArray()).thenApply {
+			onLoadedFuture.complete(Unit)
+		}.exceptionally {
+			onLoadedFuture.completeExceptionally(it)
+		}
 
 		return tasks
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt
index 74bdda35..76f97d13 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt
@@ -7,6 +7,10 @@ import com.google.gson.JsonObject
 import com.google.gson.JsonSyntaxException
 import com.google.gson.TypeAdapterFactory
 import com.google.gson.reflect.TypeToken
+import kotlinx.coroutines.async
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.future.await
+import kotlinx.coroutines.launch
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kommons.util.KOptional
 import ru.dbotthepony.kstarbound.defs.AssetReference
@@ -27,7 +31,7 @@ import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition
 import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
 import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
 import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition
-import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition
+import ru.dbotthepony.kstarbound.defs.ProjectileDefinition
 import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
 import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
 import ru.dbotthepony.kstarbound.defs.tile.RenderParameters
@@ -100,7 +104,7 @@ object Registries {
 		val futures = ArrayList<CompletableFuture<Boolean>>()
 
 		for (registry in registriesInternal)
-			futures.add(CompletableFuture.supplyAsync { registry.validate() })
+			futures.add(CompletableFuture.supplyAsync({ registry.validate() }, Starbound.EXECUTOR))
 
 		return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } }
 	}
@@ -115,10 +119,11 @@ object Registries {
 		val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
 
 		return files.map { listedFile ->
-			Starbound.EXECUTOR.submit {
+			Starbound.GLOBAL_SCOPE.launch {
 				try {
+					val elem = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()])
+
 					AssetPathStack(listedFile.computeDirectory()) {
-						val elem = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()])
 						val read = adapter.fromJsonTree(elem)
 						val keys = keyProvider(read)
 
@@ -137,7 +142,7 @@ object Registries {
 				} catch (err: Throwable) {
 					LOGGER.error("Loading ${registry.name} definition file $listedFile", err)
 				}
-			}
+			}.asCompletableFuture()
 		}
 	}
 
@@ -187,9 +192,9 @@ object Registries {
 		val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
 
 		return files.map { listedFile ->
-			Starbound.EXECUTOR.submit {
+			Starbound.GLOBAL_SCOPE.launch {
 				try {
-					val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject
+					val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject
 
 					for ((k, v) in json.entrySet()) {
 						try {
@@ -206,13 +211,13 @@ object Registries {
 				} catch (err: Exception) {
 					LOGGER.error("Loading ${registry.name} definition $listedFile", err)
 				}
-			}
+			}.asCompletableFuture()
 		}
 	}
 
-	private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
+	private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
 		try {
-			val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject
+			val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject
 			val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
 			val factory = TerrainSelectorType.factory(json, false, type)
 
@@ -228,17 +233,17 @@ object Registries {
 		val tasks = ArrayList<Future<*>>()
 
 		tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
-			Starbound.EXECUTOR.submit {
+			Starbound.GLOBAL_SCOPE.async {
 				loadTerrainSelector(listedFile, null, patches)
-			}
+			}.asCompletableFuture()
 		})
 
 		// legacy files
 		for (type in TerrainSelectorType.entries) {
 			tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
-				Starbound.EXECUTOR.submit {
+				Starbound.GLOBAL_SCOPE.async {
 					loadTerrainSelector(listedFile, type, patches)
-				}
+				}.asCompletableFuture()
 			})
 		}
 
@@ -256,8 +261,8 @@ object Registries {
 	)
 
 	private fun loadMetaMaterials(): Future<*> {
-		return Starbound.EXECUTOR.submit {
-			val read = Starbound.loadJsonAsset("/metamaterials.config") ?: return@submit
+		return Starbound.GLOBAL_SCOPE.async {
+			val read = Starbound.loadJsonAsset("/metamaterials.config").await() ?: return@async
 			val read2 = Starbound.gson.getAdapter(object : TypeToken<ImmutableList<MetaMaterialDef>>() {}).fromJsonTree(read)
 
 			for (def in read2) {
@@ -282,6 +287,6 @@ object Registries {
 					))
 				}
 			}
-		}
+		}.asCompletableFuture()
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
index 8829a5ae..04af3437 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
@@ -1,14 +1,20 @@
 package ru.dbotthepony.kstarbound
 
+import com.github.benmanes.caffeine.cache.AsyncCacheLoader
 import com.github.benmanes.caffeine.cache.Caffeine
 import com.github.benmanes.caffeine.cache.Interner
 import com.github.benmanes.caffeine.cache.Scheduler
+import com.google.common.base.Predicate
 import com.google.gson.*
 import com.google.gson.stream.JsonReader
 import it.unimi.dsi.fastutil.objects.ObjectArraySet
-import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.future.await
 import org.apache.logging.log4j.LogManager
 import org.classdump.luna.compiler.CompilerChunkLoader
 import org.classdump.luna.compiler.CompilerSettings
@@ -55,6 +61,7 @@ import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
 import ru.dbotthepony.kstarbound.json.JsonPatch
 import ru.dbotthepony.kstarbound.json.JsonPath
 import ru.dbotthepony.kstarbound.json.NativeLegacy
+import ru.dbotthepony.kstarbound.json.factory.CompletableFutureAdapter
 import ru.dbotthepony.kstarbound.math.AABBiTypeAdapter
 import ru.dbotthepony.kstarbound.math.Vector2dTypeAdapter
 import ru.dbotthepony.kstarbound.math.Vector2fTypeAdapter
@@ -80,12 +87,13 @@ import java.io.*
 import java.lang.ref.Cleaner
 import java.text.DateFormat
 import java.time.Duration
-import java.util.Collections
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.Executor
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.ForkJoinPool
+import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory
+import java.util.concurrent.ForkJoinWorkerThread
 import java.util.concurrent.Future
 import java.util.concurrent.LinkedBlockingQueue
 import java.util.concurrent.ThreadFactory
@@ -96,6 +104,8 @@ import java.util.concurrent.locks.LockSupport
 import java.util.random.RandomGenerator
 import kotlin.NoSuchElementException
 import kotlin.collections.ArrayList
+import kotlin.math.max
+import kotlin.math.min
 
 object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator {
 	const val ENGINE_VERSION = "0.0.1"
@@ -136,25 +146,44 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
 		start()
 	}
 
-	private val ioPoolCounter = AtomicInteger()
+	private fun makeExecutor(parallelism: Int, threadNameString: String, threadPriority: Int): ForkJoinPool {
+		val counter = AtomicInteger()
 
-	@JvmField
-	val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory {
-		val thread = Thread(it, "IO Worker ${ioPoolCounter.getAndIncrement()}")
-		thread.isDaemon = true
-		thread.priority = Thread.MIN_PRIORITY
-
-		thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
-			LOGGER.error("I/O thread died due to uncaught exception", e)
+		val factory = ForkJoinWorkerThreadFactory {
+			object : ForkJoinWorkerThread(it) {
+				init {
+					name = threadNameString.format(counter.getAndIncrement())
+					priority = threadPriority
+				}
+			}
 		}
 
-		return@ThreadFactory thread
-	})
+		val handler = UncaughtExceptionHandler { t, e ->
+			LOGGER.error("Worker thread died due to unhandled exception", e)
+		}
+
+		return ForkJoinPool(
+			parallelism, factory, handler, false,
+			0, parallelism + 256, 1, null, 30L, TimeUnit.SECONDS
+		)
+	}
 
 	@JvmField
-	val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
+	val IO_EXECUTOR: ExecutorService = makeExecutor(8, "Disk IO %d", MIN_PRIORITY)
+
 	@JvmField
-	val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher()
+	val IO_COROUTINES = ExecutorWithScheduler(IO_EXECUTOR, this).asCoroutineDispatcher()
+
+	@JvmField
+	val EXECUTOR: ForkJoinPool = makeExecutor(Runtime.getRuntime().availableProcessors(), "Worker %d", NORM_PRIORITY)
+	@JvmField
+	val COROUTINES = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher()
+
+	@JvmField
+	val GLOBAL_SCOPE = CoroutineScope(COROUTINES + SupervisorJob())
+
+	@JvmField
+	val IO_GLOBAL_SCOPE = CoroutineScope(IO_COROUTINES + SupervisorJob())
 
 	@JvmField
 	val CLEANER: Cleaner = Cleaner.create {
@@ -377,6 +406,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
 		registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)
 		registerTypeAdapter(WorldLayout.Companion)
 
+		registerTypeAdapterFactory(CompletableFutureAdapter)
+
 		Registries.registerAdapters(this)
 
 		registerTypeAdapter(LongRangeAdapter)
@@ -399,14 +430,33 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
 	var loaded = 0
 		private set
 
+	private suspend fun loadJsonAsset0(it: String): KOptional<JsonElement> {
+		val file = locate(it)
+
+		if (!file.isFile)
+			return KOptional()
+
+		val findPatches = locateAll("$it.patch")
+		return KOptional(JsonPatch.applyAsync(ELEMENTS_ADAPTER.read(file.asyncJsonReader().await()), findPatches))
+	}
+
 	private val jsonAssetsCache = Caffeine.newBuilder()
 		.maximumSize(4096L)
 		.expireAfterAccess(Duration.ofMinutes(5L))
 		.scheduler(this)
 		.executor(EXECUTOR)
-		.build<String, KOptional<JsonElement>>()
+		.buildAsync<String, KOptional<JsonElement>>(AsyncCacheLoader { key, executor ->
+			IO_GLOBAL_SCOPE.async {
+				try {
+					loadJsonAsset0(key)
+				} catch (err: Throwable) {
+					LOGGER.error("Exception loading JSON asset at $key", err)
+					throw err
+				}
+			}.asCompletableFuture()
+		})
 
-	fun loadJsonAsset(path: String): JsonElement? {
+	fun loadJsonAsset(path: String): CompletableFuture<JsonElement?> {
 		val filename: String
 		val jsonPath: String?
 
@@ -418,27 +468,27 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
 			jsonPath = null
 		}
 
-		val json = jsonAssetsCache.get(filename) {
-			val file = locate(it)
-
-			if (!file.isFile)
-				return@get KOptional()
-
-			val findPatches = locateAll("$filename.patch")
-			KOptional(JsonPatch.apply(ELEMENTS_ADAPTER.read(file.jsonReader()), findPatches))
-		}.orNull() ?: return null
-
-		if (jsonPath == null)
-			return json
-
-		return JsonPath.query(jsonPath).get(json)
+		return jsonAssetsCache.get(filename).thenApply { json ->
+			if (jsonPath == null || json.isEmpty)
+				json.orNull()
+			else
+				JsonPath.query(jsonPath).get(json.value)
+		}
 	}
 
-	fun loadJsonAsset(path: JsonElement, relative: String): JsonElement {
+	fun loadJsonAsset(path: JsonElement, relative: String): CompletableFuture<JsonElement> {
 		if (path is JsonPrimitive) {
-			return loadJsonAsset(AssetPathStack.relativeTo(relative, path.asString)) ?: JsonNull.INSTANCE
+			return loadJsonAsset(AssetPathStack.relativeTo(relative, path.asString)).thenApply { it ?: JsonNull.INSTANCE }
 		} else {
-			return path
+			return CompletableFuture.completedFuture(path)
+		}
+	}
+
+	fun loadJsonAsset(path: JsonElement): CompletableFuture<JsonElement> {
+		if (path is JsonPrimitive) {
+			return loadJsonAsset(path.asString).thenApply { it ?: JsonNull.INSTANCE }
+		} else {
+			return CompletableFuture.completedFuture(path)
 		}
 	}
 
@@ -650,7 +700,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
 				throw FileNotFoundException("/hobo.ttf is not a file!")
 			else {
 				val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf")
-				tempPath.writeBytes(file.read().array())
+				tempPath.writeBytes(file.read())
 				Starbound.fontPath = tempPath
 				return@supplyAsync tempPath
 			}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt
index 8d31fe53..74629090 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt
@@ -1,8 +1,10 @@
 package ru.dbotthepony.kstarbound
 
 import com.google.gson.stream.JsonReader
+import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
 import ru.dbotthepony.kstarbound.io.StarboundPak
 import ru.dbotthepony.kstarbound.util.sbIntern
+import ru.dbotthepony.kstarbound.util.supplyAsync
 import java.io.BufferedInputStream
 import java.io.File
 import java.io.FileNotFoundException
@@ -10,42 +12,12 @@ import java.io.InputStream
 import java.io.InputStreamReader
 import java.io.Reader
 import java.nio.ByteBuffer
+import java.util.concurrent.CompletableFuture
 import java.util.stream.Stream
 
 fun interface ISBFileLocator {
 	fun locate(path: String): IStarboundFile
 	fun exists(path: String): Boolean = locate(path).exists
-
-	/**
-	 * @throws IllegalStateException if file is a directory
-	 * @throws FileNotFoundException if file does not exist
-	 */
-	fun read(path: String) = locate(path).read()
-
-	/**
-	 * @throws IllegalStateException if file is a directory
-	 * @throws FileNotFoundException if file does not exist
-	 */
-	fun readDirect(path: String) = locate(path).readDirect()
-
-	/**
-	 * @throws IllegalStateException if file is a directory
-	 * @throws FileNotFoundException if file does not exist
-	 */
-	fun open(path: String) = locate(path).open()
-
-	/**
-	 * @throws IllegalStateException if file is a directory
-	 * @throws FileNotFoundException if file does not exist
-	 */
-	fun reader(path: String) = locate(path).reader()
-
-	/**
-	 * @throws IllegalStateException if file is a directory
-	 * @throws FileNotFoundException if file does not exist
-	 */
-	@Deprecated("This does not reflect json patches")
-	fun jsonReader(path: String) = locate(path).jsonReader()
 }
 
 /**
@@ -176,11 +148,34 @@ interface IStarboundFile : ISBFileLocator {
 	 * @throws IllegalStateException if file is a directory
 	 * @throws FileNotFoundException if file does not exist
 	 */
-	fun read(): ByteBuffer {
+	fun asyncRead(): CompletableFuture<ByteArray>
+
+	/**
+	 * @throws IllegalStateException if file is a directory
+	 * @throws FileNotFoundException if file does not exist
+	 */
+	fun asyncReader(): CompletableFuture<Reader> {
+		return asyncRead().thenApply { InputStreamReader(FastByteArrayInputStream(it)) }
+	}
+
+	/**
+	 * @throws IllegalStateException if file is a directory
+	 * @throws FileNotFoundException if file does not exist
+	 */
+	@Deprecated("Careful! This does not reflect json patches")
+	fun asyncJsonReader(): CompletableFuture<JsonReader> {
+		return asyncReader().thenApply { JsonReader(it).also { it.isLenient = true } }
+	}
+
+	/**
+	 * @throws IllegalStateException if file is a directory
+	 * @throws FileNotFoundException if file does not exist
+	 */
+	fun read(): ByteArray {
 		val stream = open()
 		val read = stream.readAllBytes()
 		stream.close()
-		return ByteBuffer.wrap(read)
+		return read
 	}
 
 	/**
@@ -200,16 +195,9 @@ interface IStarboundFile : ISBFileLocator {
 	 */
 	fun readDirect(): ByteBuffer {
 		val read = read()
-		val buf = ByteBuffer.allocateDirect(read.capacity())
-
-		read.position(0)
-
-		for (i in 0 until read.capacity()) {
-			buf.put(read[i])
-		}
-
+		val buf = ByteBuffer.allocateDirect(read.size)
+		buf.put(read)
 		buf.position(0)
-
 		return buf
 	}
 
@@ -230,6 +218,10 @@ interface IStarboundFile : ISBFileLocator {
 		override val name: String
 			get() = ""
 
+		override fun asyncRead(): CompletableFuture<ByteArray> {
+			throw FileNotFoundException()
+		}
+
 		override fun open(): InputStream {
 			throw FileNotFoundException()
 		}
@@ -251,6 +243,10 @@ class NonExistingFile(
 	override val exists: Boolean
 		get() = false
 
+	override fun asyncRead(): CompletableFuture<ByteArray> {
+		throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
+	}
+
 	override fun open(): InputStream {
 		throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
 	}
@@ -297,6 +293,10 @@ class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) :
 		return BufferedInputStream(real.inputStream())
 	}
 
+	override fun asyncRead(): CompletableFuture<ByteArray> {
+		return Starbound.IO_EXECUTOR.supplyAsync { real.readBytes() }
+	}
+
 	override fun equals(other: Any?): Boolean {
 		return other is IStarboundFile && computeFullPath() == other.computeFullPath()
 	}
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 5bf5f2c9..535bca0c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt
@@ -1,6 +1,11 @@
 package ru.dbotthepony.kstarbound.client.render
 
 import com.google.common.collect.ImmutableMap
+import com.google.gson.Gson
+import com.google.gson.TypeAdapter
+import com.google.gson.annotations.JsonAdapter
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kstarbound.world.api.AbstractTileState
 import ru.dbotthepony.kstarbound.world.api.TileColor
@@ -44,6 +49,7 @@ enum class RenderLayer {
 		return base
 	}
 
+	@JsonAdapter(Adapter::class)
 	data class Point(val base: RenderLayer, val offset: Long = 0L, val index: Long = 0L, val hueShift: Float = 0f, val colorVariant: TileColor = TileColor.DEFAULT) : Comparable<Point> {
 		override fun compareTo(other: Point): Int {
 			if (this === other) return 0
@@ -56,6 +62,16 @@ enum class RenderLayer {
 		}
 	}
 
+	class Adapter(gson: Gson) : TypeAdapter<Point>() {
+		override fun write(out: JsonWriter, value: Point) {
+			TODO("Not yet implemented")
+		}
+
+		override fun read(`in`: JsonReader): Point {
+			return parse(`in`.nextString())
+		}
+	}
+
 	companion object {
 		fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L, index: Long = 0L, hueShift: Float = 0f, colorVariant: TileColor = TileColor.DEFAULT): Point {
 			if (isBackground && isModifier) {
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 0b03a428..906e526f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
@@ -235,7 +235,7 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
 
 		// если у нас нет renderTemplate
 		// то мы просто не можем его отрисовать
-		val template = def.renderTemplate.value ?: return
+		val template = def.renderTemplate.value.get() ?: return
 
 		val vertexBuilder = meshBuilder
 			.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt
index 21d9c16d..e7324c49 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt
@@ -18,41 +18,47 @@ import ru.dbotthepony.kstarbound.util.AssetPathStack
 import ru.dbotthepony.kstarbound.util.sbIntern
 import java.lang.reflect.ParameterizedType
 import java.util.*
+import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ConcurrentHashMap
+import java.util.function.Consumer
 import kotlin.collections.HashMap
 
 class AssetReference<V> {
 	constructor(value: V?) {
-		lazy = lazy { value }
+		this.value = CompletableFuture.completedFuture(value)
 		path = null
 		fullPath = null
-		json = null
+		this.json = CompletableFuture.completedFuture(null)
 	}
 
-	constructor(value: () -> V?) {
-		lazy = lazy { value() }
+	constructor(value: CompletableFuture<V?>) {
+		this.value = value
 		path = null
 		fullPath = null
-		json = null
+		this.json = CompletableFuture.completedFuture(null)
 	}
 
 	constructor(path: String?, fullPath: String?, value: V?, json: JsonElement?) {
 		this.path = path
 		this.fullPath = fullPath
-		this.lazy = lazy { value }
+		this.value = CompletableFuture.completedFuture(value)
+		this.json = CompletableFuture.completedFuture(json)
+	}
+
+	constructor(path: String?, fullPath: String?, value: CompletableFuture<V?>, json: CompletableFuture<JsonElement?>) {
+		this.path = path
+		this.fullPath = fullPath
+		this.value = value
 		this.json = json
 	}
 
 	val path: String?
 	val fullPath: String?
-	val json: JsonElement?
-
-	val value: V?
-		get() = lazy.value
-
-	private val lazy: Lazy<V?>
+	val json: CompletableFuture<JsonElement?>
+	val value: CompletableFuture<V?>
 
 	companion object : TypeAdapterFactory {
+		private val LOGGER = LogManager.getLogger()
 		val EMPTY = AssetReference(null, null, null, null)
 
 		fun <V> empty() = EMPTY as AssetReference<V>
@@ -62,7 +68,7 @@ class AssetReference<V> {
 				val param = type.type as? ParameterizedType ?: return null
 
 				return object : TypeAdapter<AssetReference<T>>() {
-					private val cache = Collections.synchronizedMap(HashMap<String, Pair<T, JsonElement>>())
+					private val cache = ConcurrentHashMap<String, Pair<CompletableFuture<T?>, CompletableFuture<JsonElement?>>>()
 					private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
 					private val strings = gson.getAdapter(String::class.java)
 					private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
@@ -81,34 +87,31 @@ class AssetReference<V> {
 						} else if (`in`.peek() == JsonToken.STRING) {
 							val path = strings.read(`in`)!!
 							val fullPath = AssetPathStack.remap(path)
-							val get = cache[fullPath]
 
-							if (get != null)
-								return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)
+							val get = cache.computeIfAbsent(fullPath) {
+								val json = Starbound.loadJsonAsset(fullPath)
 
-							if (fullPath in missing)
-								return null
+								json.thenAccept {
+									if (it == null && missing.add(fullPath)) {
+										logger.error("JSON asset does not exist: $fullPath")
+									}
+								}
 
-							val json = Starbound.loadJsonAsset(fullPath)
+								val value = json.thenApplyAsync({ j ->
+	                                AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) {
+		                                adapter.fromJsonTree(j)
+	                                }
+                                }, Starbound.EXECUTOR)
 
-							if (json == null) {
-								if (missing.add(fullPath))
-									logger.error("JSON asset does not exist: $fullPath")
+								value.exceptionally {
+									LOGGER.error("Exception loading $fullPath", it)
+									null
+								}
 
-								return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, null)
+								value to json
 							}
 
-							val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) {
-								adapter.fromJsonTree(json)
-							}
-
-							if (value == null) {
-								missing.add(fullPath)
-								return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, json)
-							}
-
-							cache[fullPath] = value to json
-							return AssetReference(path.sbIntern(), fullPath.sbIntern(), value, json)
+							return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)
 						} else {
 							val json = Starbound.ELEMENTS_ADAPTER.read(`in`)
 							val value = adapter.read(JsonTreeReader(json)) ?: return null
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ClientEntityMode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ClientEntityMode.kt
new file mode 100644
index 00000000..48c7bdc0
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ClientEntityMode.kt
@@ -0,0 +1,9 @@
+package ru.dbotthepony.kstarbound.defs
+
+import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
+
+enum class ClientEntityMode(override val jsonName: String) : IStringSerializable {
+	CLIENT_SLAVE_ONLY("ClientSlaveOnly"),
+	CLIENT_MASTER_ALLOWED("ClientMasterAllowed"),
+	CLIENT_PRESENCE_MASTER("ClientPresenceMaster");
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt
index 5b6fec42..66a5a444 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt
@@ -62,7 +62,8 @@ sealed class JsonReference<E : JsonElement?>(val path: String?, val fullPath: St
 				if (`in`.peek() == JsonToken.STRING) {
 					val path = `in`.nextString()
 					val full = AssetPathStack.remapSafe(path)
-					val get = Starbound.loadJsonAsset(full) ?: return factory(path, full, null)
+					// TODO: this blocks thread, need coroutine aware version
+					val get = Starbound.loadJsonAsset(full).get() ?: return factory(path, full, null)
 					return factory(path, full, adapter.fromJsonTree(get))
 				} else {
 					return factory(null, null, adapter.read(`in`))
@@ -89,7 +90,8 @@ sealed class JsonReference<E : JsonElement?>(val path: String?, val fullPath: St
 				if (`in`.peek() == JsonToken.STRING) {
 					val path = `in`.nextString()
 					val full = AssetPathStack.remapSafe(path)
-					val get = Starbound.loadJsonAsset(full) ?: throw JsonSyntaxException("Json asset at $full does not exist")
+					// TODO: this blocks thread, need coroutine aware version
+					val get = Starbound.loadJsonAsset(full).get() ?: throw JsonSyntaxException("Json asset at $full does not exist")
 					return factory(path, full, adapter.fromJsonTree(get) ?: throw JsonSyntaxException("Json asset at $full is literal null, which is not allowed"))
 				} else {
 					return factory(null, null, adapter.read(`in`))
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt
new file mode 100644
index 00000000..da12a306
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt
@@ -0,0 +1,77 @@
+package ru.dbotthepony.kstarbound.defs
+
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableSet
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import ru.dbotthepony.kommons.math.RGBAColor
+import ru.dbotthepony.kommons.util.Either
+import ru.dbotthepony.kstarbound.client.render.RenderLayer
+import ru.dbotthepony.kstarbound.defs.actor.StatModifier
+import ru.dbotthepony.kstarbound.defs.image.SpriteReference
+import ru.dbotthepony.kstarbound.json.builder.JsonFactory
+import ru.dbotthepony.kstarbound.math.AABB
+import ru.dbotthepony.kstarbound.math.vector.Vector2d
+import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
+import ru.dbotthepony.kstarbound.world.physics.Poly
+
+@JsonFactory
+data class ProjectileDefinition(
+	val projectileName: String,
+	val description: String = "",
+	val boundBox: AABB = AABB.withSide(Vector2d.ZERO, 5.0, 5.0),
+	val speed: Double = 50.0,
+	val acceleration: Double = 0.0,
+	val power: Double = 1.0,
+	@Deprecated("", replaceWith = ReplaceWith("this.actualDamagePoly"))
+	val damagePoly: Poly? = null,
+	val piercing: Boolean = false,
+	val falldown: Boolean = false,
+	val bounces: Int = 0,
+	val actionOnCollide: JsonArray = JsonArray(),
+	val actionOnReap: JsonArray = JsonArray(),
+	val actionOnHit: JsonArray = JsonArray(),
+	val actionOnTimeout: JsonArray = JsonArray(),
+	val periodicActions: JsonArray = JsonArray(),
+	val image: SpriteReference,
+	val frameNumber: Int = 1,
+	val animationCycle: Double = 1.0,
+	val animationLoops: Boolean = true,
+	val windupFrames: Int = 0,
+	val intangibleWindup: Boolean = false,
+	val winddownFrames: Int = 0,
+	val intangibleWinddown: Boolean = false,
+	val flippable: Boolean = false,
+	val orientationLocked: Boolean = false,
+	val fullbright: Boolean = false,
+	val renderLayer: RenderLayer.Point = RenderLayer.Point(RenderLayer.Projectile),
+	val lightColor: RGBAColor = RGBAColor.TRANSPARENT_BLACK,
+	val lightPosition: Vector2d = Vector2d.ZERO,
+	val pointLight: Boolean = false,
+	val persistentAudio: AssetPath = AssetPath(""),
+
+	// Initialize timeToLive after animationCycle so we can have the default be
+	// based on animationCycle
+	val timeToLive: Double = if (animationLoops) animationCycle else 5.0,
+	val damageKindImage: AssetPath = AssetPath(""),
+	val damageKind: String = "",
+	val damageType: String = "",
+	val damageRepeatGroup: String? = null,
+	val damageRepeatTimeout: Double? = null,
+	val statusEffects: ImmutableList<EphemeralStatusEffect> = ImmutableList.of(),
+	val emitters: ImmutableSet<String> = ImmutableSet.of(),
+	val hydrophobic: Boolean = false,
+	val rayCheckToSource: Boolean = false,
+	val knockback: Double = 0.0,
+	val knockbackDirectional: Boolean = false,
+	val onlyHitTerrain: Boolean = false,
+	val clientEntityMode: ClientEntityMode = ClientEntityMode.CLIENT_MASTER_ALLOWED,
+	val masterOnly: Boolean = false,
+	val scripts: ImmutableList<AssetPath> = ImmutableList.of(),
+	val physicsForces: JsonObject = JsonObject(),
+	val physicsCollisions: JsonObject = JsonObject(),
+	val persistentStatusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
+	val statusEffectArea: Poly = Poly.EMPTY,
+) {
+	val actualDamagePoly = if (damagePoly != null) damagePoly * (1.0 / PIXELS_IN_STARBOUND_UNIT) else null
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt
index 43ec6a2c..e9f99176 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt
@@ -218,7 +218,7 @@ data class DungeonDefinition(
 
 		val anchor = validAnchors.random(world.random)
 
-		return CoroutineScope(Starbound.COROUTINE_EXECUTOR)
+		return CoroutineScope(Starbound.COROUTINES)
 			.async {
 				if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) {
 					generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID)
@@ -237,7 +237,7 @@ data class DungeonDefinition(
 		require(anchor in anchorParts) { "$anchor does not belong to $name" }
 		val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends)
 
-		return CoroutineScope(Starbound.COROUTINE_EXECUTOR)
+		return CoroutineScope(Starbound.COROUTINES)
 			.async {
 				generate0(anchor, dungeonWorld, x, y, forcePlacement, dungeonID)
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSet.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSet.kt
index 2f58b781..593da5a5 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSet.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSet.kt
@@ -56,8 +56,9 @@ class TiledTileSet private constructor(
 			return DungeonTile.INTERNER.intern(DungeonTile(ImmutableList.copyOf(brushes), ImmutableList.copyOf(rules), 0, connector))
 		}
 
+		// TODO: coroutine aware version
 		private fun load0(location: String): Either<TiledTileSet, Throwable> {
-			val locate = Starbound.loadJsonAsset(location)
+			val locate = Starbound.loadJsonAsset(location).get()
 				?: return Either.right(NoSuchElementException("Tileset at $location does not exist"))
 
 			try {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt
index 98258dab..ac6a4fab 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt
@@ -1,5 +1,6 @@
 package ru.dbotthepony.kstarbound.defs.image
 
+import com.github.benmanes.caffeine.cache.AsyncCacheLoader
 import com.github.benmanes.caffeine.cache.AsyncLoadingCache
 import com.github.benmanes.caffeine.cache.CacheLoader
 import com.github.benmanes.caffeine.cache.Caffeine
@@ -48,7 +49,9 @@ import java.util.Collections
 import java.util.Optional
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ConcurrentHashMap
+import java.util.function.BiFunction
 import java.util.function.Consumer
+import java.util.function.Function
 
 class Image private constructor(
 	val source: IStarboundFile,
@@ -169,14 +172,24 @@ class Image private constructor(
 		return whole.isTransparent(x, y, flip)
 	}
 
-	fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
+	@Deprecated("Blocks thread")
+	fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): ImmutableSet<Vector2i> {
 		return whole.worldSpaces(pixelOffset, spaceScan, flip)
 	}
 
-	fun worldSpaces(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): Set<Vector2i> {
+	@Deprecated("Blocks thread")
+	fun worldSpaces(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): ImmutableSet<Vector2i> {
 		return whole.worldSpaces(Vector2i(pixelOffset.x.toInt(), pixelOffset.y.toInt()), spaceScan, flip)
 	}
 
+	fun worldSpacesAsync(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): CompletableFuture<ImmutableSet<Vector2i>> {
+		return whole.worldSpacesAsync(pixelOffset, spaceScan, flip)
+	}
+
+	fun worldSpacesAsync(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): CompletableFuture<ImmutableSet<Vector2i>> {
+		return whole.worldSpacesAsync(Vector2i(pixelOffset.x.toInt(), pixelOffset.y.toInt()), spaceScan, flip)
+	}
+
 	private data class DataSprite(val name: String, val coordinates: Vector4i)
 	private data class SpaceScanKey(val sprite: Sprite, val pixelOffset: Vector2i, val spaceScan: Double, val flip: Boolean)
 
@@ -256,56 +269,60 @@ class Image private constructor(
 			nonEmptyRegion + Vector4i(x, y, x, y)
 		}
 
-		fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
-			return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip)) {
-				ImmutableSet.copyOf(worldSpaces0(pixelOffset, spaceScan, flip))
-			}
+		@Deprecated("Blocks thread")
+		fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): ImmutableSet<Vector2i> {
+			return worldSpacesAsync(pixelOffset, spaceScan, flip).get()
 		}
 
-		private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
-			val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
-			val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi
-			val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
-			val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
+		fun worldSpacesAsync(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): CompletableFuture<ImmutableSet<Vector2i>> {
+			return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip), BiFunction { _, _ ->
+				worldSpaces0(pixelOffset, spaceScan, flip)
+			})
+		}
 
-			val result = ObjectArraySet<Vector2i>()
+		private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): CompletableFuture<ImmutableSet<Vector2i>> {
+			return dataFuture.thenApplyAsync({
+				val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
+				val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi
+				val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
+				val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
 
-			// this is weird, but that's how original game handles this
-			// also we don't cache this info since that's a waste of precious ram
+				val result = ImmutableSet.Builder<Vector2i>()
 
-			val data = data
+				// this is weird, but that's how original game handles this
+				// also we don't cache this info since that's a waste of precious ram
 
-			for (yspace in minY until maxY) {
-				for (xspace in minX until maxX) {
-					var fillRatio = 0.0
+				for (yspace in minY until maxY) {
+					for (xspace in minX until maxX) {
+						var fillRatio = 0.0
 
-					for (y in 0 until PIXELS_IN_STARBOUND_UNITi) {
-						val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y)
+						for (y in 0 until PIXELS_IN_STARBOUND_UNITi) {
+							val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y)
 
-						if (ypixel !in 0 until height)
-							continue
-
-						for (x in 0 until PIXELS_IN_STARBOUND_UNITi) {
-							val xpixel = (xspace * PIXELS_IN_STARBOUND_UNITi + x - pixelOffset.x)
-
-							if (xpixel !in 0 until width)
+							if (ypixel !in 0 until height)
 								continue
 
-							if (!isTransparent(xpixel, height - ypixel - 1, flip, data)) {
-								fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT)
+							for (x in 0 until PIXELS_IN_STARBOUND_UNITi) {
+								val xpixel = (xspace * PIXELS_IN_STARBOUND_UNITi + x - pixelOffset.x)
+
+								if (xpixel !in 0 until width)
+									continue
+
+								if (!isTransparent(xpixel, height - ypixel - 1, flip, it)) {
+									fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT)
+								}
 							}
 						}
-					}
 
-					if (fillRatio >= spaceScan) {
-						result.add(Vector2i(xspace, yspace))
+						if (fillRatio >= spaceScan) {
+							result.add(Vector2i(xspace, yspace))
+						}
 					}
 				}
-			}
 
-			return result
+				result.build()
+			}, Starbound.EXECUTOR)
 		}
-
 	}
 
 	companion object : TypeAdapter<Image>() {
@@ -350,10 +367,9 @@ class Image private constructor(
 
 		private val spaceScanCache = Caffeine.newBuilder()
 			.expireAfterAccess(Duration.ofMinutes(30))
-			.softValues()
 			.scheduler(Starbound)
 			.executor(Starbound.EXECUTOR)
-			.build<SpaceScanKey, ImmutableSet<Vector2i>>()
+			.buildAsync<SpaceScanKey, ImmutableSet<Vector2i>>()
 
 		@JvmStatic
 		fun get(path: String): Image? {
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 fac38c8b..8a5d7422 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt
@@ -14,6 +14,10 @@ import com.google.gson.TypeAdapter
 import com.google.gson.annotations.JsonAdapter
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
+import kotlinx.coroutines.async
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.launch
+import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kommons.util.Either
 import ru.dbotthepony.kommons.math.RGBAColor
 import ru.dbotthepony.kstarbound.Registry
@@ -39,6 +43,9 @@ import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
 import ru.dbotthepony.kstarbound.util.AssetPathStack
 import ru.dbotthepony.kstarbound.world.Direction
 import ru.dbotthepony.kstarbound.world.World
+import java.util.concurrent.CompletableFuture
+import java.util.function.Function
+import java.util.function.Supplier
 
 @JsonAdapter(ObjectDefinition.Adapter::class)
 data class ObjectDefinition(
@@ -75,7 +82,7 @@ data class ObjectDefinition(
 	val soundEffectRangeMultiplier: Double = 1.0,
 	val price: Long = 1L,
 	val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
-	val touchDamage: JsonElement,
+	val touchDamage: CompletableFuture<JsonElement>,
 	val minimumLiquidLevel: Float? = null,
 	val maximumLiquidLevel: Float? = null,
 	val liquidCheckInterval: Float = 0.5f,
@@ -84,29 +91,33 @@ data class ObjectDefinition(
 	val biomePlaced: Boolean = false,
 	val printable: Boolean = false,
 	val smashOnBreak: Boolean = false,
-	val damageConfig: TileDamageParameters,
+	val damageConfig: CompletableFuture<TileDamageParameters>,
 	val flickerPeriod: PeriodicFunction? = null,
-	val orientations: ImmutableList<ObjectOrientation>,
+	val orientations: ImmutableList<Supplier<ObjectOrientation>>,
 ) {
 	fun findValidOrientation(world: World<*, *>, position: Vector2i, directionAffinity: Direction? = null, ignoreProtectedDungeons: Boolean = false): Int {
 		// If we are given a direction affinity, try and find an orientation with a
 		// matching affinity *first*
 		if (directionAffinity != null) {
 			for ((i, orientation) in orientations.withIndex()) {
-				if (orientation.directionAffinity == directionAffinity && orientation.placementValid(world, position, ignoreProtectedDungeons) && orientation.anchorsValid(world, position))
+				if (orientation.get().directionAffinity == directionAffinity && orientation.get().placementValid(world, position, ignoreProtectedDungeons) && orientation.get().anchorsValid(world, position))
 					return i
 			}
 		}
 
 		// Then, fallback and try and find any valid affinity
 		for ((i, orientation) in orientations.withIndex()) {
-			if (orientation.placementValid(world, position) && orientation.anchorsValid(world, position))
+			if (orientation.get().placementValid(world, position) && orientation.get().anchorsValid(world, position))
 				return i
 		}
 
 		return -1
 	}
 
+	companion object {
+		private val LOGGER = LogManager.getLogger()
+	}
+
 	class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() {
 		@JsonFactory(logMisses = false)
 		data class PlainData(
@@ -153,11 +164,10 @@ data class ObjectDefinition(
 			val biomePlaced: Boolean = false,
 		)
 
-		private val objectRef = gson.getAdapter(JsonReference.Object::class.java)
 		private val basic = gson.getAdapter(PlainData::class.java)
 		private val damageConfig = gson.getAdapter(TileDamageParameters::class.java)
 		private val damageTeam = gson.getAdapter(DamageTeam::class.java)
-		private val orientations = gson.getAdapter(ObjectOrientation::class.java)
+		private val orientations = ObjectOrientation.Adapter(gson)
 		private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
 		private val emitters = gson.listAdapter<ParticleEmissionEntry>()
 
@@ -179,13 +189,21 @@ data class ObjectDefinition(
 			val printable = basic.hasObjectItem && read.get("printable", basic.scannable)
 			val smashOnBreak = read.get("smashOnBreak", basic.smashable)
 
-			val getDamageParams = objectRef.fromJsonTree(read.get("damageTable", JsonPrimitive("/objects/defaultParameters.config:damageTable")))
-			getDamageParams?.value ?: throw JsonSyntaxException("No valid damageTable specified")
+			val getPath = read.get("damageTable", JsonPrimitive("/objects/defaultParameters.config:damageTable"))
 
-			getDamageParams.value["health"] = read["health"]
-			getDamageParams.value["harvestLevel"] = read["harvestLevel"]
+			val damageConfig = Starbound
+				.loadJsonAsset(getPath)
+				.thenApplyAsync(Function {
+					val value = it.asJsonObject.deepCopy()
+					value["health"] = read["health"]
+					value["harvestLevel"] = read["harvestLevel"]
+					damageConfig.fromJsonTree(value)
+				}, Starbound.EXECUTOR)
 
-			val damageConfig = damageConfig.fromJsonTree(getDamageParams.value)
+			damageConfig.exceptionally {
+				LOGGER.error("Exception loading damage config $getPath", it)
+				null
+			}
 
 			val flickerPeriod = if ("flickerPeriod" in read) {
 				PeriodicFunction(
@@ -199,17 +217,24 @@ data class ObjectDefinition(
 				null
 			}
 
-			val orientations = ObjectOrientation.preprocess(read.getArray("orientations"))
-				.stream()
-				.map { orientations.fromJsonTree(it) }
-				.collect(ImmutableList.toImmutableList())
+			val orientations = ImmutableList.Builder<Supplier<ObjectOrientation>>()
 
-			if ("particleEmitter" in read) {
-				orientations.forEach { it.particleEmitters.add(emitter.fromJsonTree(read["particleEmitter"])) }
-			}
+			val path = AssetPathStack.last()
 
-			if ("particleEmitters" in read) {
-				orientations.forEach { it.particleEmitters.addAll(emitters.fromJsonTree(read["particleEmitters"])) }
+			for (v in ObjectOrientation.preprocess(read.getArray("orientations"))) {
+				val future = Starbound.GLOBAL_SCOPE.async { AssetPathStack(path) { this@Adapter.orientations.read(v as JsonObject) } }.asCompletableFuture()
+
+				future.thenAccept {
+					if ("particleEmitter" in read) {
+						it.particleEmitters.add(emitter.fromJsonTree(read["particleEmitter"]))
+					}
+
+					if ("particleEmitters" in read) {
+						it.particleEmitters.addAll(emitters.fromJsonTree(read["particleEmitters"]))
+					}
+				}
+
+				orientations.add(future::get)
 			}
 
 			return ObjectDefinition(
@@ -256,7 +281,7 @@ data class ObjectDefinition(
 				smashOnBreak = smashOnBreak,
 				damageConfig = damageConfig,
 				flickerPeriod = flickerPeriod,
-				orientations = orientations,
+				orientations = orientations.build(),
 			)
 		}
 	}
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 18482b08..356d1d43 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt
@@ -13,6 +13,7 @@ import com.google.gson.annotations.JsonAdapter
 import com.google.gson.reflect.TypeToken
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
+import kotlinx.coroutines.future.await
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kommons.gson.clear
 import ru.dbotthepony.kstarbound.math.AABB
@@ -40,9 +41,9 @@ import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
 import ru.dbotthepony.kstarbound.util.AssetPathStack
 import ru.dbotthepony.kstarbound.world.Direction
 import ru.dbotthepony.kstarbound.world.World
+import java.util.concurrent.CompletableFuture
 import kotlin.math.PI
 
-@JsonAdapter(ObjectOrientation.Adapter::class)
 data class ObjectOrientation(
 	val json: JsonObject,
 	val flipImages: Boolean = false,
@@ -142,32 +143,18 @@ data class ObjectOrientation(
 		}
 	}
 
-	class Adapter(gson: Gson) : TypeAdapter<ObjectOrientation>() {
+	class Adapter(gson: Gson) {
 		private val vectors = gson.getAdapter(Vector2f::class.java)
 		private val vectorsi = gson.getAdapter(Vector2i::class.java)
 		private val vectorsd = gson.getAdapter(Vector2d::class.java)
 		private val drawables = gson.getAdapter(Drawable::class.java)
 		private val aabbs = gson.getAdapter(AABB::class.java)
-		private val objectRefs = gson.getAdapter(JsonReference.Object::class.java)
 		private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
 		private val emitters = gson.listAdapter<ParticleEmissionEntry>()
 		private val spaces = gson.setAdapter<Vector2i>()
 		private val materialSpaces = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, Vector2i::class.java, String::class.java).type)) as TypeAdapter<ImmutableList<Pair<Vector2i, String>>>
 
-		override fun write(out: JsonWriter, value: ObjectOrientation?) {
-			if (value == null) {
-				out.nullValue()
-				return
-			} else {
-				TODO()
-			}
-		}
-
-		override fun read(`in`: JsonReader): ObjectOrientation? {
-			if (`in`.consumeNull())
-				return null
-
-			val obj = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
+		suspend fun read(obj: JsonObject): ObjectOrientation {
 			val drawables = ArrayList<Drawable>()
 			val flipImages = obj.get("flipImages", false)
 			val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object"))
@@ -201,24 +188,20 @@ data class ObjectOrientation(
 			if ("spaceScan" in obj) {
 				occupySpaces = ImmutableSet.of()
 
-				try {
-					for (drawable in drawables) {
-						if (drawable is Drawable.Image) {
-							val bound = drawable.path.with { "default" }
-							val sprite = bound.sprite
+				for (drawable in drawables) {
+					if (drawable is Drawable.Image) {
+						val bound = drawable.path.with { "default" }
+						val sprite = bound.sprite
 
-							if (sprite != null) {
-								val new = ImmutableSet.Builder<Vector2i>()
-								new.addAll(occupySpaces)
-								new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages))
-								occupySpaces = new.build()
-							} else {
-								LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound")
-							}
+						if (sprite != null) {
+							val new = ImmutableSet.Builder<Vector2i>()
+							new.addAll(occupySpaces)
+							new.addAll(sprite.worldSpacesAsync(imagePositionI, obj["spaceScan"].asDouble, flipImages).await())
+							occupySpaces = new.build()
+						} else {
+							LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound")
 						}
 					}
-				} catch (err: Throwable) {
-					throw JsonSyntaxException("Unable to space scan image", err)
 				}
 			}
 
@@ -288,7 +271,7 @@ data class ObjectOrientation(
 			val lightPosition = obj["lightPosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO
 			val beamAngle = obj.get("beamAngle", 0.0) / 180.0 * PI
 			val statusEffectArea = obj["statusEffectArea"]?.let { vectorsd.fromJsonTree(it) }
-			val touchDamage = Starbound.loadJsonAsset(obj["touchDamage"] ?: JsonNull.INSTANCE, AssetPathStack.last())
+			val touchDamage = Starbound.loadJsonAsset(obj["touchDamage"] ?: JsonNull.INSTANCE, AssetPathStack.last()).await()
 
 			val emitters = ArrayList<ParticleEmissionEntry>()
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt
index 1c4ecd79..92cc6744 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt
@@ -27,7 +27,7 @@ data class TileDefinition(
 	val category: String,
 
 	@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
-	val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals::tileDamage),
+	val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals.onLoadedFuture.thenApply { Globals.tileDamage }),
 
 	val health: Double? = null,
 	val requiredHarvestLevel: Int? = null,
@@ -62,7 +62,7 @@ data class TileDefinition(
 	}
 
 	val actualDamageTable: TileDamageParameters by lazy {
-		val dmg = damageTable.value ?: TileDamageParameters.EMPTY
+		val dmg = damageTable.value.get() ?: TileDamageParameters.EMPTY
 
 		return@lazy if (health == null && requiredHarvestLevel == null) {
 			dmg
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt
index 766a2d37..88355d09 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt
@@ -25,7 +25,7 @@ data class TileModifierDefinition(
 	val miningSounds: ImmutableList<String> = ImmutableList.of(),
 
 	@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
-	val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals::tileDamage),
+	val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals.onLoadedFuture.thenApply { Globals.tileDamage }),
 
 	@JsonFlat
 	val descriptionData: ThingDescription,
@@ -43,7 +43,7 @@ data class TileModifierDefinition(
 	}
 
 	val actualDamageTable: TileDamageParameters by lazy {
-		val dmg = damageTable.value ?: TileDamageParameters.EMPTY
+		val dmg = damageTable.value.get() ?: TileDamageParameters.EMPTY
 
 		return@lazy if (health == null && requiredHarvestLevel == null) {
 			dmg
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt
index 85bf90c2..c7ef6ccf 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomeDefinition.kt
@@ -74,7 +74,7 @@ data class BiomeDefinition(
 			surfacePlaceables = surfacePlaceables,
 			undergroundPlaceables = undergroundPlaceables,
 
-			parallax = parallax?.value?.create(random, verticalMidPoint.toDouble(), hueShift, surfacePlaceables.firstTreeVariant()),
+			parallax = parallax?.value?.get()?.create(random, verticalMidPoint.toDouble(), hueShift, surfacePlaceables.firstTreeVariant()),
 
 			ores = (ores?.value?.evaluate(threatLevel, oresAdapter)?.map {
 				it.stream()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt
index 1eaa42d1..62f1d66a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt
@@ -45,6 +45,7 @@ import ru.dbotthepony.kstarbound.util.random.staticRandomInt
 import ru.dbotthepony.kstarbound.world.Direction
 import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity
 import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
+import java.util.concurrent.CompletableFuture
 import java.util.random.RandomGenerator
 import java.util.stream.Stream
 
@@ -58,10 +59,9 @@ data class BiomePlaceables(
 ) {
 	fun firstTreeVariant(): TreeVariant? {
 		return itemDistributions.stream()
-			.flatMap { it.data.itemStream() }
-			.map { it as? Tree }
-			.filterNotNull()
-			.flatMap { it.trees.stream() }
+			.flatMap { it.data.get().itemStream() }
+			.filter { it is Tree }
+			.flatMap { (it as Tree).trees.stream() }
 			.findAny()
 			.orElse(null)
 	}
@@ -73,10 +73,10 @@ data class BiomePlaceables(
 		val variants: Int = 1,
 		val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR,
 		@JsonFlat
-		val data: DistributionData,
+		val data: CompletableFuture<DistributionData>,
 	) {
 		fun itemToPlace(x: Int, y: Int): Placement? {
-			return data.itemToPlace(x, y, priority)
+			return data.get().itemToPlace(x, y, priority)
 		}
 	}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt
index 9ae82a0a..5c2574af 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt
@@ -46,16 +46,12 @@ data class BiomePlaceablesDefinition(
 		@JsonFlat
 		val data: DistributionItemData,
 	) {
-		init {
-			checkNotNull(distribution.value) { "Distribution data is missing" }
-		}
-
 		fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionItem {
 			return BiomePlaceables.DistributionItem(
 				priority = priority,
 				variants = variants,
 				mode = mode,
-				data = distribution.value!!.create(this, biome),
+				data = distribution.value.thenApply { (it ?: throw NullPointerException("Distribution data is missing")).create(this, biome) },
 			)
 		}
 	}
@@ -374,4 +370,4 @@ data class BiomePlaceablesDefinition(
 				.collect(ImmutableList.toImmutableList())
 		)
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt
index 6889bf2b..f1a6c0a0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt
@@ -64,7 +64,7 @@ class BushVariant(
 				ceiling = data.value.ceiling,
 				descriptions = data.value.descriptions.fixDescription("${data.key} with $modName").toMap(),
 				ephemeral = data.value.ephemeral,
-				tileDamageParameters = (data.value.damageTable?.value ?: Globals.bushDamage).copy(totalHealth = data.value.health),
+				tileDamageParameters = (data.value.damageTable?.value?.get() ?: Globals.bushDamage).copy(totalHealth = data.value.health),
 				modName = modName,
 				shapes = data.value.shapes.stream().map { Shape(it.base, it.mods[modName] ?: ImmutableList.of()) }.collect(ImmutableList.toImmutableList())
 			)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt
index 764a9b9a..c4d8eeae 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt
@@ -53,7 +53,7 @@ data class GrassVariant(
 				ephemeral = data.value.ephemeral,
 				hueShift = hueShift,
 				descriptions = data.value.descriptions.fixDescription(data.value.name).toMap(),
-				tileDamageParameters = (data.value.damageTable?.value ?: Globals.grassDamage).copy(totalHealth = data.value.health)
+				tileDamageParameters = (data.value.damageTable?.value?.get() ?: Globals.grassDamage).copy(totalHealth = data.value.health)
 			)
 		}
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt
index 946e7adc..06d75530 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt
@@ -587,7 +587,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
 			parameters.beamUpRule = params.beamUpRule
 			parameters.disableDeathDrops = params.disableDeathDrops
 			parameters.worldEdgeForceRegions = params.worldEdgeForceRegions
-			parameters.weatherPool = primaryBiome.value.weather.stream().binnedChoice(threadLevel).get().random(random).value ?: throw NullPointerException("No weather pool")
+			parameters.weatherPool = primaryBiome.value.weather.stream().binnedChoice(threadLevel).get().random(random).value.get() ?: throw NullPointerException("No weather pool")
 			parameters.primaryBiome = primaryBiome.key
 			parameters.sizeName = sizeName
 			parameters.hueShift = primaryBiome.value.hueShift(random)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt
index 175397ab..d34af729 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt
@@ -80,7 +80,7 @@ data class TreeVariant(
 				stemDropConfig = data.value.dropConfig.deepCopy(),
 				descriptions = data.value.descriptions.fixDescription(data.key).toJsonObject(),
 				ephemeral = data.value.ephemeral,
-				tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health),
+				tileDamageParameters = (data.value.damageTable?.value?.get() ?: Globals.treeDamage).copy(totalHealth = data.value.health),
 
 				foliageSettings = JsonObject(),
 				foliageDropConfig = JsonObject(),
@@ -107,7 +107,7 @@ data class TreeVariant(
 				stemDropConfig = data.value.dropConfig.deepCopy(),
 				descriptions = data.value.descriptions.fixDescription("${data.key} with ${fdata.key}").toJsonObject(),
 				ephemeral = data.value.ephemeral,
-				tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health),
+				tileDamageParameters = (data.value.damageTable?.value?.get() ?: Globals.treeDamage).copy(totalHealth = data.value.health),
 
 				foliageSettings = fdata.json.asJsonObject.deepCopy(),
 				foliageDropConfig = fdata.value.dropConfig.deepCopy(),
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt
index 701a5ecc..4ed7a454 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt
@@ -7,9 +7,12 @@ import ru.dbotthepony.kommons.io.readBinaryString
 import ru.dbotthepony.kommons.io.readVarInt
 import ru.dbotthepony.kommons.io.readVarLong
 import ru.dbotthepony.kstarbound.IStarboundFile
+import ru.dbotthepony.kstarbound.Starbound
 import ru.dbotthepony.kstarbound.getValue
 import ru.dbotthepony.kstarbound.json.readJsonObject
+import ru.dbotthepony.kstarbound.util.CarriedExecutor
 import ru.dbotthepony.kstarbound.util.sbIntern
+import ru.dbotthepony.kstarbound.util.supplyAsync
 import java.io.BufferedInputStream
 import java.io.Closeable
 import java.io.DataInputStream
@@ -19,6 +22,11 @@ import java.io.InputStream
 import java.io.RandomAccessFile
 import java.nio.channels.Channels
 import java.util.*
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.locks.ReentrantLock
+import java.util.function.Supplier
+import kotlin.concurrent.withLock
 
 private fun readHeader(reader: RandomAccessFile, required: Int) {
 	val read = reader.read()
@@ -80,6 +88,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
 			throw IllegalStateException("${computeFullPath()} is a directory")
 		}
 
+		override fun asyncRead(): CompletableFuture<ByteArray> {
+			throw IllegalStateException("${computeFullPath()} is a directory")
+		}
+
 		override fun toString(): String {
 			return "SBDirectory[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]"
 		}
@@ -113,6 +125,18 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
 			return hash
 		}
 
+		override fun asyncRead(): CompletableFuture<ByteArray> {
+			if (length > Int.MAX_VALUE.toLong())
+				throw RuntimeException("File is too big to be read in async way: $length bytes to read!")
+
+			return scheduler.schedule(offset, Supplier {
+				reader.seek(offset)
+				val bytes = ByteArray(length.toInt())
+				reader.readFully(bytes)
+				bytes
+			})
+		}
+
 		override fun open(): InputStream {
 			return object : InputStream() {
 				private var innerOffset = 0L
@@ -171,6 +195,57 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
 		}
 	}
 
+	private fun interface ReadScheduler {
+		fun schedule(offset: Long, action: Supplier<ByteArray>): CompletableFuture<ByteArray>
+	}
+
+	// SSDs
+	private object DirectScheduler : ReadScheduler {
+		override fun schedule(offset: Long, action: Supplier<ByteArray>): CompletableFuture<ByteArray> {
+			return Starbound.IO_EXECUTOR.supplyAsync(action)
+		}
+	}
+
+	// HDDs
+	private class PriorityScheduler : ReadScheduler {
+		private val counter = AtomicInteger()
+
+		private data class Action(val offset: Long, val action: Supplier<ByteArray>, val id: Int, val future: CompletableFuture<ByteArray>) : Runnable, Comparable<Action> {
+			override fun compareTo(other: Action): Int {
+				var cmp = offset.compareTo(other.offset) // read files closer to beginning first
+				if (cmp == 0) cmp = id.compareTo(other.id) // else fulfil requests as FIFO
+				return cmp
+			}
+
+			override fun run() {
+				future.complete(action.get())
+			}
+		}
+
+		private val lock = ReentrantLock()
+		private val queue = PriorityQueue<Action>()
+		private val carrier = CarriedExecutor(Starbound.IO_EXECUTOR)
+
+		override fun schedule(offset: Long, action: Supplier<ByteArray>): CompletableFuture<ByteArray> {
+			val future = CompletableFuture<ByteArray>()
+			val task = Action(offset, action, counter.getAndIncrement(), future)
+
+			lock.withLock {
+				queue.add(task)
+			}
+
+			carrier.execute {
+				lock.withLock { queue.remove() }.run()
+			}
+
+			return future
+		}
+	}
+
+	// TODO: we need to determine whenever we are on SSD, or on HDD.
+	//  if we are on SSD, assuming it is an HDD won't hurt performance much
+	private val scheduler: ReadScheduler = PriorityScheduler()
+
 	private val reader by object : ThreadLocal<RandomAccessFile>() {
 		override fun initialValue(): RandomAccessFile {
 			return RandomAccessFile(path, "r")
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt
index 0ec6b13e..588a6dd7 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt
@@ -32,7 +32,7 @@ class ActiveItemStack(entry: ItemRegistry.Entry, config: JsonObject, parameters:
 	val animator: Animator
 
 	init {
-		var animationConfig = Starbound.loadJsonAsset(lookupProperty("animation"), entry.directory) ?: JsonNull.INSTANCE
+		var animationConfig = Starbound.loadJsonAsset(lookupProperty("animation"), entry.directory).get() ?: JsonNull.INSTANCE
 		val animationCustom = lookupProperty("animationCustom")
 
 		if (!animationCustom.isJsonNull) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt
index 98b6740e..e9fe879a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt
@@ -4,6 +4,10 @@ import com.google.common.collect.ImmutableSet
 import com.google.gson.JsonObject
 import com.google.gson.JsonPrimitive
 import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
+import kotlinx.coroutines.async
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.future.await
+import kotlinx.coroutines.launch
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kommons.gson.contains
 import ru.dbotthepony.kommons.gson.get
@@ -140,9 +144,9 @@ object ItemRegistry {
 			val files = fileTree[type.extension ?: continue] ?: continue
 
 			for (file in files) {
-				futures.add(Starbound.EXECUTOR.submit {
+				futures.add(Starbound.GLOBAL_SCOPE.launch {
 					try {
-						val read = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(file.jsonReader()), patches[file.computeFullPath()]).asJsonObject
+						val read = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(file.asyncJsonReader().await()), patches[file.computeFullPath()]).asJsonObject
 						val readData = data.fromJsonTree(read)
 
 						tasks.add {
@@ -164,7 +168,7 @@ object ItemRegistry {
 					} catch (err: Throwable) {
 						LOGGER.error("Reading item definition $file", err)
 					}
-				})
+				}.asCompletableFuture())
 			}
 		}
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt
index d8b4bf9d..5c724ab0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt
@@ -5,6 +5,7 @@ import com.google.gson.JsonElement
 import com.google.gson.JsonNull
 import com.google.gson.JsonObject
 import com.google.gson.JsonSyntaxException
+import kotlinx.coroutines.future.await
 import org.apache.logging.log4j.LogManager
 import ru.dbotthepony.kommons.gson.get
 import ru.dbotthepony.kstarbound.IStarboundFile
@@ -146,5 +147,23 @@ enum class JsonPatch(val key: String) {
 
 			return base
 		}
+
+		@Suppress("NAME_SHADOWING")
+		suspend fun applyAsync(base: JsonElement, source: Collection<IStarboundFile>?): JsonElement {
+			source ?: return base
+			var base = base
+
+			for (patch in source) {
+				val read = Starbound.ELEMENTS_ADAPTER.read(patch.asyncJsonReader().await())
+
+				if (read !is JsonArray) {
+					LOGGER.error("$patch root element is not an array")
+				} else {
+					base = apply(base, read, patch)
+				}
+			}
+
+			return base
+		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CompletableFutureAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CompletableFutureAdapter.kt
new file mode 100644
index 00000000..45f458ab
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CompletableFutureAdapter.kt
@@ -0,0 +1,32 @@
+package ru.dbotthepony.kstarbound.json.factory
+
+import com.google.gson.Gson
+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.JsonWriter
+import ru.dbotthepony.kommons.gson.consumeNull
+import java.lang.reflect.ParameterizedType
+import java.util.concurrent.CompletableFuture
+
+object CompletableFutureAdapter : TypeAdapterFactory {
+	override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
+		if (type.rawType == CompletableFuture::class.java) {
+			val type = type.type as? ParameterizedType ?: return null
+			val parent = gson.getAdapter(TypeToken.get(type.actualTypeArguments[0])) as TypeAdapter<Any?>
+
+			return object : TypeAdapter<CompletableFuture<Any?>>() {
+				override fun write(out: JsonWriter, value: CompletableFuture<Any?>?) {
+					parent.write(out, value?.get())
+				}
+
+				override fun read(`in`: JsonReader): CompletableFuture<Any?>? {
+					return CompletableFuture.completedFuture(parent.read(`in`))
+				}
+			} as TypeAdapter<T>
+		}
+
+		return null
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt
index bd1c3791..d75fd2bb 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt
@@ -348,7 +348,7 @@ fun provideRootBindings(lua: LuaEnvironment) {
 	lua.globals["root"] = table
 
 	table["assetJson"] = luaFunction { path: ByteString ->
-		returnBuffer.setTo(from(Starbound.loadJsonAsset(path.decode())))
+		returnBuffer.setTo(from(Starbound.loadJsonAsset(path.decode()).get()))
 	}
 
 	table["makeCurrentVersionedJson"] = luaStub("makeCurrentVersionedJson")
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt
index c402b68e..e5eb86ae 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt
@@ -38,13 +38,11 @@ import ru.dbotthepony.kstarbound.util.random.random
 import ru.dbotthepony.kstarbound.util.toStarboundString
 import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
 import java.io.File
-import java.lang.ref.Cleaner
 import java.sql.DriverManager
 import java.util.UUID
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.locks.ReentrantLock
-import kotlin.math.min
 
 sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
 	private fun makedir(file: File) {
@@ -65,7 +63,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
 	private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
 	val universe = ServerUniverse(universeFolder)
 	val chat = ChatHandler(this)
-	val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
+	val globalScope = CoroutineScope(Starbound.COROUTINES + SupervisorJob())
 
 	private val database = DriverManager.getConnection("jdbc:sqlite:${File(universeFolder, "universe.db").absolutePath.replace('\\', '/')}")
 	private val databaseCleanable = Starbound.CLEANER.register(this, database::close)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt
index 8245f23c..c90dd528 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt
@@ -88,6 +88,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
 	val coroutines = asCoroutineDispatcher()
 	val scope = CoroutineScope(coroutines + SupervisorJob())
 
+	init {
+		priority = 7
+	}
+
 	private fun nextDeadline(): Long {
 		if (isShutdown || eventQueue.isNotEmpty())
 			return 0L
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
index a42b920a..13660473 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
@@ -406,10 +406,10 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 	}
 
 	fun anyCellSatisfies(x: Int, y: Int, distance: Int, predicate: CellPredicate): Boolean {
-		for (tx in x - distance .. x + distance) {
-			for (ty in y - distance .. y + distance) {
+		for (tx in (x - distance) .. (x + distance)) {
+			for (ty in (y - distance) .. (y + distance)) {
 				val ix = geometry.x.cell(tx)
-				val iy = geometry.x.cell(ty)
+				val iy = geometry.y.cell(ty)
 
 				if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
 					return true
@@ -587,8 +587,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		 * of world generation, everything else is kinda okay to be performed
 		 * on main world thread, so concurrent access is not needed for now.
 		 *
-		 * ArrayChunkMap does ~not need~ needs synchronization too, unless we use CopyOnWriteArrayList
-		 * for "existing" chunks list.
+		 * ArrayChunkMap does ~not~ need synchronization, because 2D array is thread-safe to be read
+		 * by multiple thread while one is writing to it (but it might leave to race condition if
+		 * we try to read chunks which are currently being initialized).
 		 */
 		private const val CONCURRENT_SPARSE_CHUNK_MAP = false
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt
index fb3c88d8..02934275 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt
@@ -986,7 +986,7 @@ class Animator() {
 	companion object {
 		// lame
 		fun load(path: String): Animator {
-			val json = Starbound.loadJsonAsset(path)
+			val json = Starbound.loadJsonAsset(path).get()
 
 			if (json == null) {
 				if (missing.add(path)) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt
new file mode 100644
index 00000000..8cdc21ae
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt
@@ -0,0 +1,23 @@
+package ru.dbotthepony.kstarbound.world.entities
+
+import ru.dbotthepony.kstarbound.defs.EntityType
+import ru.dbotthepony.kstarbound.math.AABB
+import java.io.DataOutputStream
+
+class ProjectileEntity() : DynamicEntity() {
+	override val type: EntityType
+		get() = EntityType.PROJECTILE
+
+	override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
+		TODO("Not yet implemented")
+	}
+
+	override val metaBoundingBox: AABB
+		get() = TODO("Not yet implemented")
+	override val movement: MovementController
+		get() = TODO("Not yet implemented")
+
+	private fun setup() {
+
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt
index 08ce5c52..fcc66b1c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt
@@ -76,7 +76,7 @@ class PlayerEntity() : HumanoidActorEntity() {
 
 	val inventory = PlayerInventory()
 	val songbook = Songbook(this)
-	val effectAnimator = if (Globals.player.effectsAnimator.value == null) Animator() else Animator(Globals.player.effectsAnimator.value!!)
+	val effectAnimator = if (Globals.player.effectsAnimator.value.get() == null) Animator() else Animator(Globals.player.effectsAnimator.value.get()!!)
 	override val statusController = StatusController(this, Globals.player.statusControllerSettings)
 	val techController = TechController(this)
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt
index e82e1dae..386731cc 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt
@@ -174,7 +174,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
 	}
 
 	val orientation: ObjectOrientation? get() {
-		return config.value.orientations.getOrNull(orientationIndex.toInt())
+		return config.value.orientations.getOrNull(orientationIndex.toInt())?.get()
 	}
 
 	protected val mergedJson = ManualLazy {
@@ -328,7 +328,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
 		val orientation = orientation
 
 		if (orientation != null) {
-			val touchDamageConfig = mergeJson(config.value.touchDamage.deepCopy(), orientation.touchDamage)
+			val touchDamageConfig = mergeJson(config.value.touchDamage.get().deepCopy(), orientation.touchDamage)
 
 			if (!touchDamageConfig.isJsonNull) {
 				sources.add(Starbound.gson.fromJson(touchDamageConfig, DamageSource::class.java).copy(sourceEntityId = entityID, team = team.get()))
@@ -352,11 +352,11 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
 	val animator: Animator
 
 	init {
-		if (config.value.animation?.value != null) {
-			if (config.value.animationCustom.size() > 0 && config.value.animation!!.json != null) {
-				animator = Animator(Starbound.gson.fromJson(mergeJson(config.value.animation!!.json!!, config.value.animationCustom), AnimationDefinition::class.java))
+		if (config.value.animation?.value?.get() != null) {
+			if (config.value.animationCustom.size() > 0 && config.value.animation!!.json.get() != null) {
+				animator = Animator(Starbound.gson.fromJson(mergeJson(config.value.animation!!.json.get()!!.deepCopy(), config.value.animationCustom), AnimationDefinition::class.java))
 			} else {
-				animator = Animator(config.value.animation!!.value!!)
+				animator = Animator(config.value.animation!!.value.get()!!)
 			}
 		} else {
 			animator = Animator()
@@ -566,7 +566,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
 		flickerPeriod?.update(delta, world.random)
 
 		if (!isRemote) {
-			tileHealth.tick(config.value.damageConfig, delta)
+			tileHealth.tick(config.value.damageConfig.get(), delta)
 
 			if (tileHealth.isHealthy) {
 				lastClosestSpaceToDamageSource = null
@@ -744,7 +744,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
 		if (unbreakable)
 			return false
 
-		tileHealth.damage(config.value.damageConfig, source, damage)
+		tileHealth.damage(config.value.damageConfig.get(), source, damage)
 
 		if (damageSpaces.isNotEmpty()) {
 			lastClosestSpaceToDamageSource = damageSpaces.minBy { it.toDoubleVector().distanceSquared(source) }