diff --git a/gradle.properties b/gradle.properties index 1f1a09ca..c520c243 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.10.2 +kommonsVersion=2.11.0 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index a3138602..692d96ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -104,7 +104,7 @@ fun main() { Starbound.mailboxInitialized.submit { val server = IntegratedStarboundServer(File("./")) - //val world = ServerWorld.create(server, WorldGeometry(Vector2i(3000, 2000), true, false), LegacyWorldStorage.file(db)) + val world = ServerWorld.load(server, LegacyWorldStorage.file(db)).get() //world.thread.start() //ply = PlayerEntity(client.world!!) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index 105ef0cc..7574ceec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -244,7 +244,7 @@ object Registries { try { val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") - val factory = TerrainSelectorType.factory(json) + val factory = TerrainSelectorType.factory(json, false) terrainSelectors.add { terrainSelectors.add(name, factory) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 2459b951..6da62d58 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -58,6 +58,8 @@ import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.util.Directives +import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise @@ -104,10 +106,12 @@ object Starbound : ISBFileLocator { return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits) } + private val LOGGER = LogManager.getLogger() + val thread = Thread(::universeThread, "Starbound Universe") - val mailbox = MailboxExecutorService(thread) - val mailboxBootstrapped = MailboxExecutorService(thread) - val mailboxInitialized = MailboxExecutorService(thread) + val mailbox = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } + val mailboxBootstrapped = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } + val mailboxInitialized = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } init { thread.isDaemon = true @@ -121,6 +125,11 @@ object Starbound : ISBFileLocator { val thread = Thread(it, "Starbound Storage IO ${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) + } + return@ThreadFactory thread }) @@ -146,8 +155,6 @@ object Starbound : ISBFileLocator { @JvmField val STRINGS: Interner = interner(5) - private val LOGGER = LogManager.getLogger() - val gson: Gson = with(GsonBuilder()) { serializeNulls() setDateFormat(DateFormat.LONG) @@ -236,6 +243,8 @@ object Starbound : ISBFileLocator { registerTypeAdapterFactory(ThingDescription.Factory(STRINGS)) registerTypeAdapterFactory(TerrainSelectorType.Companion) + registerTypeAdapter(Directives.Companion) + registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE)) registerTypeAdapter(InventoryIcon.Companion) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index d08b9734..d5479e6e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -62,6 +62,8 @@ import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity +import ru.dbotthepony.kstarbound.server.StarboundServer +import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.world.Direction @@ -117,7 +119,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { } }, null, false) - val mailbox = MailboxExecutorService(thread) + val mailbox = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } val capabilities: GLCapabilities var viewportX: Int = 0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt index 6e41d1b0..9c220453 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt @@ -116,7 +116,7 @@ class Parallax( return Layer( parallaxValue = parallax.map({ Vector2d(it, it) }, { it }), - repeat = repeatX to repeatY, + repeat = Either.left(repeatX to repeatY), tileLimitTop = tileLimitTop, tileLimitBottom = tileLimitBottom, verticalOrigin = verticalOrigin, @@ -138,9 +138,9 @@ class Parallax( data class Layer( var directives: Directives, val textures: ImmutableList, - val alpha: Double, + val alpha: Double = 1.0, val parallaxValue: Vector2d, - val repeat: Pair, + val repeat: Either, Pair>, val tileLimitTop: Double? = null, val tileLimitBottom: Double? = null, val verticalOrigin: Double, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 04bb0fec..9cccef8a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -9,6 +9,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdateP import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.Clock +import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExecutionSpinner import java.io.Closeable import java.io.File @@ -30,7 +31,7 @@ sealed class StarboundServer(val root: File) : Closeable { val worlds: MutableList = Collections.synchronizedList(ArrayList()) val serverID = threadCounter.getAndIncrement() - val mailbox = MailboxExecutorService() + val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) val thread = Thread(spinner, "Starbound Server $serverID") val universe = ServerUniverse() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 6f16c4c2..334f04f0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -21,6 +21,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpd import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener @@ -483,15 +484,17 @@ class ServerWorld private constructor( fun load(server: StarboundServer, storage: WorldStorage): CompletableFuture { return storage.loadMetadata().thenApply { - val meta = it.map { Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) }.orThrow { NoSuchElementException("No world metadata is present") } + AssetPathStack("/") { _ -> + val meta = it.map { Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) }.orThrow { NoSuchElementException("No world metadata is present") } - val world = create(server, WorldTemplate.fromJson(meta.worldTemplate), storage) - world.playerSpawnPosition = meta.playerStart - world.respawnInWorld = meta.respawnInWorld - world.adjustPlayerSpawn = meta.adjustPlayerStart - world.centralStructure = meta.centralStructure - world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) - world + val world = create(server, WorldTemplate.fromJson(meta.worldTemplate), storage) + world.playerSpawnPosition = meta.playerStart + world.respawnInWorld = meta.respawnInWorld + world.adjustPlayerSpawn = meta.adjustPlayerStart + world.centralStructure = meta.centralStructure + world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) + world + } } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt index 0a64eaf9..187d45c3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt @@ -1,8 +1,13 @@ package ru.dbotthepony.kstarbound.util +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2ObjectMap import it.unimi.dsi.fastutil.objects.Object2ObjectMaps +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kstarbound.Starbound class Directives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap) { constructor() : this(Object2ObjectAVLTreeMap()) @@ -14,8 +19,8 @@ class Directives private constructor(private val directivesInternal: Object2Obje } // assume it is just "name=value" - val key = directives.substringBefore('=') - val value = directives.substringAfter('=') + val key = Starbound.STRINGS.intern(directives.substringBefore('=')) + val value = Starbound.STRINGS.intern(directives.substringAfter('=')) directivesInternal[key] = value } else { // gets interesting @@ -24,8 +29,8 @@ class Directives private constructor(private val directivesInternal: Object2Obje throw IllegalArgumentException("Missing render directive delimiter in '$pair' (full string: '$directives')") } - val key = pair.substringBefore('=') - val value = pair.substringAfter('=') + val key = Starbound.STRINGS.intern(pair.substringBefore('=')) + val value = Starbound.STRINGS.intern(pair.substringAfter('=')) directivesInternal[key] = value } } @@ -38,7 +43,14 @@ class Directives private constructor(private val directivesInternal: Object2Obje if (directivesInternal.isEmpty()) return "Directives[empty]" else - return "Directives[?${directivesInternal.entries.joinToString("?") { "${it.key}=${it.value}" }}]" + return "Directives[$directivesString]" + } + + val directivesString by lazy { + if (directivesInternal.isEmpty()) + "" + else + "?${directivesInternal.entries.joinToString("?") { "${it.key}=${it.value}" }}" } override fun equals(other: Any?): Boolean { @@ -65,8 +77,8 @@ class Directives private constructor(private val directivesInternal: Object2Obje } // assume it is just "name=value" - val key = directives.substringBefore('=') - val value = directives.substringAfter('=') + val key = Starbound.STRINGS.intern(directives.substringBefore('=')) + val value = Starbound.STRINGS.intern(directives.substringAfter('=')) return add(key, value) } else { // gets interesting @@ -82,12 +94,28 @@ class Directives private constructor(private val directivesInternal: Object2Obje throw IllegalArgumentException("Missing render directive delimiter in '$pair' (full string: $directives)") } - val key = pair.substringBefore('=') - val value = pair.substringAfter('=') + val key = Starbound.STRINGS.intern(pair.substringBefore('=')) + val value = Starbound.STRINGS.intern(pair.substringAfter('=')) copy[key] = value } return Directives(copy) } } + + companion object : TypeAdapter() { + override fun write(out: JsonWriter, value: Directives?) { + if (value == null) + out.nullValue() + else + out.value(value.directivesString) + } + + override fun read(`in`: JsonReader): Directives? { + if (`in`.consumeNull()) + return null + + return Directives(`in`.nextString()) + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt new file mode 100644 index 00000000..d976e037 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExceptionLogger.kt @@ -0,0 +1,10 @@ +package ru.dbotthepony.kstarbound.util + +import org.apache.logging.log4j.Logger +import java.util.function.Consumer + +class ExceptionLogger(private val logger: Logger) : Consumer { + override fun accept(t: Throwable) { + logger.error("Error while executing queued task", t) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 48bbe6d0..3cc1dc49 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.util.IStruct2d @@ -19,6 +20,8 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket +import ru.dbotthepony.kstarbound.server.StarboundServer +import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell @@ -42,7 +45,7 @@ import java.util.stream.Stream abstract class World, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess, Closeable { val background = TileView.Background(this) val foreground = TileView.Foreground(this) - val mailbox = MailboxExecutorService() + val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } val sky = Sky() val geometry: WorldGeometry = template.geometry @@ -330,4 +333,8 @@ abstract class World, ChunkType : Chunk { + LOGGER.error("Error while executing queued task on $this", it) + } + + var mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger } private set private var innerWorld: World<*, *>? = null @@ -117,7 +125,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) { check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" } if (mailbox.isShutdown) - mailbox = MailboxExecutorService() + mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger } innerWorld = world world.entities[entityID] = this @@ -169,4 +177,8 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) { open fun addLights(lightCalculator: LightCalculator, xOffset: Int, yOffset: Int) { } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt index 6bf27e5e..013ac3df 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/RotateTerrainSelector.kt @@ -15,7 +15,7 @@ class RotateTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : val source: JsonObject, ) - private val source = TerrainSelectorType.create(data.source) + private val source = TerrainSelectorType.create(data.source, parameters) private val deltaX = parameters.worldWidth * data.rotationWidth private val deltaY = parameters.worldHeight * data.rotationHeight diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt index 5e06eec8..e28a96ca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt @@ -72,27 +72,41 @@ enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, * if (`in`.consumeNull()) return null - return create(objects.read(`in`)) + return load(objects.read(`in`)) } - fun factory(json: JsonObject): Factory<*, *> { + fun factory(json: JsonObject, isSerializedForm: Boolean): Factory<*, *> { val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json") - for (value in entries) { - if (value.lowercase == type) { - return Factory(value.data.adapter.fromJsonTree(json), value.data.factory as ((Any, TerrainSelectorParameters) -> AbstractTerrainSelector)) + if (isSerializedForm) { + val config = json["config"]?.asJsonObject ?: throw JsonSyntaxException("Missing 'config' element of terrain json") + + for (value in entries) { + if (value.lowercase == type) { + return Factory(value.data.adapter.fromJsonTree(config), value.data.factory as ((Any, TerrainSelectorParameters) -> AbstractTerrainSelector)) + } + } + } else { + for (value in entries) { + if (value.lowercase == type) { + return Factory(value.data.adapter.fromJsonTree(json), value.data.factory as ((Any, TerrainSelectorParameters) -> AbstractTerrainSelector)) + } } } throw IllegalArgumentException("Unknown terrain selector type $type") } - fun create(json: JsonObject): AbstractTerrainSelector<*> { - return factory(json).create(Starbound.gson.fromJson(json["parameters"] ?: throw JsonSyntaxException("Missing 'parameters' element of terrain json"), TerrainSelectorParameters::class.java)) + fun create(json: JsonObject, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { + return factory(json, false).create(parameters) } - fun create(json: JsonObject, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { - return factory(json).create(parameters) + fun load(json: JsonObject): AbstractTerrainSelector<*> { + return factory(json, true).create(Starbound.gson.fromJson(json["parameters"] ?: throw JsonSyntaxException("Missing 'parameters' element of terrain json"), TerrainSelectorParameters::class.java)) + } + + fun load(json: JsonObject, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { + return factory(json, true).create(parameters) } } }