Item drops stuff, but i accidentally inflated chunk memory requirements

This commit is contained in:
DBotThePony 2024-04-09 23:24:33 +07:00
parent 8987dc5270
commit e134554879
Signed by: DBot
GPG Key ID: DCC23B5715498507
44 changed files with 979 additions and 444 deletions

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.13.1 kommonsVersion=2.14.0
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.WorldServerConfig import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
@ -35,10 +36,10 @@ object Globals {
var player by Delegates.notNull<PlayerConfig>() var player by Delegates.notNull<PlayerConfig>()
private set private set
var actorMovementParameters = ActorMovementParameters() var actorMovementParameters by Delegates.notNull<ActorMovementParameters>()
private set private set
var movementParameters = MovementParameters() var movementParameters by Delegates.notNull<MovementParameters>()
private set private set
var client by Delegates.notNull<ClientConfig>() var client by Delegates.notNull<ClientConfig>()
@ -77,6 +78,9 @@ object Globals {
var worldServer by Delegates.notNull<WorldServerConfig>() var worldServer by Delegates.notNull<WorldServerConfig>()
private set private set
var itemDrop by Delegates.notNull<ItemDropConfig>()
private set
var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>() var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>()
private set private set
@ -149,6 +153,7 @@ object Globals {
tasks.add(load("/worldserver.config", ::worldServer)) tasks.add(load("/worldserver.config", ::worldServer))
tasks.add(load("/player.config", ::player)) tasks.add(load("/player.config", ::player))
tasks.add(load("/systemworld.config", ::systemWorld)) tasks.add(load("/systemworld.config", ::systemWorld))
tasks.add(load("/itemdrop.config", ::itemDrop))
tasks.add(load("/celestial.config", ::celestialBaseInformation)) tasks.add(load("/celestial.config", ::celestialBaseInformation))
tasks.add(load("/celestial.config", ::celestialConfig)) tasks.add(load("/celestial.config", ::celestialConfig))
tasks.add(load("/celestial/names.config", ::celestialNames)) tasks.add(load("/celestial/names.config", ::celestialNames))

View File

@ -7,6 +7,7 @@ import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction import ru.dbotthepony.kstarbound.defs.JsonFunction
@ -86,12 +87,12 @@ object Registries {
val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) } val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> { private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, KOptional<Int?>> {
return { mapper.invoke(it) to null } return { mapper.invoke(it) to KOptional() }
} }
private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int): (T) -> Pair<String, Int> { private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int?): (T) -> Pair<String, KOptional<Int?>> {
return { mapper.invoke(it) to mapperInt.invoke(it) } return { mapper.invoke(it) to KOptional(mapperInt.invoke(it)) }
} }
fun validate(): CompletableFuture<Boolean> { fun validate(): CompletableFuture<Boolean> {
@ -106,7 +107,7 @@ object Registries {
private inline fun <reified T : Any> loadRegistry( private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>, registry: Registry<T>,
files: List<IStarboundFile>, files: List<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, Int?>, noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>,
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> } noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
): List<Future<*>> { ): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
@ -124,10 +125,13 @@ object Registries {
after(read, listedFile) after(read, listedFile)
registry.add { registry.add {
if (keys.second != null) registry.add(
registry.add(keys.first, keys.second!!, read, elem, listedFile) key = keys.first,
else value = read,
registry.add(keys.first, read, elem, listedFile) id = keys.second,
json = elem,
file = listedFile
)
} }
} }
} catch (err: Throwable) { } catch (err: Throwable) {
@ -155,7 +159,7 @@ object Registries {
tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
tasks.addAll(loadRegistry(species, fileTree["species"] ?: listOf(), key(Species::kind))) tasks.addAll(loadRegistry(species, fileTree["species"] ?: listOf(), key(Species::kind)))
tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to null })) tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() }))
tasks.addAll(loadRegistry(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) tasks.addAll(loadRegistry(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
@ -203,7 +207,7 @@ object Registries {
val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) } val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) }
items.add { items.add {
items.add(def.itemName, def, json, listedFile) items.add(key = def.itemName, value = def, json = json, file = listedFile)
} }
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Loading item definition file $listedFile", err) LOGGER.error("Loading item definition file $listedFile", err)

View File

@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import java.util.Collections import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -99,7 +100,7 @@ class Registry<T : Any>(val name: String) {
} }
override fun toString(): String { override fun toString(): String {
return "Registry.Entry[key=$key, id=$id, registry=$name]" return "Entry of $name at $key/${id ?: "-"}"
} }
override val registry: Registry<T> override val registry: Registry<T>
@ -119,7 +120,7 @@ class Registry<T : Any>(val name: String) {
} }
override fun toString(): String { override fun toString(): String {
return "Registry.Ref[key=$key, bound to value=${entry != null}, registry=$name]" return "Ref of $name at $key/${if (entry != null) "bound" else "missing"}"
} }
override val registry: Registry<T> override val registry: Registry<T>
@ -180,7 +181,14 @@ class Registry<T : Any>(val name: String) {
return valid return valid
} }
fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry<T> { fun add(
key: String,
value: T,
json: JsonElement = JsonNull.INSTANCE,
file: IStarboundFile? = null,
id: KOptional<Int?> = KOptional(),
isBuiltin: Boolean = false
): Entry<T> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" } require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock { lock.withLock {
@ -188,120 +196,38 @@ class Registry<T : Any>(val name: String) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) id.ifPresent { id ->
if (id != null && id in idsInternal) {
check(!entry.isBuiltin) { "Trying to redefine builtin entry" } LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
}
entry.id?.let { }
idsInternal.remove(it)
idRefs[it]?.entry = null val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry at $key" }
id.ifPresent {
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = it
} }
entry.id = null
entry.value = value entry.value = value
entry.json = json entry.json = json
entry.file = file entry.file = file
keyRefs[key]?.entry = entry
return entry
}
}
fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
if (id in idsInternal) {
LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = id
entry.value = value
entry.json = json
entry.file = file
keyRefs[key]?.entry = entry
idRefs[id]?.entry = entry
idsInternal[id] = entry
return entry
}
}
fun add(key: String, value: T, isBuiltin: Boolean = false): Entry<T> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = null
entry.value = value
entry.json = JsonNull.INSTANCE
entry.file = null
entry.isBuiltin = isBuiltin entry.isBuiltin = isBuiltin
keyRefs[key]?.entry = entry keyRefs[key]?.entry = entry
return entry id.ifPresent { id ->
} if (id != null) {
} idRefs[id]?.entry = entry
idsInternal[id] = entry
fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry<T> { }
require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock {
if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }
if (id in idsInternal) {
LOGGER.warn("Overwriting $name with ID '$id' (new def originate from <code>; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
}
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry" }
entry.id?.let {
idsInternal.remove(it)
idRefs[it]?.entry = null
}
entry.id = id
entry.value = value
entry.json = JsonNull.INSTANCE
entry.file = null
entry.isBuiltin = isBuiltin
keyRefs[key]?.entry = entry
idRefs[id]?.entry = entry
idsInternal[id] = entry
return entry return entry
} }
} }

View File

