Functional plants

This commit is contained in:
DBotThePony 2024-04-22 16:41:05 +07:00
parent 17a3de38bc
commit 74bbc58c60
Signed by: DBot
GPG Key ID: DCC23B5715498507
30 changed files with 616 additions and 129 deletions

View File

@ -147,3 +147,6 @@ val color: TileColor = TileColor.DEFAULT
#### Dungeons #### Dungeons
* All brushes are now deterministic * All brushes are now deterministic
#### Plant drop entities (vines or steps dropping on ground)
* Collision is now determined using hull instead of rectangle

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10 kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.15.0 kommonsVersion=2.15.1
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -6,6 +6,8 @@ import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.* import com.google.gson.*
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.compiler.CompilerChunkLoader import org.classdump.luna.compiler.CompilerChunkLoader
@ -76,6 +78,7 @@ import java.io.*
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.text.DateFormat import java.text.DateFormat
import java.time.Duration import java.time.Duration
import java.util.Collections
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor import java.util.concurrent.Executor
@ -135,7 +138,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
@JvmField @JvmField
val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory { 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.isDaemon = true
thread.priority = Thread.MIN_PRIORITY thread.priority = Thread.MIN_PRIORITY
@ -151,12 +154,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
@JvmField @JvmField
val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher() 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 @JvmField
val CLEANER: Cleaner = Cleaner.create { val CLEANER: Cleaner = Cleaner.create {
val t = Thread(it, "Starbound Global Cleaner") val t = Thread(it, "Starbound Global Cleaner")

View File

@ -1,41 +1,45 @@
package ru.dbotthepony.kstarbound.collect package ru.dbotthepony.kstarbound.collect
class RandomListIterator<E>(private val elements: MutableList<E>, index: Int = 0) : MutableListIterator<E> { class RandomListIterator<E>(private val elements: MutableList<E>, private var index: Int = 0) : MutableListIterator<E> {
private var index = index - 1
override fun hasPrevious(): Boolean { override fun hasPrevious(): Boolean {
return this.index > 0 return this.index > 0
} }
override fun nextIndex(): Int { override fun nextIndex(): Int {
return this.index + 1 return this.index
} }
override fun previous(): E { override fun previous(): E {
return elements[--this.index] lastIndex = --this.index
return elements[lastIndex]
} }
override fun previousIndex(): Int { override fun previousIndex(): Int {
return (this.index - 1).coerceAtLeast(-1) return this.index - 1
} }
override fun add(element: E) { override fun add(element: E) {
elements.add(this.index++, element) elements.add(this.index++, element)
lastIndex = -1
} }
override fun hasNext(): Boolean { override fun hasNext(): Boolean {
return this.index < elements.size - 1 return this.index < elements.size
} }
private var lastIndex = -1
override fun next(): E { override fun next(): E {
return elements[++this.index] lastIndex = this.index++
return elements[lastIndex]
} }
override fun remove() { override fun remove() {
elements.removeAt(this.index--) elements.removeAt(lastIndex)
lastIndex = -1
} }
override fun set(element: E) { override fun set(element: E) {
elements[this.index] = element elements[lastIndex] = element
} }
} }

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class EntityType(override val jsonName: String, val storeName: String) : 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"), OBJECT("object", "ObjectEntity"),
VEHICLE("vehicle", "VehicleEntity"), VEHICLE("vehicle", "VehicleEntity"),
ITEM_DROP("itemDrop", "ItemDropEntity"), ITEM_DROP("itemDrop", "ItemDropEntity"),
PLANT_DROP("plantDrop", "PlantDropEntity"), // wat PLANT_DROP("plantDrop", "PlantDropEntity"),
PROJECTILE("projectile", "ProjectileEntity"), PROJECTILE("projectile", "ProjectileEntity"),
STAGEHAND("stagehand", "StagehandEntity"), STAGEHAND("stagehand", "StagehandEntity"),
MONSTER("monster", "MonsterEntity"), MONSTER("monster", "MonsterEntity"),

View File

@ -10,6 +10,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
@ -225,8 +226,13 @@ class DungeonPart(data: JsonData) {
return true return true
return world.waitForRegionAndJoin(Vector2i(x, y), reader.size) { 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<Boolean> { tx, ty, tile -> reader.walkTiles<Boolean> { 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) return@walkTiles KOptional(false)
} }
@ -235,12 +241,17 @@ class DungeonPart(data: JsonData) {
}.orElse(true) }.orElse(true)
} }
fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean { fun canPlace(x: Int, y: Int, world: ServerWorld, allowAlways: Boolean = this.overrideAllowAlways): Boolean {
if (overrideAllowAlways || reader.size.x == 0 || reader.size.y == 0) if (allowAlways || reader.size.x == 0 || reader.size.y == 0)
return true return true
val cells = Object2DArray(reader.size.x, reader.size.y) { tx, ty ->
world.getCell(x + tx, y + ty)
}
return reader.walkTiles<Boolean> { tx, ty, tile -> return reader.walkTiles<Boolean> { 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) return@walkTiles KOptional(false)
} }

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.defs.tile.isObjectTile
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.api.AbstractCell
@JsonAdapter(DungeonRule.Adapter::class) @JsonAdapter(DungeonRule.Adapter::class)
abstract class DungeonRule { abstract class DungeonRule {
@ -139,11 +140,11 @@ abstract class DungeonRule {
return false 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 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 return true
} }
@ -161,7 +162,7 @@ abstract class DungeonRule {
override val requiresLiquid: Boolean override val requiresLiquid: Boolean
get() = true 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) val cell = world.parent.template.cellInfo(x, y)
return cell.oceanLiquid.isNotEmptyLiquid && cell.oceanLiquidLevel > y return cell.oceanLiquid.isNotEmptyLiquid && cell.oceanLiquidLevel > y
} }
@ -172,7 +173,7 @@ abstract class DungeonRule {
} }
object MustNotContainLiquid : 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) val cell = world.parent.template.cellInfo(x, y)
return cell.oceanLiquid.isEmptyLiquid || cell.oceanLiquidLevel <= y return cell.oceanLiquid.isEmptyLiquid || cell.oceanLiquidLevel <= y
} }
@ -186,20 +187,17 @@ abstract class DungeonRule {
override val requiresSolid: Boolean override val requiresSolid: Boolean
get() = true 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) if (world.markSurfaceLevel != null)
return y < world.markSurfaceLevel return y < world.markSurfaceLevel
val cell = world.parent.getCell(x, y)
if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)) if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y))
return false return false
return cell.foreground.material.isNotEmptyTile && !world.isClearingTileEntityAt(x, y) return cell.foreground.material.isNotEmptyTile && !world.isClearingTileEntityAt(x, y)
} }
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean {
val cell = world.getCell(x, y)
return cell.foreground.material.isNotEmptyTile return cell.foreground.material.isNotEmptyTile
} }
@ -212,16 +210,14 @@ abstract class DungeonRule {
override val requiresOpen: Boolean override val requiresOpen: Boolean
get() = true 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) if (world.markSurfaceLevel != null)
return y >= world.markSurfaceLevel return y >= world.markSurfaceLevel
val cell = world.parent.getCell(x, y)
return cell.foreground.material.isEmptyTile || cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(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 { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean {
val cell = world.getCell(x, y)
return cell.foreground.material.isEmptyTile return cell.foreground.material.isEmptyTile
} }
@ -234,20 +230,17 @@ abstract class DungeonRule {
override val requiresSolid: Boolean override val requiresSolid: Boolean
get() = true 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) if (world.markSurfaceLevel != null)
return y < world.markSurfaceLevel return y < world.markSurfaceLevel
val cell = world.parent.getCell(x, y)
if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)) if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y))
return false return false
return cell.background.material.isNotEmptyTile return cell.background.material.isNotEmptyTile
} }
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean {
val cell = world.getCell(x, y)
return cell.background.material.isNotEmptyTile return cell.background.material.isNotEmptyTile
} }
@ -260,16 +253,14 @@ abstract class DungeonRule {
override val requiresOpen: Boolean override val requiresOpen: Boolean
get() = true 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) if (world.markSurfaceLevel != null)
return y >= world.markSurfaceLevel return y >= world.markSurfaceLevel
val cell = world.parent.getCell(x, y)
return cell.background.material.isEmptyTile || cell.background.material.isObjectTile && world.isClearingTileEntityAt(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 { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell): Boolean {
val cell = world.getCell(x, y)
return cell.background.material.isEmptyTile return cell.background.material.isEmptyTile
} }

