diff --git a/ADDITIONS.md b/ADDITIONS.md index 6f28aea8..369a72b6 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -52,3 +52,12 @@ val color: TileColor = TileColor.DEFAULT ### player.config * Inventory bags are no longer limited to 255 slots * However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too) + +--------------- + +### Prototypes + +#### .matierial + * Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`) + * Used by object and plant anchoring code to determine valid placement + * Used by world tile rendering code (render piece rule `Connects`) diff --git a/gradle.properties b/gradle.properties index 0692d0a5..d7d159d6 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.12.3 +kommonsVersion=2.13.1 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index 8cabe20c..bc6e77d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -106,7 +106,8 @@ object Registries { private inline fun loadRegistry( registry: Registry, files: List, - noinline keyProvider: (T) -> Pair + noinline keyProvider: (T) -> Pair, + noinline after: (T, IStarboundFile) -> Unit = { _, _ -> } ): List> { val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) } @@ -120,6 +121,8 @@ object Registries { val read = adapter.fromJsonTree(elem) val keys = keyProvider(read) + after(read, listedFile) + registry.add { if (keys.second != null) registry.add(keys.first, keys.second!!, read, elem, listedFile) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index f324d6b8..99ee7733 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -5,7 +5,6 @@ import com.github.benmanes.caffeine.cache.Scheduler import com.google.gson.* import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.AABBTypeAdapter @@ -59,11 +58,10 @@ import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.util.BlockableEventLoop +import ru.dbotthepony.kstarbound.util.ExecutorWithScheduler 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.MailboxExecutorService import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.* @@ -138,7 +136,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca @JvmField val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool() @JvmField - val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher() + val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher() @JvmField val CLEANER: Cleaner = Cleaner.create { @@ -152,7 +150,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca // Hrm. // val strings: Interner = Interner.newWeakInterner() // val strings: Interner = Interner { it } - @JvmField + @JvmField val STRINGS: Interner = interner(5) // immeasurably lazy and fragile solution, too bad! diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 17ef3029..fedc064f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -62,6 +62,7 @@ import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.formatBytesShort +import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.RayDirection import ru.dbotthepony.kstarbound.world.LightCalculator @@ -548,8 +549,8 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo return world!!.getCell(x + viewportCellX, y + viewportCellY) } - override fun getCellDirect(x: Int, y: Int): AbstractCell { - return world!!.getCellDirect(x + viewportCellX, y + viewportCellY) + override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean { + return world!!.setCell(x + viewportCellX, y + viewportCellY, cell, chunkState) } override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt index 2834a9c2..a1f0f74e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt @@ -2,9 +2,19 @@ package ru.dbotthepony.kstarbound.client.world import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.ChunkState +import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell -class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos){ +class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos) { + override val state: ChunkState + get() = ChunkState.FULL + + // we don't care about current status since we are always considered to be fully loaded + override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean { + return setCell(x, y, cell) + } + override fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) { super.foregroundChanges(x, y, cell) 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 3f6bb8bd..a3cb3367 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -47,7 +47,7 @@ class ClientWorld( throw RuntimeException("unreachable code") } - override val isRemote: Boolean + override val isClient: Boolean get() = true val renderRegionWidth = determineChunkSize(geometry.size.x) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt index d8f43c38..4477d219 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt @@ -33,7 +33,7 @@ data class UniverseServerConfig( if (floatingDungeon != null) { if (t !is FloatingDungeonWorldParameters) return false - if (t.primaryDungeon != floatingDungeon) return false + if (t.primaryDungeon.key.left() != floatingDungeon) return false } return true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt index 2ee3e088..92f6fb98 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt @@ -7,4 +7,5 @@ class WorldServerConfig( val playerStartRegionMaximumTries: Int = 1, val playerStartRegionMaximumVerticalSearch: Int = 1, val playerStartRegionSize: Vector2d, + val spawnDungeonRetries: Int = 1, ) 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 5e19eac3..e64e1b82 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs.dungeon import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet +import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import it.unimi.dsi.fastutil.objects.Object2IntArrayMap import kotlinx.coroutines.CoroutineScope @@ -16,6 +17,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.random.random import java.util.concurrent.CompletableFuture import java.util.random.RandomGenerator @@ -41,7 +43,7 @@ data class DungeonDefinition( val metadata: Metadata, // relevant for PNG defined dungeons val tiles: ImageTileSet = ImageTileSet(), - val parts: ImmutableList, + private val parts: ImmutableList, ) { @JsonFactory data class Metadata( @@ -67,22 +69,39 @@ data class DungeonDefinition( get() = metadata.name init { - parts.forEach { it.bind(this) } + tiles.spewWarnings(name) + } - for (anchor in metadata.anchor) { - if (!parts.any { it.name == anchor }) { - throw JsonSyntaxException("Dungeon contains $anchor as anchor, but there is no such part") + private val directory = AssetPathStack.last() + + val actualParts: ImmutableList by lazy { + AssetPathStack(directory) { + val build = parts.stream().map { Starbound.gson.fromJson(it, DungeonPart::class.java) }.collect(ImmutableList.toImmutableList()) + build.forEach { it.bind(this) } + + for (anchor in metadata.anchor) { + if (!build.any { it.name == anchor }) { + throw JsonSyntaxException("Dungeon contains $anchor as anchor, but there is no such part") + } } + + build } } - val partMap: ImmutableMap = parts.stream().map { it.name to it }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) - val anchorParts: ImmutableList = metadata.anchor.stream().map { anchor -> parts.first { it.name == anchor } }.collect(ImmutableList.toImmutableList()) + + val partMap: ImmutableMap by lazy { + actualParts.stream().map { it.name to it }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + } + + val anchorParts: ImmutableList by lazy { + metadata.anchor.stream().map { anchor -> actualParts.first { it.name == anchor } }.collect(ImmutableList.toImmutableList()) + } private fun connectableParts(connector: DungeonPart.JigsawConnector): List { val result = ArrayList() - for (part in parts) { + for (part in actualParts) { if (!part.doesNotConnectTo(connector.part)) { for (pconnector in part.connectors) { if (pconnector.connectsTo(connector)) { @@ -183,7 +202,7 @@ data class DungeonDefinition( } } - world.pressurizeLiquids() + world.applyFinalTouches() } fun generate(world: ServerWorld, random: RandomGenerator, x: Int, y: Int, markSurfaceAndTerrain: Boolean, forcePlacement: Boolean, dungeonID: Int = 0, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture { @@ -201,7 +220,7 @@ data class DungeonDefinition( return CoroutineScope(Starbound.COROUTINE_EXECUTOR) .async { - if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, world)) { + if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) { generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID) if (commit) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt index 9812f2c8..f365b518 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt @@ -189,7 +189,7 @@ abstract class DungeonRule { if (world.markSurfaceLevel != null) return y < world.markSurfaceLevel - val cell = world.parent.chunkMap.getCell(x, y) + val cell = world.parent.getCell(x, y) if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)) return false @@ -198,7 +198,7 @@ abstract class DungeonRule { } override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.chunkMap.getCell(x, y) + val cell = world.getCell(x, y) return cell.foreground.material.isNotEmptyTile } @@ -215,12 +215,12 @@ abstract class DungeonRule { if (world.markSurfaceLevel != null) return y >= world.markSurfaceLevel - val cell = world.parent.chunkMap.getCell(x, y) + val cell = world.parent.getCell(x, y) return cell.foreground.material.isEmptyTile || cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y) } override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.chunkMap.getCell(x, y) + val cell = world.getCell(x, y) return cell.foreground.material.isEmptyTile } @@ -237,7 +237,7 @@ abstract class DungeonRule { if (world.markSurfaceLevel != null) return y < world.markSurfaceLevel - val cell = world.parent.chunkMap.getCell(x, y) + val cell = world.parent.getCell(x, y) if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)) return false @@ -246,7 +246,7 @@ abstract class DungeonRule { } override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.chunkMap.getCell(x, y) + val cell = world.getCell(x, y) return cell.background.material.isNotEmptyTile } @@ -263,12 +263,12 @@ abstract class DungeonRule { if (world.markSurfaceLevel != null) return y >= world.markSurfaceLevel - val cell = world.parent.chunkMap.getCell(x, y) + val cell = world.parent.getCell(x, y) return cell.background.material.isEmptyTile || cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y) } override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.chunkMap.getCell(x, y) + val cell = world.getCell(x, y) return cell.background.material.isEmptyTile } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt index 093ac44d..5b4a4ebe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt @@ -69,7 +69,7 @@ data class DungeonTile( // but thats also not a priority, since this check happens quite quickly // to have any noticeable impact on world's performance fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean { - val cell = world.parent.chunkMap.getCell(x, y) + val cell = world.parent.getCell(x, y) if (cell.dungeonId != NO_DUNGEON_ID) return false @@ -81,7 +81,7 @@ data class DungeonTile( } fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.chunkMap.getCell(x, y) + val cell = world.getCell(x, y) if (cell.dungeonId != NO_DUNGEON_ID) return false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index 8a937be8..b6f48478 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonObject import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import kotlinx.coroutines.future.await +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i @@ -16,16 +17,20 @@ import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile +import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.server.world.ServerChunk import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState import ru.dbotthepony.kstarbound.world.api.MutableTileState import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.Poly import java.util.Collections +import java.util.concurrent.CompletableFuture import java.util.function.Consumer import java.util.random.RandomGenerator @@ -63,18 +68,28 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar val parameters: JsonObject = JsonObject() ) - private val liquid = HashMap() - private val foregroundMaterial = HashMap() - private val foregroundModifier = HashMap() - private val backgroundMaterial = HashMap() - private val backgroundModifier = HashMap() + var hasGenerated = false + private set + + val targetChunkState = if (parent.template.worldParameters is FloatingDungeonWorldParameters) ChunkState.FULL else ChunkState.TERRAIN + + private val liquid = HashMap(8192, 0.5f) + private val foregroundMaterial = HashMap(8192, 0.5f) + private val foregroundModifier = HashMap(8192, 0.5f) + private val backgroundMaterial = HashMap(8192, 0.5f) + private val backgroundModifier = HashMap(8192, 0.5f) // for entity spaces which should be considered empty if they // are occupied by tile entity - private val clearTileEntitiesAt = HashSet() + private val clearTileEntitiesAt = HashSet(8192, 0.5f) // entities themselves to be removed - private val tileEntitiesToRemove = HashSet() + private val tileEntitiesToRemove = HashSet(2048, 0.5f) + + private val touchedTiles = HashSet(16384, 0.5f) + private val protectTile = HashSet(16384, 0.5f) + + private val boundingBoxes = ArrayList() fun clearTileEntityAt(x: Int, y: Int) { clearTileEntitiesAt.add(geometry.wrap(Vector2i(x, y))) @@ -92,11 +107,6 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar tileEntitiesToRemove.add(entity) } - private val touchedTiles = HashSet() - private val protectTile = HashSet() - - private val boundingBoxes = ArrayList() - private var currentBoundingBox: AABBi? = null fun touched(): Set = Collections.unmodifiableSet(touchedTiles) @@ -128,15 +138,15 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar return protectTile.contains(geometry.wrap(Vector2i(x, y))) } - private val biomeItems = HashSet() + private val biomeItems = HashSet(8192, 0.5f) private val biomeTrees = HashSet() private val itemDrops = HashMap>() private val randomizedItemDrops = HashMap>() - private val dungeonIDs = HashMap() + private val dungeonIDs = HashMap(16384, 0.5f) private var dungeonID = -1 - private val pendingLiquids = HashMap() + private val pendingLiquids = HashMap(8192, 0.5f) private val openLocalWires = HashMap>() private val globalWires = HashMap>() @@ -268,7 +278,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar val tickets = ArrayList() return try { - tickets.addAll(parent.permanentChunkTicket(region, ServerChunk.State.TERRAIN)) + tickets.addAll(parent.permanentChunkTicket(region, targetChunkState)) tickets.forEach { it.chunk.await() } block() } finally { @@ -290,7 +300,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar return waitForRegionAndJoin(AABBi(position, position + size), block) } - fun pressurizeLiquids() { + fun applyFinalTouches() { // For each liquid type, find each contiguous region of liquid, then // pressurize that region based on the highest position in the region @@ -349,6 +359,8 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar } pendingLiquids.clear() + + hasGenerated = true } private fun applyCellChangesAt(pos: Vector2i, chunk: ServerChunk) { @@ -422,64 +434,95 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar }.await() for (box in boundingBoxes) { - tickets.addAll(parent.permanentChunkTicket(box, ServerChunk.State.TERRAIN)) + tickets.addAll(parent.permanentChunkTicket(box, targetChunkState)) } // apply tiles to world per-chunk // this way we don't need to wait on all chunks to be loaded // and apply changes chunks which have been loaded right away - val tilePositionsRaw = ArrayList() + val tilePositions = HashSet( + foregroundMaterial.keys.size + .coerceAtLeast(foregroundModifier.keys.size) + .coerceAtLeast(backgroundMaterial.keys.size) + .coerceAtLeast(backgroundModifier.keys.size) + .coerceAtLeast(liquid.keys.size) + ) - tilePositionsRaw.addAll(foregroundMaterial.keys) - tilePositionsRaw.addAll(foregroundModifier.keys) - tilePositionsRaw.addAll(backgroundMaterial.keys) - tilePositionsRaw.addAll(backgroundModifier.keys) - - tilePositionsRaw.sortWith { o1, o2 -> - val cmp = o1.x.compareTo(o2.x) - if (cmp == 0) o1.y.compareTo(o2.y) else cmp - } + tilePositions.addAll(foregroundMaterial.keys) + tilePositions.addAll(foregroundModifier.keys) + tilePositions.addAll(backgroundMaterial.keys) + tilePositions.addAll(backgroundModifier.keys) + tilePositions.addAll(liquid.keys) val regions = Long2ObjectOpenHashMap>() - var previous: Vector2i? = null - for (pos in tilePositionsRaw) { - if (pos != previous) { - regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos) - previous = pos - } + for (pos in tilePositions) { + regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos) } val seenTickets = HashSet() + val waiters = ArrayList>() for (ticket in tickets.filter { seenTickets.add(it.pos) }) { // make changes to chunk only inside world's thread once it has reached TILES state - ticket.chunk.thenAcceptAsync(Consumer { - regions.get(ticket.pos.toLong())?.forEach { applyCellChangesAt(it, ticket.chunk.get()) } - }, parent.eventLoop) + waiters.add(ticket.chunk.thenAcceptAsync(Consumer { + regions.get(ticket.pos.toLong())?.forEach { + applyCellChangesAt(it, ticket.chunk.get()) + } + }, parent.eventLoop)) } - // wait for all chunks to be loaded - tickets.forEach { it.chunk.await() } + // wait for all chunks to be loaded (and cell changes to be applied) + // if any of cell change operation fails, entire generation fails... leaving world in inconsistent state, + // but also limiting damage by exiting early. + waiters.forEach { it.await() } // at this point all chunks are available, and we applied changes to tiles + if (playerStart != null) + parent.setPlayerSpawn(playerStart!!, false) - // and finally, schedule chunks to be loaded into FULL state - // this way, big dungeons won't get cut off when chunks being saved - // to disk because of multiple chunks outside player tracking area - // But this might trigger cascading world generation - // (big dungeon generates another big dungeon, and another, and so on), - // tough, so need to take care! - for (box in boundingBoxes) { - // specify timer as 0 so ticket gets removed on next world tick - parent.temporaryChunkTicket(box, 0, ServerChunk.State.FULL) + val placedObjects = placedObjects.entries.stream() + .sorted { o1, o2 -> o1.key.y.compareTo(o2.key.y) } // place objects from bottom to top + // so objects stacked on each other can be properly placed + .map { (pos, data) -> + WorldObject.create(data.prototype, pos, data.parameters) to data.direction + } + .filter { it.first != null } + .toList() + + parent.eventLoop.supplyAsync { + for ((obj, direction) in placedObjects) { + val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction) + + if (orientation != -1) { + obj.setOrientation(orientation) + obj.joinWorld(parent) + } else { + LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!") + } + } + }.await() + + if (targetChunkState != ChunkState.FULL) { + // and finally, schedule chunks to be loaded into FULL state + // this way, dungeons won't get cut off when chunks being saved + // to disk because of dungeon bleeding into neighbour chunks who + // never get promoted further + // But this might trigger cascading world generation + // (dungeon generates another dungeon, and another, and so on), + // tough, so need to take care! + for (box in boundingBoxes) { + // specify timer as 0 so ticket gets removed on next world tick + parent.temporaryChunkTicket(box, 0, ChunkState.FULL) + } } } finally { tickets.forEach { it.cancel() } } } - companion object{ + companion object { + private val LOGGER = LogManager.getLogger() private val offsets = listOf(Vector2i.POSITIVE_Y, Vector2i.NEGATIVE_Y, Vector2i.POSITIVE_X, Vector2i.NEGATIVE_X) } } 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 30ee66b5..97090bd8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt @@ -37,7 +37,8 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList) : Par for (y in 0 until image.height) { val offset = (x + y * image.width) * channels - tileData[x + y * image.width] = bytes[offset].toInt().and(0xFF) or + // 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 -0x1000000 // leading alpha as 255 } @@ -49,7 +50,8 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList) : Par for (y in 0 until image.height) { val offset = (x + y * image.width) * channels - tileData[x + y * image.width] = bytes[offset].toInt().and(0xFF) or + // 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) @@ -93,6 +95,7 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList) : Par override fun walkTiles(callback: TileCallback): KOptional { for (layer in layers) { + // walk bottom-top first, this way we will place bottom objects/tiles before top ones for (y in 0 until layer.data.height) { for (x in 0 until layer.data.width) { val get = callback(x, y, layer.palette[layer.data[x, y]]) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImageTileSet.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImageTileSet.kt index db417451..f9467a05 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImageTileSet.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImageTileSet.kt @@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kstarbound.json.getAdapter +import java.util.LinkedList // dungeons are stored as images, and each pixel // represents a different tile. To make sense @@ -19,6 +20,8 @@ import ru.dbotthepony.kstarbound.json.getAdapter class ImageTileSet(list: List = listOf()) { private val mapping = Int2ObjectOpenHashMap() + private val warnings = LinkedList() + init { for ((i, it) in list.withIndex()) { val replaced = mapping.put(it.index, it) @@ -26,11 +29,19 @@ class ImageTileSet(list: List = listOf()) { // allow duplicates of same entry because vanilla files have them. if (replaced != null && replaced != it) { val color = RGBAColor.abgr(it.index) - throw IllegalArgumentException("Two tiles are trying to take same place with index ${it.index} [${color.redInt}, ${color.greenInt}, ${color.blueInt}, ${color.alphaInt}] (list index $i):\ntile 1: $replaced\ntile 2: $it") + warnings.add("Duplicate tile [${color.redInt}, ${color.greenInt}, ${color.blueInt}, ${color.alphaInt}], overwriting: $replaced -> $it") } } } + fun spewWarnings(prefix: String) { + warnings.forEach { + LOGGER.warn("$prefix: $it") + } + + warnings.clear() + } + operator fun get(index: Int): DungeonTile? { return mapping.get(index) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledMap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledMap.kt index 7e3f10a1..f1d59964 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledMap.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledMap.kt @@ -225,8 +225,9 @@ class TiledMap(data: JsonData) : TileMap() { } override fun walkTiles(callback: TileCallback): KOptional { - for (x in this.x until this.x + width) { - for (y in this.y until this.y + height) { + // walk bottom-top first, this way we will place bottom objects/tiles before top ones + for (y in this.y until this.y + height) { + for (x in this.x until this.x + width) { val result = callback(x, y, get0(x, y)) if (result.isPresent) return result } @@ -418,8 +419,8 @@ class TiledMap(data: JsonData) : TileMap() { ObjType.RECTANGLE -> { // Used for creating custom brushes and rules - for (x in this.pos.x until this.pos.x + this.size.x) { - for (y in this.pos.y until this.pos.y + this.size.y) { + for (y in this.pos.y until this.pos.y + this.size.y) { + for (x in this.pos.x until this.pos.x + this.size.x) { val result = callback(x, y, tile) if (result.isPresent) return result } 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 7039d725..2f58b781 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSet.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSet.kt @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.set @@ -15,13 +16,15 @@ import ru.dbotthepony.kstarbound.set import java.util.concurrent.ConcurrentHashMap class TiledTileSet private constructor( - val front: ImmutableMap>, - val back: ImmutableMap>, + // plz dont modify :) + // used for speed + val front: Int2ObjectOpenHashMap>, + val back: Int2ObjectOpenHashMap>, ) { @JsonFactory data class JsonData( val properties: JsonObject = JsonObject(), - // val tilecount: Int, // we don't care + val tilecount: Int, val tileproperties: JsonObject = JsonObject(), // empty tileset? ) @@ -59,8 +62,8 @@ class TiledTileSet private constructor( try { val data = Starbound.gson.fromJson(locate, JsonData::class.java) - val front = ImmutableMap.Builder>() - val back = ImmutableMap.Builder>() + val front = Int2ObjectOpenHashMap>(data.tilecount + 40) + val back = Int2ObjectOpenHashMap>(data.tilecount + 40) for ((key, value) in data.tileproperties.entrySet()) { if (value !is JsonObject) @@ -88,7 +91,7 @@ class TiledTileSet private constructor( back[index] = makeTile(mergeBack) to mergeBack } - return Either.left(TiledTileSet(front.build(), back.build())) + return Either.left(TiledTileSet(front, back)) } catch (err: Throwable) { return Either.right(err) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSets.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSets.kt index bd338639..453c0a3c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSets.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledTileSets.kt @@ -14,11 +14,11 @@ class TiledTileSets(entries: List) { val source: String, ) - private var front = Int2ObjectOpenHashMap>() - private var back = Int2ObjectOpenHashMap>() + private var front: Int2ObjectOpenHashMap> + private var back: Int2ObjectOpenHashMap> init { - for ((firstgid, source) in entries) { + val mapped = entries.map { (firstgid, source) -> // Tiled stores tileset paths relative to the map file, which can go below // the assets root if it's referencing a tileset in another asset package. // The solution chosen here is to ignore everything in the path up until a @@ -36,11 +36,16 @@ class TiledTileSets(entries: List) { actualSource = AssetPathStack.remap(source) } - val set = TiledTileSet.load(actualSource) + firstgid to TiledTileSet.load(actualSource) + } + front = Int2ObjectOpenHashMap>(mapped.sumOf { it.second.size }) + back = Int2ObjectOpenHashMap>(mapped.sumOf { it.second.size }) + + for ((firstgid, set) in mapped) { for (i in 0 until set.size) { - front[firstgid + i] = set.front[i] ?: throw NullPointerException("aeiou") - back[firstgid + i] = set.back[i] ?: throw NullPointerException("aeiou") + front.put(firstgid + i, set.front[i] ?: throw NullPointerException("aeiou")) + back.put(firstgid + i, set.back[i] ?: throw NullPointerException("aeiou")) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/Anchor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/Anchor.kt index bda21028..ca674785 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/Anchor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/Anchor.kt @@ -1,5 +1,26 @@ package ru.dbotthepony.kstarbound.defs.`object` import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.world.World -data class Anchor(val isForeground: Boolean, val pos: Vector2i, val isTilled: Boolean, val isSoil: Boolean, val anchorMaterial: String?) +data class Anchor(val isBackground: Boolean, val position: Vector2i, val isTilled: Boolean, val isSoil: Boolean, val anchorMaterial: Registry.Ref?) { + fun isValidPlacement(world: World<*, *>, position: Vector2i): Boolean { + val cell = world.chunkMap.getCell(position + this.position) + + if (!cell.isConnectible(false, isBackground)) + return false + + if (isTilled && !cell.tile(isBackground).modifier.value.tilled) + return false + + if (isSoil && !cell.tile(isBackground).material.value.soil) + return false + + if (anchorMaterial?.entry != null && cell.tile(isBackground).material != anchorMaterial.entry) + return false + + return true + } +} 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 8b63ced3..2a4225b2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -29,9 +29,12 @@ import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.World @JsonAdapter(ObjectDefinition.Adapter::class) data class ObjectDefinition( @@ -51,7 +54,7 @@ data class ObjectDefinition( val smashDropPool: Registry.Ref? = null, val smashDropOptions: ImmutableList> = ImmutableList.of(), val animation: AssetReference? = null, - //val animation: AssetPath? = null, + val animationCustom: JsonObject = JsonObject(), val smashSounds: ImmutableSet = ImmutableSet.of(), val smashParticles: JsonArray? = null, val smashable: Boolean = false, @@ -81,6 +84,28 @@ data class ObjectDefinition( val flickerPeriod: PeriodicFunction? = null, val orientations: ImmutableList, ) { + fun findValidOrientation(world: World<*, *>, position: Vector2i, directionAffinity: Direction? = null): 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 == null || orientation.directionAffinity != directionAffinity) + continue + + if (orientation.placementValid(world, position) && orientation.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)) + return i + } + + return -1 + } + class Adapter(gson: Gson) : TypeAdapter() { @JsonFactory(logMisses = false) data class PlainData( @@ -100,6 +125,7 @@ data class ObjectDefinition( val smashDropPool: Registry.Ref? = null, val smashDropOptions: ImmutableList> = ImmutableList.of(), val animation: AssetReference? = null, + val animationCustom: JsonObject = JsonObject(), //val animation: AssetPath? = null, val smashSounds: ImmutableSet = ImmutableSet.of(), val smashParticles: JsonArray? = null, @@ -203,6 +229,7 @@ data class ObjectDefinition( smashDropPool = basic.smashDropPool, smashDropOptions = basic.smashDropOptions, animation = basic.animation, + animationCustom = basic.animationCustom, smashSounds = basic.smashSounds, smashParticles = basic.smashParticles, smashable = basic.smashable, 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 29501f66..3f62646e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -31,7 +31,11 @@ import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile +import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile +import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Side +import ru.dbotthepony.kstarbound.world.World import kotlin.math.PI @JsonAdapter(ObjectOrientation.Adapter::class) @@ -49,7 +53,7 @@ data class ObjectOrientation( val metaBoundBox: AABB?, val anchors: ImmutableSet, val anchorAny: Boolean, - val directionAffinity: Side?, + val directionAffinity: Direction?, val materialSpaces: ImmutableList>>, val interactiveSpaces: ImmutableSet, val lightPosition: Vector2i, @@ -58,6 +62,38 @@ data class ObjectOrientation( val touchDamage: JsonReference.Object?, val particleEmitters: ArrayList, ) { + fun placementValid(world: World<*, *>, position: Vector2i): Boolean { + if (occupySpaces.isEmpty()) + return true + + return occupySpaces.all { + val cell = world.chunkMap.getCell(it + position) + //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 && cell.dungeonId !in world.protectedDungeonIDs + } + } + + fun anchorsValid(world: World<*, *>, position: Vector2i): Boolean { + if (anchors.isEmpty()) + return true + + var anyValid = false + + for (anchor in anchors) { + val isValid = anchor.isValidPlacement(world, position) + + if (isValid) + anyValid = true + else if (!anchorAny) { + // println("anchor $anchor reported false for $position ${world.chunkMap.getCell(position + anchor.position)}") + return false + } + } + + return anyValid + } + companion object { fun preprocess(json: JsonArray): JsonArray { val actual = ArrayList() @@ -197,16 +233,16 @@ data class ObjectOrientation( val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) } val requireTilledAnchors = obj.get("requireTilledAnchors", false) val requireSoilAnchors = obj.get("requireSoilAnchors", false) - val anchorMaterial = obj["anchorMaterial"]?.asString + val anchorMaterial = obj["anchorMaterial"]?.asString?.let { Registries.tiles.ref(it) } val anchors = ImmutableSet.Builder() for (v in obj.get("anchors", JsonArray())) { when (v.asString.lowercase()) { - "left" -> occupySpaces.stream().filter { it.x == boundingBox.mins.x }.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } - "right" -> occupySpaces.stream().filter { it.x == boundingBox.maxs.x }.forEach { anchors.add(Anchor(true, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } - "top" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(true, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } - "bottom" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } - "background" -> occupySpaces.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } + "left" -> occupySpaces.stream().filter { it.x == boundingBox.mins.x }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } + "right" -> occupySpaces.stream().filter { it.x == boundingBox.maxs.x }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } + "top" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } + "bottom" -> occupySpaces.stream().filter { it.y == boundingBox.mins.y }.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)) } else -> throw JsonSyntaxException("Unknown anchor type $v") } } @@ -218,7 +254,7 @@ data class ObjectOrientation( anchors.add(Anchor(true, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial)) val anchorAny = obj["anchorAny"]?.asBoolean ?: false - val directionAffinity = obj["directionAffinity"]?.asString?.uppercase()?.let { Side.valueOf(it) } + val directionAffinity = obj["direction"]?.asString?.uppercase()?.let { Direction.valueOf(it) } val materialSpaces: ImmutableList> if ("materialSpaces" in obj) { @@ -227,7 +263,7 @@ data class ObjectOrientation( val collisionSpaces = obj["collisionSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces val builder = ImmutableList.Builder>() - when (val collisionType = obj.get("collisionType", "none").lowercase()) { + when (val collisionType = obj.get("collision", "none").lowercase()) { "solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.key) } "platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) } "none" -> {} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index 8a0dc80b..c0173f89 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -101,13 +101,14 @@ const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524 const val FIRST_RESERVED_DUNGEON_ID = 65520 object BuiltinMetaMaterials { - private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition( + private fun make(id: Int, name: String, collisionType: CollisionType, isConnectable: Boolean = true) = Registries.tiles.add(name, id, TileDefinition( materialId = id, materialName = "metamaterial:$name", descriptionData = ThingDescription.EMPTY, category = "meta", renderTemplate = AssetReference.empty(), renderParameters = RenderParameters.META, + isConnectable = isConnectable, isMeta = true, supportsMods = false, collisionKind = collisionType, @@ -132,12 +133,12 @@ object BuiltinMetaMaterials { /** * air */ - val EMPTY = make(65535, "empty", CollisionType.NONE) + val EMPTY = make(65535, "empty", CollisionType.NONE, isConnectable = false) /** * not set / out of bounds */ - val NULL = make(65534, "null", CollisionType.NULL) + val NULL = make(65534, "null", CollisionType.NULL, isConnectable = false) val STRUCTURE = make(65533, "structure", CollisionType.BLOCK) val BIOME = make(65527, "biome", CollisionType.BLOCK) @@ -146,7 +147,7 @@ object BuiltinMetaMaterials { val BIOME3 = make(65530, "biome3", CollisionType.BLOCK) val BIOME4 = make(65531, "biome4", CollisionType.BLOCK) val BIOME5 = make(65532, "biome5", CollisionType.BLOCK) - val BOUNDARY = make(65526, "boundary", CollisionType.SLIPPERY) + val BOUNDARY = make(65526, "boundary", CollisionType.SLIPPERY, isConnectable = false) val OBJECT_SOLID = make(65500, "objectsolid", CollisionType.BLOCK) val OBJECT_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM) 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 efb32e51..681e7827 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -31,6 +31,8 @@ data class TileDefinition( val health: Double? = null, val requiredHarvestLevel: Int? = null, + val isConnectable: Boolean = true, + @JsonFlat val descriptionData: ThingDescription, 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 94c886de..dcee8a97 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt @@ -18,6 +18,7 @@ data class TileModifierDefinition( val requiredHarvestLevel: Int? = null, val breaksWithTile: Boolean = true, val grass: Boolean = false, + val tilled: Boolean = false, val miningParticle: String? = null, val footstepSound: String? = null, 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 01f920e7..5fd1bd32 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt @@ -17,10 +17,12 @@ import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.stream import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.json.NativeLegacy @@ -114,9 +116,7 @@ data class BiomePlaceables( // Truly our hero here. val obj = when (val type = `in`.nextString()) { "treasureBoxSet" -> TreasureBox(`in`.nextString()) - "microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { it.asString }.collect( - ImmutableSet.toImmutableSet() - )) + "microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet())) "grass" -> Grass(grassVariant.read(`in`)) "bush" -> Bush(bushVariant.read(`in`)) "treePair" -> Tree(trees.read(`in`)) @@ -135,13 +135,13 @@ data class BiomePlaceables( } } - data class MicroDungeon(val microdungeons: ImmutableSet = ImmutableSet.of()) : Item() { + data class MicroDungeon(val microdungeons: ImmutableSet> = ImmutableSet.of()) : Item() { override val type: BiomePlacementItemType get() = BiomePlacementItemType.MICRO_DUNGEON override fun toJson(): JsonElement { return JsonArray().also { j -> - microdungeons.forEach { j.add(JsonPrimitive(it)) } + microdungeons.forEach { j.add(JsonPrimitive(it.key.left())) } } } } 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 70796813..cd0bc2cd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.builder.IStringSerializable @@ -62,7 +63,7 @@ data class BiomePlaceablesDefinition( } @JsonFactory - data class MicroDungeon(val microdungeons: ImmutableSet = ImmutableSet.of()) : DistributionItemData() { + data class MicroDungeon(val microdungeons: ImmutableSet> = ImmutableSet.of()) : DistributionItemData() { override val type: BiomePlacementItemType get() = BiomePlacementItemType.MICRO_DUNGEON diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt index 7ce19a3b..a32c16fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt @@ -5,8 +5,10 @@ import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory @@ -25,7 +27,7 @@ data class DungeonWorldsConfig( val dungeonBaseHeight: Int, val dungeonSurfaceHeight: Int = dungeonBaseHeight, val dungeonUndergroundLevel: Int = 0, - val primaryDungeon: String, + val primaryDungeon: Registry.Ref, val biome: String? = null, val ambientLightLevel: RGBAColor, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt index b4356e8e..e95843a9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt @@ -7,7 +7,9 @@ import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.io.readColor import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readNullableString @@ -27,7 +29,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { private set var dungeonUndergroundLevel: Int by Delegates.notNull() private set - var primaryDungeon: String by Delegates.notNull() + var primaryDungeon: Registry.Ref by Delegates.notNull() private set var ambientLightLevel: RGBAColor by Delegates.notNull() private set @@ -71,9 +73,10 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { @JsonFactory data class JsonData( + val dungeonBaseHeight: Int, val dungeonSurfaceHeight: Int, val dungeonUndergroundLevel: Int, - val primaryDungeon: String, + val primaryDungeon: Registry.Ref, val biome: String? = null, val ambientLightLevel: RGBAColor, val dayMusicTrack: String? = null, @@ -87,6 +90,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { val read = Starbound.gson.fromJson(data, JsonData::class.java) + dungeonBaseHeight = read.dungeonBaseHeight dungeonSurfaceHeight = read.dungeonSurfaceHeight dungeonUndergroundLevel = read.dungeonUndergroundLevel primaryDungeon = read.primaryDungeon @@ -102,7 +106,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { super.toJson(data, isLegacy) val serialize = Starbound.gson.toJsonTree(JsonData( - dungeonSurfaceHeight, dungeonUndergroundLevel, primaryDungeon, biome, ambientLightLevel, dayMusicTrack, nightMusicTrack, dayAmbientNoises, nightAmbientNoises + dungeonBaseHeight, dungeonSurfaceHeight, dungeonUndergroundLevel, primaryDungeon, biome, ambientLightLevel, dayMusicTrack, nightMusicTrack, dayAmbientNoises, nightAmbientNoises )) as JsonObject for ((k, v) in serialize.entrySet()) { @@ -116,7 +120,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { dungeonBaseHeight = stream.readInt() dungeonSurfaceHeight = stream.readInt() dungeonUndergroundLevel = stream.readInt() - primaryDungeon = stream.readInternedString() + primaryDungeon = Registries.dungeons.ref(stream.readInternedString()) biome = stream.readNullableString() ambientLightLevel = stream.readColor() dayMusicTrack = stream.readNullableString() @@ -131,7 +135,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { stream.writeInt(dungeonBaseHeight) stream.writeInt(dungeonSurfaceHeight) stream.writeInt(dungeonUndergroundLevel) - stream.writeBinaryString(primaryDungeon) + stream.writeBinaryString(primaryDungeon.key.left()) stream.writeNullableString(biome) stream.writeColor(ambientLightLevel) stream.writeNullableString(dayMusicTrack) @@ -145,6 +149,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() { val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!") val parameters = FloatingDungeonWorldParameters() + parameters.typeName = typeName parameters.worldSize = config.worldSize parameters.threatLevel = config.threatLevel parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it }) 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 882b74ed..22f124ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid @@ -33,6 +34,7 @@ import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.writeStruct2d import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.getAdapter import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.readJsonElement @@ -131,7 +133,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { val layerMinHeight: Int, val layerBaseHeight: Int, - val dungeons: ImmutableSet, + val dungeons: ImmutableSet>, val dungeonXVariance: Int, val primaryRegion: Region, @@ -146,7 +148,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { constructor(stream: DataInputStream) : this( stream.readInt(), stream.readInt(), - ImmutableSet.copyOf(stream.readCollection { readInternedString() }), + ImmutableSet.copyOf(stream.readCollection { Registries.dungeons.ref(readInternedString()) }), stream.readInt(), Region(stream), @@ -163,7 +165,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { stream.writeInt(layerMinHeight) stream.writeInt(layerBaseHeight) - stream.writeCollection(dungeons) { writeBinaryString(it) } + stream.writeCollection(dungeons) { writeBinaryString(it.key.left()) } stream.writeInt(dungeonXVariance) primaryRegion.write(stream) @@ -287,6 +289,18 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { var coreLayer: Layer by Delegates.notNull() private set + val layers: List get() { + val layers = ArrayList() + + layers.add(spaceLayer) + layers.add(atmosphereLayer) + layers.add(surfaceLayer) + layers.addAll(undergroundLayers) + layers.add(coreLayer) + + return layers + } + override val type: VisitableWorldParametersType get() = VisitableWorldParametersType.TERRESTRIAL @@ -397,7 +411,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() { private val biomePairs by lazy { Starbound.gson.pairAdapter() } private val vectors2d by lazy { Starbound.gson.getAdapter(Vector2d::class.java) } private val vectors2i by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } - private val dungeonPools by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(WeightedList::class.java, String::class.java)) as TypeAdapter> } + private val dungeonPools by lazy { Starbound.gson.getAdapter>>() } fun generate(typeName: String, sizeName: String, seed: Long): TerrestrialWorldParameters { return generate(typeName, sizeName, random(seed)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index 79047a83..3cf4eb30 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -118,6 +118,9 @@ class WorldLayout { } fun findContainingCell(x: Int): Pair { + if (boundaries.isEmpty) // entire world strip is within single cell + return 0 to worldGeometry.x.cell(x) + val wx = worldGeometry.x.cell(x) if (wx < boundaries.first()) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 603c0851..bd0c7ae8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -1,8 +1,8 @@ package ru.dbotthepony.kstarbound.defs.world import com.github.benmanes.caffeine.cache.Caffeine -import com.github.benmanes.caffeine.cache.Scheduler import com.google.gson.JsonObject +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d @@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition @@ -19,11 +20,12 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.math.quintic2 import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.util.random.staticRandomInt import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.physics.Poly -import java.time.Duration +import java.util.concurrent.CopyOnWriteArrayList import java.util.random.RandomGenerator class WorldTemplate(val geometry: WorldGeometry) { @@ -43,16 +45,20 @@ class WorldTemplate(val geometry: WorldGeometry) { val threatLevel: Double get() = worldParameters?.threatLevel ?: 0.0 - private val customTerrainRegions = ArrayList() + // CoW because these are updated by dungeons across threads (we can't synchronize these updates + // because tile generation always happen on worker threads) + private val customTerrainRegions = CopyOnWriteArrayList() constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) { this.seed = seed + this.worldParameters = worldParameters this.skyParameters = skyParameters this.worldLayout = worldParameters.createLayout(seed) } constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, random: RandomGenerator) : this(WorldGeometry(worldParameters.worldSize, true, false)) { this.seed = random.nextLong() + this.worldParameters = worldParameters this.skyParameters = skyParameters this.worldLayout = worldParameters.createLayout(random) } @@ -227,6 +233,50 @@ class WorldTemplate(val geometry: WorldGeometry) { return result } + data class Dungeon( + val dungeon: Registry.Entry, + val baseHeight: Int, + val baseX: Int, + val xVariance: Int, + val forcePlacement: Boolean, + val blendWithTerrain: Boolean, + ) + + fun gatherDungeons(): List { + val dungeons = ArrayList() + + when (val worldParameters = worldParameters) { + is FloatingDungeonWorldParameters -> { + if (worldParameters.primaryDungeon.isPresent) { + dungeons.add(Dungeon(worldParameters.primaryDungeon.entry!!, worldParameters.dungeonBaseHeight, 0, 0, true, false)) + } else { + LOGGER.error("Floating dungeon world's primary dungeon ${worldParameters.primaryDungeon.key.left()} is missing... What happens now?") + } + } + + is TerrestrialWorldParameters -> { + for (layer in worldParameters.layers) { + if (layer.dungeons.isNotEmpty()) { + val dungeonSpacing = geometry.size.x / layer.dungeons.size + var dungeonOffset = staticRandomInt(0, geometry.size.x, seed, layer.layerBaseHeight, "dungeon") + + for (dungeon in layer.dungeons) { + if (dungeon.isPresent) { + dungeons.add(Dungeon(dungeon.entry!!, layer.layerBaseHeight, dungeonOffset, layer.dungeonXVariance, false, true)) + dungeonOffset += dungeonSpacing + dungeonOffset = geometry.x.cell(dungeonOffset) + } else { + LOGGER.error("Primary dungeon ${dungeon.key.left()} at layer Y ${layer.layerBaseHeight} is missing") + } + } + } + } + } + } + + return dungeons + } + class CellInfo(val x: Int, val y: Int) { var foreground: Registry.Ref = BuiltinMetaMaterials.EMPTY.ref var foregroundMod: Registry.Ref = BuiltinMetaMaterials.EMPTY_MOD.ref @@ -455,6 +505,8 @@ class WorldTemplate(val geometry: WorldGeometry) { } companion object { + private val LOGGER = LogManager.getLogger() + suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate { val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!") val visitable = params.visitableParameters ?: throw IllegalArgumentException("$coordinate of $universe is not visitable") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt index cbcea78f..2a54bd56 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityDestroyPacket.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import java.io.DataInputStream import java.io.DataOutputStream @@ -28,7 +29,7 @@ class EntityDestroyPacket(val entityID: Int, val finalNetState: ByteArrayList, v connection.disconnect("Removing entity with ID $entityID outside of allowed range ${connection.entityIDRange}") } else { connection.enqueue { - entities[entityID]?.remove() + entities[entityID]?.remove(if (isDeath) AbstractEntity.RemovalReason.REMOTE_DYING else AbstractEntity.RemovalReason.REMOTE_REMOVAL) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt index ff3ca10b..ca76b96f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt @@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.collect.RandomListIterator import ru.dbotthepony.kstarbound.collect.RandomSubList import java.io.DataInputStream import java.io.DataOutputStream +import java.util.concurrent.CopyOnWriteArrayList // original engine does not have "networked list", so it is always networked // the dumb way on legacy protocol @@ -42,6 +43,11 @@ class NetworkedList( private var isInterpolating = false private var currentTime = 0.0 private var isRemote = false + private val listeners = CopyOnWriteArrayList() + + fun addListener(listener: Runnable) { + listeners.add(listener) + } private fun purgeBacklog() { while (backlog.size >= maxBacklogSize) { @@ -89,6 +95,7 @@ class NetworkedList( } purgeBacklog() + listeners.forEach { it.run() } } override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { @@ -139,6 +146,7 @@ class NetworkedList( } purgeBacklog() + listeners.forEach { it.run() } } else { readInitial(data, false) } @@ -235,6 +243,7 @@ class NetworkedList( elements.add(index, element) backlog.add(currentVersion() to Entry(index, element)) purgeBacklog() + listeners.forEach { it.run() } } override fun addAll(index: Int, elements: Collection): Boolean { @@ -252,6 +261,7 @@ class NetworkedList( backlog.clear() backlog.add(currentVersion() to clearEntry) elements.clear() + listeners.forEach { it.run() } } override fun listIterator(): MutableListIterator { @@ -285,6 +295,7 @@ class NetworkedList( val element = elements.removeAt(index) backlog.add(currentVersion() to Entry(index)) purgeBacklog() + listeners.forEach { it.run() } return element } @@ -308,6 +319,7 @@ class NetworkedList( val old = elements.set(index, element) backlog.add(currentVersion() to Entry(index, element)) purgeBacklog() + listeners.forEach { it.run() } return old } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 983dcbe2..0b8fe2ac 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -52,8 +52,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread private val worlds = HashMap>() val universe = ServerUniverse() val chat = ChatHandler(this) - val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob()) - val eventLoopScope = CoroutineScope(asCoroutineDispatcher() + SupervisorJob()) + val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob()) private val systemWorlds = HashMap>() @@ -64,7 +63,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread fun loadSystemWorld(location: Vector3i): CompletableFuture { return supplyAsync { systemWorlds.computeIfAbsent(location) { - scope.async { loadSystemWorld0(location) }.asCompletableFuture() + globalScope.async { loadSystemWorld0(location) }.asCompletableFuture() } }.thenCompose { it } } @@ -89,6 +88,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld { val config = Globals.instanceWorlds[location.name] ?: throw NoSuchElementException("No such instance world ${location.name}") + LOGGER.info("Creating instance world $location") val random = random(config.seed ?: System.nanoTime()) val visitable = when (config.type.lowercase()) { @@ -111,9 +111,17 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread val template = WorldTemplate(visitable, config.skyParameters, random) val world = ServerWorld.create(this, template, WorldStorage.NULL, location) - world.setProperty("ephemeral", JsonPrimitive(!config.persistent)) + try { + world.setProperty("ephemeral", JsonPrimitive(!config.persistent)) + + world.eventLoop.start() + world.prepare().await() + } catch (err: Throwable) { + LOGGER.fatal("Exception while creating instance world at $location!", err) + world.eventLoop.shutdown() + throw err + } - world.eventLoop.start() return world } @@ -138,7 +146,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread if (world != null) { world } else { - val future = scope.async { loadWorld0(location) }.asCompletableFuture() + val future = globalScope.async { loadWorld0(location) }.asCompletableFuture() worlds[location] = future future } @@ -239,7 +247,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread return@removeIf false } - eventLoopScope.launch { + scope.launch { try { it.get().tick() } catch (err: Throwable) { @@ -289,6 +297,11 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread worlds.values.forEach { if (it.isDone && !it.isCompletedExceptionally) { it.get().eventLoop.awaitTermination(10L, TimeUnit.SECONDS) + + if (!it.get().eventLoop.isTerminated) { + LOGGER.warn("World ${it.get()} did not shutdown in 10 seconds, forcing termination. This might leave world in inconsistent state!") + it.get().eventLoop.shutdownNow() + } } it.cancel(true) 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 60cad1c8..9e4afb7f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -14,9 +14,7 @@ import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i -import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.dungeon.DungeonRule import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID @@ -33,16 +31,17 @@ import ru.dbotthepony.kstarbound.defs.tile.isNullTile import ru.dbotthepony.kstarbound.defs.tile.supportsModifier import ru.dbotthepony.kstarbound.defs.world.Biome import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables +import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandomDouble -import ru.dbotthepony.kstarbound.util.random.staticRandomInt import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.api.AbstractCell @@ -62,27 +61,13 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(world, pos) { - /** - * Determines the state chunk is in, chunks are written to persistent storage - * only if they are in [FULL] state to avoid partially loaded or partially generated - * chunks from making its way into persistent storage. - */ - enum class State { - FRESH, // Nothing is loaded - - TERRAIN, - MICRO_DUNGEONS, - CAVE_LIQUID, - FULL; - } - - var state: State = State.FRESH + override var state: ChunkState = ChunkState.FRESH private set private var isBusy = false private var idleTicks = 0 private var ticks = 0 - private val targetState = Channel(Int.MAX_VALUE) + private val targetState = Channel(Int.MAX_VALUE) private val permanent = ArrayList() private val temporary = ObjectAVLTreeSet() private var nextTicketID = 0 @@ -90,14 +75,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk= State.MICRO_DUNGEONS) { + if (nextState >= ChunkState.MICRO_DUNGEONS) { val neighbours = ArrayList() try { @@ -159,8 +144,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { - if (world.template.worldLayout == null) { + ChunkState.TERRAIN -> { + if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) { // skip since no cells will be generated anyway cells.value.fill(AbstractCell.EMPTY) } else { @@ -169,30 +154,31 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { + ChunkState.MICRO_DUNGEONS -> { // skip if we have no layout - if (world.template.worldLayout != null) { + if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) { placeMicroDungeons() } } - State.CAVE_LIQUID -> { + ChunkState.CAVE_LIQUID -> { // skip if we have no layout - if (world.template.worldLayout != null) { + if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) { generateLiquid() } } - State.FULL -> { + ChunkState.FULL -> { CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await() // skip if we have no layout - if (world.template.worldLayout != null) { + if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) { placeGrass() } } - State.FRESH -> throw RuntimeException() + ChunkState.FRESH -> throw RuntimeException() + ChunkState.EMPTY -> {} // do nothing } bumpState(nextState) @@ -217,7 +203,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk= 0) { "Invalid ticket time: $time" } ticketsLock.withLock { @@ -258,6 +245,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk state) return false + return setCell(x, y, cell) + } + interface ITicket { fun cancel() val isCanceled: Boolean @@ -306,7 +298,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk= state) { "Tried to downgrade $this state from $state to $newState" } this.state = newState @@ -515,24 +507,24 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk 600 else idleTicks > 32000 + shouldUnload = if (state == ChunkState.FULL) idleTicks > 600 else idleTicks > 32000 } else { idleTicks = 0 } if (shouldUnload) { - unload() + // unload() return } - if (state != State.FULL) + if (state != ChunkState.FULL) return super.tick() @@ -566,18 +558,18 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk tile.material = biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY - BuiltinMetaMaterials.BIOME1 -> tile.material = biome?.subBlocks?.getOrNull(0)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY - BuiltinMetaMaterials.BIOME2 -> tile.material = biome?.subBlocks?.getOrNull(1)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY - BuiltinMetaMaterials.BIOME3 -> tile.material = biome?.subBlocks?.getOrNull(2)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY - BuiltinMetaMaterials.BIOME4 -> tile.material = biome?.subBlocks?.getOrNull(3)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY - BuiltinMetaMaterials.BIOME5 -> tile.material = biome?.subBlocks?.getOrNull(4)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY - else -> {} - } + val indexOf = BuiltinMetaMaterials.BIOME_META_MATERIALS.indexOf(tile.material) - tile.hueShift = biome?.hueShift(tile.material) ?: 0f + if (indexOf != -1) { + if (indexOf == 0) { + tile.material = biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY + } else { + tile.material = biome?.subBlocks?.getOrNull(indexOf - 1)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY + } + + tile.hueShift = biome?.hueShift(tile.material) ?: tile.hueShift + } if (biome == null && tile.modifier == BuiltinMetaMaterials.BIOME_MOD) { tile.modifier = BuiltinMetaMaterials.EMPTY_MOD @@ -727,6 +719,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk() + private suspend fun placeMicroDungeons() { val placements = CompletableFuture.supplyAsync(Supplier { val placements = ArrayList() @@ -755,12 +749,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { @@ -273,6 +263,53 @@ class ServerWorld private constructor( try { isBusy++ + val random = random(template.seed) + var currentDungeonID = 0 + + // primary dungeons must be generated sequentially since they can get very large + // and to avoid dungeons colliding with each other, we must generate them first, one by one + for (dungeon in template.gatherDungeons()) { + var spawnDungeonRetries = Globals.worldServer.spawnDungeonRetries + + do { + var x = dungeon.baseX + + if (dungeon.xVariance != 0) + x += random.nextInt(dungeon.xVariance) + + x = geometry.x.cell(x) + + LOGGER.info("Trying to place dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}...") + + val dungeonWorld = try { + dungeon.dungeon.value.generate(this, random(random.nextLong()), x, dungeon.baseHeight, dungeon.blendWithTerrain, dungeon.forcePlacement, dungeonID = currentDungeonID).await() + } catch (err: Throwable) { + LOGGER.error("Exception while placing dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}", err) + // continue + break + } + + if (dungeonWorld.hasGenerated) { + LOGGER.info("Placed dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}") + + if (dungeon.dungeon.value.metadata.protected) { + protectedDungeonIDs.add(currentDungeonID) + } + + if (dungeon.dungeon.value.metadata.gravity != null) { + // TODO: set gravity here + } + + if (dungeon.dungeon.value.metadata.breathable != null) { + // TODO: set "breathable" here + } + + currentDungeonID++ + break + } + } while (--spawnDungeonRetries > 0) + } + if (playerSpawnPosition == Vector2d.ZERO) { playerSpawnPosition = findPlayerStart() } @@ -285,7 +322,7 @@ class ServerWorld private constructor( // everything inside our own thread, not anywhere else // This way, external callers can properly wait for preparations to complete fun prepare(): CompletableFuture<*> { - return scope.launch { prepare0() }.asCompletableFuture() + return eventLoop.scope.launch { prepare0() }.asCompletableFuture() } private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d { @@ -332,7 +369,7 @@ class ServerWorld private constructor( // dungeon. for (i in 0 until Globals.worldServer.playerStartRegionMaximumVerticalSearch) { - if (!chunkMap.getCell(pos.x.toInt(), pos.y.toInt()).liquid.state.isEmptyLiquid) { + if (!getCell(pos.x.toInt(), pos.y.toInt()).liquid.state.isEmptyLiquid) { break } @@ -380,29 +417,29 @@ class ServerWorld private constructor( return ServerChunk(this, pos) } - fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITicket? { + fun permanentChunkTicket(pos: ChunkPos, target: ChunkState = ChunkState.FULL): ServerChunk.ITicket? { return chunkMap.compute(pos)?.permanentTicket(target) } - fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List { + fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): List { return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() } - fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List { + fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): List { return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() } - fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITimedTicket? { + fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ChunkState = ChunkState.FULL): ServerChunk.ITimedTicket? { return chunkMap.compute(pos)?.temporaryTicket(time, target) } - fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List { + fun temporaryChunkTicket(region: AABBi, time: Int, target: ChunkState = ChunkState.FULL): List { require(time >= 0) { "Invalid ticket time: $time" } return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() } - fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List { + fun temporaryChunkTicket(region: AABB, time: Int, target: ChunkState = ChunkState.FULL): List { require(time >= 0) { "Invalid ticket time: $time" } return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index cbbc523f..e8d1b25e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -41,14 +41,6 @@ import java.util.concurrent.atomic.AtomicBoolean // couples ServerWorld and ServerConnection together, // allowing ServerConnection client to track ServerWorld state class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) { - init { - LOGGER.info("Accepted ${client.alias()}") - - client.worldStartAcknowledged = false - client.tracker = this - client.worldID = world.worldID - } - private var skyVersion = 0L // this is required because of dumb shit regarding flash time // if we network sky state on each tick then it will guarantee epilepsy attack @@ -62,6 +54,12 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p init { entityVersions.defaultReturnValue(-1L) + + LOGGER.info("Accepted ${client.alias()}") + + client.worldStartAcknowledged = false + client.tracker = this + client.worldID = world.worldID } fun send(packet: IPacket) = client.send(packet) @@ -118,11 +116,11 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p return entityVersions.containsKey(entity.entityID) } - fun forget(entity: AbstractEntity, isDeath: Boolean = false) { + fun forget(entity: AbstractEntity, reason: AbstractEntity.RemovalReason) { val version = entityVersions.remove(entity.entityID) if (version != -1L) { - send(EntityDestroyPacket(entity.entityID, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, isDeath)) + send(EntityDestroyPacket(entity.entityID, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, reason.dying)) } } @@ -263,7 +261,8 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p for ((id, entity) in itr) { if (id in client.entityIDRange) { - entity.remove() + // remove entities owned by that player + entity.remove(AbstractEntity.RemovalReason.REMOTE_REMOVAL) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt index d5e067b7..8315d9a2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt @@ -1,5 +1,10 @@ package ru.dbotthepony.kstarbound.util +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel import org.apache.logging.log4j.LogManager import java.util.PriorityQueue import java.util.concurrent.Callable @@ -12,7 +17,6 @@ import java.util.concurrent.RejectedExecutionException import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.Condition import java.util.concurrent.locks.LockSupport import java.util.function.Supplier @@ -29,14 +33,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer return executeAt - System.nanoTime() } - fun shouldEnqueue(): Boolean { - if (executeAt <= System.nanoTime()) - return perform() + fun shouldEnqueue(isShutdown: Boolean): Boolean { + if (isShutdown || executeAt <= System.nanoTime()) + return perform(isShutdown) return true } - fun perform(): Boolean { + fun perform(isShutdown: Boolean): Boolean { if (repeat) { if (isFixedDelay) { // fixed delay @@ -67,7 +71,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer executeAt = now + timeDelay + deadlineMargin - (now - timeBefore) } - return true + return !isShutdown } else { run() return false @@ -79,6 +83,8 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer private val eventQueue = LinkedBlockingQueue>() private val scheduledQueue = PriorityQueue>() + val coroutines = asCoroutineDispatcher() + val scope = CoroutineScope(coroutines + SupervisorJob()) private fun nextDeadline(): Long { if (isShutdown) @@ -95,7 +101,6 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer @Volatile private var isShutdown = false - private var isRunning = true private fun eventLoopIteration(): Boolean { @@ -122,22 +127,22 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } } - if (scheduledQueue.isNotEmpty() && !isShutdown) { + if (scheduledQueue.isNotEmpty()) { val executed = ArrayList>() var lastSize: Int do { lastSize = executed.size - while (scheduledQueue.isNotEmpty() && scheduledQueue.peek()!!.executeAt <= System.nanoTime() && !isShutdown) { + while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.executeAt <= System.nanoTime())) { executedAnything = true val poll = scheduledQueue.poll()!! - if (poll.perform()) { + if (poll.perform(isShutdown)) { executed.add(poll) } } - } while (lastSize != executed.size && !isShutdown) + } while (lastSize != executed.size) scheduledQueue.addAll(executed) } @@ -152,6 +157,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer if (isShutdown && isRunning) { while (eventLoopIteration()) {} isRunning = false + scope.cancel(CancellationException("EventLoop shut down")) performShutdown() } } @@ -160,7 +166,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } final override fun execute(command: Runnable) { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") if (currentThread() === this) { @@ -178,7 +184,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } final override fun submit(task: Callable): CompletableFuture { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") if (currentThread() === this) { @@ -216,7 +222,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer fun isSameThread() = this === currentThread() final override fun submit(task: Runnable): CompletableFuture<*> { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") if (currentThread() === this) { @@ -239,7 +245,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } final override fun submit(task: Runnable, result: T): CompletableFuture { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") if (currentThread() === this) { @@ -264,14 +270,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } final override fun invokeAll(tasks: Collection>): List> { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") return tasks.map { submit(it) } } final override fun invokeAll(tasks: Collection>, timeout: Long, unit: TimeUnit): List> { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") val futures = tasks.map { submit(it) } @@ -280,14 +286,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } final override fun invokeAny(tasks: Collection>): T { - if (isShutdown) - throw RejectedExecutionException("EventLoop is shutting down") + if (!isRunning) + throw RejectedExecutionException("EventLoop shut down") return submit(tasks.first()).get() } final override fun invokeAny(tasks: Collection>, timeout: Long, unit: TimeUnit): T { - if (isShutdown) + if (!isRunning) throw RejectedExecutionException("EventLoop is shutting down") return submit(tasks.first()).get(timeout, unit) @@ -311,6 +317,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } isRunning = false + scope.cancel(CancellationException("EventLoop shut down")) performShutdown() } else { // wake up thread @@ -323,37 +330,34 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer } - private fun shutdownNow0() { - while (eventQueue.isNotEmpty()) { - val remove = eventQueue.remove() - - try { - remove.future.cancel(false) - } catch (err: Throwable) { - LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err) - } - } - - while (scheduledQueue.isNotEmpty()) { - val remove = scheduledQueue.remove() - - try { - remove.cancel(false) - } catch (err: Throwable) { - LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err) - } - } - - isRunning = false - performShutdown() - } - final override fun shutdownNow(): List { if (!isShutdown) { isShutdown = true if (currentThread() === this) { - shutdownNow0() + while (eventQueue.isNotEmpty()) { + val remove = eventQueue.remove() + + try { + remove.future.cancel(false) + } catch (err: Throwable) { + LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err) + } + } + + while (scheduledQueue.isNotEmpty()) { + val remove = scheduledQueue.remove() + + try { + remove.cancel(false) + } catch (err: Throwable) { + LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err) + } + } + + isRunning = false + scope.cancel(CancellationException("EventLoop shut down")) + performShutdown() } else { eventQueue.add(TaskPair(CompletableFuture()) { }) } @@ -393,7 +397,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false) execute { - if (task.shouldEnqueue()) + if (task.shouldEnqueue(isShutdown)) scheduledQueue.add(task) } @@ -404,7 +408,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer val task = ScheduledTask(callable, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false) execute { - if (task.shouldEnqueue()) + if (task.shouldEnqueue(isShutdown)) scheduledQueue.add(task) } @@ -420,7 +424,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(period, unit), false) execute { - if (task.shouldEnqueue()) + if (task.shouldEnqueue(isShutdown)) scheduledQueue.add(task) } @@ -436,7 +440,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(delay, unit), true) execute { - if (task.shouldEnqueue()) + if (task.shouldEnqueue(isShutdown)) scheduledQueue.add(task) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutorWithScheduler.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutorWithScheduler.kt new file mode 100644 index 00000000..17c1999d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutorWithScheduler.kt @@ -0,0 +1,58 @@ +package ru.dbotthepony.kstarbound.util + +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.Delayed +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class ExecutorWithScheduler(val executor: ExecutorService, val scheduler: ScheduledExecutorService) : ExecutorService by executor, ScheduledExecutorService { + override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> { + return scheduler.schedule(Runnable { + executor.submit(command) + }, delay, unit) + } + + private class Proxy(val future: CompletableFuture, val parent: ScheduledFuture<*>) : Future by future, ScheduledFuture { + override fun compareTo(other: Delayed?): Int { + return parent.compareTo(other) + } + + override fun getDelay(unit: TimeUnit): Long { + return parent.getDelay(unit) + } + } + + // won't react to cancels... man. + override fun schedule(callable: Callable, delay: Long, unit: TimeUnit): ScheduledFuture { + val future = CompletableFuture>() + val scheduled = scheduler.schedule(Callable { future.complete(CompletableFuture.supplyAsync(callable::call, executor)) }, delay, unit) + return Proxy(future.thenCompose { it }, scheduled) + } + + override fun scheduleAtFixedRate( + command: Runnable, + initialDelay: Long, + period: Long, + unit: TimeUnit + ): ScheduledFuture<*> { + return scheduler.scheduleAtFixedRate(Runnable { + executor.submit(command) + }, initialDelay, period, unit) + } + + override fun scheduleWithFixedDelay( + command: Runnable, + initialDelay: Long, + delay: Long, + unit: TimeUnit + ): ScheduledFuture<*> { + return scheduler.scheduleWithFixedDelay(Runnable { + executor.submit(command) + }, initialDelay, delay, unit) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index ae60f69f..f3d7fef0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -6,6 +6,7 @@ import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState +import ru.dbotthepony.kstarbound.server.world.ServerChunk import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ImmutableCell @@ -42,6 +43,8 @@ abstract class Chunk, This : Chunk, This : Chunk, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess { @@ -58,17 +53,16 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk) { } - protected open fun onRemove(world: World<*, *>, isDeath: Boolean) { } + protected open fun onRemove(world: World<*, *>, reason: RemovalReason) { } val networkGroup = MasterElement(NetworkedGroup()) abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) @@ -125,20 +134,26 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable) { + super.onJoinWorld(world) world.dynamicEntities.add(this) movement.initialize(world, spatialEntry) forceChunkRepos = true } - override fun onRemove(world: World<*, *>, isDeath: Boolean) { + override fun onRemove(world: World<*, *>, reason: RemovalReason) { + super.onRemove(world, reason) world.dynamicEntities.remove(this) movement.remove() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt index e161a4c6..724b70fd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt @@ -110,8 +110,8 @@ class PlayerEntity() : HumanoidActorEntity("/") { metaFixture = spatialEntry!!.Fixture() } - override fun onRemove(world: World<*, *>, isDeath: Boolean) { - super.onRemove(world, isDeath) + override fun onRemove(world: World<*, *>, reason: RemovalReason) { + super.onRemove(world, reason) metaFixture?.remove() metaFixture = null } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt index 86e6c425..96020206 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -1,10 +1,21 @@ package ru.dbotthepony.kstarbound.world.entities.tile +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt +import ru.dbotthepony.kstarbound.server.world.ServerChunk +import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.entities.AbstractEntity @@ -16,21 +27,16 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { protected val yTilePositionNet = networkedSignedInt() init { - xTilePositionNet.addListener(::updateSpatialIndex) - yTilePositionNet.addListener(::updateSpatialIndex) + xTilePositionNet.addListener(::onPositionUpdated) + yTilePositionNet.addListener(::onPositionUpdated) } abstract val metaBoundingBox: AABB - protected open fun updateSpatialIndex() { - val spatialEntry = spatialEntry ?: return - spatialEntry.fixture.move(metaBoundingBox + position) - } - var xTilePosition: Int get() = xTilePositionNet.get() set(value) { - if (isSpawned) { + if (isInWorld) { xTilePositionNet.accept(world.geometry.x.cell(value)) } else { xTilePositionNet.accept(value) @@ -40,7 +46,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { var yTilePosition: Int get() = yTilePositionNet.get() set(value) { - if (isSpawned) { + if (isInWorld) { yTilePositionNet.accept(world.geometry.x.cell(value)) } else { yTilePositionNet.accept(value) @@ -60,19 +66,167 @@ abstract class TileEntity(path: String) : AbstractEntity(path) { /** * Tile positions this entity occupies in world (in world coordinates, not relative) */ - abstract val occupySpaces: Set + abstract val occupySpaces: Collection + + /** + * Tile positions this entity physically occupies in world (in world coordinates, not relative) + */ + abstract val materialSpaces: Collection>> /** * Tile positions this entity is rooted in world (in world coordinates, not relative) */ - abstract val roots: Set + abstract val roots: Collection abstract fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean - override fun onJoinWorld(world: World<*, *>) { - updateSpatialIndex() + private var needToUpdateSpaces = false + private var needToUpdateRoots = false + private val currentMaterialSpaces = HashSet>>() + private val currentRoots = HashSet() + + protected open fun markSpacesDirty() { + needToUpdateSpaces = true } - override fun onRemove(world: World<*, *>, isDeath: Boolean) { + protected open fun markRootsDirty() { + needToUpdateRoots = true + } + + private fun updateSpatialPosition() { + val spatialEntry = spatialEntry ?: return + spatialEntry.fixture.move(metaBoundingBox + position) + } + + protected open fun onPositionUpdated() { + updateSpatialPosition() + markSpacesDirty() + } + + override fun onJoinWorld(world: World<*, *>) { + super.onJoinWorld(world) + updateSpatialPosition() + markSpacesDirty() + } + + override fun onRemove(world: World<*, *>, reason: RemovalReason) { + super.onRemove(world, reason) + + if (world.isServer && (reason.removal || reason.remote)) { + // remove occupied spaces + if (!updateMaterialSpaces(listOf())) { + // we were unable to remove all spaces... + // create a task to try not to leave world in inconsistent state! + val currentMaterialSpaces = currentMaterialSpaces + + world.eventLoop.scope.launch { + world as ServerWorld + val tickets = ArrayList() + + try { + currentMaterialSpaces.forEach { (p) -> + tickets.add(world.permanentChunkTicket(ChunkPos(world.geometry.x.chunkFromCell(p.x), world.geometry.x.chunkFromCell(p.y)), ChunkState.EMPTY) ?: return@forEach) + } + + tickets.forEach { it.chunk.await() } + + for (space in currentMaterialSpaces) { + val cell = world.getCell(space.first).mutable() + + if (cell.foreground.material == space.second.orEmptyTile) + cell.foreground.empty() + + if (!world.setCell(space.first, cell)) { + LOGGER.warn("Unable to clear tile entity space at ${space.first}, world left in inconsistent state.") + } + } + } finally { + tickets.forEach { it.cancel() } + } + } + } + } + } + + protected fun updateMaterialSpaces(desired: Collection>>): Boolean { + val toRemove = ArrayList>>() + val toPlace = ArrayList>>() + + for (space in currentMaterialSpaces) { + if (desired.any { it == space }) + continue + + // we need to remove this space + toRemove.add(space) + } + + for (space in desired) { + if (currentMaterialSpaces.any { it == space }) + continue + + // we need to put this space + toPlace.add(space) + } + + if (toRemove.isEmpty() && toPlace.isEmpty()) + return true // we're clear! + + var clear = true + + for (space in toRemove) { + val cell = world.getCell(space.first).mutable() + + if (cell.foreground.material == space.second.orEmptyTile) + cell.foreground.empty() + + if (world.setCell(space.first, cell)) { + currentMaterialSpaces.remove(space) + } else { + clear = false + } + } + + for (space in toPlace) { + val cell = world.getCell(space.first).mutable() + + // already satisfied that placement + if (cell.foreground.material == space.second.orEmptyTile) { + currentMaterialSpaces.add(space) + continue + } + + cell.foreground.empty() + cell.foreground.material = space.second.orEmptyTile + + if (world.setCell(space.first, cell)) { + currentMaterialSpaces.add(space) + } else { + clear = false + } + } + + return clear + } + + fun updateMaterialSpacesNow() { + needToUpdateSpaces = false + + // only server can update entity tiles + // even if this tile entity is owned by client + if (world.isServer) { + needToUpdateSpaces = !updateMaterialSpaces(materialSpaces) + } + } + + override fun tick() { + super.tick() + + if (needToUpdateSpaces) { + updateMaterialSpacesNow() + } + } + + companion object { + private val LOGGER = LogManager.getLogger() } } 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 1eefc045..2d9d0824 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 @@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractRequest +import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectType import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor import ru.dbotthepony.kstarbound.defs.tile.TileDamage @@ -79,8 +80,12 @@ open class WorldObject(val config: Registry.Entry) : TileEntit uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull)) - for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) { - parameters[k] = v + loadParameters(data.get("parameters") { JsonObject() }) + } + + open fun loadParameters(parameters: JsonObject) { + for ((k, v) in parameters.entrySet()) { + this.parameters[k] = v } } @@ -111,9 +116,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkGroup.upstream.add(it) - it.addListener(Runnable { - invalidate() - }) + it.addListener { invalidate() } } val orientation: ObjectOrientation? get() { @@ -139,17 +142,45 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) } - var materialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) } + val networkedMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) } + + private val materialSpaces0 = LazyData { + networkedMaterialSpaces.map { (it.first + tilePosition) to it.second } + } + + override val materialSpaces by materialSpaces0 + + private val occupySpaces0 = LazyData { + (orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet()) + } + + override val occupySpaces: ImmutableSet by occupySpaces0 + + override val roots: Set + get() = setOf() + + private val anchorPositions0 = LazyData { + immutableSet { + orientation?.anchors?.forEach { accept(it.position + tilePosition) } + } + } + + val anchorPositions: ImmutableSet by anchorPositions0 init { networkGroup.upstream.add(xTilePositionNet) networkGroup.upstream.add(yTilePositionNet) + + networkedMaterialSpaces.addListener { + materialSpaces0.invalidate() + markSpacesDirty() + } } var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) } var health by networkedFloat().also { networkGroup.upstream.add(it) } - var orientationIndex by networkedPointer().also { + private var orientationIndex by networkedPointer(-1L).also { networkGroup.upstream.add(it) it.addListener(Runnable { invalidate() }) } @@ -194,10 +225,16 @@ open class WorldObject(val config: Registry.Entry) : TileEntit init { if (config.value.animation?.value != null) { - animator = Animator(config.value.animation!!.value!!).also { networkGroup.upstream.add(it.networkGroup) } + 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)) + } else { + animator = Animator(config.value.animation!!.value!!) + } } else { - animator = Animator().also { networkGroup.upstream.add(it.networkGroup) } + animator = Animator() } + + networkGroup.upstream.add(animator.networkGroup) } val unbreakable by LazyData { @@ -246,35 +283,79 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } init { - networkedRenderKeys.addListener(Runnable { drawablesCache.invalidate() }) + networkedRenderKeys.addListener { drawablesCache.invalidate() } } val drawables: List by drawablesCache - private val occupySpaces0 = LazyData { - (orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet()) + private fun updateOrientation() { + setOrientation(config.value.findValidOrientation(world, tilePosition, direction)) } - override val occupySpaces: ImmutableSet by occupySpaces0 + fun setOrientation(index: Int) { + if (orientationIndex.toInt() == index) + return - override val roots: Set - get() = setOf() + orientationIndex = index.toLong() - private val anchorPositions0 = LazyData { - immutableSet { - orientation?.anchors?.forEach { accept(it.pos + tilePosition) } + val orientation = orientation + + if (orientation != null) { + if (orientation.directionAffinity != null) { + direction = orientation.directionAffinity + } + + networkedMaterialSpaces.clear() + networkedMaterialSpaces.addAll(orientation.materialSpaces) + } else { + networkedMaterialSpaces.clear() } } - val anchorPositions: ImmutableSet by anchorPositions0 + override fun onPositionUpdated() { + super.onPositionUpdated() - override fun updateSpatialIndex() { - super.updateSpatialIndex() occupySpaces0.invalidate() anchorPositions0.invalidate() + materialSpaces0.invalidate() + + if (isInWorld && world.isServer) { + updateMaterialSpaces(listOf()) // remove old spaces after moving before updating orientation + + // update orientation + updateOrientation() + + // reapply world spaces + updateMaterialSpacesNow() + } } - fun getRenderParam(key: String): String? { + override fun onJoinWorld(world: World<*, *>) { + super.onJoinWorld(world) + + val orientation = orientation + + if (orientation == null) + updateOrientation() // try to find valid orientation + else { + if (orientation.directionAffinity != null) { + direction = orientation.directionAffinity + } + + networkedMaterialSpaces.clear() + networkedMaterialSpaces.addAll(orientation.materialSpaces) + } + + setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString) + + for ((k, v) in lookupProperty(JsonPath("animationParts")) { JsonObject() }.asJsonObject.entrySet()) { + animator.setPartTag(k, "partImage", v.asString) + } + + updateMaterialSpacesNow() + } + + fun getRenderParam(key: String): String { return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default" } @@ -300,7 +381,6 @@ open class WorldObject(val config: Registry.Entry) : TileEntit override fun interact(request: InteractRequest): InteractAction { val diff = world.geometry.diff(request.sourcePos, position) - // val result = if (!interactAction.isJsonNull) { return InteractAction(interactAction.asString, entityID, interactData) @@ -337,11 +417,6 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } } - override fun onJoinWorld(world: World<*, *>) { - super.onJoinWorld(world) - setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString) - } - override fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { if (unbreakable) return false @@ -415,5 +490,21 @@ open class WorldObject(val config: Registry.Entry) : TileEntit result.deserialize(content) return result } + + fun create(prototype: Registry.Entry, position: Vector2i, parameters: JsonObject = JsonObject()): WorldObject? { + val result = when (prototype.value.objectType) { + ObjectType.OBJECT -> WorldObject(prototype) + ObjectType.LOUNGEABLE -> LoungeableObject(prototype) + ObjectType.CONTAINER -> ContainerObject(prototype) + //ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE") + //ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER") + //ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS") + else -> null + } + + result?.loadParameters(parameters) + result?.tilePosition = position + return result + } } }