From ac55422c3bed953790e8725c2c73367bfcd13d8a Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 26 Apr 2024 18:52:45 +0700 Subject: [PATCH] Sequential or parallel disk access now handled properly --- .../ru/dbotthepony/kstarbound/Globals.kt | 46 ++++--- .../ru/dbotthepony/kstarbound/Registries.kt | 39 +++--- .../ru/dbotthepony/kstarbound/Starbound.kt | 120 +++++++++++++----- .../kstarbound/StarboundFileSystem.kt | 84 ++++++------ .../kstarbound/client/render/RenderLayer.kt | 16 +++ .../kstarbound/client/render/TileRenderer.kt | 2 +- .../kstarbound/defs/AssetReference.kt | 71 ++++++----- .../kstarbound/defs/ClientEntityMode.kt | 9 ++ .../kstarbound/defs/JsonReference.kt | 6 +- .../kstarbound/defs/ProjectileDefinition.kt | 77 +++++++++++ .../defs/dungeon/DungeonDefinition.kt | 4 +- .../kstarbound/defs/dungeon/TiledTileSet.kt | 3 +- .../kstarbound/defs/image/Image.kt | 90 +++++++------ .../defs/object/ObjectDefinition.kt | 69 ++++++---- .../defs/object/ObjectOrientation.kt | 49 +++---- .../kstarbound/defs/tile/TileDefinition.kt | 4 +- .../defs/tile/TileModifierDefinition.kt | 4 +- .../kstarbound/defs/world/BiomeDefinition.kt | 2 +- .../kstarbound/defs/world/BiomePlaceables.kt | 12 +- .../defs/world/BiomePlaceablesDefinition.kt | 8 +- .../kstarbound/defs/world/BushVariant.kt | 2 +- .../kstarbound/defs/world/GrassVariant.kt | 2 +- .../defs/world/TerrestrialWorldParameters.kt | 2 +- .../kstarbound/defs/world/TreeVariant.kt | 4 +- .../dbotthepony/kstarbound/io/StarboundPak.kt | 75 +++++++++++ .../kstarbound/item/ActiveItemStack.kt | 2 +- .../kstarbound/item/ItemRegistry.kt | 10 +- .../dbotthepony/kstarbound/json/JsonPatch.kt | 19 +++ .../json/factory/CompletableFutureAdapter.kt | 32 +++++ .../kstarbound/lua/bindings/RootBindings.kt | 2 +- .../kstarbound/server/StarboundServer.kt | 4 +- .../kstarbound/util/BlockableEventLoop.kt | 4 + .../ru/dbotthepony/kstarbound/world/World.kt | 11 +- .../kstarbound/world/entities/Animator.kt | 2 +- .../world/entities/ProjectileEntity.kt | 23 ++++ .../world/entities/player/PlayerEntity.kt | 2 +- .../world/entities/tile/WorldObject.kt | 16 +-- 37 files changed, 637 insertions(+), 290 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/ClientEntityMode.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CompletableFutureAdapter.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt 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 load(path: String, accept: KMutableProperty0, adapter: Lazy>): Future<*> { - val file = Starbound.loadJsonAsset(path) + val onLoadedFuture = CompletableFuture() + + private suspend fun load(path: String, accept: KMutableProperty0, adapter: Lazy>) { + 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 load(path: String, accept: KMutableProperty0): Future<*> { - return load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) }) + private inline fun load(path: String, accept: KMutableProperty0): CompletableFuture<*> { + return Starbound.GLOBAL_SCOPE.launch { load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) }) }.asCompletableFuture() } fun load(): List> { - val tasks = ArrayList>() + val tasks = ArrayList>() 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>() 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>) { + private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map>) { 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>() 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>() {}).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 { + 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>() + .buildAsync>(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 { 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 { 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 { + 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 + + /** + * @throws IllegalStateException if file is a directory + * @throws FileNotFoundException if file does not exist + */ + fun asyncReader(): CompletableFuture { + 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 { + 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 { + throw FileNotFoundException() + } + override fun open(): InputStream { throw FileNotFoundException() } @@ -251,6 +243,10 @@ class NonExistingFile( override val exists: Boolean get() = false + override fun asyncRead(): CompletableFuture { + 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 { + 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 { override fun compareTo(other: Point): Int { if (this === other) return 0 @@ -56,6 +62,16 @@ enum class RenderLayer { } } + class Adapter(gson: Gson) : TypeAdapter() { + 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 { 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) { + 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, json: CompletableFuture) { + 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 + val json: CompletableFuture + val value: CompletableFuture companion object : TypeAdapterFactory { + private val LOGGER = LogManager.getLogger() val EMPTY = AssetReference(null, null, null, null) fun empty() = EMPTY as AssetReference @@ -62,7 +68,7 @@ class AssetReference { val param = type.type as? ParameterizedType ?: return null return object : TypeAdapter>() { - private val cache = Collections.synchronizedMap(HashMap>()) + private val cache = ConcurrentHashMap, CompletableFuture>>() private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter private val strings = gson.getAdapter(String::class.java) private val missing = Collections.synchronizedSet(ObjectOpenHashSet()) @@ -81,34 +87,31 @@ class AssetReference { } 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(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(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 = ImmutableList.of(), + val emitters: ImmutableSet = 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 = ImmutableList.of(), + val physicsForces: JsonObject = JsonObject(), + val physicsCollisions: JsonObject = JsonObject(), + val persistentStatusEffects: ImmutableList> = 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 { - 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 { + @Deprecated("Blocks thread") + fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): ImmutableSet { return whole.worldSpaces(pixelOffset, spaceScan, flip) } - fun worldSpaces(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): Set { + @Deprecated("Blocks thread") + fun worldSpaces(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): ImmutableSet { return whole.worldSpaces(Vector2i(pixelOffset.x.toInt(), pixelOffset.y.toInt()), spaceScan, flip) } + fun worldSpacesAsync(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): CompletableFuture> { + return whole.worldSpacesAsync(pixelOffset, spaceScan, flip) + } + + fun worldSpacesAsync(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): CompletableFuture> { + 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 { - 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 { + return worldSpacesAsync(pixelOffset, spaceScan, flip).get() } - private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set { - 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> { + return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip), BiFunction { _, _ -> + worldSpaces0(pixelOffset, spaceScan, flip) + }) + } - val result = ObjectArraySet() + private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): CompletableFuture> { + 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() - 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() { @@ -350,10 +367,9 @@ class Image private constructor( private val spaceScanCache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(30)) - .softValues() .scheduler(Starbound) .executor(Starbound.EXECUTOR) - .build>() + .buildAsync>() @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> = ImmutableList.of(), - val touchDamage: JsonElement, + val touchDamage: CompletableFuture, 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, val flickerPeriod: PeriodicFunction? = null, - val orientations: ImmutableList, + val orientations: ImmutableList>, ) { 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() { @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() @@ -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>() - 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() { + 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() private val spaces = gson.setAdapter() private val materialSpaces = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, Vector2i::class.java, String::class.java).type)) as TypeAdapter>> - 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() 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() - 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() + 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() 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 = AssetReference(Globals::tileDamage), + val damageTable: AssetReference = 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 = ImmutableList.of(), @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) - val damageTable: AssetReference = AssetReference(Globals::tileDamage), + val damageTable: AssetReference = 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, ) { 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 { + 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 { + 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): CompletableFuture + } + + // SSDs + private object DirectScheduler : ReadScheduler { + override fun schedule(offset: Long, action: Supplier): CompletableFuture { + 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, val id: Int, val future: CompletableFuture) : Runnable, Comparable { + 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() + private val carrier = CarriedExecutor(Starbound.IO_EXECUTOR) + + override fun schedule(offset: Long, action: Supplier): CompletableFuture { + val future = CompletableFuture() + 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() { 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?): 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 create(gson: Gson, type: TypeToken): TypeAdapter? { + 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 + + return object : TypeAdapter>() { + override fun write(out: JsonWriter, value: CompletableFuture?) { + parent.write(out, value?.get()) + } + + override fun read(`in`: JsonReader): CompletableFuture? { + return CompletableFuture.completedFuture(parent.read(`in`)) + } + } as TypeAdapter + } + + 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>() 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, ChunkType : Chunk, ChunkType : Chunk) : 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) : 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) : 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) : 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) : 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) }