Sequential or parallel disk access now handled properly

This commit is contained in:
DBotThePony 2024-04-26 18:52:45 +07:00
parent 999f3a8d4f
commit ac55422c3b
Signed by: DBot
GPG Key ID: DCC23B5715498507
37 changed files with 637 additions and 290 deletions

View File

@ -4,6 +4,9 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.TypeAdapter 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfig import ru.dbotthepony.kstarbound.defs.ClientConfig
@ -135,32 +138,31 @@ object Globals {
words.build() words.build()
} }
private fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: Lazy<TypeAdapter<T>>): Future<*> { val onLoadedFuture = CompletableFuture<Unit>()
val file = Starbound.loadJsonAsset(path)
private suspend fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: Lazy<TypeAdapter<T>>) {
val file = Starbound.loadJsonAsset(path).await()
if (file == null) { if (file == null) {
LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!") LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!")
return CompletableFuture.completedFuture(Unit)
} else { } else {
return Starbound.EXECUTOR.submit { try {
try { AssetPathStack("/") {
AssetPathStack("/") { accept.set(adapter.value.fromJsonTree(file))
accept.set(adapter.value.fromJsonTree(file))
}
} catch (err: Throwable) {
LOGGER.fatal("Error while reading $path, expect bad things to happen!", err)
throw err
} }
} 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<*> { private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>): CompletableFuture<*> {
return load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) }) return Starbound.GLOBAL_SCOPE.launch { load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) }) }.asCompletableFuture()
} }
fun load(): List<Future<*>> { fun load(): List<Future<*>> {
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<CompletableFuture<*>>()
tasks.add(load("/default_actor_movement.config", ::actorMovementParameters)) tasks.add(load("/default_actor_movement.config", ::actorMovementParameters))
tasks.add(load("/default_movement.config", ::movementParameters)) tasks.add(load("/default_movement.config", ::movementParameters))
@ -184,12 +186,18 @@ object Globals {
tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage))
tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, lazy { Starbound.gson.mapAdapter() })) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
tasks.add(load("/currencies.config", ::currencies, lazy { Starbound.gson.mapAdapter() })) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
tasks.add(load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() })) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() }) }.asCompletableFuture())
tasks.add(load("/instance_worlds.config", ::instanceWorlds, lazy { Starbound.gson.mapAdapter() })) 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 return tasks
} }

View File

@ -7,6 +7,10 @@ import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.AssetReference 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.animation.ParticleConfig
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition 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.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.RenderParameters import ru.dbotthepony.kstarbound.defs.tile.RenderParameters
@ -100,7 +104,7 @@ object Registries {
val futures = ArrayList<CompletableFuture<Boolean>>() val futures = ArrayList<CompletableFuture<Boolean>>()
for (registry in registriesInternal) 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() } } 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) } val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
return files.map { listedFile -> return files.map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.GLOBAL_SCOPE.launch {
try { try {
val elem = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()])
AssetPathStack(listedFile.computeDirectory()) { AssetPathStack(listedFile.computeDirectory()) {
val elem = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()])
val read = adapter.fromJsonTree(elem) val read = adapter.fromJsonTree(elem)
val keys = keyProvider(read) val keys = keyProvider(read)
@ -137,7 +142,7 @@ object Registries {
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Loading ${registry.name} definition file $listedFile", err) 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) } val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
return files.map { listedFile -> return files.map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.GLOBAL_SCOPE.launch {
try { 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()) { for ((k, v) in json.entrySet()) {
try { try {
@ -206,13 +211,13 @@ object Registries {
} catch (err: Exception) { } catch (err: Exception) {
LOGGER.error("Loading ${registry.name} definition $listedFile", err) 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 { 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 name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val factory = TerrainSelectorType.factory(json, false, type) val factory = TerrainSelectorType.factory(json, false, type)
@ -228,17 +233,17 @@ object Registries {
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.addAll((files["terrain"] ?: listOf()).map { listedFile -> tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.GLOBAL_SCOPE.async {
loadTerrainSelector(listedFile, null, patches) loadTerrainSelector(listedFile, null, patches)
} }.asCompletableFuture()
}) })
// legacy files // legacy files
for (type in TerrainSelectorType.entries) { for (type in TerrainSelectorType.entries) {
tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile -> tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit { Starbound.GLOBAL_SCOPE.async {
loadTerrainSelector(listedFile, type, patches) loadTerrainSelector(listedFile, type, patches)
} }.asCompletableFuture()
}) })
} }
@ -256,8 +261,8 @@ object Registries {
) )
private fun loadMetaMaterials(): Future<*> { private fun loadMetaMaterials(): Future<*> {
return Starbound.EXECUTOR.submit { return Starbound.GLOBAL_SCOPE.async {
val read = Starbound.loadJsonAsset("/metamaterials.config") ?: return@submit val read = Starbound.loadJsonAsset("/metamaterials.config").await() ?: return@async
val read2 = Starbound.gson.getAdapter(object : TypeToken<ImmutableList<MetaMaterialDef>>() {}).fromJsonTree(read) val read2 = Starbound.gson.getAdapter(object : TypeToken<ImmutableList<MetaMaterialDef>>() {}).fromJsonTree(read)
for (def in read2) { for (def in read2) {
@ -282,6 +287,6 @@ object Registries {
)) ))
} }
} }
} }.asCompletableFuture()
} }
} }

View File