@ -16,7 +16,7 @@ import java.io.DataOutputStream
class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket { class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() }) constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() })
constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList<ImmutableCell>(CHUNK_SIZE * CHUNK_SIZE).also { constructor(chunk: Chunk<*, *, *>) : this(chunk.pos, ArrayList<ImmutableCell>(CHUNK_SIZE * CHUNK_SIZE).also {
for (x in 0 until CHUNK_SIZE) { for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) { for (y in 0 until CHUNK_SIZE) {
it.add(chunk.getCell(x, y).immutable()) it.add(chunk.getCell(x, y).immutable())

View File

@ -1,12 +1,17 @@
package ru.dbotthepony.kstarbound.client.world package ru.dbotthepony.kstarbound.client.world
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos) { class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk, ClientChunk.ChunkCell>(world, pos) {
inner class ChunkCell(x: Int, y: Int) : Chunk<ClientWorld, ClientChunk, ClientChunk.ChunkCell>.ChunkCell(x, y)
override val cells: Object2DArray<ChunkCell> = Object2DArray(width, height, ::ChunkCell)
override val state: ChunkState override val state: ChunkState
get() = ChunkState.FULL get() = ChunkState.FULL

View File

@ -279,7 +279,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
val tickets = ArrayList<ServerChunk.ITicket>() val tickets = ArrayList<ServerChunk.ITicket>()
return try { return try {
tickets.addAll(parent.permanentChunkTicket(region, targetChunkState)) tickets.addAll(parent.permanentChunkTicket(region, targetChunkState).await())
tickets.forEach { it.chunk.await() } tickets.forEach { it.chunk.await() }
block() block()
} finally { } finally {
@ -435,7 +435,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
}.await() }.await()
for (box in boundingBoxes) { for (box in boundingBoxes) {
tickets.addAll(parent.permanentChunkTicket(box, targetChunkState)) tickets.addAll(parent.permanentChunkTicket(box, targetChunkState).await())
} }
// apply tiles to world per-chunk // apply tiles to world per-chunk

View File

@ -14,6 +14,7 @@ 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.Object2ObjectLinkedOpenHashMap
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
import org.lwjgl.stb.STBIEOFCallback import org.lwjgl.stb.STBIEOFCallback
@ -190,7 +191,7 @@ class Image private constructor(
return whole.isTransparent(x, y, flip) return whole.isTransparent(x, y, flip)
} }
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List<Vector2i> { fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
return whole.worldSpaces(pixelOffset, spaceScan, flip) return whole.worldSpaces(pixelOffset, spaceScan, flip)
} }
@ -204,7 +205,7 @@ class Image private constructor(
override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height
/** /**
* returns integer in ABGR format if it is RGB or RGBA picture, * returns integer in big-endian ABGR format if it is RGB or RGBA picture,
* otherwise returns pixels as-is * otherwise returns pixels as-is
*/ */
operator fun get(x: Int, y: Int): Int { operator fun get(x: Int, y: Int): Int {
@ -214,10 +215,10 @@ class Image private constructor(
val data = data.join() val data = data.join()
when (amountOfChannels) { when (amountOfChannels) {
4 -> return data[offset].toInt().and(0xFF) or 4 -> return data[offset].toInt().and(0xFF) or // red
data[offset + 1].toInt().and(0xFF).shl(8) or data[offset + 1].toInt().and(0xFF).shl(8) or // green
data[offset + 2].toInt().and(0xFF).shl(16) or data[offset + 2].toInt().and(0xFF).shl(16) or // blue
data[offset + 3].toInt().and(0xFF).shl(24) data[offset + 3].toInt().and(0xFF).shl(24) // alpha
3 -> return data[offset].toInt().and(0xFF) or 3 -> return data[offset].toInt().and(0xFF) or
data[offset + 1].toInt().and(0xFF).shl(8) or data[offset + 1].toInt().and(0xFF).shl(8) or
@ -248,7 +249,7 @@ class Image private constructor(
if (x !in 0 until width) return true if (x !in 0 until width) return true
if (y !in 0 until height) return true if (y !in 0 until height) return true
if (amountOfChannels != 4) return false if (amountOfChannels != 4) return false
return this[x, y, flip] and 0xFF != 0x0 return this[x, y, flip] and -0x1000000 == 0x0
} }
val nonEmptyRegion by lazy { val nonEmptyRegion by lazy {
@ -285,7 +286,7 @@ class Image private constructor(
Vector4i(0, 0, width, height) Vector4i(0, 0, width, height)
} }
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): List<Vector2i> { fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels") if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels")
val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
@ -293,7 +294,7 @@ class Image private constructor(
val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi val maxY = (height + pixelOffset.y + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
val result = ArrayList<Vector2i>() val result = ObjectArraySet<Vector2i>()
// this is weird, but that's how original game handles this // this is weird, but that's how original game handles this
// also we don't cache this info since that's a waste of precious ram // also we don't cache this info since that's a waste of precious ram
@ -314,7 +315,7 @@ class Image private constructor(
if (xpixel !in 0 until width) if (xpixel !in 0 until width)
continue continue
if (isTransparent(xpixel, ypixel, flip)) { if (!isTransparent(xpixel, ypixel, flip)) {
fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT) fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT)
} }
} }

View File

@ -0,0 +1,22 @@
package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.MovementParameters
data class ItemDropConfig(
val randomizedDistance: Double = 1.0,
val randomizedSpeed: Double = 5.0,
val throwSpeed: Double = 30.0,
val throwIntangibleTime: Double = 1.0,
val velocity: Double = 60.0,
val velocityApproach: Double = 300.0,
val pickupDistance: Double = 1.5,
val combineChance: Float = 0.02f, // for random.nextFloat()
val combineRadius: Double = 0.5,
val afterTakenLife: Double = 2.0,
val movementSettings: MovementParameters = MovementParameters.EMPTY,
) {
val combineRadiusBox = AABB(Vector2d(-combineRadius, -combineRadius), Vector2d(combineRadius, combineRadius))
}

View File

@ -201,7 +201,7 @@ data class ObjectOrientation(
val sprite = bound.sprite ?: throw IllegalStateException("Not a valid sprite reference: ${bound.raw} (${bound.imagePath} / ${bound.spritePath})") val sprite = bound.sprite ?: throw IllegalStateException("Not a valid sprite reference: ${bound.raw} (${bound.imagePath} / ${bound.spritePath})")
val new = ImmutableSet.Builder<Vector2i>() val new = ImmutableSet.Builder<Vector2i>()
new.addAll(occupySpaces) // new.addAll(occupySpaces)
new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages)) new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages))
occupySpaces = new.build() occupySpaces = new.build()
} }

View File

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMap import it.unimi.dsi.fastutil.objects.Object2DoubleMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
@ -101,7 +102,7 @@ const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524
const val FIRST_RESERVED_DUNGEON_ID = 65520 const val FIRST_RESERVED_DUNGEON_ID = 65520
object BuiltinMetaMaterials { object BuiltinMetaMaterials {
private fun make(id: Int, name: String, collisionType: CollisionType, isConnectable: Boolean = true) = Registries.tiles.add(name, id, TileDefinition( private fun make(id: Int, name: String, collisionType: CollisionType, isConnectable: Boolean = true) = Registries.tiles.add(key = name, id = KOptional(id), value = TileDefinition(
materialId = id, materialId = id,
materialName = "metamaterial:$name", materialName = "metamaterial:$name",
descriptionData = ThingDescription.EMPTY, descriptionData = ThingDescription.EMPTY,
@ -121,7 +122,7 @@ object BuiltinMetaMaterials {
)) ))
), isBuiltin = true) ), isBuiltin = true)
private fun makeMod(id: Int, name: String) = Registries.tileModifiers.add(name, id, TileModifierDefinition( private fun makeMod(id: Int, name: String) = Registries.tileModifiers.add(key = name, id = KOptional(id), value = TileModifierDefinition(
modId = id, modId = id,
modName = "metamod:$name", modName = "metamod:$name",
descriptionData = ThingDescription.EMPTY, descriptionData = ThingDescription.EMPTY,
@ -157,7 +158,7 @@ object BuiltinMetaMaterials {
val BIOME_MOD = makeMod(65534, "biome") val BIOME_MOD = makeMod(65534, "biome")
val UNDERGROUND_BIOME_MOD = makeMod(65533, "underground_biome") val UNDERGROUND_BIOME_MOD = makeMod(65533, "underground_biome")
val NO_LIQUID = Registries.liquid.add("empty", 0, LiquidDefinition( val NO_LIQUID = Registries.liquid.add(key = "empty", id = KOptional(0), value = LiquidDefinition(
name = "metaliquid:empty", name = "metaliquid:empty",
liquidId = 0, liquidId = 0,
color = RGBAColor.TRANSPARENT_BLACK, color = RGBAColor.TRANSPARENT_BLACK,

View File

@ -309,6 +309,10 @@ class WorldTemplate(val geometry: WorldGeometry) {
return cellCache.get(Vector2i(x, y)) return cellCache.get(Vector2i(x, y))
} }
fun cellInfo(pos: Vector2i): CellInfo {
return cellCache.get(pos)
}
private fun cellInfo0(x: Int, y: Int): CellInfo { private fun cellInfo0(x: Int, y: Int): CellInfo {
val info = CellInfo(x, y) val info = CellInfo(x, y)
val layout = worldLayout ?: return info val layout = worldLayout ?: return info

View File

@ -33,7 +33,7 @@ open class ItemStack {
constructor(descriptor: ItemDescriptor) { constructor(descriptor: ItemDescriptor) {
this.config = descriptor.ref this.config = descriptor.ref
this.count = descriptor.count this.size = descriptor.count
this.parameters = descriptor.parameters.deepCopy() this.parameters = descriptor.parameters.deepCopy()
} }
@ -51,36 +51,41 @@ open class ItemStack {
changeset = CHANGESET.incrementAndGet() changeset = CHANGESET.incrementAndGet()
} }
var count: Long = 0L var size: Long = 0L
set(value) { set(value) {
field = value.coerceAtLeast(0L) val newValue = value.coerceAtLeast(0L)
if (field != newValue) {
field = newValue
changeset = CHANGESET.incrementAndGet()
}
} }
val config: Registry.Ref<IItemDefinition> val config: Registry.Ref<IItemDefinition>
val parameters: JsonObject val parameters: JsonObject
val isEmpty: Boolean val isEmpty: Boolean
get() = count <= 0 || config.isEmpty get() = size <= 0 || config.isEmpty
val isNotEmpty: Boolean val isNotEmpty: Boolean
get() = count > 0 && config.isPresent get() = size > 0 && config.isPresent
val maxStackSize: Long val maxStackSize: Long
get() = config.value?.maxStack ?: 0L get() = config.value?.maxStack ?: 0L
fun grow(amount: Long) { fun grow(amount: Long) {
count += amount size += amount
} }
fun shrink(amount: Long) { fun shrink(amount: Long) {
count -= amount size -= amount
} }
fun createDescriptor(): ItemDescriptor { fun createDescriptor(): ItemDescriptor {
if (isEmpty) if (isEmpty)
return ItemDescriptor.EMPTY return ItemDescriptor.EMPTY
return ItemDescriptor(config.key.left(), count, parameters.deepCopy()) return ItemDescriptor(config.key.left(), size, parameters.deepCopy())
} }
// faster than creating an item descriptor and writing it (because it avoids copying and allocation) // faster than creating an item descriptor and writing it (because it avoids copying and allocation)
@ -91,7 +96,7 @@ open class ItemStack {
stream.writeJsonElement(JsonNull.INSTANCE) stream.writeJsonElement(JsonNull.INSTANCE)
} else { } else {
stream.writeBinaryString(config.key.left()) stream.writeBinaryString(config.key.left())
stream.writeVarLong(count) stream.writeVarLong(size)
stream.writeJsonElement(parameters) stream.writeJsonElement(parameters)
} }
} }
@ -109,30 +114,20 @@ open class ItemStack {
fun mergeFrom(other: ItemStack, simulate: Boolean) { fun mergeFrom(other: ItemStack, simulate: Boolean) {
if (isStackable(other)) { if (isStackable(other)) {
val newCount = (count + other.count).coerceAtMost(maxStackSize) val newCount = (size + other.size).coerceAtMost(maxStackSize)
val diff = newCount - count val diff = newCount - size
other.count -= diff other.size -= diff
if (!simulate) if (!simulate)
count = newCount size = newCount
} }
} }
fun lenientEquals(other: Any?): Boolean {
if (other !is ItemStack)
return false
if (isEmpty)
return other.isEmpty
return other.count == count && other.config == config
}
fun isStackable(other: ItemStack): Boolean { fun isStackable(other: ItemStack): Boolean {
if (isEmpty || other.isEmpty) if (isEmpty || other.isEmpty)
return false return false
return count != 0L && other.count != 0L && maxStackSize < count && other.config == config && other.parameters == parameters return size != 0L && other.size != 0L && maxStackSize > size && other.config == config && other.parameters == parameters
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -142,7 +137,7 @@ open class ItemStack {
if (isEmpty) if (isEmpty)
return other.isEmpty return other.isEmpty
return other.count == count && other.config == config && other.parameters == parameters return other.size == size && other.config == config && other.parameters == parameters
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -153,14 +148,14 @@ open class ItemStack {
if (isEmpty) if (isEmpty)
return "ItemStack.EMPTY" return "ItemStack.EMPTY"
return "ItemDescriptor[${config.value?.itemName}, count = $count, params = $parameters]" return "ItemDescriptor[${config.value?.itemName}, count = $size, params = $parameters]"
} }
fun copy(): ItemStack { fun copy(): ItemStack {
if (isEmpty) if (isEmpty)
return this return this
return ItemStack(ItemDescriptor(config, count, parameters.deepCopy())) return ItemStack(ItemDescriptor(config, size, parameters.deepCopy()))
} }
fun toJson(): JsonObject? { fun toJson(): JsonObject? {
@ -169,7 +164,7 @@ open class ItemStack {
return JsonObject().also { return JsonObject().also {
it.add("name", JsonPrimitive(config.key.left())) it.add("name", JsonPrimitive(config.key.left()))
it.add("count", JsonPrimitive(count)) it.add("count", JsonPrimitive(size))
it.add("parameters", parameters.deepCopy()) it.add("parameters", parameters.deepCopy())
} }
} }
@ -181,7 +176,7 @@ open class ItemStack {
return allocator.newTable(0, 3).also { return allocator.newTable(0, 3).also {
it.rawset("name", config.key.left()) it.rawset("name", config.key.left())
it.rawset("count", count) it.rawset("count", size)
it.rawset("parameters", allocator.from(parameters)) it.rawset("parameters", allocator.from(parameters))
} }
} }

View File

@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectFailurePacke
import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.EnvironmentUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.EnvironmentUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.FindUniqueEntityResponsePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.GiveItemPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket
@ -63,6 +64,7 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.EntityInteractPacke
import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.RequestDropPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -442,7 +444,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("TileLiquidUpdate") LEGACY.skip("TileLiquidUpdate")
LEGACY.add(::TileDamageUpdatePacket) LEGACY.add(::TileDamageUpdatePacket)
LEGACY.skip("TileModificationFailure") LEGACY.skip("TileModificationFailure")
LEGACY.skip("GiveItem") LEGACY.add(::GiveItemPacket)
LEGACY.add(::EnvironmentUpdatePacket) LEGACY.add(::EnvironmentUpdatePacket)
LEGACY.skip("UpdateTileProtection") LEGACY.skip("UpdateTileProtection")
LEGACY.skip("SetDungeonGravity") LEGACY.skip("SetDungeonGravity")
@ -455,7 +457,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("ModifyTileList") LEGACY.skip("ModifyTileList")
LEGACY.add(::DamageTileGroupPacket) LEGACY.add(::DamageTileGroupPacket)
LEGACY.skip("CollectLiquid") LEGACY.skip("CollectLiquid")
LEGACY.skip("RequestDrop") LEGACY.add(::RequestDropPacket)
LEGACY.skip("SpawnEntity") LEGACY.skip("SpawnEntity")
LEGACY.skip("ConnectWire") LEGACY.skip("ConnectWire")
LEGACY.skip("DisconnectAllWires") LEGACY.skip("DisconnectAllWires")

View File

@ -0,0 +1,19 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream
class GiveItemPacket(val descriptor: ItemDescriptor) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(ItemDescriptor(stream))
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
descriptor.write(stream)
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,33 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.GiveItemPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.io.DataInputStream
import java.io.DataOutputStream
class RequestDropPacket(val id: Int) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readSignedVarInt())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeSignedVarInt(id)
}
override fun play(connection: ServerConnection) {
connection.enqueue {
val item = entities[id] as? ItemDropEntity ?: return@enqueue
val player = connection.playerEntity ?: return@enqueue
if (item.canTake) {
val take = item.take(player)
if (take.isNotEmpty) {
connection.send(GiveItemPacket(take.createDescriptor()))
}
}
}
}
}

View File

@ -6,6 +6,7 @@ import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import ru.dbotthepony.kommons.io.BinaryStringCodec import ru.dbotthepony.kommons.io.BinaryStringCodec
import ru.dbotthepony.kommons.io.BooleanValueCodec import ru.dbotthepony.kommons.io.BooleanValueCodec
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.RGBACodec import ru.dbotthepony.kommons.io.RGBACodec
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.UnsignedVarIntCodec import ru.dbotthepony.kommons.io.UnsignedVarIntCodec
@ -15,6 +16,7 @@ import ru.dbotthepony.kommons.io.VarLongValueCodec
import ru.dbotthepony.kommons.io.Vector2dCodec import ru.dbotthepony.kommons.io.Vector2dCodec
import ru.dbotthepony.kommons.io.Vector2fCodec import ru.dbotthepony.kommons.io.Vector2fCodec
import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.readVarLong
@ -156,6 +158,18 @@ fun networkedColor(value: RGBAColor = RGBAColor.BLACK) = networkedData(value, RG
// networks enum as unsigned variable length integer // networks enum as unsigned variable length integer
fun <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java)) fun <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java))
fun <E : Enum<E>> networkedEnum(values: List<E>, value: E = values.first()) = BasicNetworkedElement(value, VarIntValueCodec.map({ values[this] }, { ordinal }))
// networks enum as a int32_t on legacy protocol
fun <E : Enum<E>> networkedEnumSortOfStupid(value: E): BasicNetworkedElement<E, Int> {
val codec = StreamCodec.Enum(value::class.java)
return BasicNetworkedElement(value, codec, IntValueCodec, { it.ordinal }, { codec.values[it] })
}
// networks enum as a int32_t on legacy protocol
fun <E : Enum<E>> networkedEnumSortOfStupid(values: List<E>, value: E = values.first()): BasicNetworkedElement<E, Int> {
return BasicNetworkedElement(value, VarIntValueCodec.map({ values[this] }, { ordinal }), IntValueCodec, { it.ordinal }, { values[it] })
}
// networks enum as a signed variable length integer on legacy protocol // networks enum as a signed variable length integer on legacy protocol
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> { fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {

View File

@ -12,6 +12,7 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.DoubleSupplier import java.util.function.DoubleSupplier
import kotlin.math.absoluteValue
import kotlin.math.roundToLong import kotlin.math.roundToLong
// works solely with doubles, but networks as either float, double or fixed point // works solely with doubles, but networks as either float, double or fixed point
@ -21,7 +22,10 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
fun read(data: DataInputStream): Double fun read(data: DataInputStream): Double
fun areDifferent(a: Double, b: Double): Boolean { fun areDifferent(a: Double, b: Double): Boolean {
return a != b // comparing doubles using direct comparison is bad for networking
// return a != b
// compare by epsilon
return (a - b).absoluteValue > 0.0000001
} }
} }
@ -45,7 +49,10 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
} }
override fun areDifferent(a: Double, b: Double): Boolean { override fun areDifferent(a: Double, b: Double): Boolean {
return a.toFloat() != b.toFloat() // comparing floats using direct comparison is bad for networking
// return a != b
// compare by epsilon
return (a - b).absoluteValue > 0.00001f
} }
} }

View File

@ -20,10 +20,10 @@ class AvatarBag(val avatar: Avatar, val config: InventoryConfig.Bag, val filter:
fun mergeFrom(value: ItemStack, simulate: Boolean) { fun mergeFrom(value: ItemStack, simulate: Boolean) {
if (item == null) { if (item == null) {
if (!simulate) { if (!simulate) {
item = value.copy().also { it.count = value.count.coerceAtMost(value.maxStackSize) } item = value.copy().also { it.size = value.size.coerceAtMost(value.maxStackSize) }
} }
value.count -= value.maxStackSize value.size -= value.maxStackSize
} else { } else {
item!!.mergeFrom(value, simulate) item!!.mergeFrom(value, simulate)
} }

View File

@ -495,6 +495,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
it.eventLoop.shutdown() it.eventLoop.shutdown()
} else { } else {
shipWorld = it shipWorld = it
shipWorld.sky.referenceClock = server.universeClock
// shipWorld.sky.startFlying(true, true) // shipWorld.sky.startFlying(true, true)
shipWorld.eventLoop.start() shipWorld.eventLoop.start()
enqueueWarp(WarpAlias.OwnShip) enqueueWarp(WarpAlias.OwnShip)

View File

@ -4,13 +4,11 @@ import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
@ -26,19 +24,14 @@ import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.Clock import ru.dbotthepony.kstarbound.util.JVMClock
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier
sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") { sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
init { init {
@ -54,6 +47,17 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
val chat = ChatHandler(this) val chat = ChatHandler(this)
val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob()) val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
val settings = ServerSettings()
val channels = ServerChannels(this)
val lock = ReentrantLock()
var isClosed = false
private set
var serverUUID: UUID = UUID.randomUUID()
protected set
val universeClock = JVMClock()
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>() private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld { private suspend fun loadSystemWorld0(location: Vector3i): ServerSystemWorld {
@ -75,6 +79,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
val world = ServerWorld.create(this, template, WorldStorage.Nothing, location) val world = ServerWorld.create(this, template, WorldStorage.Nothing, location)
try { try {
world.sky.referenceClock = universeClock
world.eventLoop.start() world.eventLoop.start()
world.prepare().await() world.prepare().await()
} catch (err: Throwable) { } catch (err: Throwable) {
@ -114,6 +119,9 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
try { try {
world.setProperty("ephemeral", JsonPrimitive(!config.persistent)) world.setProperty("ephemeral", JsonPrimitive(!config.persistent))
if (config.useUniverseClock)
world.sky.referenceClock = universeClock
world.eventLoop.start() world.eventLoop.start()
world.prepare().await() world.prepare().await()
} catch (err: Throwable) { } catch (err: Throwable) {
@ -177,17 +185,6 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
return loadSystemWorld(location.location) return loadSystemWorld(location.location)
} }
val settings = ServerSettings()
val channels = ServerChannels(this)
val lock = ReentrantLock()
var isClosed = false
private set
var serverUUID: UUID = UUID.randomUUID()
protected set
val universeClock = Clock()
init { init {
scheduleAtFixedRate(Runnable { scheduleAtFixedRate(Runnable {
channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds)) channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds))
@ -266,6 +263,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
private fun tickNormal() { private fun tickNormal() {
try { try {
// universeClock.nanos += Starbound.TIMESTEP_NANOS
channels.connections.forEach { channels.connections.forEach {
try { try {
it.tick() it.tick()

View File

@ -15,6 +15,7 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID
@ -50,6 +51,7 @@ import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.api.MutableTileState import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -60,7 +62,13 @@ import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk, ServerChunk.ChunkCell>(world, pos) {
inner class ChunkCell(x: Int, y: Int) : Chunk<ServerWorld, ServerChunk, ServerChunk.ChunkCell>.ChunkCell(x, y) {
}
override val cells: Object2DArray<ChunkCell> = Object2DArray(width, height, ::ChunkCell)
override var state: ChunkState = ChunkState.FRESH override var state: ChunkState = ChunkState.FRESH
private set private set
@ -105,7 +113,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// from not sufficiently generated neighbours // from not sufficiently generated neighbours
for (neighbour in pos.neighbours()) { for (neighbour in pos.neighbours()) {
val ticket = world.permanentChunkTicket(neighbour, state) ?: continue val ticket = world.permanentChunkTicket(neighbour, state).await() ?: continue
neighbours.add(ticket) neighbours.add(ticket)
} }
@ -147,10 +155,18 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
ChunkState.TERRAIN -> { ChunkState.TERRAIN -> {
if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) { if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) {
// skip since no cells will be generated anyway // skip since no cells will be generated anyway
cells.value.fill(AbstractCell.EMPTY)
for (x in 0 until width) {
for (y in 0 until height) {
cells[x, y].setStateQuiet(AbstractCell.EMPTY)
}
}
signalChunkContentsUpdated()
} else { } else {
// tiles can be generated concurrently without any consequences // tiles can be generated concurrently without any consequences
CompletableFuture.runAsync(Runnable { prepareCells() }, Starbound.EXECUTOR).await() CompletableFuture.runAsync(Runnable { prepareCells() }, Starbound.EXECUTOR).await()
signalChunkContentsUpdated()
} }
} }
@ -386,21 +402,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
fun copyCells(): Object2DArray<ImmutableCell> { fun copyCells(): Object2DArray<ImmutableCell> {
if (cells.isInitialized()) { return Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { x, y -> cells[x, y].state }
return Object2DArray(cells.value)
} else {
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)
}
} }
data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null) data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null)
fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult { fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult {
if (!cells.isInitialized()) { val cellState = cells[pos.x, pos.y]
return DamageResult(TileDamageResult.NONE) val cell = cellState.state
}
val cell = cells.value[pos.x, pos.y]
if (cell.isIndestructible || cell.tile(isBackground).material.value.isMeta) { if (cell.isIndestructible || cell.tile(isBackground).material.value.isMeta) {
return DamageResult(TileDamageResult.NONE) return DamageResult(TileDamageResult.NONE)
@ -414,7 +423,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
result = TileDamageResult.PROTECTED result = TileDamageResult.PROTECTED
} }
val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y] val health = if (isBackground) cellState.backgroundHealth else cellState.foregroundHealth
val tile = cell.tile(isBackground) val tile = cell.tile(isBackground)
val params = if (!damage.type.isPenetrating && tile.modifier.value.breaksWithTile) { val params = if (!damage.type.isPenetrating && tile.modifier.value.breaksWithTile) {
@ -427,68 +436,70 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
onTileHealthUpdate(pos.x, pos.y, isBackground, health) onTileHealthUpdate(pos.x, pos.y, isBackground, health)
if (health.isDead) { if (health.isDead) {
if (isBackground) { damagedCells.remove(pos)
damagedTilesBackground.remove(pos)
} else { val drops = ArrayList<ItemDescriptor>()
damagedTilesForeground.remove(pos)
}
val copyHealth = health.copy() val copyHealth = health.copy()
val mCell = cell.mutable() val mCell = cell.mutable()
val mTile = mCell.tile(isBackground) val mTile = mCell.tile(isBackground)
if (health.isHarvested && mTile.material.value.itemDrop != null) {
drops.add(ItemDescriptor(mTile.material.value.itemDrop!!, 1L))
}
mTile.material = BuiltinMetaMaterials.EMPTY mTile.material = BuiltinMetaMaterials.EMPTY
mTile.color = TileColor.DEFAULT mTile.color = TileColor.DEFAULT
mTile.hueShift = 0f mTile.hueShift = 0f
if (tile.modifier.value.breaksWithTile) { if (tile.modifier.value.breaksWithTile) {
if (health.isHarvested && mTile.modifier.value.itemDrop != null) {
drops.add(ItemDescriptor(mTile.modifier.value.itemDrop!!, 1L))
}
mTile.modifier = BuiltinMetaMaterials.EMPTY_MOD mTile.modifier = BuiltinMetaMaterials.EMPTY_MOD
} }
for (item in drops) {
val entity = ItemDropEntity(item)
entity.position = (pos + this.pos.tile).toDoubleVector() + Vector2d(0.5, 0.5)
entity.joinWorld(world)
}
if (isBackground && cell.foreground.material.isEmptyTile) {
val info = world.template.cellInfo(pos + this.pos.tile)
if (info.oceanLiquid.isNotEmptyLiquid && !info.encloseLiquids && pos.y < info.oceanLiquidLevel) {
mCell.liquid.setInfinite(info.oceanLiquid.entry!!, (info.oceanLiquidLevel - pos.y).toFloat())
}
}
setCell(pos.x, pos.y, mCell.immutable()) setCell(pos.x, pos.y, mCell.immutable())
health.reset() health.reset()
return DamageResult(result, copyHealth, cell) return DamageResult(result, copyHealth, cell)
} else { } else {
if (isBackground) { damagedCells.add(pos)
damagedTilesBackground.add(pos)
} else {
damagedTilesForeground.add(pos)
}
return DamageResult(result, health, cell) return DamageResult(result, health, cell)
} }
} }
private val damagedTilesForeground = ObjectArraySet<Vector2i>() private val damagedCells = ObjectArraySet<Vector2i>()
private val damagedTilesBackground = ObjectArraySet<Vector2i>()
fun tileDamagePackets(): List<TileDamageUpdatePacket> { fun tileDamagePackets(): List<TileDamageUpdatePacket> {
val result = ArrayList<TileDamageUpdatePacket>() val result = ArrayList<TileDamageUpdatePacket>()
if (tileHealthBackground.isInitialized()) { for (x in 0 until width) {
val tileHealthBackground = tileHealthBackground.value for (y in 0 until height) {
val health = cells[x, y].backgroundHealth
for (x in 0 until CHUNK_SIZE) { if (!health.isHealthy) {
for (y in 0 until CHUNK_SIZE) { result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, true, health))
val health = tileHealthBackground[x, y]
if (!health.isHealthy) {
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, true, health))
}
} }
}
}
if (tileHealthForeground.isInitialized()) { val health2 = cells[x, y].foregroundHealth
val tileHealthForeground = tileHealthForeground.value
for (x in 0 until CHUNK_SIZE) { if (!health2.isHealthy) {
for (y in 0 until CHUNK_SIZE) { result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, false, health2))
val health = tileHealthForeground[x, y]
if (!health.isHealthy) {
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, false, health))
}
} }
} }
} }
@ -529,24 +540,23 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
super.tick() super.tick()
if (cells.isInitialized() && (damagedTilesBackground.isNotEmpty() || damagedTilesForeground.isNotEmpty())) { damagedCells.removeIf { (x, y) ->
val tileHealthBackground = tileHealthBackground.value val health = cells[x, y].foregroundHealth
val tileHealthForeground = tileHealthForeground.value val health2 = cells[x, y].backgroundHealth
val cells = cells.value
damagedTilesBackground.removeIf { (x, y) -> var any = false
val health = tileHealthBackground[x, y]
val result = !health.tick(cells[x, y].background.material.value.actualDamageTable)
onTileHealthUpdate(x, y, true, health)
result
}
damagedTilesForeground.removeIf { (x, y) -> if (health.isTicking) {
val health = tileHealthForeground[x, y] any = health.tick(cells[x, y].state.foreground.material.value.actualDamageTable) || any
val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable)
onTileHealthUpdate(x, y, false, health) onTileHealthUpdate(x, y, false, health)
result
} }
if (health2.isTicking) {
any = health2.tick(cells[x, y].state.background.material.value.actualDamageTable) || any
onTileHealthUpdate(x, y, false, health2)
}
!any
} }
} }
@ -581,22 +591,15 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> { fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
if (cells.isInitialized()) { return Object2DArray(width, height) { a, b -> cells[a, b].state.toLegacyNet() }
val cells = cells.value
return Object2DArray(width, height) { a, b -> cells[a, b].toLegacyNet() }
} else {
return Object2DArray(width, height, LegacyNetworkCellState.NULL)
}
} }
private fun prepareCells() { private fun prepareCells() {
val cells = cells.value
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
val info = world.template.cellInfo(pos.tileX + x, pos.tileY + y) val info = world.template.cellInfo(pos.tileX + x, pos.tileY + y)
val state = cells[x, y].mutable() val state = cells[x, y].state.mutable()
state.blockBiome = info.blockBiomeIndex state.blockBiome = info.blockBiomeIndex
state.envBiome = info.environmentBiomeIndex state.envBiome = info.environmentBiomeIndex
@ -640,17 +643,15 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
cells[x, y] = state.immutable() cells[x, y].setStateQuiet(state.immutable())
} }
} }
} }
private fun finalizeCells() { private fun finalizeCells() {
val cells = cells.value
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
val cell = cells[x, y].mutable() val cell = cells[x, y].state.mutable()
val info by lazy { world.template.cellInfo(pos.tileX + x, pos.tileY + y) } val info by lazy { world.template.cellInfo(pos.tileX + x, pos.tileY + y) }
if (cell.liquid.isInfinite) { if (cell.liquid.isInfinite) {
@ -675,7 +676,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
replaceBiomeBlocks(cell, info) replaceBiomeBlocks(cell, info)
cells[x, y] = cell.immutable() cells[x, y].state = cell.immutable()
} }
} }
} }
@ -708,13 +709,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
private fun replaceBiomeBlocks() { private fun replaceBiomeBlocks() {
val cells = cells.value
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
val cell = cells[x, y].mutable() val cell = cells[x, y].state.mutable()
replaceBiomeBlocks(cell, world.template.cellInfo(pos.tileX + x, pos.tileY + y)) replaceBiomeBlocks(cell, world.template.cellInfo(pos.tileX + x, pos.tileY + y))
cells[x, y] = cell.immutable() cells[x, y].state = cell.immutable()
} }
} }
} }
@ -790,12 +789,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
private fun placeGrass() { private fun placeGrass() {
val cells = cells.value
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
val biome = world.template.cellInfo(pos.tileX + x, pos.tileY + y).blockBiome ?: continue val biome = world.template.cellInfo(pos.tileX + x, pos.tileY + y).blockBiome ?: continue
val cell = cells[x, y] val cell = cells[x, y].state
// determine layer for grass mod calculation // determine layer for grass mod calculation
val isBackground = cell.foreground.material.isEmptyTile val isBackground = cell.foreground.material.isEmptyTile
@ -856,7 +853,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
modify.background.modifierHueShift = biome.hueShift(modify.background.modifier) modify.background.modifierHueShift = biome.hueShift(modify.background.modifier)
modify.foreground.modifierHueShift = biome.hueShift(modify.foreground.modifier) modify.foreground.modifierHueShift = biome.hueShift(modify.foreground.modifier)
cells[x, y] = modify.immutable() cells[x, y].state = modify.immutable()
} }
} }
} }