View File

@ -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.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.api.AbstractCell
@JsonAdapter(DungeonTile.Adapter::class) @JsonAdapter(DungeonTile.Adapter::class)
data class DungeonTile( data class DungeonTile(
@ -68,28 +69,24 @@ data class DungeonTile(
// TODO: find a way around this, to make dungeons less restricted by this // 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 // but thats also not a priority, since this check happens quite quickly
// to have any noticeable impact on world's performance // to have any noticeable impact on world's performance
fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean { fun canPlace(x: Int, y: Int, world: DungeonWorld, cell: AbstractCell = world.parent.getCell(x, y)): Boolean {
val cell = world.parent.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID) if (cell.dungeonId != NO_DUNGEON_ID)
return false return false
if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y)) if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
return false 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 { fun canPlace(x: Int, y: Int, world: ServerWorld, cell: AbstractCell = world.getCell(x, y)): Boolean {
val cell = world.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID) if (cell.dungeonId != NO_DUNGEON_ID)
return false return false
if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y)) if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
return false 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) { fun place(x: Int, y: Int, phase: DungeonBrush.Phase, world: DungeonWorld) {

View File

@ -15,8 +15,12 @@ import ru.dbotthepony.kstarbound.defs.image.Image
import java.lang.ref.Reference import java.lang.ref.Reference
class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : PartReader(part) { class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : PartReader(part) {
override val size: Vector2i override val size: Vector2i by lazy {
get() = if (images.isEmpty()) Vector2i.ZERO else images.first().size 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 // ObjectArrayList doesn't check for concurrent modifications
private val layers = ObjectArrayList<Layer>() private val layers = ObjectArrayList<Layer>()

View File

@ -26,8 +26,12 @@ class TiledPartReader(part: DungeonPart, parts: Stream<String>) : PartReader(par
// also why would you ever want multiple maps specified lmao // also why would you ever want multiple maps specified lmao
// it already has layers and everything else you would ever need // it already has layers and everything else you would ever need
override val size: Vector2i override val size: Vector2i by lazy {
get() = maps.firstOrNull()?.size ?: Vector2i.ZERO if (maps.isEmpty())
return@lazy Vector2i.ZERO
Vector2i(maps.maxOf { it.size.x }, maps.maxOf { it.size.y })
}
override fun bind(def: DungeonDefinition) { override fun bind(def: DungeonDefinition) {

View File

@ -4,7 +4,6 @@ import com.github.benmanes.caffeine.cache.AsyncLoadingCache
import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache 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.ImmutableList
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray 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.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL45 import org.lwjgl.opengl.GL45
@ -50,7 +48,7 @@ import java.util.Collections
import java.util.Optional import java.util.Optional
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock import java.util.function.Consumer
class Image private constructor( class Image private constructor(
val source: IStarboundFile, val source: IStarboundFile,
@ -108,6 +106,9 @@ class Image private constructor(
} }
val data: ByteBuffer val data: ByteBuffer
get() = dataCache.get(source).join()
val dataFuture: CompletableFuture<ByteBuffer>
get() = dataCache.get(source) get() = dataCache.get(source)
val texture: GLTexture2D get() { val texture: GLTexture2D get() {
@ -125,10 +126,12 @@ class Image private constructor(
client.named2DTextures1.get(this) { client.named2DTextures1.get(this) {
val tex = GLTexture2D(width, height, GL45.GL_RGBA8) 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.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST
}, client)
tex tex
} }
@ -334,19 +337,19 @@ class Image private constructor(
return ReadDirectData(data, getWidth[0], getHeight[0], components[0]) return ReadDirectData(data, getWidth[0], getHeight[0], components[0])
} }
private val dataCache: LoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder() private val dataCache: AsyncLoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.weigher<IStarboundFile, ByteBuffer> { key, value -> value.capacity() } .weigher<IStarboundFile, ByteBuffer> { key, value -> value.capacity() }
.maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */)) .maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */))
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.EXECUTOR) // SCREENED_EXECUTOR shouldn't be used here .executor(Starbound.IO_EXECUTOR)
.build { readImageDirect(it).data } .buildAsync(CacheLoader { readImageDirect(it).data })
private val spaceScanCache = Caffeine.newBuilder() private val spaceScanCache = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(30)) .expireAfterAccess(Duration.ofMinutes(30))
.softValues() .softValues()
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR) .executor(Starbound.EXECUTOR)
.build<SpaceScanKey, ImmutableSet<Vector2i>>() .build<SpaceScanKey, ImmutableSet<Vector2i>>()
@JvmStatic @JvmStatic

View File

@ -20,7 +20,6 @@ import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.quintic2 import ru.dbotthepony.kstarbound.math.quintic2
import ru.dbotthepony.kstarbound.util.random.random 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.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
@ -112,14 +111,12 @@ class WorldTemplate(val geometry: WorldGeometry) {
return data return data
} }
fun findSensiblePlayerStart(): Vector2d? { fun findSensiblePlayerStart(random: RandomGenerator): Vector2d? {
val layout = worldLayout ?: return null val layout = worldLayout ?: return null
if (layout.playerStartSearchRegions.isEmpty()) if (layout.playerStartSearchRegions.isEmpty())
return null return null
val random = random()
for (i in 0 until Globals.worldTemplate.playerStartSearchTries) { for (i in 0 until Globals.worldTemplate.playerStartSearchTries) {
val region = layout.playerStartSearchRegions.random(random) val region = layout.playerStartSearchRegions.random(random)
val x = random.nextInt(region.mins.x, region.maxs.x) val x = random.nextInt(region.mins.x, region.maxs.x)
@ -298,23 +295,30 @@ class WorldTemplate(val geometry: WorldGeometry) {
var backgroundCave = false var backgroundCave = false
} }
private val cellCache = Caffeine.newBuilder() // as said by Ben Manes, if cache is write-heavy, it is up to end users
.maximumSize(1_500_000L) // plentiful of space, and allows for high hit ratio (around 79%) in most situations // to stripe it into multiple distinct caches (so write buffer doesn't get overflown and force
// downside is memory consumption, but why should it matter when we save 80% of cpu time? // to be drained in place)
.expireAfterAccess(Duration.ofSeconds(20)) // https://github.com/ben-manes/caffeine/issues/1320#issuecomment-1812884592
.executor(Starbound.SCREENED_EXECUTOR) private val cellCache = Array(256) {
.scheduler(Starbound) Caffeine.newBuilder()
// .recordStats() .maximumSize(50_000L) // plentiful of space, and allows for high hit ratio (around 79%) in most situations
.build<Vector2i, CellInfo> { (x, y) -> cellInfo0(x, y) } // 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<Vector2i, CellInfo> { (x, y) -> cellInfo0(x, y) }
}
fun cellInfo(x: Int, y: Int): CellInfo { fun cellInfo(x: Int, y: Int): CellInfo {
worldLayout ?: return CellInfo(x, y) 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 { fun cellInfo(pos: Vector2i): CellInfo {
worldLayout ?: return CellInfo(pos.x, pos.y) 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 { private fun cellInfo0(x: Int, y: Int): CellInfo {

View File

@ -128,9 +128,16 @@ fun InputStream.readAABB(): AABB {
return AABB(readVector2d(), readVector2d()) return AABB(readVector2d(), readVector2d())
} }
fun InputStream.readAABB(isLegacy: Boolean): AABB {
if (isLegacy)
return readAABBLegacy()
else
return readAABB()
}
fun OutputStream.writeAABBLegacy(value: AABB) { fun OutputStream.writeAABBLegacy(value: AABB) {
writeStruct2f(value.mins.toFloatVector()) writeStruct2d(value.mins, true)
writeStruct2f(value.maxs.toFloatVector()) writeStruct2d(value.maxs, true)
} }
fun OutputStream.writeAABBLegacyOptional(value: KOptional<AABB>) { fun OutputStream.writeAABBLegacyOptional(value: KOptional<AABB>) {
@ -150,6 +157,11 @@ fun OutputStream.writeAABB(value: AABB) {
writeStruct2d(value.maxs) writeStruct2d(value.maxs)
} }
fun OutputStream.writeAABB(value: AABB, isLegacy: Boolean) {
writeStruct2d(value.mins, isLegacy)
writeStruct2d(value.maxs, isLegacy)
}
private fun InputStream.readBoolean(): Boolean { private fun InputStream.readBoolean(): Boolean {
val read = read() val read = read()

View File

@ -149,7 +149,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
val results = newTable() val results = newTable()
for (connection in self.inputNodes[index.toInt()].connections) { 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) { if (entity != null) {
results[entity.entityID] = connection.index results[entity.entityID] = connection.index
@ -163,7 +163,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
val results = newTable() val results = newTable()
for (connection in self.outputNodes[index.toInt()].connections) { 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) { if (entity != null) {
results[entity.entityID] = connection.index results[entity.entityID] = connection.index

View File

@ -277,6 +277,37 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
return AABB(pos, pos + Vector2d(width, height)) return AABB(pos, pos + Vector2d(width, height))
} }
fun ofPoints(points: Collection<Vector2d>): 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<Vector2i>): 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 ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)) @JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
} }

View File

@ -17,8 +17,8 @@ class ConnectWirePacket(val target: WireConnection, val source: WireConnection)
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
connection.enqueue { connection.enqueue {
val target = entityIndex.tileEntityAt(target.entityLocation) as? WorldObject ?: return@enqueue val target = entityIndex.tileEntityAt(target.entityLocation, WorldObject::class) ?: return@enqueue
val source = entityIndex.tileEntityAt(source.entityLocation) as? WorldObject ?: return@enqueue val source = entityIndex.tileEntityAt(source.entityLocation, WorldObject::class) ?: return@enqueue
val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueue val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueue
val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueue val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueue

View File

@ -21,7 +21,7 @@ class DisconnectAllWiresPacket(val pos: Vector2i, val node: WireNode) : IServerP
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
connection.enqueue { 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) val node = if (node.isInput) target.inputNodes.getOrNull(node.index) else target.outputNodes.getOrNull(node.index)
node?.removeAllConnections() node?.removeAllConnections()
} }

View File

@ -67,9 +67,9 @@ class LegacyWireProcessor(val world: ServerWorld) {
launch { launch {
ticket.chunk.await() 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 // if entity exists, add it to working entities and find more not loaded entities
populateWorking(findEntity) populateWorking(findEntity)
} else { } else {

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -70,6 +71,7 @@ import kotlin.concurrent.withLock
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.math.min
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
override var state: ChunkState = ChunkState.FRESH override var state: ChunkState = ChunkState.FRESH
@ -731,7 +733,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF) pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF)
) )
val pacer = ExecutionTimePacer(500_000L, 40L)
val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "microdungeon placement")) val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "microdungeon placement"))
for (placement in placements) { for (placement in placements) {
@ -758,15 +759,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY)) if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY))
continue continue
val collision = anchor.reader.walkTiles<Boolean> { x, y, tile -> // this is quite ugly code flow, but we should try to avoid double-walking
if (tile.usesPlaces && world.getCell(pos.x + x, pos.y + y).dungeonId != NO_DUNGEON_ID) { // over all dungeon tiles (DungeonTile#canPlace already checks for absence of other dungeons on their place,
return@walkTiles KOptional(true) // so we only need to tell DungeonPart to not force-place)
} if (anchor.canPlace(pos.x, pos.y, world, false)) {
return@walkTiles KOptional()
}.orElse(false)
if (!collision && anchor.canPlace(pos.x, pos.y, world)) {
try { try {
dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await() dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await()
} catch (err: Throwable) { } catch (err: Throwable) {
@ -778,7 +774,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// some breathing room for other code, since placement checking is performance intense operation // some breathing room for other code, since placement checking is performance intense operation
if (!world.isInPreparation && world.clients.isNotEmpty()) if (!world.isInPreparation && world.clients.isNotEmpty())
pacer.measureAndSuspend() delay(min(60L, anchor.reader.size.x * anchor.reader.size.y / 40L))
} }
} }
} }