@ -1,14 +1,20 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.AsyncCacheLoader
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.github.benmanes.caffeine.cache.Scheduler import com.github.benmanes.caffeine.cache.Scheduler
import com.google.common.base.Predicate
import com.google.gson.* import com.google.gson.*
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.objects.ObjectArraySet 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.Runnable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher 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.apache.logging.log4j.LogManager
import org.classdump.luna.compiler.CompilerChunkLoader import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings 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.JsonPatch
import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.factory.CompletableFutureAdapter
import ru.dbotthepony.kstarbound.math.AABBiTypeAdapter import ru.dbotthepony.kstarbound.math.AABBiTypeAdapter
import ru.dbotthepony.kstarbound.math.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.math.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.math.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.math.Vector2fTypeAdapter
@ -80,12 +87,13 @@ import java.io.*
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.text.DateFormat import java.text.DateFormat
import java.time.Duration import java.time.Duration
import java.util.Collections
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool 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.Future
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
@ -96,6 +104,8 @@ import java.util.concurrent.locks.LockSupport
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.NoSuchElementException import kotlin.NoSuchElementException
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.max
import kotlin.math.min
object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator { object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator {
const val ENGINE_VERSION = "0.0.1" const val ENGINE_VERSION = "0.0.1"
@ -136,25 +146,44 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
start() start()
} }
private val ioPoolCounter = AtomicInteger() private fun makeExecutor(parallelism: Int, threadNameString: String, threadPriority: Int): ForkJoinPool {
val counter = AtomicInteger()
@JvmField val factory = ForkJoinWorkerThreadFactory {
val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory { object : ForkJoinWorkerThread(it) {
val thread = Thread(it, "IO Worker ${ioPoolCounter.getAndIncrement()}") init {
thread.isDaemon = true name = threadNameString.format(counter.getAndIncrement())
thread.priority = Thread.MIN_PRIORITY priority = threadPriority
}
thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e -> }
LOGGER.error("I/O thread died due to uncaught exception", e)
} }
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 @JvmField
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool() val IO_EXECUTOR: ExecutorService = makeExecutor(8, "Disk IO %d", MIN_PRIORITY)
@JvmField @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 @JvmField
val CLEANER: Cleaner = Cleaner.create { val CLEANER: Cleaner = Cleaner.create {
@ -377,6 +406,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER) registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)
registerTypeAdapter(WorldLayout.Companion) registerTypeAdapter(WorldLayout.Companion)
registerTypeAdapterFactory(CompletableFutureAdapter)
Registries.registerAdapters(this) Registries.registerAdapters(this)
registerTypeAdapter(LongRangeAdapter) registerTypeAdapter(LongRangeAdapter)
@ -399,14 +430,33 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
var loaded = 0 var loaded = 0
private set 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() private val jsonAssetsCache = Caffeine.newBuilder()
.maximumSize(4096L) .maximumSize(4096L)
.expireAfterAccess(Duration.ofMinutes(5L)) .expireAfterAccess(Duration.ofMinutes(5L))
.scheduler(this) .scheduler(this)
.executor(EXECUTOR) .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 filename: String
val jsonPath: String? val jsonPath: String?
@ -418,27 +468,27 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
jsonPath = null jsonPath = null
} }
val json = jsonAssetsCache.get(filename) { return jsonAssetsCache.get(filename).thenApply { json ->
val file = locate(it) if (jsonPath == null || json.isEmpty)
json.orNull()
if (!file.isFile) else
return@get KOptional() JsonPath.query(jsonPath).get(json.value)
}
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)
} }
fun loadJsonAsset(path: JsonElement, relative: String): JsonElement { fun loadJsonAsset(path: JsonElement, relative: String): CompletableFuture<JsonElement> {
if (path is JsonPrimitive) { 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 { } 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!") throw FileNotFoundException("/hobo.ttf is not a file!")
else { else {
val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf") val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf")
tempPath.writeBytes(file.read().array()) tempPath.writeBytes(file.read())
Starbound.fontPath = tempPath Starbound.fontPath = tempPath
return@supplyAsync tempPath return@supplyAsync tempPath
} }

View File

@ -1,8 +1,10 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import ru.dbotthepony.kstarbound.io.StarboundPak import ru.dbotthepony.kstarbound.io.StarboundPak
import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.util.supplyAsync
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -10,42 +12,12 @@ import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.io.Reader import java.io.Reader
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.concurrent.CompletableFuture
import java.util.stream.Stream import java.util.stream.Stream
fun interface ISBFileLocator { fun interface ISBFileLocator {
fun locate(path: String): IStarboundFile fun locate(path: String): IStarboundFile
fun exists(path: String): Boolean = locate(path).exists 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 IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist * @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 stream = open()
val read = stream.readAllBytes() val read = stream.readAllBytes()
stream.close() stream.close()
return ByteBuffer.wrap(read) return read
} }
/** /**
@ -200,16 +195,9 @@ interface IStarboundFile : ISBFileLocator {
*/ */
fun readDirect(): ByteBuffer { fun readDirect(): ByteBuffer {
val read = read() val read = read()
val buf = ByteBuffer.allocateDirect(read.capacity()) val buf = ByteBuffer.allocateDirect(read.size)
buf.put(read)
read.position(0)
for (i in 0 until read.capacity()) {
buf.put(read[i])
}
buf.position(0) buf.position(0)
return buf return buf
} }
@ -230,6 +218,10 @@ interface IStarboundFile : ISBFileLocator {
override val name: String override val name: String
get() = "" get() = ""
override fun asyncRead(): CompletableFuture<ByteArray> {
throw FileNotFoundException()
}
override fun open(): InputStream { override fun open(): InputStream {
throw FileNotFoundException() throw FileNotFoundException()
} }
@ -251,6 +243,10 @@ class NonExistingFile(
override val exists: Boolean override val exists: Boolean
get() = false get() = false
override fun asyncRead(): CompletableFuture<ByteArray> {
throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
}
override fun open(): InputStream { override fun open(): InputStream {
throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist") 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()) return BufferedInputStream(real.inputStream())
} }
override fun asyncRead(): CompletableFuture<ByteArray> {
return Starbound.IO_EXECUTOR.supplyAsync { real.readBytes() }
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return other is IStarboundFile && computeFullPath() == other.computeFullPath() return other is IStarboundFile && computeFullPath() == other.computeFullPath()
} }

View File

@ -1,6 +1,11 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import com.google.common.collect.ImmutableMap 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.world.api.AbstractTileState import ru.dbotthepony.kstarbound.world.api.AbstractTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
@ -44,6 +49,7 @@ enum class RenderLayer {
return base 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> { 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 { override fun compareTo(other: Point): Int {
if (this === other) return 0 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 { companion object {
fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L, index: Long = 0L, hueShift: Float = 0f, colorVariant: TileColor = TileColor.DEFAULT): Point { fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L, index: Long = 0L, hueShift: Float = 0f, colorVariant: TileColor = TileColor.DEFAULT): Point {
if (isBackground && isModifier) { if (isBackground && isModifier) {

View File

@ -235,7 +235,7 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
// если у нас нет renderTemplate // если у нас нет renderTemplate
// то мы просто не можем его отрисовать // то мы просто не можем его отрисовать
val template = def.renderTemplate.value ?: return val template = def.renderTemplate.value.get() ?: return
val vertexBuilder = meshBuilder val vertexBuilder = meshBuilder
.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!) .getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!)

View File

@ -18,41 +18,47 @@ import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.sbIntern
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
import kotlin.collections.HashMap import kotlin.collections.HashMap
class AssetReference<V> { class AssetReference<V> {
constructor(value: V?) { constructor(value: V?) {
lazy = lazy { value } this.value = CompletableFuture.completedFuture(value)
path = null path = null
fullPath = null fullPath = null
json = null this.json = CompletableFuture.completedFuture(null)
} }
constructor(value: () -> V?) { constructor(value: CompletableFuture<V?>) {
lazy = lazy { value() } this.value = value
path = null path = null
fullPath = null fullPath = null
json = null this.json = CompletableFuture.completedFuture(null)
} }
constructor(path: String?, fullPath: String?, value: V?, json: JsonElement?) { constructor(path: String?, fullPath: String?, value: V?, json: JsonElement?) {
this.path = path this.path = path
this.fullPath = fullPath 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 this.json = json
} }
val path: String? val path: String?
val fullPath: String? val fullPath: String?
val json: JsonElement? val json: CompletableFuture<JsonElement?>
val value: CompletableFuture<V?>
val value: V?
get() = lazy.value
private val lazy: Lazy<V?>
companion object : TypeAdapterFactory { companion object : TypeAdapterFactory {
private val LOGGER = LogManager.getLogger()
val EMPTY = AssetReference(null, null, null, null) val EMPTY = AssetReference(null, null, null, null)
fun <V> empty() = EMPTY as AssetReference<V> fun <V> empty() = EMPTY as AssetReference<V>
@ -62,7 +68,7 @@ class AssetReference<V> {
val param = type.type as? ParameterizedType ?: return null val param = type.type as? ParameterizedType ?: return null
return object : TypeAdapter<AssetReference<T>>() { 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 adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
private val strings = gson.getAdapter(String::class.java) private val strings = gson.getAdapter(String::class.java)
private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>()) private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
@ -81,34 +87,31 @@ class AssetReference<V> {
} else if (`in`.peek() == JsonToken.STRING) { } else if (`in`.peek() == JsonToken.STRING) {
val path = strings.read(`in`)!! val path = strings.read(`in`)!!
val fullPath = AssetPathStack.remap(path) val fullPath = AssetPathStack.remap(path)
val get = cache[fullPath]
if (get != null) val get = cache.computeIfAbsent(fullPath) {
return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second) val json = Starbound.loadJsonAsset(fullPath)
if (fullPath in missing) json.thenAccept {
return null 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) { value.exceptionally {
if (missing.add(fullPath)) LOGGER.error("Exception loading $fullPath", it)
logger.error("JSON asset does not exist: $fullPath") null
}
return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, null) value to json
} }
val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) { return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)
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)
} else { } else {
val json = Starbound.ELEMENTS_ADAPTER.read(`in`) val json = Starbound.ELEMENTS_ADAPTER.read(`in`)
val value = adapter.read(JsonTreeReader(json)) ?: return null val value = adapter.read(JsonTreeReader(json)) ?: return null

View File

@ -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");
}

View File

@ -62,7 +62,8 @@ sealed class JsonReference<E : JsonElement?>(val path: String?, val fullPath: St
if (`in`.peek() == JsonToken.STRING) { if (`in`.peek() == JsonToken.STRING) {
val path = `in`.nextString() val path = `in`.nextString()
val full = AssetPathStack.remapSafe(path) 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)) return factory(path, full, adapter.fromJsonTree(get))
} else { } else {
return factory(null, null, adapter.read(`in`)) 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) { if (`in`.peek() == JsonToken.STRING) {
val path = `in`.nextString() val path = `in`.nextString()
val full = AssetPathStack.remapSafe(path) 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")) return factory(path, full, adapter.fromJsonTree(get) ?: throw JsonSyntaxException("Json asset at $full is literal null, which is not allowed"))
} else { } else {
return factory(null, null, adapter.read(`in`)) return factory(null, null, adapter.read(`in`))

View File

@ -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
}

View File

@ -218,7 +218,7 @@ data class DungeonDefinition(
val anchor = validAnchors.random(world.random) val anchor = validAnchors.random(world.random)
return CoroutineScope(Starbound.COROUTINE_EXECUTOR) return CoroutineScope(Starbound.COROUTINES)
.async { .async {
if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) { if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) {
generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID) 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" } require(anchor in anchorParts) { "$anchor does not belong to $name" }
val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends) val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends)
return CoroutineScope(Starbound.COROUTINE_EXECUTOR) return CoroutineScope(Starbound.COROUTINES)
.async { .async {
generate0(anchor, dungeonWorld, x, y, forcePlacement, dungeonID) generate0(anchor, dungeonWorld, x, y, forcePlacement, dungeonID)

View File

@ -56,8 +56,9 @@ class TiledTileSet private constructor(
return DungeonTile.INTERNER.intern(DungeonTile(ImmutableList.copyOf(brushes), ImmutableList.copyOf(rules), 0, connector)) 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> { 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")) ?: return Either.right(NoSuchElementException("Tileset at $location does not exist"))
try { try {

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs.image 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.AsyncLoadingCache
import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
@ -48,7 +49,9 @@ import java.util.Collections
import java.util.Optional import java.util.Optional
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.function.BiFunction
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Function
class Image private constructor( class Image private constructor(
val source: IStarboundFile, val source: IStarboundFile,
@ -169,14 +172,24 @@ class Image private constructor(
return whole.isTransparent(x, y, flip) 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) 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) 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 DataSprite(val name: String, val coordinates: Vector4i)
private data class SpaceScanKey(val sprite: Sprite, val pixelOffset: Vector2i, val spaceScan: Double, val flip: Boolean) 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) nonEmptyRegion + Vector4i(x, y, x, y)
} }
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> { @Deprecated("Blocks thread")
return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip)) { fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): ImmutableSet<Vector2i> {
ImmutableSet.copyOf(worldSpaces0(pixelOffset, spaceScan, flip)) return worldSpacesAsync(pixelOffset, spaceScan, flip).get()
}
} }
private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> { fun worldSpacesAsync(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): CompletableFuture<ImmutableSet<Vector2i>> {
val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip), BiFunction { _, _ ->
val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi worldSpaces0(pixelOffset, spaceScan, flip)
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 }
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 val result = ImmutableSet.Builder<Vector2i>()
// also we don't cache this info since that's a waste of precious ram
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 (yspace in minY until maxY) {
for (xspace in minX until maxX) { for (xspace in minX until maxX) {
var fillRatio = 0.0 var fillRatio = 0.0
for (y in 0 until PIXELS_IN_STARBOUND_UNITi) { for (y in 0 until PIXELS_IN_STARBOUND_UNITi) {
val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y) val ypixel = (yspace * PIXELS_IN_STARBOUND_UNITi + y - pixelOffset.y)
if (ypixel !in 0 until height) 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)
continue continue
if (!isTransparent(xpixel, height - ypixel - 1, flip, data)) { for (x in 0 until PIXELS_IN_STARBOUND_UNITi) {
fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT) 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) { if (fillRatio >= spaceScan) {
result.add(Vector2i(xspace, yspace)) result.add(Vector2i(xspace, yspace))
}
} }
} }
}
return result result.build()
}, Starbound.EXECUTOR)
} }
} }
companion object : TypeAdapter<Image>() { companion object : TypeAdapter<Image>() {
@ -350,10 +367,9 @@ class Image private constructor(
private val spaceScanCache = Caffeine.newBuilder() private val spaceScanCache = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(30)) .expireAfterAccess(Duration.ofMinutes(30))
.softValues()
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
.build<SpaceScanKey, ImmutableSet<Vector2i>>() .buildAsync<SpaceScanKey, ImmutableSet<Vector2i>>()
@JvmStatic @JvmStatic
fun get(path: String): Image? { fun get(path: String): Image? {

View File

@ -14,6 +14,10 @@ import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter 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.util.Either
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.Registry 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.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World 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) @JsonAdapter(ObjectDefinition.Adapter::class)
data class ObjectDefinition( data class ObjectDefinition(
@ -75,7 +82,7 @@ data class ObjectDefinition(
val soundEffectRangeMultiplier: Double = 1.0, val soundEffectRangeMultiplier: Double = 1.0,
val price: Long = 1L, val price: Long = 1L,
val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(), val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
val touchDamage: JsonElement, val touchDamage: CompletableFuture<JsonElement>,
val minimumLiquidLevel: Float? = null, val minimumLiquidLevel: Float? = null,
val maximumLiquidLevel: Float? = null, val maximumLiquidLevel: Float? = null,
val liquidCheckInterval: Float = 0.5f, val liquidCheckInterval: Float = 0.5f,
@ -84,29 +91,33 @@ data class ObjectDefinition(
val biomePlaced: Boolean = false, val biomePlaced: Boolean = false,
val printable: Boolean = false, val printable: Boolean = false,
val smashOnBreak: Boolean = false, val smashOnBreak: Boolean = false,
val damageConfig: TileDamageParameters, val damageConfig: CompletableFuture<TileDamageParameters>,
val flickerPeriod: PeriodicFunction? = null, 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 { 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 // If we are given a direction affinity, try and find an orientation with a
// matching affinity *first* // matching affinity *first*
if (directionAffinity != null) { if (directionAffinity != null) {
for ((i, orientation) in orientations.withIndex()) { 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 return i
} }
} }
// Then, fallback and try and find any valid affinity // Then, fallback and try and find any valid affinity
for ((i, orientation) in orientations.withIndex()) { 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 i
} }
return -1 return -1
} }
companion object {
private val LOGGER = LogManager.getLogger()
}
class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() { class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() {
@JsonFactory(logMisses = false) @JsonFactory(logMisses = false)
data class PlainData( data class PlainData(
@ -153,11 +164,10 @@ data class ObjectDefinition(
val biomePlaced: Boolean = false, val biomePlaced: Boolean = false,
) )
private val objectRef = gson.getAdapter(JsonReference.Object::class.java)
private val basic = gson.getAdapter(PlainData::class.java) private val basic = gson.getAdapter(PlainData::class.java)
private val damageConfig = gson.getAdapter(TileDamageParameters::class.java) private val damageConfig = gson.getAdapter(TileDamageParameters::class.java)
private val damageTeam = gson.getAdapter(DamageTeam::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 emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
private val emitters = gson.listAdapter<ParticleEmissionEntry>() private val emitters = gson.listAdapter<ParticleEmissionEntry>()
@ -179,13 +189,21 @@ data class ObjectDefinition(
val printable = basic.hasObjectItem && read.get("printable", basic.scannable) val printable = basic.hasObjectItem && read.get("printable", basic.scannable)
val smashOnBreak = read.get("smashOnBreak", basic.smashable) val smashOnBreak = read.get("smashOnBreak", basic.smashable)
val getDamageParams = objectRef.fromJsonTree(read.get("damageTable", JsonPrimitive("/objects/defaultParameters.config:damageTable"))) val getPath = read.get("damageTable", JsonPrimitive("/objects/defaultParameters.config:damageTable"))
getDamageParams?.value ?: throw JsonSyntaxException("No valid damageTable specified")
getDamageParams.value["health"] = read["health"] val damageConfig = Starbound
getDamageParams.value["harvestLevel"] = read["harvestLevel"] .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) { val flickerPeriod = if ("flickerPeriod" in read) {
PeriodicFunction( PeriodicFunction(
@ -199,17 +217,24 @@ data class ObjectDefinition(
null null
} }
val orientations = ObjectOrientation.preprocess(read.getArray("orientations")) val orientations = ImmutableList.Builder<Supplier<ObjectOrientation>>()
.stream()
.map { orientations.fromJsonTree(it) }
.collect(ImmutableList.toImmutableList())
if ("particleEmitter" in read) { val path = AssetPathStack.last()
orientations.forEach { it.particleEmitters.add(emitter.fromJsonTree(read["particleEmitter"])) }
}
if ("particleEmitters" in read) { for (v in ObjectOrientation.preprocess(read.getArray("orientations"))) {
orientations.forEach { it.particleEmitters.addAll(emitters.fromJsonTree(read["particleEmitters"])) } 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( return ObjectDefinition(
@ -256,7 +281,7 @@ data class ObjectDefinition(
smashOnBreak = smashOnBreak, smashOnBreak = smashOnBreak,
damageConfig = damageConfig, damageConfig = damageConfig,
flickerPeriod = flickerPeriod, flickerPeriod = flickerPeriod,
orientations = orientations, orientations = orientations.build(),
) )
} }
} }

View File

@ -13,6 +13,7 @@ import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.clear import ru.dbotthepony.kommons.gson.clear
import ru.dbotthepony.kstarbound.math.AABB 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.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import java.util.concurrent.CompletableFuture
import kotlin.math.PI import kotlin.math.PI
@JsonAdapter(ObjectOrientation.Adapter::class)
data class ObjectOrientation( data class ObjectOrientation(
val json: JsonObject, val json: JsonObject,
val flipImages: Boolean = false, 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 vectors = gson.getAdapter(Vector2f::class.java)
private val vectorsi = gson.getAdapter(Vector2i::class.java) private val vectorsi = gson.getAdapter(Vector2i::class.java)
private val vectorsd = gson.getAdapter(Vector2d::class.java) private val vectorsd = gson.getAdapter(Vector2d::class.java)
private val drawables = gson.getAdapter(Drawable::class.java) private val drawables = gson.getAdapter(Drawable::class.java)
private val aabbs = gson.getAdapter(AABB::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 emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
private val emitters = gson.listAdapter<ParticleEmissionEntry>() private val emitters = gson.listAdapter<ParticleEmissionEntry>()
private val spaces = gson.setAdapter<Vector2i>() 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>>> 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?) { suspend fun read(obj: JsonObject): 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`)
val drawables = ArrayList<Drawable>() val drawables = ArrayList<Drawable>()
val flipImages = obj.get("flipImages", false) val flipImages = obj.get("flipImages", false)
val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object")) val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object"))
@ -201,24 +188,20 @@ data class ObjectOrientation(
if ("spaceScan" in obj) { if ("spaceScan" in obj) {
occupySpaces = ImmutableSet.of() occupySpaces = ImmutableSet.of()
try { for (drawable in drawables) {
for (drawable in drawables) { if (drawable is Drawable.Image) {
if (drawable is Drawable.Image) { val bound = drawable.path.with { "default" }
val bound = drawable.path.with { "default" } val sprite = bound.sprite
val sprite = bound.sprite
if (sprite != null) { if (sprite != null) {
val new = ImmutableSet.Builder<Vector2i>() val new = ImmutableSet.Builder<Vector2i>()
new.addAll(occupySpaces) new.addAll(occupySpaces)
new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages)) new.addAll(sprite.worldSpacesAsync(imagePositionI, obj["spaceScan"].asDouble, flipImages).await())
occupySpaces = new.build() occupySpaces = new.build()
} else { } else {
LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound") 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 lightPosition = obj["lightPosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO
val beamAngle = obj.get("beamAngle", 0.0) / 180.0 * PI val beamAngle = obj.get("beamAngle", 0.0) / 180.0 * PI
val statusEffectArea = obj["statusEffectArea"]?.let { vectorsd.fromJsonTree(it) } 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>() val emitters = ArrayList<ParticleEmissionEntry>()

View File

@ -27,7 +27,7 @@ data class TileDefinition(
val category: String, val category: String,
@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) @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 health: Double? = null,
val requiredHarvestLevel: Int? = null, val requiredHarvestLevel: Int? = null,
@ -62,7 +62,7 @@ data class TileDefinition(
} }
val actualDamageTable: TileDamageParameters by lazy { 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) { return@lazy if (health == null && requiredHarvestLevel == null) {
dmg dmg

View File

@ -25,7 +25,7 @@ data class TileModifierDefinition(
val miningSounds: ImmutableList<String> = ImmutableList.of(), val miningSounds: ImmutableList<String> = ImmutableList.of(),
@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals::tileDamage), val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals.onLoadedFuture.thenApply { Globals.tileDamage }),
@JsonFlat @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
@ -43,7 +43,7 @@ data class TileModifierDefinition(
} }
val actualDamageTable: TileDamageParameters by lazy { 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) { return@lazy if (health == null && requiredHarvestLevel == null) {
dmg dmg

View File

@ -74,7 +74,7 @@ data class BiomeDefinition(
surfacePlaceables = surfacePlaceables, surfacePlaceables = surfacePlaceables,
undergroundPlaceables = undergroundPlaceables, 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 { ores = (ores?.value?.evaluate(threatLevel, oresAdapter)?.map {
it.stream() it.stream()

View File

@ -45,6 +45,7 @@ import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.util.concurrent.CompletableFuture
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
@ -58,10 +59,9 @@ data class BiomePlaceables(
) { ) {
fun firstTreeVariant(): TreeVariant? { fun firstTreeVariant(): TreeVariant? {
return itemDistributions.stream() return itemDistributions.stream()
.flatMap { it.data.itemStream() } .flatMap { it.data.get().itemStream() }
.map { it as? Tree } .filter { it is Tree }
.filterNotNull() .flatMap { (it as Tree).trees.stream() }
.flatMap { it.trees.stream() }
.findAny() .findAny()
.orElse(null) .orElse(null)
} }
@ -73,10 +73,10 @@ data class BiomePlaceables(
val variants: Int = 1, val variants: Int = 1,
val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR, val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR,
@JsonFlat @JsonFlat
val data: DistributionData, val data: CompletableFuture<DistributionData>,
) { ) {
fun itemToPlace(x: Int, y: Int): Placement? { fun itemToPlace(x: Int, y: Int): Placement? {
return data.itemToPlace(x, y, priority) return data.get().itemToPlace(x, y, priority)
} }
} }

View File

@ -46,16 +46,12 @@ data class BiomePlaceablesDefinition(
@JsonFlat @JsonFlat
val data: DistributionItemData, val data: DistributionItemData,
) { ) {
init {
checkNotNull(distribution.value) { "Distribution data is missing" }
}
fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionItem { fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionItem {
return BiomePlaceables.DistributionItem( return BiomePlaceables.DistributionItem(
priority = priority, priority = priority,
variants = variants, variants = variants,
mode = mode, 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()) .collect(ImmutableList.toImmutableList())
) )
} }
} }

View File

@ -64,7 +64,7 @@ class BushVariant(
ceiling = data.value.ceiling, ceiling = data.value.ceiling,
descriptions = data.value.descriptions.fixDescription("${data.key} with $modName").toMap(), descriptions = data.value.descriptions.fixDescription("${data.key} with $modName").toMap(),
ephemeral = data.value.ephemeral, 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, modName = modName,
shapes = data.value.shapes.stream().map { Shape(it.base, it.mods[modName] ?: ImmutableList.of()) }.collect(ImmutableList.toImmutableList()) shapes = data.value.shapes.stream().map { Shape(it.base, it.mods[modName] ?: ImmutableList.of()) }.collect(ImmutableList.toImmutableList())
) )

View File

@ -53,7 +53,7 @@ data class GrassVariant(
ephemeral = data.value.ephemeral, ephemeral = data.value.ephemeral,
hueShift = hueShift, hueShift = hueShift,
descriptions = data.value.descriptions.fixDescription(data.value.name).toMap(), 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)
) )
} }
} }

View File

@ -587,7 +587,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
parameters.beamUpRule = params.beamUpRule parameters.beamUpRule = params.beamUpRule
parameters.disableDeathDrops = params.disableDeathDrops parameters.disableDeathDrops = params.disableDeathDrops
parameters.worldEdgeForceRegions = params.worldEdgeForceRegions 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.primaryBiome = primaryBiome.key
parameters.sizeName = sizeName parameters.sizeName = sizeName
parameters.hueShift = primaryBiome.value.hueShift(random) parameters.hueShift = primaryBiome.value.hueShift(random)

View File

@ -80,7 +80,7 @@ data class TreeVariant(
stemDropConfig = data.value.dropConfig.deepCopy(), stemDropConfig = data.value.dropConfig.deepCopy(),
descriptions = data.value.descriptions.fixDescription(data.key).toJsonObject(), descriptions = data.value.descriptions.fixDescription(data.key).toJsonObject(),
ephemeral = data.value.ephemeral, 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(), foliageSettings = JsonObject(),
foliageDropConfig = JsonObject(), foliageDropConfig = JsonObject(),
@ -107,7 +107,7 @@ data class TreeVariant(
stemDropConfig = data.value.dropConfig.deepCopy(), stemDropConfig = data.value.dropConfig.deepCopy(),
descriptions = data.value.descriptions.fixDescription("${data.key} with ${fdata.key}").toJsonObject(), descriptions = data.value.descriptions.fixDescription("${data.key} with ${fdata.key}").toJsonObject(),
ephemeral = data.value.ephemeral, 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(), foliageSettings = fdata.json.asJsonObject.deepCopy(),
foliageDropConfig = fdata.value.dropConfig.deepCopy(), foliageDropConfig = fdata.value.dropConfig.deepCopy(),

View File

@ -7,9 +7,12 @@ import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kstarbound.IStarboundFile import ru.dbotthepony.kstarbound.IStarboundFile
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.getValue import ru.dbotthepony.kstarbound.getValue
import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.util.supplyAsync
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.Closeable import java.io.Closeable
import java.io.DataInputStream import java.io.DataInputStream
@ -19,6 +22,11 @@ import java.io.InputStream
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.channels.Channels import java.nio.channels.Channels
import java.util.* 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) { private fun readHeader(reader: RandomAccessFile, required: Int) {
val read = reader.read() val read = reader.read()
@ -80,6 +88,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
throw IllegalStateException("${computeFullPath()} is a directory") throw IllegalStateException("${computeFullPath()} is a directory")
} }
override fun asyncRead(): CompletableFuture<ByteArray> {
throw IllegalStateException("${computeFullPath()} is a directory")
}
override fun toString(): String { override fun toString(): String {
return "SBDirectory[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]" return "SBDirectory[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]"
} }
@ -113,6 +125,18 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
return hash 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 { override fun open(): InputStream {
return object : InputStream() { return object : InputStream() {
private var innerOffset = 0L 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>() { private val reader by object : ThreadLocal<RandomAccessFile>() {
override fun initialValue(): RandomAccessFile { override fun initialValue(): RandomAccessFile {
return RandomAccessFile(path, "r") return RandomAccessFile(path, "r")

View File

@ -32,7 +32,7 @@ class ActiveItemStack(entry: ItemRegistry.Entry, config: JsonObject, parameters:
val animator: Animator val animator: Animator
init { 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") val animationCustom = lookupProperty("animationCustom")
if (!animationCustom.isJsonNull) { if (!animationCustom.isJsonNull) {

View File

@ -4,6 +4,10 @@ import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet 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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
@ -140,9 +144,9 @@ object ItemRegistry {
val files = fileTree[type.extension ?: continue] ?: continue val files = fileTree[type.extension ?: continue] ?: continue
for (file in files) { for (file in files) {
futures.add(Starbound.EXECUTOR.submit { futures.add(Starbound.GLOBAL_SCOPE.launch {
try { 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) val readData = data.fromJsonTree(read)
tasks.add { tasks.add {
@ -164,7 +168,7 @@ object ItemRegistry {
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Reading item definition $file", err) LOGGER.error("Reading item definition $file", err)
} }
}) }.asCompletableFuture())
} }
} }

View File

@ -5,6 +5,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kstarbound.IStarboundFile import ru.dbotthepony.kstarbound.IStarboundFile
@ -146,5 +147,23 @@ enum class JsonPatch(val key: String) {
return base 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
}
} }
} }

View File

@ -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
}
}

View File

@ -348,7 +348,7 @@ fun provideRootBindings(lua: LuaEnvironment) {
lua.globals["root"] = table lua.globals["root"] = table
table["assetJson"] = luaFunction { path: ByteString -> table["assetJson"] = luaFunction { path: ByteString ->
returnBuffer.setTo(from(Starbound.loadJsonAsset(path.decode()))) returnBuffer.setTo(from(Starbound.loadJsonAsset(path.decode()).get()))
} }
table["makeCurrentVersionedJson"] = luaStub("makeCurrentVersionedJson") table["makeCurrentVersionedJson"] = luaStub("makeCurrentVersionedJson")

View File

@ -38,13 +38,11 @@ import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.toStarboundString import ru.dbotthepony.kstarbound.util.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import java.io.File import java.io.File
import java.lang.ref.Cleaner
import java.sql.DriverManager import java.sql.DriverManager
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.math.min
sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") { sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
private fun makedir(file: File) { 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>>() private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
val universe = ServerUniverse(universeFolder) val universe = ServerUniverse(universeFolder)
val chat = ChatHandler(this) 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 database = DriverManager.getConnection("jdbc:sqlite:${File(universeFolder, "universe.db").absolutePath.replace('\\', '/')}")
private val databaseCleanable = Starbound.CLEANER.register(this, database::close) private val databaseCleanable = Starbound.CLEANER.register(this, database::close)

View File

@ -88,6 +88,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val coroutines = asCoroutineDispatcher() val coroutines = asCoroutineDispatcher()
val scope = CoroutineScope(coroutines + SupervisorJob()) val scope = CoroutineScope(coroutines + SupervisorJob())
init {
priority = 7
}
private fun nextDeadline(): Long { private fun nextDeadline(): Long {
if (isShutdown || eventQueue.isNotEmpty()) if (isShutdown || eventQueue.isNotEmpty())
return 0L return 0L

View File

@ -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 { fun anyCellSatisfies(x: Int, y: Int, distance: Int, predicate: CellPredicate): Boolean {
for (tx in x - distance .. x + distance) { for (tx in (x - distance) .. (x + distance)) {
for (ty in y - distance .. y + distance) { for (ty in (y - distance) .. (y + distance)) {
val ix = geometry.x.cell(tx) 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))) { if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
return true 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 * of world generation, everything else is kinda okay to be performed
* on main world thread, so concurrent access is not needed for now. * on main world thread, so concurrent access is not needed for now.
* *
* ArrayChunkMap does ~not need~ needs synchronization too, unless we use CopyOnWriteArrayList * ArrayChunkMap does ~not~ need synchronization, because 2D array is thread-safe to be read
* for "existing" chunks list. * 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 private const val CONCURRENT_SPARSE_CHUNK_MAP = false
} }

View File

@ -986,7 +986,7 @@ class Animator() {
companion object { companion object {
// lame // lame
fun load(path: String): Animator { fun load(path: String): Animator {
val json = Starbound.loadJsonAsset(path) val json = Starbound.loadJsonAsset(path).get()
if (json == null) { if (json == null) {
if (missing.add(path)) { if (missing.add(path)) {

View File

@ -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() {
}
}

View File

@ -76,7 +76,7 @@ class PlayerEntity() : HumanoidActorEntity() {
val inventory = PlayerInventory() val inventory = PlayerInventory()
val songbook = Songbook(this) 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) override val statusController = StatusController(this, Globals.player.statusControllerSettings)
val techController = TechController(this) val techController = TechController(this)

View File

@ -174,7 +174,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
val orientation: ObjectOrientation? get() { val orientation: ObjectOrientation? get() {
return config.value.orientations.getOrNull(orientationIndex.toInt()) return config.value.orientations.getOrNull(orientationIndex.toInt())?.get()
} }
protected val mergedJson = ManualLazy { protected val mergedJson = ManualLazy {
@ -328,7 +328,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
val orientation = orientation val orientation = orientation
if (orientation != null) { 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) { if (!touchDamageConfig.isJsonNull) {
sources.add(Starbound.gson.fromJson(touchDamageConfig, DamageSource::class.java).copy(sourceEntityId = entityID, team = team.get())) 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 val animator: Animator
init { init {
if (config.value.animation?.value != null) { if (config.value.animation?.value?.get() != null) {
if (config.value.animationCustom.size() > 0 && config.value.animation!!.json != null) { if (config.value.animationCustom.size() > 0 && config.value.animation!!.json.get() != null) {
animator = Animator(Starbound.gson.fromJson(mergeJson(config.value.animation!!.json!!, config.value.animationCustom), AnimationDefinition::class.java)) animator = Animator(Starbound.gson.fromJson(mergeJson(config.value.animation!!.json.get()!!.deepCopy(), config.value.animationCustom), AnimationDefinition::class.java))
} else { } else {
animator = Animator(config.value.animation!!.value!!) animator = Animator(config.value.animation!!.value.get()!!)
} }
} else { } else {
animator = Animator() animator = Animator()
@ -566,7 +566,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
flickerPeriod?.update(delta, world.random) flickerPeriod?.update(delta, world.random)
if (!isRemote) { if (!isRemote) {
tileHealth.tick(config.value.damageConfig, delta) tileHealth.tick(config.value.damageConfig.get(), delta)
if (tileHealth.isHealthy) { if (tileHealth.isHealthy) {
lastClosestSpaceToDamageSource = null lastClosestSpaceToDamageSource = null
@ -744,7 +744,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
if (unbreakable) if (unbreakable)
return false return false
tileHealth.damage(config.value.damageConfig, source, damage) tileHealth.damage(config.value.damageConfig.get(), source, damage)
if (damageSpaces.isNotEmpty()) { if (damageSpaces.isNotEmpty()) {
lastClosestSpaceToDamageSource = damageSpaces.minBy { it.toDoubleVector().distanceSquared(source) } lastClosestSpaceToDamageSource = damageSpaces.minBy { it.toDoubleVector().distanceSquared(source) }