View File

@ -348,7 +348,7 @@ class ServerWorld private constructor(
Vector2d(pos.x + Globals.worldServer.playerStartRegionSize.x / 2, pos.y + Globals.worldServer.playerStartRegionSize.y), Vector2d(pos.x + Globals.worldServer.playerStartRegionSize.x / 2, pos.y + Globals.worldServer.playerStartRegionSize.y),
) )
val region = permanentChunkTicket(spawnRect) val region = permanentChunkTicket(spawnRect).await()
tickets.addAll(region) tickets.addAll(region)
region.forEach { it.chunk.await() } region.forEach { it.chunk.await() }
@ -378,7 +378,7 @@ class ServerWorld private constructor(
Vector2d(pos.x + Globals.worldServer.playerStartRegionSize.x / 2, pos.y + Globals.worldServer.playerStartRegionSize.y), Vector2d(pos.x + Globals.worldServer.playerStartRegionSize.x / 2, pos.y + Globals.worldServer.playerStartRegionSize.y),
) )
val region = permanentChunkTicket(spawnRect) val region = permanentChunkTicket(spawnRect).await()
tickets.addAll(region) tickets.addAll(region)
region.forEach { it.chunk.await() } region.forEach { it.chunk.await() }
@ -417,32 +417,32 @@ class ServerWorld private constructor(
return ServerChunk(this, pos) return ServerChunk(this, pos)
} }
fun permanentChunkTicket(pos: ChunkPos, target: ChunkState = ChunkState.FULL): ServerChunk.ITicket? { fun permanentChunkTicket(pos: ChunkPos, target: ChunkState = ChunkState.FULL): CompletableFuture<ServerChunk.ITicket?> {
return chunkMap.compute(pos)?.permanentTicket(target) return eventLoop.supplyAsync { chunkMap.compute(pos)?.permanentTicket(target) }
} }
fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITicket> { fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): CompletableFuture<List<ServerChunk.ITicket>> {
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { permanentChunkTicket(it, target).get() } }
} }
fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITicket> { fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): CompletableFuture<List<ServerChunk.ITicket>> {
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { permanentChunkTicket(it, target).get() } }
} }
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ChunkState = ChunkState.FULL): ServerChunk.ITimedTicket? { fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ChunkState = ChunkState.FULL): CompletableFuture<ServerChunk.ITimedTicket?> {
return chunkMap.compute(pos)?.temporaryTicket(time, target) return eventLoop.supplyAsync { chunkMap.compute(pos)?.temporaryTicket(time, target) }
} }
fun temporaryChunkTicket(region: AABBi, time: Int, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITimedTicket> { fun temporaryChunkTicket(region: AABBi, time: Int, target: ChunkState = ChunkState.FULL): CompletableFuture<List<ServerChunk.ITimedTicket>> {
require(time >= 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { temporaryChunkTicket(it, time, target).get() } }
} }
fun temporaryChunkTicket(region: AABB, time: Int, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITimedTicket> { fun temporaryChunkTicket(region: AABB, time: Int, target: ChunkState = ChunkState.FULL): CompletableFuture<List<ServerChunk.ITimedTicket>> {
require(time >= 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { temporaryChunkTicket(it, time, target).get() } }
} }
@JsonFactory @JsonFactory

View File

@ -2,11 +2,14 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
@ -180,7 +183,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
for (pos in newTrackedChunks) { for (pos in newTrackedChunks) {
if (pos !in tickets) { if (pos !in tickets) {
val ticket = world.permanentChunkTicket(pos) ?: continue val ticket = world.permanentChunkTicket(pos).get() ?: continue
val thisTicket = Ticket(ticket, pos) val thisTicket = Ticket(ticket, pos)
tickets[pos] = thisTicket tickets[pos] = thisTicket
@ -208,6 +211,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
} }
val unseen = IntArrayList(entityVersions.keys) val unseen = IntArrayList(entityVersions.keys)
val changePackets = Int2ObjectOpenHashMap<Int2ObjectOpenHashMap<ByteArrayList>>()
for (entity in trackingEntities) { for (entity in trackingEntities) {
val id = entity.entityID val id = entity.entityID
@ -230,7 +234,13 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
} else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) { } else if (entity.networkGroup.upstream.hasChangedSince(entityVersions.get(id))) {
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy) val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
entityVersions.put(id, version) entityVersions.put(id, version)
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data))) changePackets.computeIfAbsent(entity.connectionID, Int2ObjectFunction { Int2ObjectOpenHashMap() }).put(entity.entityID, data)
}
}
if (changePackets.isNotEmpty()) {
for ((connectionID, map) in changePackets) {
send(EntityUpdateSetPacket(connectionID, map))
} }
} }

