diff --git a/ADDITIONS.md b/ADDITIONS.md index e45c2471..fc8d689f 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -41,6 +41,10 @@ val color: TileColor = TileColor.DEFAULT * Tiled map behavior is unchanged, and marks their position only. ## .terrain + +Please keep in mind that if you use new format or new terrain selectors original clients will +probably explode upon joining worlds where new terrain selectors are utilized. + * All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them * They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}` * `min`, `max` and `minmax` terrain selectors now also accept next format: `{"name": "namedselector", "seedBias": 4}` @@ -48,8 +52,8 @@ val color: TileColor = TileColor.DEFAULT * `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`) * `displacement` terrain selector has `xClamp` added, works like `yClamp` * `rotate` terrain selector has `rotationWidth` (defaults to `0.5`) and `rotationHeight` (defaults to `0.0`) added, which are multiplied by world's size and world's height respectively to determine rotation point center - * `min` terrain selector added, opposite of existing `max` (json format is the same as `max`) - * `cache` terrain selector removed due it not being documented, and having little practical value + * Added `min` terrain selector, opposite of existing `max` (json format is the same as `max`) + * Removed `cache` terrain selector due it not being documented, and having little practical value * `perlin` terrain selector now accepts `type`, `frequency` and `amplitude` values (naming inconsistency fix) * `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix); * `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`) diff --git a/gradle.properties b/gradle.properties index cc2195f7..702281ea 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.16.1 +kommonsVersion=2.17.0 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index 36d86cc6..e4abc6f2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.UniverseServerConfig import ru.dbotthepony.kstarbound.defs.WorldServerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig +import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters @@ -114,6 +115,9 @@ object Globals { var itemParameters by Delegates.notNull() private set + var shipUpgrades by Delegates.notNull() + private set + private var profanityFilterInternal by Delegates.notNull>() val profanityFilter: ImmutableSet by lazy { @@ -219,6 +223,7 @@ object Globals { tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) + tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades)) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 482f29ac..bfc3050b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -294,7 +294,9 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS) val gson: Gson = with(GsonBuilder()) { - // serializeNulls() + // serialize explicit nulls, FactoryAdapter don't serialize null fields + // This is required because some fucktard fucking fuck put get/optX combo in some places instead of optX directly, or even opt/optX + serializeNulls() setDateFormat(DateFormat.LONG) setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) setPrettyPrinting() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index dbfa98d5..eca4fdac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -131,14 +131,6 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn } } - override fun onChannelClosed() { - super.onChannelClosed() - - if (pendingDisconnect) { - disconnectNow() - } - } - fun bootstrap(address: SocketAddress = channel.remoteAddress(), asLegacy: Boolean = false) { LOGGER.info("Trying to connect to remote server at $address with ${if (asLegacy) "legacy" else "native"} protocol") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index de0fd13a..028f18e5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -26,6 +26,7 @@ import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult +import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity @@ -72,6 +73,12 @@ class ClientWorld( override val connectionID: Int get() = client.activeConnection?.connectionID ?: throw IllegalStateException("ClientWorld exists without active connection") + public override var centralStructure: WorldStructure + get() = super.centralStructure + set(value) { + super.centralStructure = value + } + val renderRegionWidth = determineChunkSize(geometry.size.x) val renderRegionHeight = determineChunkSize(geometry.size.y) val renderRegionsX = geometry.size.x / renderRegionWidth diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt index d3ee611a..ee24ce69 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt @@ -14,6 +14,8 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.json.FastJsonTreeReader +import ru.dbotthepony.kstarbound.json.popJsonElement import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.sbIntern import java.lang.reflect.ParameterizedType @@ -59,7 +61,7 @@ class AssetReference { companion object : TypeAdapterFactory { private val LOGGER = LogManager.getLogger() - val EMPTY = AssetReference(null, null, null, null) + private val EMPTY = AssetReference(null, null, null, null) fun empty() = EMPTY as AssetReference @@ -117,8 +119,8 @@ class AssetReference { 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 + val json = `in`.popJsonElement() + val value = adapter.read(FastJsonTreeReader(json)) ?: return null return AssetReference(null, null, value, json) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt index ba995a28..3df0ca09 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt @@ -1,9 +1,11 @@ package ru.dbotthepony.kstarbound.defs import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters +import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.json.builder.JsonFactory import java.util.function.Predicate @@ -16,6 +18,7 @@ data class UniverseServerConfig( val queuedFlightWaitTime: Double = 0.0, val useNewWireProcessing: Boolean = true, + val speciesShips: ImmutableMap>>, ) { @JsonFactory data class WorldPredicate( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt index b44ee033..33dac76c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt @@ -40,10 +40,10 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList) : Par val offset = (x + y * image.width) * 4 // flip image as we go - tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or - bytes[offset + 1].toInt().and(0xFF).shl(8) or - bytes[offset + 2].toInt().and(0xFF).shl(16) or - bytes[offset + 3].toInt().and(0xFF).shl(24) + tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or // red + bytes[offset + 1].toInt().and(0xFF).shl(8) or // green + bytes[offset + 2].toInt().and(0xFF).shl(16) or // blue + bytes[offset + 3].toInt().and(0xFF).shl(24) // alpha } } 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 5c9f9ddd..e012ce84 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -151,6 +151,10 @@ class Image private constructor( val whole = Sprite("this", 0, 0, width, height) val nonEmptyRegion get() = whole.nonEmptyRegion + override fun toString(): String { + return "Image[$source of $width, $height]" + } + /** * returns integer in ABGR format */ @@ -201,9 +205,12 @@ class Image private constructor( override val u1: Float = (x.toFloat() + this.width.toFloat()) / this@Image.width override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height + override fun toString(): String { + return "Sprite[at $x, $y -> $width, $height of ${this@Image}]" + } + /** - * returns integer in big-endian ABGR format if it is RGB or RGBA picture, - * otherwise returns pixels as-is + * returns integer in little-endian RGBA/big-endian ABGR format */ operator fun get(x: Int, y: Int, data: ByteBuffer = this@Image.data): Int { require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } @@ -217,7 +224,7 @@ class Image private constructor( } /** - * returns integer in ABGR format + * returns integer in little-endian RGBA/big-endian ABGR format */ operator fun get(x: Int, y: Int, flip: Boolean, data: ByteBuffer = this@Image.data): Int { if (flip) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt index d2ed9fd3..0220ddea 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt @@ -175,7 +175,7 @@ class TreasurePoolDefinition(pieces: List) { val pool = ImmutableList.Builder>() val fill = ImmutableList.Builder>>() var poolRounds: IPoolRounds = OneRound - val allowDuplication = things["allowDuplication"]?.asBoolean ?: false + val allowDuplication = things["allowDuplication"]?.asBoolean ?: true things["poolRounds"]?.let { if (it is JsonPrimitive) { 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 ae66dc38..2aa7f5da 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -91,6 +91,7 @@ data class ObjectDefinition( val damageConfig: CompletableFuture, val flickerPeriod: PeriodicFunction? = null, val orientations: ImmutableList>, + val uniqueId: String? = null, ) { 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 @@ -159,6 +160,7 @@ data class ObjectDefinition( val health: Double = 1.0, val rooting: Boolean = false, val biomePlaced: Boolean = false, + val uniqueId: String? = null, ) private val basic = gson.getAdapter(PlainData::class.java) @@ -283,6 +285,7 @@ data class ObjectDefinition( damageConfig = damageConfig, flickerPeriod = flickerPeriod, orientations = orientations.build(), + uniqueId = basic.uniqueId, ) } } 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 0f88d179..203b3db9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -66,15 +66,17 @@ data class ObjectOrientation( if (occupySpaces.isEmpty()) return true - var valid = occupySpaces.all { - val cell = world.chunkMap.getCell(it + position) + val localSpaces = occupySpaces.map { it + position } + + var valid = localSpaces.all { + val cell = world.chunkMap.getCell(it) //if (!cell.foreground.material.isEmptyTile) println("not empty tile: ${it + position}, space $it, pos $position") //if (cell.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}") cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || !world.isDungeonIDProtected(cell.dungeonId)) } if (valid) { - valid = !world.entityIndex.any(AABB.ofPoints(occupySpaces), Predicate { it is WorldObject && occupySpaces.any { s -> s in it.occupySpaces } }) + valid = !world.entityIndex.any(AABB.ofPoints(localSpaces), Predicate { it is WorldObject && localSpaces.any { s -> s in it.occupySpaces } }) } return valid @@ -189,7 +191,7 @@ data class ObjectOrientation( var occupySpaces = obj["spaces"]?.let { spaces.fromJsonTree(it) } ?: ImmutableSet.of(Vector2i.ZERO) if ("spaceScan" in obj) { - occupySpaces = ImmutableSet.of() + // occupySpaces = ImmutableSet.of() for (drawable in drawables) { if (drawable is Drawable.Image) { @@ -239,7 +241,7 @@ data class ObjectOrientation( "right" -> occupySpaces.stream().filter { it.x == maxX }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "top" -> occupySpaces.stream().filter { it.y == maxY }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "bottom" -> occupySpaces.stream().filter { it.y == minY }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } - "background" -> occupySpaces.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } + "background" -> occupySpaces.forEach { anchors.add(Anchor(true, it, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } else -> throw JsonSyntaxException("Unknown anchor type $v") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt index 247d0c71..bf1b5d87 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldStructure.kt @@ -2,47 +2,175 @@ package ru.dbotthepony.kstarbound.defs.world import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.image.SpriteReference +import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition +import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.json.NativeLegacy +import ru.dbotthepony.kstarbound.json.builder.JsonAlias import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonIgnore import ru.dbotthepony.kstarbound.world.Direction @JsonFactory data class WorldStructure( val region: AABBi = AABBi(Vector2i.ZERO, Vector2i.ZERO), val anchorPosition: Vector2i = Vector2i.ZERO, - var config: JsonElement = JsonObject(), + val config: JsonObject = JsonObject(), val backgroundOverlays: ImmutableList = ImmutableList.of(), val foregroundOverlays: ImmutableList = ImmutableList.of(), + + val blockKey: AssetReference> = AssetReference.empty(), + val blockImage: SpriteReference? = null, + val backgroundBlocks: ImmutableList = ImmutableList.of(), val foregroundBlocks: ImmutableList = ImmutableList.of(), val objects: ImmutableList = ImmutableList.of(), - val flaggedBlocks: ImmutableMap> = ImmutableMap.of(), + val flaggedBlocks: ImmutableMap> = ImmutableMap.of(), ) { - init { - if (config == JsonNull.INSTANCE) { - // so it is not omitted - config = JsonObject() - } - } + @JsonFactory + data class Overlay( + @JsonAlias("position") + val min: Vector2d, + val image: AssetPath, + val fullbright: Boolean = false) @JsonFactory - data class Overlay(val min: Vector2d, val image: String, val fullbright: Boolean) - - @JsonFactory - data class Block(val position: Vector2i, val materialId: Either, val residual: Boolean) + data class Block(val position: Vector2i, val materialId: NativeLegacy.Tile, val residual: Boolean) @JsonFactory data class Obj( - val position: Vector2i, - val name: String, - val direction: Direction, - val parameters: JsonElement, + val position: Vector2i = Vector2i.ZERO, + val name: Registry.Ref, + val direction: Direction = Direction.LEFT, + val parameters: JsonObject = JsonObject(), val residual: Boolean = false, ) + + @JsonFactory + data class BlockKey( + val value: RGBAColor, + val foregroundBlock: Boolean = false, + val backgroundBlock: Boolean = false, + val foregroundMat: Registry.Ref = BuiltinMetaMaterials.STRUCTURE.ref, + val backgroundMat: Registry.Ref = BuiltinMetaMaterials.STRUCTURE.ref, + val foregroundResidual: Boolean = false, + val backgroundResidual: Boolean = false, + val `object`: Registry.Ref = Registries.worldObjects.emptyRef, + val objectDirection: Direction = Direction.LEFT, + val objectParameters: JsonObject = JsonObject(), + val objectResidual: Boolean = false, + val flags: ImmutableSet = ImmutableSet.of(), + val anchor: Boolean = false, + ) + + fun resolve(position: Vector2i): WorldStructure { + val blockKey = blockKey.value.get() ?: return copy( + anchorPosition = position, + backgroundBlocks = ImmutableList.of(), + foregroundBlocks = ImmutableList.of(), + objects = ImmutableList.of(), + flaggedBlocks = ImmutableMap.of(), + ) + + val sprite = blockImage?.sprite ?: return copy( + anchorPosition = position, + backgroundBlocks = ImmutableList.of(), + foregroundBlocks = ImmutableList.of(), + objects = ImmutableList.of(), + flaggedBlocks = ImmutableMap.of(), + ) + + var anchorPosition: Vector2i? = null + + val backgroundBlocks = ArrayList() + val foregroundBlocks = ArrayList() + val objects = ArrayList() + val flaggedBlocks = HashMap>() + + val mapped = Int2ObjectOpenHashMap() + + for (v in blockKey) { + mapped[v.value.toRGBA()] = v + } + + for (x in 0 until sprite.width) { + for (y in sprite.height - 1 downTo 0) { // reverses order of objects + // flip image so image visible top maps to world's top + val color = sprite[x, sprite.height - y - 1] + val block = mapped[color] + + if (block == null) { + LOGGER.error("No such block with color index $color at $x, $y in $sprite") + continue + } + + val pos = Vector2i(x, y) + + if (block.foregroundBlock) { + foregroundBlocks.add(Block(pos, NativeLegacy.Tile(block.foregroundMat), block.foregroundResidual)) + } + + if (block.backgroundBlock) { + backgroundBlocks.add(Block(pos, NativeLegacy.Tile(block.backgroundMat), block.backgroundResidual)) + } + + if (block.`object`.isPresent) { + objects.add(Obj( + pos, + block.`object`, + block.objectDirection, + block.objectParameters, + block.objectResidual + )) + } + + if (block.anchor) { + check(anchorPosition == null) { "Duplicate anchor position (previous was at ${anchorPosition!! - position}, new at $x, $y)" } + anchorPosition = Vector2i(x, y) + } + + for (flag in block.flags) + flaggedBlocks.computeIfAbsent(flag) { HashSet() }.add(pos) + } + } + + val diff = position - (anchorPosition ?: position) + + return copy( + anchorPosition = (anchorPosition ?: position) + diff, + + backgroundBlocks = backgroundBlocks.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()), + foregroundBlocks = foregroundBlocks.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()), + + backgroundOverlays = backgroundOverlays.stream().map { it.copy(min = it.min + diff) }.collect(ImmutableList.toImmutableList()), + foregroundOverlays = foregroundOverlays.stream().map { it.copy(min = it.min + diff) }.collect(ImmutableList.toImmutableList()), + + objects = objects.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()), + + flaggedBlocks = flaggedBlocks.entries + .stream() + .map { it.key to it.value.stream().map { it + diff }.collect(ImmutableSet.toImmutableSet()) } + .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })), + ) + } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt index 4567fa21..221167fc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -165,6 +165,9 @@ class FactoryAdapter private constructor( out.beginObject() for (type in types) { + if (type.isIgnored) + continue + if (type.isFlat) { check(!asJsonArray) @@ -183,12 +186,17 @@ class FactoryAdapter private constructor( } } else { val (field, adapter) = type + val getValue = field.get(value) + + // god fucking damn it + if (!asJsonArray && getValue === null) + continue if (!asJsonArray) out.name(field.name) @Suppress("unchecked_cast") - (adapter as TypeAdapter).write(out, (field as KProperty1).get(value)) + (adapter as TypeAdapter).write(out, field.get(value)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index 98ef4b47..f341784d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -234,6 +234,8 @@ fun luaFunctionN(name: String, callable: ExecutionContext.(ArgumentIterator) -> } override fun invoke(context: ExecutionContext, args: Array) { + context.returnBuffer.setTo() + try { callable.invoke(context, ArgumentIterator.of(context, name, args)) } catch (err: ClassCastException) { @@ -252,6 +254,8 @@ fun luaFunctionNS(name: String, callable: ExecutionContext.(ArgumentIterator) -> } override fun invoke(context: ExecutionContext, args: Array) { + context.returnBuffer.setTo() + try { callable.invoke(context, ArgumentIterator.of(context, name, args)).run(context) } catch (err: ClassCastException) { @@ -270,6 +274,8 @@ fun luaFunctionArray(callable: ExecutionContext.(Array) -> Unit): LuaF } override fun invoke(context: ExecutionContext, args: Array) { + context.returnBuffer.setTo() + try { callable.invoke(context, args) } catch (err: ClassCastException) { @@ -288,6 +294,8 @@ fun luaFunction(callable: ExecutionContext.() -> Unit): LuaFunction<*, *, *, *, } override fun invoke(context: ExecutionContext) { + context.returnBuffer.setTo() + try { callable.invoke(context) } catch (err: ClassCastException) { @@ -306,6 +314,8 @@ fun luaFunction(callable: ExecutionContext.(T) -> Unit): LuaFunction luaFunction(callable: ExecutionContext.(T, T2) -> Unit): LuaFunction } override fun invoke(context: ExecutionContext, arg1: T, arg2: T2) { + context.returnBuffer.setTo() + try { callable.invoke(context, arg1, arg2) } catch (err: ClassCastException) { @@ -342,6 +354,8 @@ fun luaFunction(callable: ExecutionContext.(T, T2, T3) -> Unit): Lua } override fun invoke(context: ExecutionContext, arg1: T, arg2: T2, arg3: T3) { + context.returnBuffer.setTo() + try { callable.invoke(context, arg1, arg2, arg3) } catch (err: ClassCastException) { @@ -360,6 +374,8 @@ fun luaFunction(callable: ExecutionContext.(T, T2, T3, T4) -> Un } override fun invoke(context: ExecutionContext, arg1: T, arg2: T2, arg3: T3, arg4: T4) { + context.returnBuffer.setTo() + try { callable.invoke(context, arg1, arg2, arg3, arg4) } catch (err: ClassCastException) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index 1c9836ba..66087876 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -459,7 +459,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { returnBuffer.setTo(tableOf(*entities.toTypedArray())) } - callbacks["spawnMonster"] = luaStub("spawnMonster") + callbacks["spawnMonster"] = luaFunction { + // TODO + returnBuffer.setTo(0) + } callbacks["spawnNpc"] = luaStub("spawnNpc") callbacks["spawnStagehand"] = luaStub("spawnStagehand") callbacks["spawnProjectile"] = luaStub("spawnProjectile") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt index 72272497..aadf9218 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -551,7 +551,7 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") { val id = it.nextInteger() val function = it.nextString().decode() - val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("Entity with ID $id does not exist") + val entity = self.entities[id.toInt()] ?: return@luaFunctionN //?: throw LuaRuntimeException("Entity with ID $id does not exist") if (entity !is ScriptedEntity) throw LuaRuntimeException("$entity is not scripted entity") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index fa69b415..0f64bfce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -119,12 +119,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : inGame() } - protected open fun onChannelClosed() { - isConnected = false - LOGGER.info("$this is terminated") - scope.cancel("$this is terminated") - } - fun bind(channel: Channel) { scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable -> disconnect("Uncaught exception in one of connection' coroutines: $throwable") @@ -142,7 +136,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : channel.pipeline().addLast(this) channel.closeFuture().addListener { - onChannelClosed() + isConnected = false + LOGGER.info("$channel is closed") + scope.cancel("$channel is closed") + + disconnect("Connection closed") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt index cac34a4e..ba46db88 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt @@ -68,9 +68,11 @@ data class ClientConnectPacket( connection.nickname = connection.server.reserveNickname(playerName, "Player_${connection.connectionID}") connection.shipUpgrades = shipUpgrades + connection.shipUpgradesQueue.trySend(shipUpgrades) connection.uuid = playerUuid + connection.playerSpecies = playerSpecies - connection.receiveShipChunks(shipChunks) + connection.loadShipChunks(shipChunks) connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation)) connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.time)) connection.channel.flush() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 1d8370dc..aff933e2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -1,9 +1,12 @@ package ru.dbotthepony.kstarbound.server +import com.google.gson.JsonNull import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive import io.netty.channel.ChannelHandlerContext -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -21,9 +24,12 @@ import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpMode import ru.dbotthepony.kstarbound.defs.WorldID +import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.SkyType import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionSide @@ -35,17 +41,20 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPac import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker -import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.ActionPacer +import ru.dbotthepony.kstarbound.world.SystemWorld import ru.dbotthepony.kstarbound.world.SystemWorldLocation import ru.dbotthepony.kstarbound.world.UniversePos import java.util.UUID import java.util.concurrent.Future import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.stream.Collectors import kotlin.collections.HashMap +import kotlin.math.min import kotlin.properties.Delegates // serverside part of connection @@ -61,6 +70,9 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn // packets which interact with world must be // executed on world's thread fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit): Boolean { + if (isDisconnecting.get()) + return false + val isInWorld = tracker?.enqueue(task) != null if (!isInWorld) { @@ -73,7 +85,37 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn lateinit var shipWorld: ServerWorld private set + var playerSpecies: String by Delegates.notNull() var uuid: UUID? = null + var systemWorldShip: SystemWorld.Ship? = null + private set + + val shipUpgradesQueue = Channel(256) + + private suspend fun shipUpgradesLoop() { + while (true) { + var upgrades = shipUpgradesQueue.receive() + + val ships = Globals.universeServer.speciesShips[playerSpecies] ?: continue + val oldLevel = shipWorld.getProperty("ship.level", JsonPrimitive(0)).asInt + val newLevel = min(ships.size - 1, upgrades.shipLevel) + + if (oldLevel < newLevel) { + for (i in oldLevel + 1 .. newLevel) { + val structure = ships[i].value.await() ?: continue + shipWorld.replaceCentralStructure(structure).join() + upgrades = upgrades.apply(Starbound.gson.fromJson(structure.config["shipUpgrades"] ?: JsonNull.INSTANCE) ?: continue) + } + } + + shipWorld.setProperty("ship.level", JsonPrimitive(upgrades.shipLevel)) + shipWorld.setProperty("ship.maxFuel", JsonPrimitive(upgrades.maxFuel)) + shipWorld.setProperty("ship.crewSize", JsonPrimitive(upgrades.crewSize)) + shipWorld.setProperty("ship.fuelEfficiency", JsonPrimitive(upgrades.fuelEfficiency)) + systemWorldShip?.speed = upgrades.shipSpeed + shipUpgrades = upgrades + } + } init { connectionID = server.channels.nextConnectionID() @@ -81,6 +123,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn rpc.add("team.fetchTeamStatus") { JsonObject() } + + rpc.add("ship.applyShipUpgrades") { + // we must consider upgrades sequentially since upgrading the ship will modify ship upgrades data + shipUpgradesQueue.trySend(shipUpgrades.apply(Starbound.gson.fromJson(it) ?: return@add InternedJsonElementAdapter.of(true))) + InternedJsonElementAdapter.of(true) + } } override fun toString(): String { @@ -93,61 +141,26 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn return "$nickname <$connectionID/$uuid>" } - private val shipChunks = HashMap>() - private val modifiedShipChunks = ObjectOpenHashSet() - var shipChunkSource by Delegates.notNull() - private set + private val legacyWorldStorage = LegacyWorldStorage.Memory { + // TODO: uncomment this once ALL entity types are implemented + // sendContextUpdates(it) + } override fun setupLegacy() { super.setupLegacy() - shipChunkSource = LegacyWorldStorage.Memory({ shipChunks[it]?.orNull() }, { key, value -> shipChunks[key] = KOptional(value) }) } override fun setupNative() { super.setupNative() - shipChunkSource = LegacyWorldStorage.Memory({ shipChunks[it]?.orNull() }, { key, value -> shipChunks[key] = KOptional(value) }) } - fun receiveShipChunks(chunks: Map>) { - check(shipChunks.isEmpty()) { "Already has ship chunks" } - shipChunks.putAll(chunks) + fun loadShipChunks(chunks: Map>) { + legacyWorldStorage.load(chunks.entries.stream().map { it.key to it.value.orNull() }.filter { it.second != null }.collect(Collectors.toMap({ it.first }, { it.second!! }))) } private var remoteVersion = 0L private var saveClientContextTask: Future<*>? = null - override fun onChannelClosed() { - tickTask?.cancel(false) - sendUniverseTimeTask?.cancel(false) - playerEntity = null - - saveClientContextTask?.cancel(false) - tracker?.remove("Connection channel closed") - tracker = null - - saveClientContext() - super.onChannelClosed() - - warpQueue.close() - server.channels.freeConnectionID(connectionID) - server.channels.connections.remove(this) - server.freeNickname(nickname) - - systemWorld?.removeClient(this) - systemWorld = null - - announceDisconnect("Connection to remote host is lost.") - - if (::shipWorld.isInitialized) { - shipWorld.eventLoop.shutdown() - } - - if (countedTowardsPlayerCount) { - countedTowardsPlayerCount = false - server.channels.decrementPlayerCount() - } - } - private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?) private val warpQueue = Channel(capacity = 10) @@ -296,6 +309,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn this.systemWorld = world var ship = world.addClient(this, location = actualInWorldLocation).await() + systemWorldShip = ship shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world)) shipCoordinate = UniversePos(world.location) systemWorldLocation = actualInWorldLocation @@ -375,6 +389,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination this.systemWorld = world ship = world.addClient(this).await() + systemWorldShip = ship val newParams = ship.location.skyParameters(world) @@ -486,67 +501,122 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn warpQueue.trySend(WarpRequest(destination, deploy, ifFailed)) } - private var tickTask: Future<*>? = null + private var sendContextUpdatesTask: Future<*>? = null private var sendUniverseTimeTask: Future<*>? = null - private fun tick() { + private fun sendContextUpdates(shipWorldChanges: Map> = mapOf()) { if (isConnected && isReady) { val entries = rpc.write() - if (entries != null || modifiedShipChunks.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) { + if (entries != null || shipWorldChanges.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) { val (data, version) = server2clientGroup.write(remoteVersion, isLegacy) remoteVersion = version send(ClientContextUpdatePacket( entries ?: listOf(), - KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }), + KOptional(shipWorldChanges), KOptional(data))) - - modifiedShipChunks.clear() } } } + // set to true so failed connection attempts don't appear in chat private var announcedDisconnect = true + private val isDisconnecting = AtomicBoolean() - private fun announceDisconnect(reason: String) { - if (!announcedDisconnect && nickname.isNotBlank()) { - if (reason.isBlank()) { - server.chat.systemMessage("Player '$nickname' disconnected") - } else { - server.chat.systemMessage("Player '$nickname' disconnected ($reason)") + private suspend fun disconnect0(reason: String) { + // initiate shipworld shutdown + if (::shipWorld.isInitialized) { + shipWorld.eventLoop.shutdown() + } + + // stop being counted towards player count, vanish from connection list + server.channels.freeConnectionID(connectionID) + server.channels.connections.remove(this) + server.freeNickname(nickname) + + if (countedTowardsPlayerCount) { + countedTowardsPlayerCount = false + server.channels.decrementPlayerCount() + } + + // big try-catch block to cleanup mess if we had exception during disconnection + try { + // announce disconnect + if (!announcedDisconnect) { + if (reason.isBlank()) { + server.chat.systemMessage("Player '$nickname' disconnected") + } else { + server.chat.systemMessage("Player '$nickname' disconnected ($reason)") + } + + announcedDisconnect = true } - announcedDisconnect = true + // cancel periodic tasks + sendContextUpdatesTask?.cancel(false) + sendUniverseTimeTask?.cancel(false) + saveClientContextTask?.cancel(false) + + // remove from world before saving client context + tracker?.remove("Disconnect: $reason") + tracker = null + + // write server state + saveClientContext() + + // remove from system world + systemWorld?.removeClient(this) + systemWorld = null + + playerEntity = null + + // stop coroutines + scope.cancel(CancellationException("Client disconnect: $reason")) + + if (channel.isOpen && ::shipWorld.isInitialized) { + // if channel is still open, send one last update packet + shipWorld.eventLoop.shutdown() + + while (!shipWorld.eventLoop.isTerminated) { + delay(10L) + } + + legacyWorldStorage.commit() + legacyWorldStorage.waitAsync() + + // send pending updates + sendContextUpdates() + channel.flush() + } + + isReady = false + + if (channel.isOpen) { + // say goodbye, if channel is still open + channel.write(ServerDisconnectPacket(reason)) + channel.flush() + channel.close() + } + } catch (err: Throwable) { + LOGGER.error("Exception while disconnecting $this", err) + } finally { + // don't leave residue entities in worlds if we encountered an exception + tracker?.remove("Disconnect: $reason") + tracker = null + systemWorld?.removeClient(this) + systemWorld = null } } override fun disconnect(reason: String) { - LOGGER.info("${alias()} disconnect initiated with reason $reason") - announceDisconnect(reason) - - if (channel.isOpen) { - // send pending updates - tick() - channel.flush() - } - - isReady = false - - tracker?.remove("Disconnect") - tracker = null - saveClientContext() - - if (channel.isOpen) { - // say goodbye - channel.write(ServerDisconnectPacket(reason)) - channel.flush() - channel.close() + if (isDisconnecting.compareAndSet(false, true)) { + server.scope.launch { disconnect0(reason) } } } override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { - if (!channel.isOpen) + if (!channel.isOpen || isDisconnecting.get()) return if (msg is IServerPacket) { @@ -587,10 +657,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn null } - shipUpgrades = shipUpgrades.addCapability("planetTravel") - shipUpgrades = shipUpgrades.addCapability("teleport") - shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3) - scope.launch { warpEventLoop() } if (context == null) { @@ -620,8 +686,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn countedTowardsPlayerCount = true server.channels.incrementPlayerCount() - tickTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { - tick() + sendContextUpdatesTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { + sendContextUpdates() }, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) sendUniverseTimeTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { @@ -629,10 +695,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn send(UniverseTimeUpdatePacket(server.universeClock.time), false) }, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS) - if (isLegacy) { - scope.launch { celestialRequestsHandler() } + scope.launch { celestialRequestsHandler() } - server.loadShipWorld(this, shipChunkSource).thenAccept { + if (isLegacy) { + server.loadShipWorld(this, legacyWorldStorage).thenAccept { if (!isConnected || !channel.isOpen) { LOGGER.warn("$this disconnected before loaded their ShipWorld") it.eventLoop.shutdown() @@ -640,7 +706,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipWorld = it shipWorld.sky.referenceClock = server.universeClock // shipWorld.sky.startFlying(true, true) - shipWorld.eventLoop.start() + + if (!shipWorld.eventLoop.isAlive) + shipWorld.eventLoop.start() + + scope.launch { shipUpgradesLoop() } scope.launch { loadDataAndDispatchEventLoops() } } }.exceptionally { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 2729b194..963c7d76 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -22,12 +22,14 @@ import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters import ru.dbotthepony.kstarbound.defs.world.WorldTemplate +import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElementInflated import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated import ru.dbotthepony.kstarbound.json.writeJsonObject +import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerUniverse @@ -39,6 +41,7 @@ import ru.dbotthepony.kstarbound.util.JVMClock import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.toStarboundString import ru.dbotthepony.kstarbound.util.uuidFromStarboundString +import ru.dbotthepony.kstarbound.world.WorldGeometry import java.io.File import java.sql.DriverManager import java.util.Collections @@ -419,17 +422,48 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread } fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture { - return supplyAsync { + return scope.async { val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null")) val existing = worlds[id] if (existing != null) throw IllegalStateException("Already has $id!") - val world = ServerWorld.load(this, storage, id) - worlds[id] = world - world - }.thenCompose { it } + try { + val world = ServerWorld.load(this@StarboundServer, storage, id) + worlds[id] = world + return@async world.await() + } catch (err: ServerWorld.WorldMetadataMissingException) { + LOGGER.info("Creating new client shipworld for $connection") + val world = ServerWorld.create(this@StarboundServer, WorldGeometry(Vector2i(2048, 2048)), storage, id) + + try { + val structure = Globals.universeServer.speciesShips[connection.playerSpecies]?.firstOrNull()?.value?.get() ?: throw NoSuchElementException("No ship structure for species ${connection.playerSpecies}") + world.eventLoop.start() + world.replaceCentralStructure(structure).join() + + val currentUpgrades = connection.shipUpgrades + .apply(Globals.shipUpgrades) + .apply(Starbound.gson.fromJson(structure.config.get("shipUpgrades") ?: throw NoSuchElementException("No shipUpgrades element in world structure config for species ${connection.playerSpecies}")) ?: throw NullPointerException("World structure config.shipUpgrades is null for species ${connection.playerSpecies}")) + + connection.shipUpgrades = currentUpgrades + world.setProperty("invinciblePlayers", JsonPrimitive(true)) + world.setProperty("ship.level", JsonPrimitive(0)) + world.setProperty("ship.fuel", JsonPrimitive(0)) + world.setProperty("ship.maxFuel", JsonPrimitive(currentUpgrades.maxFuel)) + world.setProperty("ship.crewSize", JsonPrimitive(currentUpgrades.crewSize)) + world.setProperty("ship.fuelEfficiency", JsonPrimitive(currentUpgrades.fuelEfficiency)) + + world.saveMetadata() + } catch (err: Throwable) { + world.eventLoop.shutdown() + throw err + } + + worlds[id] = CompletableFuture.completedFuture(world) + return@async world + } + }.asCompletableFuture() } fun notifyWorldUnloaded(worldID: WorldID) { @@ -453,7 +487,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread isDaemon = false } - private val occupiedNicknames = ObjectArraySet() + private val occupiedNicknames = ObjectOpenHashSet() fun reserveNickname(name: String, alternative: String): String { synchronized(occupiedNicknames) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt index a373386a..f5a361c6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt @@ -5,6 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine import com.google.gson.JsonObject import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.future.await @@ -19,6 +20,7 @@ import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.xxhash32 import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.VersionRegistry @@ -31,6 +33,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.ScheduledCoroutineExecutor +import ru.dbotthepony.kstarbound.util.supplyAsync import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkState @@ -47,9 +50,7 @@ import java.io.DataOutputStream import java.io.File import java.lang.ref.Cleaner import java.sql.DriverManager -import java.time.Duration import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.function.Function import java.util.function.Supplier @@ -61,7 +62,7 @@ import java.util.zip.InflaterInputStream sealed class LegacyWorldStorage() : WorldStorage() { protected abstract fun load(at: ByteKey): CompletableFuture protected abstract fun write(at: ByteKey, value: ByteArray) - protected abstract val executor: Executor + protected val executor = CarriedExecutor(Starbound.IO_EXECUTOR) protected val scope by lazy { CoroutineScope(ScheduledCoroutineExecutor(executor) + SupervisorJob()) @@ -357,30 +358,51 @@ sealed class LegacyWorldStorage() : WorldStorage() { write(metadataKey, buff.array.copyOf(buff.length)) } - class Memory(private val get: (ByteKey) -> ByteArray?, private val set: (ByteKey, ByteArray) -> Unit) : LegacyWorldStorage() { - private val pending = HashMap() - override val executor: Executor = Executor { it.run() } + class Memory(private val listener: (changes: Map>) -> Unit) : LegacyWorldStorage() { + private val changed = ObjectOpenHashSet() + private val memory = HashMap() + + fun load(memory: Map) { + executor.execute { + changed.clear() + this.memory.clear() + this.memory.putAll(memory) + } + } override fun load(at: ByteKey): CompletableFuture { - return CompletableFuture.completedFuture(pending[at] ?: get(at)) + return executor.supplyAsync { memory[at] } } override fun write(at: ByteKey, value: ByteArray) { - // set(at, value) - pending[at] = value + executor.execute { + if (at !in memory || !memory[at].contentEquals(value)) { + memory[at] = value + changed.add(at) + } + } } - override fun close() {} + override fun close() { + executor.execute { commit() } + executor.wait(300L, TimeUnit.SECONDS) + } + + suspend fun waitAsync() { + executor.execute { commit() } + executor.waitAsync() + } override fun commit() { - pending.entries.forEach { (k, v) -> set(k, v) } - pending.clear() + executor.execute { + val result = changed.associateWith { KOptional.ofNullable(memory[it]) } + changed.clear() + listener(result) + } } } class DB5(private val database: BTreeDB5) : LegacyWorldStorage() { - override val executor = CarriedExecutor(Starbound.IO_EXECUTOR) - override fun load(at: ByteKey): CompletableFuture { return CompletableFuture.supplyAsync(Supplier { database.read(at).orNull() }, executor) } @@ -400,7 +422,6 @@ sealed class LegacyWorldStorage() : WorldStorage() { } class SQL(path: File) : LegacyWorldStorage() { - override val executor = CarriedExecutor(Starbound.IO_EXECUTOR) private val connection = DriverManager.getConnection("jdbc:sqlite:${path.canonicalPath.replace('\\', '/')}") private val cleaner: Cleaner.Cleanable @@ -464,10 +485,5 @@ sealed class LegacyWorldStorage() : WorldStorage() { companion object { private val LOGGER = LogManager.getLogger() private val metadataKey = ByteKey(0, 0, 0, 0, 0) - - fun memory(): Memory { - val map = HashMap() - return Memory(map::get, map::set) - } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 5b24c179..b066c59e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -12,6 +12,7 @@ import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID @@ -557,7 +558,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(it.value.y) }.collect(ImmutableList.toImmutableList()) + } else { + dungeonGravityInternal.entries.stream().map { it.key to Either.right(it.value) }.collect(ImmutableList.toImmutableList()) + } ) - storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata)))) + if (storage is LegacyWorldStorage) { + Starbound.legacyStoreJson { + storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata)))) + } + } else { + Starbound.storeJson { + storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata)))) + } + } } private var uncleanShutdown = false @@ -199,6 +224,14 @@ class ServerWorld private constructor( eventLoop.scheduleAtFixedRate(Runnable { tick(Starbound.TIMESTEP) }, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) + + if (worldID is WorldID.ShipWorld) { + // due to dumb logic on legacy client side, committing frequently causes very noticeable performance degradation + eventLoop.scheduleWithFixedDelay(Runnable { + if (!uncleanShutdown) + storage.commit() + }, 20L, 20L, TimeUnit.SECONDS) + } } private var placementTaskID = 0L @@ -289,6 +322,117 @@ class ServerWorld private constructor( override val connectionID: Int get() = 0 + private suspend fun replaceCentralStructure0(structure: WorldStructure) { + val tickets = ArrayList() + + try { + var region = AABB + .ofPoints(centralStructure.objects.map { it.position }) + .combine(AABB.ofPoints(centralStructure.foregroundBlocks.map { it.position })) + .combine(AABB.ofPoints(centralStructure.backgroundBlocks.map { it.position })) + .enlarge(4.0, 4.0) + + run { + val getTickets = permanentChunkTicket(region, ChunkState.FULL).await() + tickets.addAll(getTickets) + getTickets.forEach { it.chunk.await() } + } + + // remove old structure + for (obj in centralStructure.objects) { + if (!obj.residual) { + val entities = entityIndex.tileEntitiesAt(obj.position) + + for (entity in entities) { + if (entity is WorldObject && entity.config == obj.name.entry && entity.tilePosition == obj.position) { + entity.remove(AbstractEntity.RemovalReason.REMOVED) + } + } + } + } + + for (block in centralStructure.backgroundBlocks) { + if (!block.residual) { + val cell = getCell(block.position).mutable() + + if (cell.background.material.ref == block.materialId.native) { + cell.background.empty() + check(setCell(block.position, cell)) + } + } + } + + for (block in centralStructure.foregroundBlocks) { + if (!block.residual) { + val cell = getCell(block.position).mutable() + + if (cell.foreground.material.ref == block.materialId.native) { + cell.foreground.empty() + check(setCell(block.position, cell)) + } + } + } + + // put new structure + centralStructure = structure.resolve(geometry.size / 2) + + region = AABB + .ofPoints(centralStructure.objects.map { it.position }) + .combine(AABB.ofPoints(centralStructure.foregroundBlocks.map { it.position })) + .combine(AABB.ofPoints(centralStructure.backgroundBlocks.map { it.position })) + .enlarge(4.0, 4.0) + + run { + val getTickets = permanentChunkTicket(region, ChunkState.FULL).await() + tickets.addAll(getTickets) + getTickets.forEach { it.chunk.await() } + } + + playerSpawnPosition = centralStructure.flaggedBlocks["playerSpawn"]?.firstOrNull()?.toDoubleVector() ?: (geometry.size.toDoubleVector() / 2.0) + + for (block in centralStructure.backgroundBlocks) { + val cell = getCell(block.position).mutable() + + if (cell.background.material.isEmptyTile) { + cell.background.material = block.materialId.native.entry ?: BuiltinMetaMaterials.EMPTY + check(setCell(block.position, cell)) + } + } + + for (block in centralStructure.foregroundBlocks) { + val cell = getCell(block.position).mutable() + + if (cell.foreground.material.isEmptyTile) { + cell.foreground.material = block.materialId.native.entry ?: BuiltinMetaMaterials.EMPTY + check(setCell(block.position, cell)) + } + } + + for (obj in centralStructure.objects) { + val config = obj.name.entry ?: continue + val orientation = config.value.findValidOrientation(this, obj.position, obj.direction, true) + + if (orientation == -1) { + if (obj.residual) { + LOGGER.debug("Tried to put residual object '{}' at {} for central structure, but it can't be placed there!", config.key, obj.position) + } else { + LOGGER.error("Tried to put object '${config.key}' at ${obj.position} for central structure, but it can't be placed there!") + } + } else { + val create = WorldObject.create(config, obj.position, obj.parameters.deepCopy()) ?: continue + create.orientationIndex = orientation.toLong() + create.joinWorld(this) + } + } + } finally { + tickets.forEach { it.cancel() } + } + } + + fun replaceCentralStructure(structure: WorldStructure): Job { + return eventLoop.scope.launch { replaceCentralStructure0(structure) } + } + override fun switchDungeonIDProtection(id: Int, enable: Boolean): Boolean { val updated = super.switchDungeonIDProtection(id, enable) @@ -717,8 +861,14 @@ class ServerWorld private constructor( } } + private var scheduledMetadataSave: ScheduledFuture<*>? = null + override fun setProperty0(key: String, value: JsonElement) { broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) })) + + if (scheduledMetadataSave == null || scheduledMetadataSave!!.isDone) { + scheduledMetadataSave = eventLoop.schedule(Runnable { saveMetadata() }, 10L, TimeUnit.SECONDS) + } } override fun chunkFactory(pos: ChunkPos): ServerChunk { @@ -831,9 +981,13 @@ class ServerWorld private constructor( val centralStructure: WorldStructure, val protectedDungeonIds: ImmutableSet, val worldProperties: JsonObject, - val spawningEnabled: Boolean + val spawningEnabled: Boolean, + val dungeonIdGravity: ImmutableList>> = ImmutableList.of(), + val dungeonIdBreathable: ImmutableList> = ImmutableList.of(), ) + class WorldMetadataMissingException : NoSuchElementException("No world metadata is present") + companion object { private val LOGGER = LogManager.getLogger() @@ -868,7 +1022,8 @@ class ServerWorld private constructor( LOGGER.info("Attempting to load world at $worldID") return storage.loadMetadata().thenApply { - it ?: throw NoSuchElementException("No world metadata is present") + it ?: throw WorldMetadataMissingException() + LOGGER.info("Loading world at $worldID") AssetPathStack("/") { _ -> val meta = Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt index 0ced266f..4542d512 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.util +import kotlinx.coroutines.delay import org.apache.logging.log4j.LogManager import java.lang.ref.Reference import java.util.concurrent.ConcurrentLinkedDeque @@ -85,6 +86,12 @@ class CarriedExecutor(private val parent: Executor, private val allowExecutionIn } } + suspend fun waitAsync() { + while (isCarried.get()) { + delay(10L) + } + } + companion object { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt index 10f92ae7..e89f1ebf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt @@ -80,10 +80,18 @@ class RelativeClock() : IClock { ) fun toJson(): JsonElement { - return Starbound.gson.toJsonTree(JsonData(time, if (pointOfReferenceSet) pointOfReference else null)) + return JsonObject().also { + it["elapsedTime"] = time + it["lastEpochTime"] = if (pointOfReferenceSet) JsonPrimitive(pointOfReference) else JsonNull.INSTANCE + } } fun fromJson(json: JsonElement) { + if (json.isJsonNull) { + pointOfReferenceSet = false + return + } + val data = Starbound.gson.fromJson(json, JsonData::class.java) time = data.elapsedTime diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index e7153643..02638fb1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -306,7 +306,8 @@ abstract class World, ChunkType : Chunk() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt index 3e912870..db8133b7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -132,6 +132,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co craftingProgress = data.get("craftingProgress", 0.0) isInitialized = data.get("initialized", true) items.fromJson(data.get("items", JsonArray()), resize = true) + ageItemsTimer.fromJson(data.get("ageItemsTimer") ?: JsonNull.INSTANCE) } override fun serialize(data: JsonObject) { @@ -144,6 +145,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co data["craftingProgress"] = craftingProgress data["initialized"] = isInitialized data["items"] = items.toJson(true) + data["ageItemsTimer"] = ageItemsTimer.toJson() } private fun randomizeContents(random: RandomGenerator, threatLevel: Double) { @@ -197,7 +199,6 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co if (!isRemote) { if (isInitialized) return - isInitialized = true val seed = lookupProperty("treasureSeed") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 325b688a..72badc6d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -14,6 +14,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.apache.logging.log4j.LogManager import org.classdump.luna.ByteString import org.classdump.luna.Table +import ru.dbotthepony.kommons.gson.JsonArrayCollector import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kstarbound.math.vector.Vector2i @@ -47,6 +48,7 @@ import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.io.Vector2iCodec import ru.dbotthepony.kstarbound.json.JsonPath +import ru.dbotthepony.kstarbound.json.jsonArrayOf import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec @@ -110,6 +112,28 @@ open class WorldObject(val config: Registry.Entry) : TileEntit if ("uniqueId" in data) uniqueID.accept(data["uniqueId"]?.asStringOrNull) + + if ("inputWireNodes" in data) { + val inputWireNodes = data.getAsJsonArray("inputWireNodes") + + for ((i, value) in inputWireNodes.withIndex()) { + if (i >= inputNodes.size) + break + + inputNodes[i].deserialize(value) + } + } + + if ("outputWireNodes" in data) { + val outputWireNodes = data.getAsJsonArray("outputWireNodes") + + for ((i, value) in outputWireNodes.withIndex()) { + if (i >= outputNodes.size) + break + + outputNodes[i].deserialize(value) + } + } } open fun loadParameters(parameters: JsonObject) { @@ -129,6 +153,9 @@ open class WorldObject(val config: Registry.Entry) : TileEntit data["orientationIndex"] = orientationIndex data["interactive"] = isInteractive + data["inputWireNodes"] = inputNodes.stream().map { it.serialize() }.collect(JsonArrayCollector) + data["outputWireNodes"] = outputNodes.stream().map { it.serialize() }.collect(JsonArrayCollector) + val scriptStorage = lua.globals["storage"] if (scriptStorage != null && scriptStorage is Table) { @@ -159,6 +186,12 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } } + init { + if (config.value.uniqueId != null) { + uniqueID.accept(config.value.uniqueId) + } + } + protected val orientationLazies = ArrayList>() protected val parametersLazies = ArrayList>() protected val spacesLazies = ArrayList>() @@ -304,6 +337,31 @@ open class WorldObject(val config: Registry.Entry) : TileEntit lua.invokeGlobal("onNodeConnectionChange") } } + + + + fun serialize(): JsonObject { + return JsonObject().also { + it["connections"] = connectionsInternal.stream().map { jsonArrayOf(it.entityLocation, it.index) }.collect(JsonArrayCollector) + it["state"] = state + } + } + + fun deserialize(value: JsonElement) { + connectionsInternal.clear() + state = false + + if (value is JsonObject) { + state = value.get("state", false) + val connections = value.get("connections", JsonArray()) + + for (obj in connections) { + if (obj is JsonArray) { + connectionsInternal.add(WireConnection(vectors.fromJsonTree(obj[0]), obj[1].asInt)) + } + } + } + } } val inputNodes: ImmutableList = lookupProperty("inputNodes") { JsonArray() } @@ -813,6 +871,10 @@ open class WorldObject(val config: Registry.Entry) : TileEntit isInteractive = !interactAction.isJsonNull } + override fun toString(): String { + return "WorldObject[${config.key}, at $tilePosition]" + } + companion object { private val LOGGER = LogManager.getLogger() private val lightColorPath = JsonPath("lightColor")