View File

@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.server.world
import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
@ -221,7 +220,7 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
.maximumSize(1024L) .maximumSize(1024L)
.softValues() .softValues()
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR) .executor(Starbound.EXECUTOR)
.build() .build()
fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> { fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {

View File

@ -382,10 +382,11 @@ class ServerWorld private constructor(
//} //}
val tickets = ArrayList<ServerChunk.ITicket>() val tickets = ArrayList<ServerChunk.ITicket>()
val random = if (hint == null) random(template.seed) else random()
try { try {
LOGGER.info("Trying to find player spawn position...") 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 var previous = pos
LOGGER.info("Trying to find player spawn position near $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) { if (previous != pos) {
LOGGER.info("Still trying to find player spawn position near $pos...") LOGGER.info("Still trying to find player spawn position near $pos...")

View File

@ -82,7 +82,12 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
private suspend fun damageTilesLoop() { private suspend fun damageTilesLoop() {
while (true) { while (true) {
val (positions, isBackground, sourcePosition, damage, source) = damageTilesQueue.receive() 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) { } catch (err: CancellationException) {
client.send(TileModificationFailurePacket(modifications)) client.send(TileModificationFailurePacket(modifications))
} catch (err: Throwable) {
client.send(TileModificationFailurePacket(modifications))
LOGGER.error("Exception in player modify tiles loop", err)
} }
} }
} }

View File

@ -15,6 +15,8 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Predicate 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 // 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). // 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? return first(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as TileEntity?
} }
fun <T : TileEntity> tileEntityAt(pos: Vector2i, type: KClass<T>): 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<TileEntity> { fun tileEntitiesAt(pos: Vector2i): MutableList<TileEntity> {
return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as MutableList<TileEntity> return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as MutableList<TileEntity>
} }
fun <T : TileEntity> tileEntitiesAt(pos: Vector2i, type: KClass<T>): MutableList<T> {
return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces && type.isSuperclassOf(it::class) }) as MutableList<T>
}
fun iterate(rect: AABB, visitor: (AbstractEntity) -> Unit, withEdges: Boolean = true) { fun iterate(rect: AABB, visitor: (AbstractEntity) -> Unit, withEdges: Boolean = true) {
walk<Unit>(rect, { visitor(it); KOptional() }, withEdges) walk<Unit>(rect, { visitor(it); KOptional() }, withEdges)
} }