View File

@ -1,36 +0,0 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kommons.util.ITimeSource
class Clock : ITimeSource {
var origin = System.nanoTime()
private set
var baseline = 0L
private set
var isPaused = false
private set
fun set(nanos: Long) {
origin = System.nanoTime()
baseline = nanos
}
fun pause() {
if (!isPaused) {
baseline += System.nanoTime() - origin
isPaused = true
}
}
fun unpause() {
if (isPaused) {
origin = System.nanoTime()
isPaused = false
}
}
override val nanos: Long
get() = if (isPaused) baseline else (System.nanoTime() - origin) + baseline
}

View File

@ -0,0 +1,166 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import java.io.DataInputStream
import java.io.DataOutputStream
interface IClock {
val nanos: Long
val micros: Long get() = nanos / 1_000L
val millis: Long get() = nanos / 1_000_000L
val seconds: Double get() = (nanos / 1_000L) / 1_000_000.0
}
// this is stupid, but legacy protocol requires it
// Used for timing in-game events which must be persistent
// And some worlds are not persistent (they get their own clocks,
// such as instance worlds, e.g. Outpost, or Creon Embassy from Elithian Races mod)
// https://www.pcgamingwiki.com/wiki/Category:Persistent describes it as:
// Gameplay continues even when player is not playing the game,
// and the game state is either simulated on a remote server or
// changes over time are calculated when the player returns to the game.
class RelativeClock() : IClock {
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
read(stream, isLegacy)
}
private var pointOfReference = 0L
private var pointOfReferenceSet = false
override var nanos: Long = 0L
private set
fun set(age: Long) {
pointOfReferenceSet = false
nanos = age
}
fun update(newPointOfReference: Long) {
if (pointOfReferenceSet) {
val diff = newPointOfReference - pointOfReference
if (diff > 0L)
nanos += diff
}
pointOfReference = newPointOfReference
}
fun update(newPointOfReference: Double) {
return update((newPointOfReference * 1_000_000_000.0).toLong())
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBoolean(pointOfReferenceSet)
if (isLegacy) {
if (pointOfReferenceSet)
stream.writeDouble(pointOfReference / 1_000_000_000.0)
stream.writeDouble(nanos / 1_000_000_000.0)
} else {
if (pointOfReferenceSet)
stream.writeLong(pointOfReference)
stream.writeLong(nanos)
}
}
fun read(stream: DataInputStream, isLegacy: Boolean) {
pointOfReferenceSet = stream.readBoolean()
if (isLegacy) {
if (pointOfReferenceSet)
pointOfReference = (stream.readDouble() * 1_000_000_000.0).toLong()
nanos = (stream.readDouble() * 1_000_000_000.0).toLong()
} else {
if (pointOfReferenceSet)
pointOfReference = stream.readLong()
nanos = stream.readLong()
}
}
}
class JVMClock : IClock {
var origin = System.nanoTime()
private set
var baseline = 0L
private set
var isPaused = false
private set
fun set(nanos: Long) {
origin = System.nanoTime()
baseline = nanos
}
fun pause() {
if (!isPaused) {
baseline += System.nanoTime() - origin
isPaused = true
}
}
fun unpause() {
if (isPaused) {
origin = System.nanoTime()
isPaused = false
}
}
override val nanos: Long
get() = if (isPaused) baseline else (System.nanoTime() - origin) + baseline
}
class GameTimer(val time: Double = 0.0) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readDouble(isLegacy)) {
timer = stream.readDouble(isLegacy)
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeDouble(time, isLegacy)
stream.writeDouble(timer, isLegacy)
}
var timer = time
private set
fun reset() {
timer = time
}
var hasFinished: Boolean
get() = timer <= 0.0
set(value) {
if (value)
timer = 0.0
else
timer = time
}
val percent: Double
get() = if (time != 0.0) timer / time else 0.0
fun invert() {
timer = time - timer
}
fun tick(delta: Double = Starbound.TIMESTEP): Boolean {
timer = (timer - delta).coerceAtLeast(0.0)
return timer == 0.0
}
fun wrapTick(delta: Double = Starbound.TIMESTEP): Boolean {
val result = tick(delta)
if (result) reset()
return result
}
}

View File

@ -1,39 +0,0 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kstarbound.Starbound
class GameTimer(val time: Double = 0.0) {
var timer = time
private set
fun reset() {
timer = time
}
var hasFinished: Boolean
get() = timer <= 0.0
set(value) {
if (value)
timer = 0.0
else
timer = time
}
val percent: Double
get() = if (time != 0.0) timer / time else 0.0
fun invert() {
timer = time - timer
}
fun tick(delta: Double = Starbound.TIMESTEP): Boolean {
timer = (timer - delta).coerceAtLeast(0.0)
return timer == 0.0
}
fun wrapTick(delta: Double = Starbound.TIMESTEP): Boolean {
val result = tick(delta)
if (result) reset()
return result
}
}

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kommons.util.JVMTimeSource
import java.util.* import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -56,7 +55,7 @@ class MailboxExecutorService(@Volatile var thread: Thread = Thread.currentThread
@Volatile @Volatile
private var isTerminated = false private var isTerminated = false
private val timeOrigin = JVMTimeSource() private val timeOrigin = JVMClock()
var exceptionHandler: Consumer<Throwable>? = null var exceptionHandler: Consumer<Throwable>? = null

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
@ -12,7 +13,13 @@ import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.max
import kotlin.math.min
/** /**
* Чанк мира * Чанк мира
@ -24,7 +31,7 @@ import java.util.concurrent.CopyOnWriteArraySet
* *
* Весь игровой мир будет измеряться в Starbound Unit'ах * Весь игровой мир будет измеряться в Starbound Unit'ах
*/ */
abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>( abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This, CellType>, CellType : Chunk<WorldType, This, CellType>.ChunkCell>(
val world: WorldType, val world: WorldType,
val pos: ChunkPos, val pos: ChunkPos,
) : ICellAccess { ) : ICellAccess {
@ -61,58 +68,153 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val aabbd = aabb.toDoubleAABB() val aabbd = aabb.toDoubleAABB()
// TODO: maybe fit them into "width" and "height" variables added recently? // TODO: maybe fit them into "width" and "height" variables added recently?
protected val cells = lazy { protected abstract val cells: Object2DArray<CellType>
Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL)
private var hasDirtyCollisions = false
// bulk mark collision dirty of neighbour chunks
protected fun signalChunkContentsUpdated() {
val signalPositions = ArrayList<Vector2i>()
for (x in 1 .. 2) {
for (y in 1 .. 2) {
signalPositions.add(pos.tile + Vector2i(width + x, height + y))
signalPositions.add(pos.tile + Vector2i(width, height + y))
signalPositions.add(pos.tile + Vector2i(width + x, height))
signalPositions.add(pos.tile + Vector2i(-x, -y))
signalPositions.add(pos.tile + Vector2i(0, -y))
signalPositions.add(pos.tile + Vector2i(-x, 0))
}
}
for (pos in signalPositions) {
val actualCellPosition = world.geometry.wrap(pos)
val chunk = world.chunkMap[world.geometry.chunkFromCell(actualCellPosition)] ?: continue
chunk.hasDirtyCollisions = true
chunk.cells[actualCellPosition.x - chunk.pos.tileX, actualCellPosition.y - chunk.pos.tileY].collisionCacheDirty = true
}
} }
protected val tileHealthForeground = lazy { private val collisionsLock = Any()
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
fun getCollisions(x: Int, y: Int, target: MutableCollection<CollisionPoly>) {
if (hasDirtyCollisions) {
synchronized(collisionsLock) {
if (hasDirtyCollisions) {
var minX = width
var minY = height
var maxX = 0
var maxY = 0
for (x in 0 until width) {
for (y in 0 until height) {
if (cells[x, y].collisionCacheDirty) {
minX = min(minX, x)
minY = min(minY, y)
maxX = max(maxX, x)
maxY = max(maxY, y)
}
}
}
for (x in minX .. maxX) {
for (y in minY .. maxY) {
val cell = cells[x, y]
if (cell.collisionCacheDirty) {
cell.collisionCache.clear()
getBlocksMarchingSquares(pos.tileX + x, pos.tileY + y, world.foreground, CollisionType.DYNAMIC, cell.collisionCache)
getBlockPlatforms(pos.tileX + x, pos.tileY + y, world.foreground, CollisionType.PLATFORM, cell.collisionCache)
cell.collisionCacheDirty = false
}
}
}
}
}
hasDirtyCollisions = false
}
target.addAll(cells[x, y].collisionCache)
} }
protected val tileHealthBackground = lazy { abstract inner class ChunkCell(val x: Int, val y: Int) {
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() } private var actualState: ImmutableCell = AbstractCell.NULL
var state: ImmutableCell
get() = actualState
set(value) {
if (actualState != value) {
foregroundHealth.reset()
backgroundHealth.reset()
hasDirtyCollisions = true
collisionCacheDirty = true
for (xoff in -2 .. 2) {
for (yoff in -2 .. 2) {
val actualCellPosition = world.geometry.wrap(pos.tile + Vector2i(x + xoff, y + yoff))
val chunk = world.chunkMap[world.geometry.chunkFromCell(actualCellPosition)] ?: continue
chunk.hasDirtyCollisions = true
chunk.cells[actualCellPosition.x - chunk.pos.tileX, actualCellPosition.y - chunk.pos.tileY].collisionCacheDirty = true
}
}
val old = actualState
actualState = value
if (old.foreground != value.foreground) {
foregroundChanges(x, y, value)
}
if (old.background != value.background) {
backgroundChanges(x, y, value)
}
if (old.liquid != value.liquid) {
liquidChanges(x, y, value)
}
cellChanges(x, y, value)
}
}
/**
* Does not trigger any change events
*/
fun setStateQuiet(state: ImmutableCell) {
foregroundHealth.reset()
backgroundHealth.reset()
hasDirtyCollisions = true
collisionCacheDirty = true
actualState = state
}
var collisionCacheDirty = true
val foregroundHealth = TileHealth.Tile()
val backgroundHealth = TileHealth.Tile()
val collisionCache = ObjectArrayList<CollisionPoly>(2) // no CME checks
} }
fun loadCells(source: Object2DArray<out AbstractCell>) { fun loadCells(source: Object2DArray<out AbstractCell>) {
val ours = cells.value val ours = cells
source.checkSizeEquals(ours) source.checkSizeEquals(ours)
for (x in 0 until CHUNK_SIZE) { for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) { for (y in 0 until CHUNK_SIZE) {
ours[x, y] = source[x, y].immutable() ours[x, y].state = source[x, y].immutable()
} }
} }
} }
override fun getCell(x: Int, y: Int): AbstractCell { override fun getCell(x: Int, y: Int): AbstractCell {
if (!cells.isInitialized()) return cells[x, y].state
return AbstractCell.NULL
return cells.value[x, y]
} }
final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL cells[x, y].state = cell.immutable()
val new = cell.immutable()
if (old != new) {
cells.value[x, y] = new
if (old.foreground != new.foreground) {
foregroundChanges(x, y, new)
}
if (old.background != new.background) {
backgroundChanges(x, y, new)
}
if (old.liquid != new.liquid) {
liquidChanges(x, y, new)
}
cellChanges(x, y, new)
}
return true return true
} }

View File

@ -16,12 +16,8 @@ private fun circulate(value: Int, bounds: Int): Int {
} }
/** /**
* Сетка чанков идёт как и сетка тайлов. * Coordinate, representing direct positions of chunks in [World.ChunkMap], with some
* * helper methods and properties
* * Вправо у нас положительный X
* * Влево у нас отрицательный X
* * Вверх у нас положительный Y
* * Вниз у нас отрицательный Y
*/ */
data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> { data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())

View File

@ -5,7 +5,6 @@ import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.math.linearInterpolation import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.util.value
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -24,6 +23,7 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedEnumStupid
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedJson import ru.dbotthepony.kstarbound.network.syncher.networkedJson
import ru.dbotthepony.kstarbound.network.syncher.networkedVec2f import ru.dbotthepony.kstarbound.network.syncher.networkedVec2f
import ru.dbotthepony.kstarbound.util.IClock
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sin import kotlin.math.sin
@ -71,6 +71,12 @@ class Sky() {
var destination: SkyParameters? = null var destination: SkyParameters? = null
private set private set
var referenceClock: IClock? = null
set(value) {
field = value
time = value?.seconds ?: time
}
val speedupTime: Double get() { val speedupTime: Double get() {
if (enterHyperspace) { if (enterHyperspace) {
return Globals.sky.hyperspaceSpeedupTime.coerceAtLeast(0.01) return Globals.sky.hyperspaceSpeedupTime.coerceAtLeast(0.01)
@ -187,7 +193,7 @@ class Sky() {
fun tick(delta: Double = Starbound.TIMESTEP) { fun tick(delta: Double = Starbound.TIMESTEP) {
time += delta time = referenceClock?.seconds ?: (time + delta)
flashTimer = (flashTimer - delta).coerceAtLeast(0.0) flashTimer = (flashTimer - delta).coerceAtLeast(0.0)
if (flyingType != FlyingType.NONE) { if (flyingType != FlyingType.NONE) {

View File

@ -21,7 +21,7 @@ import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.util.Clock import ru.dbotthepony.kstarbound.util.JVMClock
import ru.dbotthepony.kstarbound.util.random.MWCRandom import ru.dbotthepony.kstarbound.util.random.MWCRandom
import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
@ -36,7 +36,7 @@ import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
import kotlin.math.sqrt import kotlin.math.sqrt
abstract class SystemWorld(val location: Vector3i, val clock: Clock, val universe: Universe) { abstract class SystemWorld(val location: Vector3i, val clock: JVMClock, val universe: Universe) {
val random = random() val random = random()
abstract val entities: Map<UUID, Entity> abstract val entities: Map<UUID, Entity>
abstract val ships: Map<UUID, Ship> abstract val ships: Map<UUID, Ship>

View File

@ -95,6 +95,9 @@ sealed class TileHealth() {
damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent
} }
val isTicking: Boolean
get() = !isHealthy && !isDead
fun tick(config: TileDamageConfig, delta: Double = Starbound.TIMESTEP): Boolean { fun tick(config: TileDamageConfig, delta: Double = Starbound.TIMESTEP): Boolean {
if (isDead || isHealthy) if (isDead || isHealthy)
return false return false

View File

@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
@ -45,7 +46,7 @@ import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType, *>>(val template: WorldTemplate) : ICellAccess {
val background = TileView.Background(this) val background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
val sky = Sky(template.skyParameters) val sky = Sky(template.skyParameters)
@ -74,8 +75,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
abstract fun chunks(): List<ChunkType> abstract fun chunks(): List<ChunkType>
abstract fun remove(x: Int, y: Int) abstract fun remove(x: Int, y: Int)
private val chunkCache = arrayOfNulls<Chunk<*, *>>(4)
operator fun get(pos: ChunkPos) = get(pos.x, pos.y) operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
fun compute(pos: ChunkPos) = compute(pos.x, pos.y) fun compute(pos: ChunkPos) = compute(pos.x, pos.y)
@ -221,6 +220,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val lock = ReentrantLock() val lock = ReentrantLock()
val entities = Int2ObjectOpenHashMap<AbstractEntity>() val entities = Int2ObjectOpenHashMap<AbstractEntity>()
val entityList = CopyOnWriteArrayList<AbstractEntity>()
val entityIndex = SpatialIndex<AbstractEntity>(geometry) val entityIndex = SpatialIndex<AbstractEntity>(geometry)
val dynamicEntities = ArrayList<DynamicEntity>() val dynamicEntities = ArrayList<DynamicEntity>()
@ -278,7 +278,19 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
})).join() })).join()
entities.values.forEach { it.tick() } entityList.forEach {
try {
if (it.isInWorld) // entities might remove other entities during tick
it.tick()
} catch (err: Throwable) {
if (it.isRemote && isServer) {
LOGGER.error("Exception ticking client spawned entity $it, removing", err)
it.remove(AbstractEntity.RemovalReason.REMOVED)
} else {
LOGGER.error("Exception ticking entity $it", err)
}
}
}
for (chunk in chunkMap.chunks()) for (chunk in chunkMap.chunks())
chunk.tick() chunk.tick()
@ -327,13 +339,16 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> { fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> {
val result = ArrayList<CollisionPoly>() val result = ObjectArrayList<CollisionPoly>() // no CME checks
val tiles = aabb.encasingIntAABB() val tiles = aabb.encasingIntAABB()
for (x in tiles.mins.x .. tiles.maxs.x) { for (x in tiles.mins.x .. tiles.maxs.x) {
for (y in tiles.mins.y .. tiles.maxs.y) { for (y in tiles.mins.y .. tiles.maxs.y) {
getBlocksMarchingSquares(x, y, foreground, CollisionType.DYNAMIC, result) val cx = geometry.x.cell(x)
getBlockPlatforms(x, y, foreground, CollisionType.PLATFORM, result) val cy = geometry.y.cell(y)
val chunk = chunkMap[geometry.x.chunkFromCell(cx), geometry.y.chunkFromCell(cy)] ?: continue
chunk.getCollisions(cx - chunk.pos.tileX, cy - chunk.pos.tileY, result)
} }
} }

View File

@ -46,6 +46,10 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loop
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2())) return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
} }
fun chunkFromCell(x: Int, y: Int): ChunkPos {
return ChunkPos(this.x.chunkFromCell(x), this.y.chunkFromCell(y))
}
fun chunkFromCell(pos: IStruct2f): ChunkPos { fun chunkFromCell(pos: IStruct2f): ChunkPos {
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2())) return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
} }

View File

@ -34,6 +34,13 @@ data class MutableLiquidState(
isInfinite = false isInfinite = false
} }
fun setInfinite(state: Registry.Entry<LiquidDefinition>, pressure: Float) {
level = 1f
this.state = state
this.pressure = pressure
this.isInfinite = true
}
override fun mutable(): MutableLiquidState { override fun mutable(): MutableLiquidState {
return this return this
} }

View File

@ -130,6 +130,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
innerWorld = world innerWorld = world
world.entities[entityID] = this world.entities[entityID] = this
world.entityList.add(this)
spatialEntry = world.entityIndex.Entry(this) spatialEntry = world.entityIndex.Entry(this)
onJoinWorld(world) onJoinWorld(world)
} }
@ -140,6 +141,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
mailbox.shutdownNow() mailbox.shutdownNow()
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
world.entityList.remove(this)
try { try {
onRemove(world, reason) onRemove(world, reason)

View File

@ -330,11 +330,8 @@ class ActorMovementController() : MovementController() {
if (isOnGround) { if (isOnGround) {
groundMovementSustainTimer = GameTimer(maxGroundSustain) groundMovementSustainTimer = GameTimer(maxGroundSustain)
} else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) { } else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) {
val collideAny = localHitboxes val collideAny = computeLocalHitboxes()
.map { it + Vector2d(0.0, -groundCheckDistance) } .any { world.polyIntersects(it + Vector2d(0.0, -groundCheckDistance), { it.type >= CollisionType.PLATFORM }) }
.anyMatch {
world.polyIntersects(it, { it.type >= CollisionType.PLATFORM })
}
if (collideAny) if (collideAny)
groundMovementSustainTimer = GameTimer(0.0) groundMovementSustainTimer = GameTimer(0.0)

View File

@ -50,7 +50,7 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
override fun render(client: StarboundClient, layers: LayeredRenderer) { override fun render(client: StarboundClient, layers: LayeredRenderer) {
layers.add(RenderLayer.Overlay.point()) { layers.add(RenderLayer.Overlay.point()) {
val hitboxes = movement.localHitboxes.toList() val hitboxes = movement.computeLocalHitboxes()
if (hitboxes.isEmpty()) return@add if (hitboxes.isEmpty()) return@add
hitboxes.forEach { it.render(client) } hitboxes.forEach { it.render(client) }

View File

@ -0,0 +1,208 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedItem
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.util.RelativeClock
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Predicate
import kotlin.math.min
class ItemDropEntity() : DynamicEntity("/") {
// int32_t, but networked as proper enum
// костыль (именно DEAD состояние), но требуют оригинальные клиенты
// мда.
enum class State(override val jsonName: String) : IStringSerializable {
INTANGIBLE("Intangible"),
AVAILABLE("Available"),
TAKEN("Taken"),
DEAD("Dead");
}
var state by networkedEnum(State.entries).also { networkGroup.upstream.add(it) }
private set
var owningEntity by networkedSignedInt().also { networkGroup.upstream.add(it) }
private set
override val movement: MovementController = MovementController().also { networkGroup.upstream.add(it.networkGroup) }
var item by networkedItem().also { networkGroup.upstream.add(it) }
private set
var shouldNotExpire = false
val age = RelativeClock()
var intangibleTimer = GameTimer(0.0)
private set
init {
movement.applyParameters(Globals.itemDrop.movementSettings)
if (movement.movementParameters.physicsEffectCategories == null) {
movement.applyParameters(MovementParameters(physicsEffectCategories = itemdropCat))
}
movement.applyParameters(MovementParameters(collisionPoly = Either.left(Poly(AABB(Vector2d(-0.5, -0.5), Vector2d(0.5, 0.5))))))
}
constructor(item: ItemDescriptor) : this() {
this.item = ItemStack.create(item)
this.owningEntity = 0
this.state = State.AVAILABLE
}
constructor(item: ItemStack) : this() {
this.item = item.copy()
this.owningEntity = 0
this.state = State.AVAILABLE
}
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
item = ItemStack.create(ItemDescriptor(stream))
shouldNotExpire = stream.readBoolean()
age.read(stream, isLegacy)
intangibleTimer = GameTimer(stream, isLegacy)
}
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
item.write(stream)
stream.writeBoolean(shouldNotExpire)
age.write(stream, isLegacy)
intangibleTimer.write(stream, isLegacy)
}
fun setIntangibleTime(time: Double) {
intangibleTimer = GameTimer(time)
if (state == State.AVAILABLE)
state = State.INTANGIBLE
}
override fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
TODO("Not yet implemented")
}
override fun setProperty0(key: JsonPath, value: JsonElement) {
TODO("Not yet implemented")
}
override val type: EntityType
get() = EntityType.ITEM_DROP
val canTake: Boolean get() {
return state == State.AVAILABLE && owningEntity == 0 && item.isNotEmpty
}
fun take(by: AbstractEntity): ItemStack {
if (canTake) {
state = State.TAKEN
age.set(0L)
owningEntity = by.entityID
return item.copy()
}
return ItemStack.EMPTY
}
private var stayAliveFor = -1.0
override fun tick() {
super.tick()
if (!isRemote) {
if (item.isEmpty) {
// remove from world
if (isInWorld) // got removed by other item
remove(RemovalReason.REMOVED)
return
}
if (state != State.TAKEN)
age.update(world.sky.time)
else if (stayAliveFor > 0.0) {
stayAliveFor -= Starbound.TIMESTEP
if (stayAliveFor <= 0.0) {
state = State.DEAD
remove(RemovalReason.REMOVED)
return
}
}
if (owningEntity != 0) {
// move towards picking player
val entity = world.entities[owningEntity]
if (entity == null) {
// Our owning entity left, disappear quickly
state = State.DEAD
remove(RemovalReason.REMOVED)
} else if (stayAliveFor == -1.0) {
val diff = world.geometry.diff(entity.position, position)
movement.approachVelocity(diff.unitVector * Globals.itemDrop.velocity, Globals.itemDrop.velocityApproach)
if (diff.length < Globals.itemDrop.pickupDistance) {
stayAliveFor = 0.05 // stay alive a little longer so pickup "animation" doesn't get cut off early
}
}
movement.applyParameters(noGravity)
} else {
// Rarely, check for other drops near us and combine with them if possible.
if (canTake && world.random.nextFloat() < Globals.itemDrop.combineChance && item.size < item.maxStackSize) {
val find = world.entityIndex.query(Globals.itemDrop.combineRadiusBox + position, filter = Predicate {
it is ItemDropEntity && it !== this && it.canTake && it.item.size != it.item.maxStackSize && it.position.distance(position) <= Globals.itemDrop.combineRadius && it.item.isStackable(item) })
for (entity in find) {
entity as ItemDropEntity
val newSize = min(item.size + entity.item.size, item.maxStackSize)
val diff = newSize - item.size
if (diff <= 0) break
item.size += diff
if (entity.item.size == diff) {
// we need to do this instead of updating item stack size
// because if we network empty itemstack legacy clients will crash
// because of no safeguard check inside ItemDrop::render
// Clients will crash anyway if we network item they don't know about, lol.
entity.state = State.DEAD
entity.remove(RemovalReason.REMOVED)
} else {
entity.item.size -= diff
}
entity.item.size -= diff
age.set(min(age.nanos, entity.age.nanos))
// Average the position and velocity of the drop we merged with
//movement.position += world.geometry.diff(movement.position, entity.movement.position) / 2.0
//movement.velocity += world.geometry.diff(movement.velocity, entity.movement.velocity) / 2.0
}
}
movement.applyParameters(gravity)
}
}
}
companion object {
private val itemdropCat = ImmutableSet.of("itemdrop")
private val noGravity = MovementParameters(collisionEnabled = false, gravityEnabled = false)
private val gravity = MovementParameters(collisionEnabled = true, gravityEnabled = true)
}
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.io.DoubleValueCodec import ru.dbotthepony.kommons.io.DoubleValueCodec
import ru.dbotthepony.kommons.io.FloatValueCodec import ru.dbotthepony.kommons.io.FloatValueCodec
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
@ -13,6 +14,7 @@ import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.times import ru.dbotthepony.kommons.vector.times
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
@ -53,8 +55,21 @@ open class MovementController() {
world0 = null world0 = null
} }
val localHitboxes: Stream<Poly> fun computeLocalHitboxes(): List<Poly> {
get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } } val poly = movementParameters.collisionPoly ?: return listOf()
if (poly.isLeft) {
return listOf(poly.left().rotate(rotation) + position)
} else {
val build = ObjectArrayList<Poly>(poly.right().size)
for (p in poly.right()) {
build.add(p.rotate(rotation) + position)
}
return build
}
}
open fun shouldCollideWithType(type: CollisionType): Boolean { open fun shouldCollideWithType(type: CollisionType): Boolean {
return type !== CollisionType.NONE return type !== CollisionType.NONE
@ -102,7 +117,7 @@ open class MovementController() {
fun updateFixtures() { fun updateFixtures() {
val spatialEntry = spatialEntry ?: return val spatialEntry = spatialEntry ?: return
fixturesChangeset++ fixturesChangeset++
val localHitboxes = localHitboxes.toList() val localHitboxes = computeLocalHitboxes()
while (fixtures.size > localHitboxes.size) { while (fixtures.size > localHitboxes.size) {
fixtures.last().remove() fixtures.last().remove()
@ -145,7 +160,8 @@ open class MovementController() {
var appliedForceRegion: Boolean = false var appliedForceRegion: Boolean = false
protected set protected set
var movementParameters: MovementParameters = MovementParameters.EMPTY var movementParameters: MovementParameters = Globals.movementParameters
protected set
var gravityMultiplier = 1.0 var gravityMultiplier = 1.0
var isGravityDisabled = false var isGravityDisabled = false
@ -267,12 +283,22 @@ open class MovementController() {
val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) + val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) +
(movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude (movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude
val localHitboxes = localHitboxes.toList() val localHitboxes = computeLocalHitboxes()
val aabb = localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get()
if (localHitboxes.isEmpty())
return // whut
var aabb = localHitboxes.first().aabb
for (i in 1 until localHitboxes.size) {
aabb = aabb.combine(localHitboxes[i].aabb)
}
var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection) var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection)
queryBounds = queryBounds.combine(queryBounds + movement) queryBounds = queryBounds.combine(queryBounds + movement)
val polies = world.queryTileCollisions(queryBounds).filter(this::shouldCollideWithBody) val polies = world.queryTileCollisions(queryBounds)
polies.removeIf { !shouldCollideWithBody(it) }
val results = ArrayList<CollisionResult>(localHitboxes.size) val results = ArrayList<CollisionResult>(localHitboxes.size)
@ -405,6 +431,8 @@ open class MovementController() {
} }
} }
protected data class BodyPair(val body: CollisionPoly, val distance: Double)
protected fun collisionSweep( protected fun collisionSweep(
body: Poly, staticBodies: List<CollisionPoly>, body: Poly, staticBodies: List<CollisionPoly>,
movement: Vector2d, ignorePlatforms: Boolean, movement: Vector2d, ignorePlatforms: Boolean,
@ -419,11 +447,13 @@ open class MovementController() {
var totalCorrection = Vector2d.ZERO var totalCorrection = Vector2d.ZERO
var movingCollisionId: Int? = null var movingCollisionId: Int? = null
val sorted = staticBodies.stream() val sorted = ObjectArrayList<BodyPair>(staticBodies.size)
.map { it to (it.poly.aabb.centre - sortCenter).lengthSquared }
.sorted { o1, o2 -> o1.second.compareTo(o2.second) } for (sbody in staticBodies) {
.map { it.first } sorted.add(BodyPair(sbody, (sbody.poly.aabb.centre - sortCenter).lengthSquared))
.toList() }
sorted.sortWith { o1, o2 -> o1.distance.compareTo(o2.distance) }
if (slopeCorrection) { if (slopeCorrection) {
// Starbound: First try separating with our ground sliding cheat. // Starbound: First try separating with our ground sliding cheat.
@ -526,7 +556,7 @@ open class MovementController() {
} }
protected fun collisionSeparate( protected fun collisionSeparate(
poly: Poly, staticBodies: List<CollisionPoly>, poly: Poly, staticBodies: List<BodyPair>,
ignorePlatforms: Boolean, maximumPlatformCorrection: Double, ignorePlatforms: Boolean, maximumPlatformCorrection: Double,
upward: Boolean, separationTolerance: Double upward: Boolean, separationTolerance: Double
): CollisionSeparation { ): CollisionSeparation {
@ -534,7 +564,7 @@ open class MovementController() {
var intersects = false var intersects = false
var correctedPoly = poly var correctedPoly = poly
for (body in staticBodies) { for ((body) in staticBodies) {
if (ignorePlatforms && body.type === CollisionType.PLATFORM) if (ignorePlatforms && body.type === CollisionType.PLATFORM)
continue continue
@ -559,7 +589,7 @@ open class MovementController() {
separation.solutionFound = true separation.solutionFound = true
if (intersects) { if (intersects) {
for (body in staticBodies) { for ((body) in staticBodies) {
if (body.type === CollisionType.PLATFORM) if (body.type === CollisionType.PLATFORM)
continue continue
@ -576,6 +606,15 @@ open class MovementController() {
return separation return separation
} }
fun applyParameters(changes: MovementParameters) {
updateParameters(this.movementParameters.merge(changes))
}
fun updateParameters(parameters: MovementParameters) {
this.movementParameters = parameters
this.mass = parameters.mass ?: this.mass
}
companion object { companion object {
const val SEPARATION_STEPS = 3 const val SEPARATION_STEPS = 3
const val SEPARATION_TOLERANCE = 0.001 const val SEPARATION_TOLERANCE = 0.001

View File

@ -125,7 +125,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
try { try {
currentMaterialSpaces.forEach { (p) -> currentMaterialSpaces.forEach { (p) ->
tickets.add(world.permanentChunkTicket(ChunkPos(world.geometry.x.chunkFromCell(p.x), world.geometry.x.chunkFromCell(p.y)), ChunkState.EMPTY) ?: return@forEach) tickets.add(world.permanentChunkTicket(world.geometry.chunkFromCell(p.x, p.y), ChunkState.EMPTY).await() ?: return@forEach)
} }
tickets.forEach { it.chunk.await() } tickets.forEach { it.chunk.await() }

View File

@ -178,7 +178,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) } var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) }
var health by networkedFloat().also { networkGroup.upstream.add(it) } var health by networkedFloat(config.value.health).also { networkGroup.upstream.add(it) }
private var orientationIndex by networkedPointer(-1L).also { private var orientationIndex by networkedPointer(-1L).also {
networkGroup.upstream.add(it) networkGroup.upstream.add(it)
@ -415,6 +415,27 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
} }
if (world.isServer && !unbreakable) {
var shouldBreak = false
if (health <= 0.0)
shouldBreak = true
if (!shouldBreak && tileHealth.isDead)
shouldBreak = true
if (!shouldBreak) {
val orientation = orientation
if (orientation != null && !orientation.anchorsValid(world, tilePosition)) {
shouldBreak = true
}
}
if (shouldBreak) {
}
}
} }
override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean { override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {

View File

@ -3,8 +3,8 @@ package ru.dbotthepony.kstarbound.world.physics
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
data class CollisionPoly( data class CollisionPoly(
val poly: Poly, val poly: Poly = Poly.EMPTY,
val type: CollisionType, val type: CollisionType = CollisionType.NULL,
val bounceFactor: Double = 0.0, val bounceFactor: Double = 0.0,
val velocity: Vector2d = Vector2d.ZERO val velocity: Vector2d = Vector2d.ZERO
) )