From 74bbc58c60dd578055a26427df6ebb02b14b342b Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 22 Apr 2024 16:41:05 +0700 Subject: [PATCH] Functional plants --- ADDITIONS.md | 3 + gradle.properties | 2 +- .../ru/dbotthepony/kstarbound/Starbound.kt | 11 +- .../kstarbound/collect/RandomListIterator.kt | 24 +- .../dbotthepony/kstarbound/defs/EntityType.kt | 3 +- .../kstarbound/defs/dungeon/DungeonPart.kt | 19 +- .../kstarbound/defs/dungeon/DungeonRule.kt | 35 +-- .../kstarbound/defs/dungeon/DungeonTile.kt | 13 +- .../defs/dungeon/ImagePartReader.kt | 8 +- .../defs/dungeon/TiledPartReader.kt | 8 +- .../kstarbound/defs/image/Image.kt | 23 +- .../kstarbound/defs/world/WorldTemplate.kt | 32 +- .../ru/dbotthepony/kstarbound/io/Streams.kt | 16 +- .../lua/bindings/WorldObjectBindings.kt | 4 +- .../ru/dbotthepony/kstarbound/math/AABB.kt | 31 ++ .../packets/serverbound/ConnectWirePacket.kt | 4 +- .../serverbound/DisconnectAllWiresPacket.kt | 2 +- .../server/world/LegacyWireProcessor.kt | 4 +- .../kstarbound/server/world/ServerChunk.kt | 18 +- .../kstarbound/server/world/ServerUniverse.kt | 3 +- .../kstarbound/server/world/ServerWorld.kt | 5 +- .../server/world/ServerWorldTracker.kt | 10 +- .../kstarbound/world/EntityIndex.kt | 10 + .../world/entities/DynamicEntity.kt | 3 - .../world/entities/MovementController.kt | 2 +- .../world/entities/tile/PlantEntity.kt | 153 +++++++++- .../world/entities/tile/PlantPieceEntity.kt | 285 ++++++++++++++++++ .../world/entities/tile/WorldObject.kt | 4 +- .../world/terrain/KarstCaveTerrainSelector.kt | 6 +- .../world/terrain/WormCaveTerrainSelector.kt | 4 +- 30 files changed, 616 insertions(+), 129 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt diff --git a/ADDITIONS.md b/ADDITIONS.md index 8a4e668c..ad12c3a4 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -147,3 +147,6 @@ val color: TileColor = TileColor.DEFAULT #### Dungeons * All brushes are now deterministic + +#### Plant drop entities (vines or steps dropping on ground) + * Collision is now determined using hull instead of rectangle diff --git a/gradle.properties b/gradle.properties index f4c46836..27c52b3f 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.15.0 +kommonsVersion=2.15.1 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index aa22e49a..33249044 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -6,6 +6,8 @@ import com.github.benmanes.caffeine.cache.Scheduler import com.google.gson.* import com.google.gson.stream.JsonReader import it.unimi.dsi.fastutil.objects.ObjectArraySet +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet +import kotlinx.coroutines.Runnable import kotlinx.coroutines.asCoroutineDispatcher import org.apache.logging.log4j.LogManager import org.classdump.luna.compiler.CompilerChunkLoader @@ -76,6 +78,7 @@ import java.io.* import java.lang.ref.Cleaner import java.text.DateFormat import java.time.Duration +import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executor @@ -135,7 +138,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca @JvmField val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory { - val thread = Thread(it, "Starbound Storage IO ${ioPoolCounter.getAndIncrement()}") + val thread = Thread(it, "IO Worker ${ioPoolCounter.getAndIncrement()}") thread.isDaemon = true thread.priority = Thread.MIN_PRIORITY @@ -151,12 +154,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca @JvmField val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher() - // this is required for Caffeine since it ignores scheduler - // (and suffers noticeable throughput penalty) in rescheduleCleanUpIfIncomplete() - // if executor is specified as ForkJoinPool.commonPool() - @JvmField - val SCREENED_EXECUTOR: ExecutorService = object : ExecutorService by EXECUTOR {} - @JvmField val CLEANER: Cleaner = Cleaner.create { val t = Thread(it, "Starbound Global Cleaner") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt index 03ceeb73..a02131ce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/RandomListIterator.kt @@ -1,41 +1,45 @@ package ru.dbotthepony.kstarbound.collect -class RandomListIterator(private val elements: MutableList, index: Int = 0) : MutableListIterator { - private var index = index - 1 - +class RandomListIterator(private val elements: MutableList, private var index: Int = 0) : MutableListIterator { override fun hasPrevious(): Boolean { return this.index > 0 } override fun nextIndex(): Int { - return this.index + 1 + return this.index } override fun previous(): E { - return elements[--this.index] + lastIndex = --this.index + return elements[lastIndex] } override fun previousIndex(): Int { - return (this.index - 1).coerceAtLeast(-1) + return this.index - 1 } override fun add(element: E) { elements.add(this.index++, element) + lastIndex = -1 } override fun hasNext(): Boolean { - return this.index < elements.size - 1 + return this.index < elements.size } + private var lastIndex = -1 + override fun next(): E { - return elements[++this.index] + lastIndex = this.index++ + return elements[lastIndex] } override fun remove() { - elements.removeAt(this.index--) + elements.removeAt(lastIndex) + lastIndex = -1 } override fun set(element: E) { - elements[this.index] = element + elements[lastIndex] = element } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index 18d6e064..db99eb54 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.kstarbound.defs -import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.json.builder.IStringSerializable enum class EntityType(override val jsonName: String, val storeName: String) : IStringSerializable { @@ -8,7 +7,7 @@ enum class EntityType(override val jsonName: String, val storeName: String) : IS OBJECT("object", "ObjectEntity"), VEHICLE("vehicle", "VehicleEntity"), ITEM_DROP("itemDrop", "ItemDropEntity"), - PLANT_DROP("plantDrop", "PlantDropEntity"), // wat + PLANT_DROP("plantDrop", "PlantDropEntity"), PROJECTILE("projectile", "ProjectileEntity"), STAGEHAND("stagehand", "StagehandEntity"), MONSTER("monster", "MonsterEntity"), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonPart.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonPart.kt index 8d3d5be1..8714aff2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonPart.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonPart.kt @@ -10,6 +10,7 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import kotlinx.coroutines.future.await import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.math.vector.Vector2i @@ -225,8 +226,13 @@ class DungeonPart(data: JsonData) { return true return world.waitForRegionAndJoin(Vector2i(x, y), reader.size) { + val cells = Object2DArray(reader.size.x, reader.size.y) { tx, ty -> + world.parent.getCell(x + tx, y + ty) + } + reader.walkTiles { tx, ty, tile -> - if (!tile.canPlace(x + tx, y + ty, world)) { + // TMX allows to define objects with out-of-bounds coordinates... + if (!tile.canPlace(x + tx, y + ty, world, cells.getOrNull(tx, ty) ?: world.parent.getCell(x + tx, y + ty))) { return@walkTiles KOptional(false) } @@ -235,12 +241,17 @@ class DungeonPart(data: JsonData) { }.orElse(true) } - fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean { - if (overrideAllowAlways || reader.size.x == 0 || reader.size.y == 0) + fun canPlace(x: Int, y: Int, world: ServerWorld, allowAlways: Boolean = this.overrideAllowAlways): Boolean { + if (allowAlways || reader.size.x == 0 || reader.size.y == 0) return true + val cells = Object2DArray(reader.size.x, reader.size.y) { tx, ty -> + world.getCell(x + tx, y + ty) + } + return reader.walkTiles { tx, ty, tile -> - if (!tile.canPlace(x + tx, y + ty, world)) { + // TMX allows to define objects with out-of-bounds coordinates... + if (!tile.canPlace(x + tx, y + ty, world, cells.getOrNull(tx, ty) ?: world.getCell(x + tx, y + ty))) { return@walkTiles KOptional(false) } 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 b02e902e..5db9c75b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt @@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.defs.tile.isObjectTile import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.world.api.AbstractCell @JsonAdapter(DungeonRule.Adapter::class) abstract class DungeonRule { @@ -139,11 +140,11 @@ abstract class DungeonRule { return false } - open fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + open fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell = world.parent.getCell(x, y)): Boolean { return true } - open fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { + open fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell = world.getCell(x, y)): Boolean { return true } @@ -161,7 +162,7 @@ abstract class DungeonRule { override val requiresLiquid: Boolean get() = true - override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell): Boolean { val cell = world.parent.template.cellInfo(x, y) return cell.oceanLiquid.isNotEmptyLiquid && cell.oceanLiquidLevel > y } @@ -172,7 +173,7 @@ abstract class DungeonRule { } object MustNotContainLiquid : DungeonRule() { - override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell): Boolean { val cell = world.parent.template.cellInfo(x, y) return cell.oceanLiquid.isEmptyLiquid || cell.oceanLiquidLevel <= y } @@ -186,20 +187,17 @@ abstract class DungeonRule { override val requiresSolid: Boolean get() = true - override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell): Boolean { if (world.markSurfaceLevel != null) return y < world.markSurfaceLevel - val cell = world.parent.getCell(x, y) - if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)) return false return cell.foreground.material.isNotEmptyTile && !world.isClearingTileEntityAt(x, y) } - override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.getCell(x, y) + override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean { return cell.foreground.material.isNotEmptyTile } @@ -212,16 +210,14 @@ abstract class DungeonRule { override val requiresOpen: Boolean get() = true - override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell): Boolean { if (world.markSurfaceLevel != null) return y >= world.markSurfaceLevel - 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.getCell(x, y) + override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean { return cell.foreground.material.isEmptyTile } @@ -234,20 +230,17 @@ abstract class DungeonRule { override val requiresSolid: Boolean get() = true - override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell): Boolean { if (world.markSurfaceLevel != null) return y < world.markSurfaceLevel - val cell = world.parent.getCell(x, y) - if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)) return false return cell.background.material.isNotEmptyTile } - override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.getCell(x, y) + override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean { return cell.background.material.isNotEmptyTile } @@ -260,16 +253,14 @@ abstract class DungeonRule { override val requiresOpen: Boolean get() = true - override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean { + override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell): Boolean { if (world.markSurfaceLevel != null) return y >= world.markSurfaceLevel - 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.getCell(x, y) + override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean { 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 be3b29ff..01900e70 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt @@ -16,6 +16,7 @@ import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.getAdapter import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.world.api.AbstractCell @JsonAdapter(DungeonTile.Adapter::class) data class DungeonTile( @@ -68,28 +69,24 @@ data class DungeonTile( // TODO: find a way around this, to make dungeons less restricted by this // 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.getCell(x, y) - + fun canPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell = world.parent.getCell(x, y)): Boolean { if (cell.dungeonId != NO_DUNGEON_ID) return false if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y)) return false - return rules.none { !it.checkTileCanPlace(x, y, world) } + return rules.none { !it.checkTileCanPlace(x, y, world, cell) } } - fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean { - val cell = world.getCell(x, y) - + fun canPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell = world.getCell(x, y)): Boolean { if (cell.dungeonId != NO_DUNGEON_ID) return false if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y)) return false - return rules.none { !it.checkTileCanPlace(x, y, world) } + return rules.none { !it.checkTileCanPlace(x, y, world, cell) } } fun place(x: Int, y: Int, phase: DungeonBrush.Phase, world: DungeonWorld) { 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 428e7303..b44ee033 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt @@ -15,8 +15,12 @@ import ru.dbotthepony.kstarbound.defs.image.Image import java.lang.ref.Reference class ImagePartReader(part: DungeonPart, val images: ImmutableList) : PartReader(part) { - override val size: Vector2i - get() = if (images.isEmpty()) Vector2i.ZERO else images.first().size + override val size: Vector2i by lazy { + if (images.isEmpty()) + return@lazy Vector2i.ZERO + + Vector2i(images.maxOf { it.size.x }, images.maxOf { it.size.y }) + } // ObjectArrayList doesn't check for concurrent modifications private val layers = ObjectArrayList() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledPartReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledPartReader.kt index 3c7b6350..ee83d939 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledPartReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/TiledPartReader.kt @@ -26,8 +26,12 @@ class TiledPartReader(part: DungeonPart, parts: Stream) : PartReader(par // also why would you ever want multiple maps specified lmao // it already has layers and everything else you would ever need - override val size: Vector2i - get() = maps.firstOrNull()?.size ?: Vector2i.ZERO + override val size: Vector2i by lazy { + if (maps.isEmpty()) + return@lazy Vector2i.ZERO + + Vector2i(maps.maxOf { it.size.x }, maps.maxOf { it.size.y }) + } override fun bind(def: DungeonDefinition) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index be0bc5ec..9396c8a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -4,7 +4,6 @@ import com.github.benmanes.caffeine.cache.AsyncLoadingCache import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.LoadingCache -import com.github.benmanes.caffeine.cache.Scheduler import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet import com.google.gson.JsonArray @@ -15,7 +14,6 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap -import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL45 @@ -50,7 +48,7 @@ import java.util.Collections import java.util.Optional import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.locks.ReentrantLock +import java.util.function.Consumer class Image private constructor( val source: IStarboundFile, @@ -108,6 +106,9 @@ class Image private constructor( } val data: ByteBuffer + get() = dataCache.get(source).join() + + val dataFuture: CompletableFuture get() = dataCache.get(source) val texture: GLTexture2D get() { @@ -125,10 +126,12 @@ class Image private constructor( client.named2DTextures1.get(this) { val tex = GLTexture2D(width, height, GL45.GL_RGBA8) - tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, data) + dataFuture.thenAcceptAsync(Consumer { + tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, data) - tex.textureMinFilter = GL45.GL_NEAREST - tex.textureMagFilter = GL45.GL_NEAREST + tex.textureMinFilter = GL45.GL_NEAREST + tex.textureMagFilter = GL45.GL_NEAREST + }, client) tex } @@ -334,19 +337,19 @@ class Image private constructor( return ReadDirectData(data, getWidth[0], getHeight[0], components[0]) } - private val dataCache: LoadingCache = Caffeine.newBuilder() + private val dataCache: AsyncLoadingCache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(1)) .weigher { key, value -> value.capacity() } .maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */)) .scheduler(Starbound) - .executor(Starbound.EXECUTOR) // SCREENED_EXECUTOR shouldn't be used here - .build { readImageDirect(it).data } + .executor(Starbound.IO_EXECUTOR) + .buildAsync(CacheLoader { readImageDirect(it).data }) private val spaceScanCache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(30)) .softValues() .scheduler(Starbound) - .executor(Starbound.SCREENED_EXECUTOR) + .executor(Starbound.EXECUTOR) .build>() @JvmStatic 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 aade96f5..a707f2a9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -20,7 +20,6 @@ import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid 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 @@ -112,14 +111,12 @@ class WorldTemplate(val geometry: WorldGeometry) { return data } - fun findSensiblePlayerStart(): Vector2d? { + fun findSensiblePlayerStart(random: RandomGenerator): Vector2d? { val layout = worldLayout ?: return null if (layout.playerStartSearchRegions.isEmpty()) return null - val random = random() - for (i in 0 until Globals.worldTemplate.playerStartSearchTries) { val region = layout.playerStartSearchRegions.random(random) val x = random.nextInt(region.mins.x, region.maxs.x) @@ -298,23 +295,30 @@ class WorldTemplate(val geometry: WorldGeometry) { var backgroundCave = false } - private val cellCache = Caffeine.newBuilder() - .maximumSize(1_500_000L) // plentiful of space, and allows for high hit ratio (around 79%) in most situations - // downside is memory consumption, but why should it matter when we save 80% of cpu time? - .expireAfterAccess(Duration.ofSeconds(20)) - .executor(Starbound.SCREENED_EXECUTOR) - .scheduler(Starbound) - // .recordStats() - .build { (x, y) -> cellInfo0(x, y) } + // as said by Ben Manes, if cache is write-heavy, it is up to end users + // to stripe it into multiple distinct caches (so write buffer doesn't get overflown and force + // to be drained in place) + // https://github.com/ben-manes/caffeine/issues/1320#issuecomment-1812884592 + private val cellCache = Array(256) { + Caffeine.newBuilder() + .maximumSize(50_000L) // plentiful of space, and allows for high hit ratio (around 79%) in most situations + // downside is memory consumption, but why should it matter when we save 80% of cpu time? + .expireAfterAccess(Duration.ofSeconds(20)) + .executor(Starbound.EXECUTOR) + .scheduler(Starbound) + // .recordStats() + .build { (x, y) -> cellInfo0(x, y) } + } fun cellInfo(x: Int, y: Int): CellInfo { worldLayout ?: return CellInfo(x, y) - return cellCache.get(Vector2i(x, y)) + val vec = Vector2i(x, y) + return cellCache[vec.hashCode() and 255].get(vec) } fun cellInfo(pos: Vector2i): CellInfo { worldLayout ?: return CellInfo(pos.x, pos.y) - return cellCache.get(pos) + return cellCache[pos.hashCode() and 255].get(pos) } private fun cellInfo0(x: Int, y: Int): CellInfo { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index dab4bf35..256732f5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -128,9 +128,16 @@ fun InputStream.readAABB(): AABB { return AABB(readVector2d(), readVector2d()) } +fun InputStream.readAABB(isLegacy: Boolean): AABB { + if (isLegacy) + return readAABBLegacy() + else + return readAABB() +} + fun OutputStream.writeAABBLegacy(value: AABB) { - writeStruct2f(value.mins.toFloatVector()) - writeStruct2f(value.maxs.toFloatVector()) + writeStruct2d(value.mins, true) + writeStruct2d(value.maxs, true) } fun OutputStream.writeAABBLegacyOptional(value: KOptional) { @@ -150,6 +157,11 @@ fun OutputStream.writeAABB(value: AABB) { writeStruct2d(value.maxs) } +fun OutputStream.writeAABB(value: AABB, isLegacy: Boolean) { + writeStruct2d(value.mins, isLegacy) + writeStruct2d(value.maxs, isLegacy) +} + private fun InputStream.readBoolean(): Boolean { val read = read() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt index cfed6f3a..d07e808c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -149,7 +149,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { val results = newTable() for (connection in self.inputNodes[index.toInt()].connections) { - val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation) as? WorldObject + val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) if (entity != null) { results[entity.entityID] = connection.index @@ -163,7 +163,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { val results = newTable() for (connection in self.outputNodes[index.toInt()].connections) { - val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation) as? WorldObject + val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) if (entity != null) { results[entity.entityID] = connection.index diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt index 93b6654c..6c1228ca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -277,6 +277,37 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) { return AABB(pos, pos + Vector2d(width, height)) } + fun ofPoints(points: Collection): AABB { + if (points.isEmpty()) + return NEVER + + val minX = points.minOf { it.x } + val maxX = points.maxOf { it.x } + val minY = points.minOf { it.y } + val maxY = points.maxOf { it.y } + + return AABB( + Vector2d(minX, minY), + Vector2d(maxX, maxY), + ) + } + + @JvmName("ofPointsI") + fun ofPoints(points: Collection): AABB { + if (points.isEmpty()) + return NEVER + + val minX = points.minOf { it.x }.toDouble() + val maxX = points.maxOf { it.x }.toDouble() + val minY = points.minOf { it.y }.toDouble() + val maxY = points.maxOf { it.y }.toDouble() + + return AABB( + Vector2d(minX, minY), + Vector2d(maxX, maxY), + ) + } + @JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO) @JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt index 58adcbc0..66f78e1f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt @@ -17,8 +17,8 @@ class ConnectWirePacket(val target: WireConnection, val source: WireConnection) override fun play(connection: ServerConnection) { connection.enqueue { - val target = entityIndex.tileEntityAt(target.entityLocation) as? WorldObject ?: return@enqueue - val source = entityIndex.tileEntityAt(source.entityLocation) as? WorldObject ?: return@enqueue + val target = entityIndex.tileEntityAt(target.entityLocation, WorldObject::class) ?: return@enqueue + val source = entityIndex.tileEntityAt(source.entityLocation, WorldObject::class) ?: return@enqueue val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueue val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueue diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt index 000bf3d2..0b6a53e3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt @@ -21,7 +21,7 @@ class DisconnectAllWiresPacket(val pos: Vector2i, val node: WireNode) : IServerP override fun play(connection: ServerConnection) { connection.enqueue { - val target = entityIndex.tileEntityAt(pos) as? WorldObject ?: return@enqueue + val target = entityIndex.tileEntityAt(pos, WorldObject::class) ?: return@enqueue val node = if (node.isInput) target.inputNodes.getOrNull(node.index) else target.outputNodes.getOrNull(node.index) node?.removeAllConnections() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt index c6f08534..0471f2b8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt @@ -67,9 +67,9 @@ class LegacyWireProcessor(val world: ServerWorld) { launch { ticket.chunk.await() - val findEntity = world.entityIndex.tileEntityAt(pos) + val findEntity = world.entityIndex.tileEntityAt(pos, WorldObject::class) - if (findEntity is WorldObject) { + if (findEntity != null) { // if entity exists, add it to working entities and find more not loaded entities populateWorking(findEntity) } else { 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 d00f27b8..99402ee2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.server.world import com.google.gson.JsonObject import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine @@ -70,6 +71,7 @@ import kotlin.concurrent.withLock import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException +import kotlin.math.min class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(world, pos) { override var state: ChunkState = ChunkState.FRESH @@ -731,7 +733,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { x, y, tile -> - if (tile.usesPlaces && world.getCell(pos.x + x, pos.y + y).dungeonId != NO_DUNGEON_ID) { - return@walkTiles KOptional(true) - } - - return@walkTiles KOptional() - }.orElse(false) - - if (!collision && anchor.canPlace(pos.x, pos.y, world)) { + // this is quite ugly code flow, but we should try to avoid double-walking + // over all dungeon tiles (DungeonTile#canPlace already checks for absence of other dungeons on their place, + // so we only need to tell DungeonPart to not force-place) + if (anchor.canPlace(pos.x, pos.y, world, false)) { try { dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await() } catch (err: Throwable) { @@ -778,7 +774,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk> { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 3e9f5cd4..18274dee 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -382,10 +382,11 @@ class ServerWorld private constructor( //} val tickets = ArrayList() + val random = if (hint == null) random(template.seed) else random() try { LOGGER.info("Trying to find player spawn position...") - var pos = hint ?: CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart() }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble()) + var pos = hint ?: CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble()) var previous = pos LOGGER.info("Trying to find player spawn position near $pos...") @@ -442,7 +443,7 @@ class ServerWorld private constructor( } } - pos = CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart() }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble()) + pos = CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble()) if (previous != pos) { LOGGER.info("Still trying to find player spawn position near $pos...") 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 f631a4dd..10fdc5a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -82,7 +82,12 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p private suspend fun damageTilesLoop() { while (true) { val (positions, isBackground, sourcePosition, damage, source) = damageTilesQueue.receive() - world.damageTiles(positions, isBackground, sourcePosition, damage, source, tileModificationBudget) + + try { + world.damageTiles(positions, isBackground, sourcePosition, damage, source, tileModificationBudget) + } catch (err: Throwable) { + LOGGER.error("Exception in player damage tiles loop", err) + } } } @@ -102,6 +107,9 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p } } catch (err: CancellationException) { client.send(TileModificationFailurePacket(modifications)) + } catch (err: Throwable) { + client.send(TileModificationFailurePacket(modifications)) + LOGGER.error("Exception in player modify tiles loop", err) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt index 7c811e8a..4a40c8b4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt @@ -15,6 +15,8 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import java.util.concurrent.atomic.AtomicInteger import java.util.function.Predicate +import kotlin.reflect.KClass +import kotlin.reflect.full.isSuperclassOf // After some thinking, I decided to go with separate spatial index over // using chunk/chunkmap as spatial indexing of entities (just like original engine does). @@ -283,10 +285,18 @@ class EntityIndex(val geometry: WorldGeometry) { return first(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as TileEntity? } + fun tileEntityAt(pos: Vector2i, type: KClass): T? { + return first(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces && type.isSuperclassOf(it::class) }) as T? + } + fun tileEntitiesAt(pos: Vector2i): MutableList { return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as MutableList } + fun tileEntitiesAt(pos: Vector2i, type: KClass): MutableList { + return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces && type.isSuperclassOf(it::class) }) as MutableList + } + fun iterate(rect: AABB, visitor: (AbstractEntity) -> Unit, withEdges: Boolean = true) { walk(rect, { visitor(it); KOptional() }, withEdges) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt index 9e741812..1ff9bc8d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -12,8 +12,6 @@ import ru.dbotthepony.kstarbound.world.World * Entities with dynamics (Player, Drops, Projectiles, NPCs, etc) */ abstract class DynamicEntity() : AbstractEntity() { - private var forceChunkRepos = false - override var position get() = movement.position set(value) { @@ -52,7 +50,6 @@ abstract class DynamicEntity() : AbstractEntity() { super.onJoinWorld(world) world.dynamicEntities.add(this) movement.initialize(world, spatialEntry) - forceChunkRepos = true metaFixture = spatialEntry!!.Fixture() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index 8f477979..e98ca9fa 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -553,7 +553,7 @@ open class MovementController() { movement = movement + totalCorrection, correction = totalCorrection, isStuck = false, - isOnGround = -totalCorrection.dot(determineGravity()) > SEPARATION_TOLERANCE, + isOnGround = totalCorrection.unitVector.dot(determineGravity().unitVector) >= 0.5, movingCollisionId = movingCollisionId, collisionType = maxCollided, // groundSlope = Vector2d.POSITIVE_Y, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt index 3e45041d..e315f75a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt @@ -2,9 +2,14 @@ package ru.dbotthepony.kstarbound.world.entities.tile import com.google.common.collect.ImmutableSet import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.TypeAdapter +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap +import it.unimi.dsi.fastutil.ints.Int2ObjectFunction +import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager @@ -29,7 +34,6 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isMetaTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile -import ru.dbotthepony.kstarbound.defs.tile.isNullTile import ru.dbotthepony.kstarbound.defs.world.BushVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant @@ -64,13 +68,14 @@ import java.io.DataInputStream import java.io.DataOutputStream import java.util.* import java.util.random.RandomGenerator +import kotlin.collections.ArrayList import kotlin.math.absoluteValue class PlantEntity() : TileEntity() { @JsonFactory data class Piece( val image: String, - val offset: Vector2d, + var offset: Vector2d, var segmentIdx: Int, val isStructuralSegment: Boolean, val kind: Kind, @@ -147,7 +152,7 @@ class PlantEntity() : TileEntity() { isCeiling = data.get("ceiling", false) stemDropConfig = data["stemDropConfig"] as? JsonObject ?: JsonObject() foliageDropConfig = data["foliageDropConfig"] as? JsonObject ?: JsonObject() - saplingDropConfig = data["saplingDropConfig"] as? JsonObject ?: JsonObject() + saplingDropConfig = data["saplingDropConfig"] ?: JsonObject() descriptions = data["descriptions"] as? JsonObject ?: JsonObject() isEphemeral = data.get("ephemeral", false) fallsWhenDead = data.get("fallsWhenDead", false) @@ -220,7 +225,7 @@ class PlantEntity() : TileEntity() { private set var foliageDropConfig: JsonObject = JsonObject() private set - var saplingDropConfig: JsonObject = JsonObject() + var saplingDropConfig: JsonElement = JsonObject() private set var descriptions: JsonObject = JsonObject() private set @@ -229,6 +234,7 @@ class PlantEntity() : TileEntity() { constructor(config: TreeVariant, random: RandomGenerator) : this() { isCeiling = config.ceiling + fallsWhenDead = true stemDropConfig = (config.stemDropConfig as? JsonObject)?.deepCopy() ?: JsonObject() foliageDropConfig = (config.foliageDropConfig as? JsonObject)?.deepCopy() ?: JsonObject() @@ -532,7 +538,7 @@ class PlantEntity() : TileEntity() { isCeiling = stream.readBoolean() stemDropConfig = stream.readJsonElement() as JsonObject foliageDropConfig = stream.readJsonElement() as JsonObject - saplingDropConfig = stream.readJsonElement() as JsonObject + saplingDropConfig = stream.readJsonElement() descriptions = stream.readJsonElement() as JsonObject isEphemeral = stream.readBoolean() @@ -640,8 +646,24 @@ class PlantEntity() : TileEntity() { override fun tick(delta: Double) { super.tick(delta) - if (world.isServer && piecesInternal.isEmpty()) { - remove(RemovalReason.REMOVED) + if (world.isServer) { + if (piecesInternal.isEmpty()) { + remove(RemovalReason.REMOVED) + } else if (roots.isNotEmpty()) { + for (root in roots) { + if (world.getCell(root).foreground.material.isEmptyTile) { + if (fallsWhenDead) { + breakAtPosition(tilePosition, position) + } else { + remove(RemovalReason.DYING) + } + } + } + } + } + + if (!isRemote) { + health.tick(tileDamageParameters, delta) } } @@ -660,10 +682,123 @@ class PlantEntity() : TileEntity() { } override fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { - // TODO + if (damageSpaces.isEmpty()) + return false + + var baseDamagePosition: Vector2i = damageSpaces.first() + + for (piece in pieces) { + if (piece.isStructuralSegment) { + for (space in piece.spaces) { + for (pos in damageSpaces) { + if (world.geometry.wrap(space + tilePosition) == pos && baseDamagePosition.y < pos.y == isCeiling) { + // if this space is a "better match" for the root of the plant + baseDamagePosition = pos + } + } + } + } + } + + val (x, y) = world.geometry.diff(baseDamagePosition, tilePosition) + + // TODO: this is unnatural solution to tree damage, + // each tree piece should have its own damage status + health.damage(tileDamageParameters, source, damage) + tileDamageX = x.toDouble() + tileDamageY = y.toDouble() + tileDamageEvent.trigger() + + if (health.isDead) { + if (fallsWhenDead) { + health.reset() + breakAtPosition(baseDamagePosition, source) + } else { + remove(RemovalReason.DYING) + } + } + return false } + private fun breakAtPosition(position: Vector2i, source: Vector2d) { + val internalPos = world.geometry.diff(position, tilePosition) + var breakAtPiece = pieces.lastOrNull { it.isStructuralSegment && internalPos in it.spaces } + + // default to highest structural piece + if (breakAtPiece == null) { + breakAtPiece = pieces.lastOrNull { it.isStructuralSegment } + } + + // plant has no structural segments? this is a terrible fallback because it + // prevents destruction + breakAtPiece ?: return + + var breakPoint = position.toDoubleVector() - tilePosition + + if (breakAtPiece.spaces.isNotEmpty()) { + val bounds = AABB.ofPoints(breakAtPiece.spaces) + + breakPoint = Vector2d( + bounds.mins.x + bounds.width / 2.0, + if (isCeiling) bounds.maxs.y else bounds.mins.y + ) + } + + val droppedPieces = ArrayList() + + var idx = 0 + + while (idx < pieces.size) { + if (piecesInternal[idx].segmentIdx >= breakAtPiece.segmentIdx) { + droppedPieces.add(piecesInternal.removeAt(idx)) + } else { + idx++ + } + } + + val breakPointI = Vector2i((breakPoint.x + 0.5).toInt(), (breakPoint.y + 0.5).toInt()) + + // Calculate a new origin for the droppedPieces + for (piece in droppedPieces) { + piece.offset -= breakPoint + piece.spaces = piece.spaces.map { it - breakPointI }.toSet() + } + + val worldSpaceBreakPoint = breakPoint + tilePosition + val segments = Int2ObjectAVLTreeMap>() + + for (piece in droppedPieces) { + segments.computeIfAbsent(piece.segmentIdx, Int2ObjectFunction { ArrayList() }).add(piece) + } + + val angle = world.random.nextDouble(-0.3, 0.3) + val itr = segments.keys.iterator(segments.keys.lastInt()) + val fallVector = (source - worldSpaceBreakPoint).unitVector + var first = true + + while (itr.hasPrevious()) { + val index = itr.previousInt() + val segment = segments[index]!! + + val entity = PlantPieceEntity( + segment, + worldSpaceBreakPoint, + fallVector, + description, + isCeiling, + stemDropConfig, + foliageDropConfig, + saplingDropConfig, + first, + angle + ) + + entity.joinWorld(world) + first = false + } + } + override fun toString(): String { return "PlantEntity[at=$tilePosition, pieces=${pieces.size}]" } @@ -692,7 +827,7 @@ class PlantEntity() : TileEntity() { // First bail out if we can't fit anything we're not adjusting for (space in occupySpaces) { // TODO: conditions seems to be inverted - if (withinAdjustments(space, position) && world.entityIndex.tileEntitiesAt(space).any { it is PlantEntity }) { + if (withinAdjustments(space, position) && world.entityIndex.tileEntityAt(space, PlantEntity::class) != null) { return false } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt new file mode 100644 index 00000000..0e0f8785 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantPieceEntity.kt @@ -0,0 +1,285 @@ +package ru.dbotthepony.kstarbound.world.entities.tile + +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.defs.EntityType +import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.image.Image +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.io.readAABB +import ru.dbotthepony.kstarbound.io.readDouble +import ru.dbotthepony.kstarbound.io.readEnumStupid +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.io.writeAABB +import ru.dbotthepony.kstarbound.io.writeDouble +import ru.dbotthepony.kstarbound.io.writeEnumStupid +import ru.dbotthepony.kstarbound.io.writeStruct2d +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.math.vector.times +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.entities.DynamicEntity +import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity +import ru.dbotthepony.kstarbound.world.entities.MovementController +import ru.dbotthepony.kstarbound.world.physics.Poly +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.Collections +import java.util.stream.Collectors +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.sign + +class PlantPieceEntity() : DynamicEntity() { + override val type: EntityType + get() = EntityType.PLANT_DROP + + private var calculatedMetaBoundingBox = AABB.ZERO + private var calculatedCollisionBox = AABB.ZERO + private var calculatedCollisionHull = Poly.EMPTY + + override val metaBoundingBox: AABB + get() = calculatedMetaBoundingBox + position + + override val collisionArea: AABB + get() = calculatedCollisionBox + position + + override val movement = MovementController().also { networkGroup.upstream.add(it.networkGroup) } + var spawnedDrops by networkedBoolean().also { networkGroup.upstream.add(it) } + private set + + private val piecesInternal = ArrayList() + + val pieces: List = Collections.unmodifiableList(piecesInternal) + + var isFirst = false + private set + + var stemConfig: JsonObject = JsonObject() + private set + var foliageConfig: JsonObject = JsonObject() + private set + var saplingConfig: JsonElement = JsonNull.INSTANCE + private set + + var rotationRate = 0.0 + private set + var rotationFallThreshold = 0.0 + private set + var rotationCap = 0.0 + private set + + var timeToLive = 30.0 + private set + + data class Piece( + val image: String, + val offset: Vector2d, + val segmentIdx: Int, + val flip: Boolean, + val kind: PlantEntity.Piece.Kind, + ) { + constructor(piece: PlantEntity.Piece) : this(piece.image, piece.offset, piece.segmentIdx, piece.flip, piece.kind) + + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readInternedString(), + stream.readVector2d(isLegacy), + 0, // stream.readIntStupid(isLegacy), + stream.readBoolean(), + PlantEntity.Piece.Kind.entries[stream.readEnumStupid(isLegacy)], + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(image) + stream.writeStruct2d(offset, isLegacy) + // stream.writeIntStupid(segmentIdx, isLegacy) + stream.writeBoolean(flip) + stream.writeEnumStupid(kind.ordinal, isLegacy) + } + } + + constructor( + pieces: List, + position: Vector2d, + damageSource: Vector2d, + description: String, + upsideDown: Boolean, + stemConfig: JsonObject, + foliageConfig: JsonObject, + saplingConfig: JsonElement, + isFirst: Boolean, + angle: Double + ) : this() { + this.stemConfig = stemConfig + this.foliageConfig = foliageConfig + this.saplingConfig = saplingConfig + this.isFirst = isFirst + + this.movement.position = position + this.description = description + + if (!upsideDown) { + this.rotationRate = 0.00001 * (damageSource.x + angle).sign + this.rotationFallThreshold = PI / (3.0 + angle) + this.rotationCap = PI - this.rotationFallThreshold + } + + val stemSpaces = pieces.stream().filter { it.isStructuralSegment }.flatMap { it.spaces.stream() }.collect(Collectors.toCollection(::ObjectArraySet)) + val allSpaces = pieces.stream().flatMap { it.spaces.stream() }.collect(Collectors.toCollection(::ObjectArraySet)) + + for (piece in pieces) { + piecesInternal.add(Piece(piece)) + } + + calculatedMetaBoundingBox = AABB.ofPoints(allSpaces) + + if (pieces.any { it.isStructuralSegment } && stemSpaces.isNotEmpty()) { + calculatedCollisionBox = AABB.ofPoints(stemSpaces) + + if (stemSpaces.size >= 2) { + calculatedCollisionHull = Poly.quickhull(stemSpaces.map { it.toDoubleVector() }) + } else { + calculatedCollisionHull = Poly(calculatedCollisionBox) + } + } else { + calculatedCollisionBox = calculatedMetaBoundingBox + calculatedCollisionHull = Poly(calculatedMetaBoundingBox) + } + + //calculatedCollisionHull = calculatedCollisionHull * 0.5 + calculatedCollisionHull.aabb.centre * 0.5 + } + + constructor(stream: DataInputStream, isLegacy: Boolean) : this() { + timeToLive = stream.readDouble(isLegacy) + isFirst = stream.readBoolean() + description = stream.readInternedString() + calculatedMetaBoundingBox = stream.readAABB(isLegacy) + calculatedCollisionBox = stream.readAABB(isLegacy) + rotationRate = stream.readDouble(isLegacy) + + piecesInternal.clear() + piecesInternal.addAll(stream.readCollection { Piece(this, isLegacy) }) + + stemConfig = stream.readJsonElement() as JsonObject + foliageConfig = stream.readJsonElement() as JsonObject + saplingConfig = stream.readJsonElement() + } + + override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeDouble(timeToLive, isLegacy) + stream.writeBoolean(isFirst) + stream.writeBinaryString(description) + stream.writeAABB(calculatedMetaBoundingBox, isLegacy) + stream.writeAABB(calculatedCollisionBox, isLegacy) + stream.writeDouble(rotationRate, isLegacy) + stream.writeCollection(piecesInternal) { it.write(this, isLegacy) } + stream.writeJsonElement(stemConfig) + stream.writeJsonElement(foliageConfig) + stream.writeJsonElement(saplingConfig) + } + + override fun onJoinWorld(world: World<*, *>) { + val parameters = MovementParameters( + collisionPoly = Either.left(calculatedCollisionHull), + ignorePlatformCollision = true, + gravityMultiplier = 0.2, + physicsEffectCategories = ImmutableSet.of("plantdrop") + ) + + movement.applyParameters(parameters) + super.onJoinWorld(world) + } + + override fun tick(delta: Double) { + super.tick(delta) + + timeToLive -= delta + + if (!isRemote) { + // TODO: think up a better curve then sin + val rotationAcceleration = 0.01 * world.gravityAt(position).length * rotationRate.sign * delta + + if (movement.rotation.absoluteValue > rotationCap) + rotationRate -= rotationAcceleration + else if (movement.rotation.absoluteValue < rotationFallThreshold) + rotationRate += rotationAcceleration + + movement.rotation = rotationRate + + if (timeToLive > 0.0) { + movement.applyParameters(MovementParameters(gravityEnabled = rotationRate.absoluteValue >= rotationFallThreshold)) + + if (movement.isOnGround) { + timeToLive = 0.0 + } + } + + if ((timeToLive <= 0.0 || world.gravityAt(position).lengthSquared == 0.0) && !spawnedDrops) { + spawnedDrops = true + + for (piece in piecesInternal) { + var dropOptions = JsonArray() + + when (piece.kind) { + PlantEntity.Piece.Kind.NONE -> {} + + PlantEntity.Piece.Kind.STEM -> { + dropOptions = stemConfig.get("drops", JsonArray()) + } + + PlantEntity.Piece.Kind.FOLIAGE -> { + dropOptions = foliageConfig.get("drops", JsonArray()) + } + } + + if (dropOptions.size() > 0) { + val option = dropOptions.random(world.random).asJsonArray + + for (drop in option) { + val img = Image.get(piece.image) ?: continue + + var pos = piece.offset + img.size.toDoubleVector() * 0.5 / PIXELS_IN_STARBOUND_UNIT + pos = pos.rotate(movement.rotation) + pos += Vector2d(world.random.nextDouble(-0.2, 0.2), world.random.nextDouble(-0.2, 0.2)) + pos += position + + var descriptor = ItemDescriptor(drop) + + if (descriptor.name == "sapling") { + descriptor = descriptor.copy(parameters = saplingConfig as? JsonObject ?: JsonObject()) + } + + val entity = ItemDropEntity(descriptor) + entity.position = pos + entity.joinWorld(world) + } + } + } + + remove(RemovalReason.DYING) + return + } + } + + if (world.isServer && timeToLive <= 0.0) { + remove(RemovalReason.REMOVED) + } + } +} 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 a489a225..b4515ba9 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 @@ -269,7 +269,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit if (connectionsInternal.isNotEmpty()) { // ensure that we disconnect both ends val any = connectionsInternal.removeIf { - val otherEntity = world.entityIndex.tileEntityAt(it.entityLocation) as? WorldObject + val otherEntity = world.entityIndex.tileEntityAt(it.entityLocation, WorldObject::class) val otherConnections = if (isInput) otherEntity?.outputNodes else otherEntity?.inputNodes val any = otherConnections?.getOrNull(it.index)?.connectionsInternal?.removeIf { it.entityLocation == tilePosition && it.index == index } @@ -547,7 +547,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit val itr = node.connectionsInternal.listIterator() for (connection in itr) { - connection.otherEntity = connection.otherEntity ?: world.entityIndex.tileEntityAt(connection.entityLocation) as? WorldObject + connection.otherEntity = connection.otherEntity ?: world.entityIndex.tileEntityAt(connection.entityLocation, WorldObject::class) if (connection.otherEntity?.isInWorld == false) { // break connection if other entity got removed diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt index 022384a2..81fbb5b4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt @@ -1,8 +1,6 @@ package ru.dbotthepony.kstarbound.world.terrain import com.github.benmanes.caffeine.cache.Caffeine -import com.github.benmanes.caffeine.cache.Scheduler -import ru.dbotthepony.kommons.arrays.Double2DArray import ru.dbotthepony.kommons.arrays.Float2DArray import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound @@ -63,7 +61,7 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters .softValues() .expireAfterAccess(Duration.ofMinutes(1)) .scheduler(Starbound) - .executor(Starbound.SCREENED_EXECUTOR) + .executor(Starbound.EXECUTOR) .build(::Layer) private inner class Sector(val sector: Vector2i) { @@ -132,7 +130,7 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters .softValues() .expireAfterAccess(Duration.ofMinutes(1)) .scheduler(Starbound) - .executor(Starbound.SCREENED_EXECUTOR) + .executor(Starbound.EXECUTOR) .build(::Sector) override fun get(x: Int, y: Int): Double { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt index d8b2e712..6c4d90b4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt @@ -1,8 +1,6 @@ package ru.dbotthepony.kstarbound.world.terrain import com.github.benmanes.caffeine.cache.Caffeine -import com.github.benmanes.caffeine.cache.Scheduler -import ru.dbotthepony.kommons.arrays.Double2DArray import ru.dbotthepony.kommons.arrays.Float2DArray import ru.dbotthepony.kommons.math.linearInterpolation import ru.dbotthepony.kstarbound.math.vector.Vector2d @@ -186,7 +184,7 @@ class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters) .softValues() .expireAfterAccess(Duration.ofMinutes(1)) .scheduler(Starbound) - .executor(Starbound.SCREENED_EXECUTOR) + .executor(Starbound.EXECUTOR) .build(::Sector) override fun get(x: Int, y: Int): Double {