View File

@ -12,8 +12,6 @@ import ru.dbotthepony.kstarbound.world.World
* Entities with dynamics (Player, Drops, Projectiles, NPCs, etc) * Entities with dynamics (Player, Drops, Projectiles, NPCs, etc)
*/ */
abstract class DynamicEntity() : AbstractEntity() { abstract class DynamicEntity() : AbstractEntity() {
private var forceChunkRepos = false
override var position override var position
get() = movement.position get() = movement.position
set(value) { set(value) {
@ -52,7 +50,6 @@ abstract class DynamicEntity() : AbstractEntity() {
super.onJoinWorld(world) super.onJoinWorld(world)
world.dynamicEntities.add(this) world.dynamicEntities.add(this)
movement.initialize(world, spatialEntry) movement.initialize(world, spatialEntry)
forceChunkRepos = true
metaFixture = spatialEntry!!.Fixture() metaFixture = spatialEntry!!.Fixture()
} }

View File

@ -553,7 +553,7 @@ open class MovementController() {
movement = movement + totalCorrection, movement = movement + totalCorrection,
correction = totalCorrection, correction = totalCorrection,
isStuck = false, isStuck = false,
isOnGround = -totalCorrection.dot(determineGravity()) > SEPARATION_TOLERANCE, isOnGround = totalCorrection.unitVector.dot(determineGravity().unitVector) >= 0.5,
movingCollisionId = movingCollisionId, movingCollisionId = movingCollisionId,
collisionType = maxCollided, collisionType = maxCollided,
// groundSlope = Vector2d.POSITIVE_Y, // groundSlope = Vector2d.POSITIVE_Y,

View File

@ -2,9 +2,14 @@ package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter 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.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager 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.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isMetaTile import ru.dbotthepony.kstarbound.defs.tile.isMetaTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile 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.BushVariant
import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant
import ru.dbotthepony.kstarbound.defs.world.TreeVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant
@ -64,13 +68,14 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.* import java.util.*
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.collections.ArrayList
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class PlantEntity() : TileEntity() { class PlantEntity() : TileEntity() {
@JsonFactory @JsonFactory
data class Piece( data class Piece(
val image: String, val image: String,
val offset: Vector2d, var offset: Vector2d,
var segmentIdx: Int, var segmentIdx: Int,
val isStructuralSegment: Boolean, val isStructuralSegment: Boolean,
val kind: Kind, val kind: Kind,
@ -147,7 +152,7 @@ class PlantEntity() : TileEntity() {
isCeiling = data.get("ceiling", false) isCeiling = data.get("ceiling", false)
stemDropConfig = data["stemDropConfig"] as? JsonObject ?: JsonObject() stemDropConfig = data["stemDropConfig"] as? JsonObject ?: JsonObject()
foliageDropConfig = data["foliageDropConfig"] 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() descriptions = data["descriptions"] as? JsonObject ?: JsonObject()
isEphemeral = data.get("ephemeral", false) isEphemeral = data.get("ephemeral", false)
fallsWhenDead = data.get("fallsWhenDead", false) fallsWhenDead = data.get("fallsWhenDead", false)
@ -220,7 +225,7 @@ class PlantEntity() : TileEntity() {
private set private set
var foliageDropConfig: JsonObject = JsonObject() var foliageDropConfig: JsonObject = JsonObject()
private set private set
var saplingDropConfig: JsonObject = JsonObject() var saplingDropConfig: JsonElement = JsonObject()
private set private set
var descriptions: JsonObject = JsonObject() var descriptions: JsonObject = JsonObject()
private set private set
@ -229,6 +234,7 @@ class PlantEntity() : TileEntity() {
constructor(config: TreeVariant, random: RandomGenerator) : this() { constructor(config: TreeVariant, random: RandomGenerator) : this() {
isCeiling = config.ceiling isCeiling = config.ceiling
fallsWhenDead = true
stemDropConfig = (config.stemDropConfig as? JsonObject)?.deepCopy() ?: JsonObject() stemDropConfig = (config.stemDropConfig as? JsonObject)?.deepCopy() ?: JsonObject()
foliageDropConfig = (config.foliageDropConfig as? JsonObject)?.deepCopy() ?: JsonObject() foliageDropConfig = (config.foliageDropConfig as? JsonObject)?.deepCopy() ?: JsonObject()
@ -532,7 +538,7 @@ class PlantEntity() : TileEntity() {
isCeiling = stream.readBoolean() isCeiling = stream.readBoolean()
stemDropConfig = stream.readJsonElement() as JsonObject stemDropConfig = stream.readJsonElement() as JsonObject
foliageDropConfig = stream.readJsonElement() as JsonObject foliageDropConfig = stream.readJsonElement() as JsonObject
saplingDropConfig = stream.readJsonElement() as JsonObject saplingDropConfig = stream.readJsonElement()
descriptions = stream.readJsonElement() as JsonObject descriptions = stream.readJsonElement() as JsonObject
isEphemeral = stream.readBoolean() isEphemeral = stream.readBoolean()
@ -640,8 +646,24 @@ class PlantEntity() : TileEntity() {
override fun tick(delta: Double) { override fun tick(delta: Double) {
super.tick(delta) super.tick(delta)
if (world.isServer && piecesInternal.isEmpty()) { if (world.isServer) {
remove(RemovalReason.REMOVED) 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<Vector2i>, source: Vector2d, damage: TileDamage): Boolean { override fun damage(damageSpaces: List<Vector2i>, 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 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<Piece>()
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<ArrayList<Piece>>()
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 { override fun toString(): String {
return "PlantEntity[at=$tilePosition, pieces=${pieces.size}]" 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 // First bail out if we can't fit anything we're not adjusting
for (space in occupySpaces) { for (space in occupySpaces) {
// TODO: conditions seems to be inverted // 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 return false
} }

View File

@ -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<Piece>()
val pieces: List<Piece> = 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<PlantEntity.Piece>,
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)
}
}
}

View File

@ -269,7 +269,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
if (connectionsInternal.isNotEmpty()) { if (connectionsInternal.isNotEmpty()) {
// ensure that we disconnect both ends // ensure that we disconnect both ends
val any = connectionsInternal.removeIf { 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 otherConnections = if (isInput) otherEntity?.outputNodes else otherEntity?.inputNodes
val any = otherConnections?.getOrNull(it.index)?.connectionsInternal?.removeIf { it.entityLocation == tilePosition && it.index == index } 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<ObjectDefinition>) : TileEntit
val itr = node.connectionsInternal.listIterator() val itr = node.connectionsInternal.listIterator()
for (connection in itr) { 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) { if (connection.otherEntity?.isInWorld == false) {
// break connection if other entity got removed // break connection if other entity got removed

View File

@ -1,8 +1,6 @@
package ru.dbotthepony.kstarbound.world.terrain package ru.dbotthepony.kstarbound.world.terrain
import com.github.benmanes.caffeine.cache.Caffeine 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.arrays.Float2DArray
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -63,7 +61,7 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR) .executor(Starbound.EXECUTOR)
.build<Int, Layer>(::Layer) .build<Int, Layer>(::Layer)
private inner class Sector(val sector: Vector2i) { private inner class Sector(val sector: Vector2i) {
@ -132,7 +130,7 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR) .executor(Starbound.EXECUTOR)
.build<Vector2i, Sector>(::Sector) .build<Vector2i, Sector>(::Sector)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {

View File

@ -1,8 +1,6 @@
package ru.dbotthepony.kstarbound.world.terrain package ru.dbotthepony.kstarbound.world.terrain
import com.github.benmanes.caffeine.cache.Caffeine 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.arrays.Float2DArray
import ru.dbotthepony.kommons.math.linearInterpolation import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
@ -186,7 +184,7 @@ class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR) .executor(Starbound.EXECUTOR)
.build<Vector2i, Sector>(::Sector) .build<Vector2i, Sector>(::Sector)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {