Functional plants
This commit is contained in:
parent
17a3de38bc
commit
74bbc58c60
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -1,41 +1,45 @@
|
||||
package ru.dbotthepony.kstarbound.collect
|
||||
|
||||
class RandomListIterator<E>(private val elements: MutableList<E>, index: Int = 0) : MutableListIterator<E> {
|
||||
private var index = index - 1
|
||||
|
||||
class RandomListIterator<E>(private val elements: MutableList<E>, private var index: Int = 0) : MutableListIterator<E> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -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<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)
|
||||
}
|
||||
|
||||
@ -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<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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -15,8 +15,12 @@ import ru.dbotthepony.kstarbound.defs.image.Image
|
||||
import java.lang.ref.Reference
|
||||
|
||||
class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : 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<Layer>()
|
||||
|
@ -26,8 +26,12 @@ class TiledPartReader(part: DungeonPart, parts: Stream<String>) : 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) {
|
||||
|
||||
|
@ -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<ByteBuffer>
|
||||
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<IStarboundFile, ByteBuffer> = Caffeine.newBuilder()
|
||||
private val dataCache: AsyncLoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder()
|
||||
.expireAfterAccess(Duration.ofMinutes(1))
|
||||
.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 МиБ */))
|
||||
.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<SpaceScanKey, ImmutableSet<Vector2i>>()
|
||||
|
||||
@JvmStatic
|
||||
|
@ -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<Vector2i, CellInfo> { (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<Vector2i, CellInfo> { (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 {
|
||||
|
@ -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<AABB>) {
|
||||
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -277,6 +277,37 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
|
||||
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 NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<ServerWorld, ServerChunk>(world, pos) {
|
||||
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)
|
||||
)
|
||||
|
||||
val pacer = ExecutionTimePacer(500_000L, 40L)
|
||||
val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "microdungeon placement"))
|
||||
|
||||
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))
|
||||
continue
|
||||
|
||||
val collision = anchor.reader.walkTiles<Boolean> { 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<ServerWorld, Server
|
||||
|
||||
// some breathing room for other code, since placement checking is performance intense operation
|
||||
if (!world.isInPreparation && world.clients.isNotEmpty())
|
||||
pacer.measureAndSuspend()
|
||||
delay(min(60L, anchor.reader.size.x * anchor.reader.size.y / 40L))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.server.world
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache
|
||||
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.ObjectOpenHashSet
|
||||
import kotlinx.coroutines.future.await
|
||||
@ -221,7 +220,7 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
|
||||
.maximumSize(1024L)
|
||||
.softValues()
|
||||
.scheduler(Starbound)
|
||||
.executor(Starbound.SCREENED_EXECUTOR)
|
||||
.executor(Starbound.EXECUTOR)
|
||||
.build()
|
||||
|
||||
fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {
|
||||
|
@ -382,10 +382,11 @@ class ServerWorld private constructor(
|
||||
//}
|
||||
|
||||
val tickets = ArrayList<ServerChunk.ITicket>()
|
||||
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...")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 <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> {
|
||||
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) {
|
||||
walk<Unit>(rect, { visitor(it); KOptional() }, withEdges)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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<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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -269,7 +269,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : 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<ObjectDefinition>) : 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
|
||||
|
@ -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<Int, Layer>(::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<Vector2i, Sector>(::Sector)
|
||||
|
||||
override fun get(x: Int, y: Int): Double {
|
||||
|
@ -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<Vector2i, Sector>(::Sector)
|
||||
|
||||
override fun get(x: Int, y: Int): Double {
|
||||
|
Loading…
Reference in New Issue
Block a user