Item drops stuff, but i accidentally inflated chunk memory requirements
This commit is contained in:
parent
8987dc5270
commit
e134554879
@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
|
||||
|
||||
kotlinVersion=1.9.10
|
||||
kotlinCoroutinesVersion=1.8.0
|
||||
kommonsVersion=2.13.1
|
||||
kommonsVersion=2.14.0
|
||||
|
||||
ffiVersion=2.2.13
|
||||
lwjglVersion=3.3.0
|
||||
|
@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.MovementParameters
|
||||
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
|
||||
import ru.dbotthepony.kstarbound.defs.WorldServerConfig
|
||||
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.world.TerrestrialWorldsConfig
|
||||
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
|
||||
@ -35,10 +36,10 @@ object Globals {
|
||||
var player by Delegates.notNull<PlayerConfig>()
|
||||
private set
|
||||
|
||||
var actorMovementParameters = ActorMovementParameters()
|
||||
var actorMovementParameters by Delegates.notNull<ActorMovementParameters>()
|
||||
private set
|
||||
|
||||
var movementParameters = MovementParameters()
|
||||
var movementParameters by Delegates.notNull<MovementParameters>()
|
||||
private set
|
||||
|
||||
var client by Delegates.notNull<ClientConfig>()
|
||||
@ -77,6 +78,9 @@ object Globals {
|
||||
var worldServer by Delegates.notNull<WorldServerConfig>()
|
||||
private set
|
||||
|
||||
var itemDrop by Delegates.notNull<ItemDropConfig>()
|
||||
private set
|
||||
|
||||
var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>()
|
||||
private set
|
||||
|
||||
@ -149,6 +153,7 @@ object Globals {
|
||||
tasks.add(load("/worldserver.config", ::worldServer))
|
||||
tasks.add(load("/player.config", ::player))
|
||||
tasks.add(load("/systemworld.config", ::systemWorld))
|
||||
tasks.add(load("/itemdrop.config", ::itemDrop))
|
||||
tasks.add(load("/celestial.config", ::celestialBaseInformation))
|
||||
tasks.add(load("/celestial.config", ::celestialConfig))
|
||||
tasks.add(load("/celestial/names.config", ::celestialNames))
|
||||
|
@ -7,6 +7,7 @@ import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.stream.JsonReader
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kstarbound.defs.Json2Function
|
||||
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
|
||||
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 dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
|
||||
|
||||
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> {
|
||||
return { mapper.invoke(it) to null }
|
||||
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, KOptional<Int?>> {
|
||||
return { mapper.invoke(it) to KOptional() }
|
||||
}
|
||||
|
||||
private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int): (T) -> Pair<String, Int> {
|
||||
return { mapper.invoke(it) to mapperInt.invoke(it) }
|
||||
private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int?): (T) -> Pair<String, KOptional<Int?>> {
|
||||
return { mapper.invoke(it) to KOptional(mapperInt.invoke(it)) }
|
||||
}
|
||||
|
||||
fun validate(): CompletableFuture<Boolean> {
|
||||
@ -106,7 +107,7 @@ object Registries {
|
||||
private inline fun <reified T : Any> loadRegistry(
|
||||
registry: Registry<T>,
|
||||
files: List<IStarboundFile>,
|
||||
noinline keyProvider: (T) -> Pair<String, Int?>,
|
||||
noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>,
|
||||
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
|
||||
): List<Future<*>> {
|
||||
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
|
||||
@ -124,10 +125,13 @@ object Registries {
|
||||
after(read, listedFile)
|
||||
|
||||
registry.add {
|
||||
if (keys.second != null)
|
||||
registry.add(keys.first, keys.second!!, read, elem, listedFile)
|
||||
else
|
||||
registry.add(keys.first, read, elem, listedFile)
|
||||
registry.add(
|
||||
key = keys.first,
|
||||
value = read,
|
||||
id = keys.second,
|
||||
json = elem,
|
||||
file = listedFile
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (err: Throwable) {
|
||||
@ -155,7 +159,7 @@ object Registries {
|
||||
tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
|
||||
tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
|
||||
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(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
|
||||
tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
|
||||
@ -203,7 +207,7 @@ object Registries {
|
||||
val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) }
|
||||
|
||||
items.add {
|
||||
items.add(def.itemName, def, json, listedFile)
|
||||
items.add(key = def.itemName, value = def, json = json, file = listedFile)
|
||||
}
|
||||
} catch (err: Throwable) {
|
||||
LOGGER.error("Loading item definition file $listedFile", err)
|
||||
|
@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@ -99,7 +100,7 @@ class Registry<T : Any>(val name: 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>
|
||||
@ -119,7 +120,7 @@ class Registry<T : Any>(val name: 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>
|
||||
@ -180,7 +181,14 @@ class Registry<T : Any>(val name: String) {
|
||||
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)" }
|
||||
|
||||
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>"})")
|
||||
}
|
||||
|
||||
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
|
||||
id.ifPresent { id ->
|
||||
if (id != null && 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 || 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.json = json
|
||||
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
|
||||
|
||||
keyRefs[key]?.entry = entry
|
||||
|
||||
return 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>"})")
|
||||
id.ifPresent { id ->
|
||||
if (id != null) {
|
||||
idRefs[id]?.entry = entry
|
||||
idsInternal[id] = entry
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import java.io.DataOutputStream
|
||||
|
||||
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(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 (y in 0 until CHUNK_SIZE) {
|
||||
it.add(chunk.getCell(x, y).immutable())
|
||||
|
@ -1,12 +1,17 @@
|
||||
package ru.dbotthepony.kstarbound.client.world
|
||||
|
||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||
import ru.dbotthepony.kstarbound.world.Chunk
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.ChunkState
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
|
||||
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<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
|
||||
get() = ChunkState.FULL
|
||||
|
||||
|
@ -279,7 +279,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
|
||||
val tickets = ArrayList<ServerChunk.ITicket>()
|
||||
|
||||
return try {
|
||||
tickets.addAll(parent.permanentChunkTicket(region, targetChunkState))
|
||||
tickets.addAll(parent.permanentChunkTicket(region, targetChunkState).await())
|
||||
tickets.forEach { it.chunk.await() }
|
||||
block()
|
||||
} finally {
|
||||
@ -435,7 +435,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
|
||||
}.await()
|
||||
|
||||
for (box in boundingBoxes) {
|
||||
tickets.addAll(parent.permanentChunkTicket(box, targetChunkState))
|
||||
tickets.addAll(parent.permanentChunkTicket(box, targetChunkState).await())
|
||||
}
|
||||
|
||||
// apply tiles to world per-chunk
|
||||
|
@ -14,6 +14,7 @@ import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.lwjgl.opengl.GL45
|
||||
import org.lwjgl.stb.STBIEOFCallback
|
||||
@ -190,7 +191,7 @@ class Image private constructor(
|
||||
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)
|
||||
}
|
||||
|
||||
@ -204,7 +205,7 @@ class Image private constructor(
|
||||
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
|
||||
*/
|
||||
operator fun get(x: Int, y: Int): Int {
|
||||
@ -214,10 +215,10 @@ class Image private constructor(
|
||||
val data = data.join()
|
||||
|
||||
when (amountOfChannels) {
|
||||
4 -> return data[offset].toInt().and(0xFF) or
|
||||
data[offset + 1].toInt().and(0xFF).shl(8) or
|
||||
data[offset + 2].toInt().and(0xFF).shl(16) or
|
||||
data[offset + 3].toInt().and(0xFF).shl(24)
|
||||
4 -> return data[offset].toInt().and(0xFF) or // red
|
||||
data[offset + 1].toInt().and(0xFF).shl(8) or // green
|
||||
data[offset + 2].toInt().and(0xFF).shl(16) or // blue
|
||||
data[offset + 3].toInt().and(0xFF).shl(24) // alpha
|
||||
|
||||
3 -> return data[offset].toInt().and(0xFF) 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 (y !in 0 until height) return true
|
||||
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 {
|
||||
@ -285,7 +286,7 @@ class Image private constructor(
|
||||
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")
|
||||
|
||||
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 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
|
||||
// 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)
|
||||
continue
|
||||
|
||||
if (isTransparent(xpixel, ypixel, flip)) {
|
||||
if (!isTransparent(xpixel, ypixel, flip)) {
|
||||
fillRatio += 1.0 / (PIXELS_IN_STARBOUND_UNIT * PIXELS_IN_STARBOUND_UNIT)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
@ -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 new = ImmutableSet.Builder<Vector2i>()
|
||||
new.addAll(occupySpaces)
|
||||
// new.addAll(occupySpaces)
|
||||
new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages))
|
||||
occupySpaces = new.build()
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
|
||||
import ru.dbotthepony.kommons.math.RGBAColor
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
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
|
||||
|
||||
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,
|
||||
materialName = "metamaterial:$name",
|
||||
descriptionData = ThingDescription.EMPTY,
|
||||
@ -121,7 +122,7 @@ object BuiltinMetaMaterials {
|
||||
))
|
||||
), 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,
|
||||
modName = "metamod:$name",
|
||||
descriptionData = ThingDescription.EMPTY,
|
||||
@ -157,7 +158,7 @@ object BuiltinMetaMaterials {
|
||||
val BIOME_MOD = makeMod(65534, "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",
|
||||
liquidId = 0,
|
||||
color = RGBAColor.TRANSPARENT_BLACK,
|
||||
|
@ -309,6 +309,10 @@ class WorldTemplate(val geometry: WorldGeometry) {
|
||||
return cellCache.get(Vector2i(x, y))
|
||||
}
|
||||
|
||||
fun cellInfo(pos: Vector2i): CellInfo {
|
||||
return cellCache.get(pos)
|
||||
}
|
||||
|
||||
private fun cellInfo0(x: Int, y: Int): CellInfo {
|
||||
val info = CellInfo(x, y)
|
||||
val layout = worldLayout ?: return info
|
||||
|
@ -33,7 +33,7 @@ open class ItemStack {
|
||||
|
||||
constructor(descriptor: ItemDescriptor) {
|
||||
this.config = descriptor.ref
|
||||
this.count = descriptor.count
|
||||
this.size = descriptor.count
|
||||
this.parameters = descriptor.parameters.deepCopy()
|
||||
}
|
||||
|
||||
@ -51,36 +51,41 @@ open class ItemStack {
|
||||
changeset = CHANGESET.incrementAndGet()
|
||||
}
|
||||
|
||||
var count: Long = 0L
|
||||
var size: Long = 0L
|
||||
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 parameters: JsonObject
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = count <= 0 || config.isEmpty
|
||||
get() = size <= 0 || config.isEmpty
|
||||
|
||||
val isNotEmpty: Boolean
|
||||
get() = count > 0 && config.isPresent
|
||||
get() = size > 0 && config.isPresent
|
||||
|
||||
val maxStackSize: Long
|
||||
get() = config.value?.maxStack ?: 0L
|
||||
|
||||
fun grow(amount: Long) {
|
||||
count += amount
|
||||
size += amount
|
||||
}
|
||||
|
||||
fun shrink(amount: Long) {
|
||||
count -= amount
|
||||
size -= amount
|
||||
}
|
||||
|
||||
fun createDescriptor(): ItemDescriptor {
|
||||
if (isEmpty)
|
||||
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)
|
||||
@ -91,7 +96,7 @@ open class ItemStack {
|
||||
stream.writeJsonElement(JsonNull.INSTANCE)
|
||||
} else {
|
||||
stream.writeBinaryString(config.key.left())
|
||||
stream.writeVarLong(count)
|
||||
stream.writeVarLong(size)
|
||||
stream.writeJsonElement(parameters)
|
||||
}
|
||||
}
|
||||
@ -109,30 +114,20 @@ open class ItemStack {
|
||||
|
||||
fun mergeFrom(other: ItemStack, simulate: Boolean) {
|
||||
if (isStackable(other)) {
|
||||
val newCount = (count + other.count).coerceAtMost(maxStackSize)
|
||||
val diff = newCount - count
|
||||
other.count -= diff
|
||||
val newCount = (size + other.size).coerceAtMost(maxStackSize)
|
||||
val diff = newCount - size
|
||||
other.size -= diff
|
||||
|
||||
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 {
|
||||
if (isEmpty || other.isEmpty)
|
||||
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 {
|
||||
@ -142,7 +137,7 @@ open class ItemStack {
|
||||
if (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 {
|
||||
@ -153,14 +148,14 @@ open class ItemStack {
|
||||
if (isEmpty)
|
||||
return "ItemStack.EMPTY"
|
||||
|
||||
return "ItemDescriptor[${config.value?.itemName}, count = $count, params = $parameters]"
|
||||
return "ItemDescriptor[${config.value?.itemName}, count = $size, params = $parameters]"
|
||||
}
|
||||
|
||||
fun copy(): ItemStack {
|
||||
if (isEmpty)
|
||||
return this
|
||||
|
||||
return ItemStack(ItemDescriptor(config, count, parameters.deepCopy()))
|
||||
return ItemStack(ItemDescriptor(config, size, parameters.deepCopy()))
|
||||
}
|
||||
|
||||
fun toJson(): JsonObject? {
|
||||
@ -169,7 +164,7 @@ open class ItemStack {
|
||||
|
||||
return JsonObject().also {
|
||||
it.add("name", JsonPrimitive(config.key.left()))
|
||||
it.add("count", JsonPrimitive(count))
|
||||
it.add("count", JsonPrimitive(size))
|
||||
it.add("parameters", parameters.deepCopy())
|
||||
}
|
||||
}
|
||||
@ -181,7 +176,7 @@ open class ItemStack {
|
||||
|
||||
return allocator.newTable(0, 3).also {
|
||||
it.rawset("name", config.key.left())
|
||||
it.rawset("count", count)
|
||||
it.rawset("count", size)
|
||||
it.rawset("parameters", allocator.from(parameters))
|
||||
}
|
||||
}
|
||||
|
@ -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.EnvironmentUpdatePacket
|
||||
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.LegacyTileUpdatePacket
|
||||
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.FlyShipPacket
|
||||
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.WorldStartAcknowledgePacket
|
||||
import java.io.BufferedInputStream
|
||||
@ -442,7 +444,7 @@ class PacketRegistry(val isLegacy: Boolean) {
|
||||
LEGACY.skip("TileLiquidUpdate")
|
||||
LEGACY.add(::TileDamageUpdatePacket)
|
||||
LEGACY.skip("TileModificationFailure")
|
||||
LEGACY.skip("GiveItem")
|
||||
LEGACY.add(::GiveItemPacket)
|
||||
LEGACY.add(::EnvironmentUpdatePacket)
|
||||
LEGACY.skip("UpdateTileProtection")
|
||||
LEGACY.skip("SetDungeonGravity")
|
||||
@ -455,7 +457,7 @@ class PacketRegistry(val isLegacy: Boolean) {
|
||||
LEGACY.skip("ModifyTileList")
|
||||
LEGACY.add(::DamageTileGroupPacket)
|
||||
LEGACY.skip("CollectLiquid")
|
||||
LEGACY.skip("RequestDrop")
|
||||
LEGACY.add(::RequestDropPacket)
|
||||
LEGACY.skip("SpawnEntity")
|
||||
LEGACY.skip("ConnectWire")
|
||||
LEGACY.skip("DisconnectAllWires")
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import com.google.gson.JsonObject
|
||||
import com.google.gson.TypeAdapter
|
||||
import ru.dbotthepony.kommons.io.BinaryStringCodec
|
||||
import ru.dbotthepony.kommons.io.BooleanValueCodec
|
||||
import ru.dbotthepony.kommons.io.IntValueCodec
|
||||
import ru.dbotthepony.kommons.io.RGBACodec
|
||||
import ru.dbotthepony.kommons.io.StreamCodec
|
||||
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.Vector2fCodec
|
||||
import ru.dbotthepony.kommons.io.koptional
|
||||
import ru.dbotthepony.kommons.io.map
|
||||
import ru.dbotthepony.kommons.io.readByteArray
|
||||
import ru.dbotthepony.kommons.io.readVarInt
|
||||
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
|
||||
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
|
||||
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
|
||||
|
@ -12,6 +12,7 @@ import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.DoubleSupplier
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
// 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 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,10 +20,10 @@ class AvatarBag(val avatar: Avatar, val config: InventoryConfig.Bag, val filter:
|
||||
fun mergeFrom(value: ItemStack, simulate: Boolean) {
|
||||
if (item == null) {
|
||||
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 {
|
||||
item!!.mergeFrom(value, simulate)
|
||||
}
|
||||
|
@ -495,6 +495,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
it.eventLoop.shutdown()
|
||||
} else {
|
||||
shipWorld = it
|
||||
shipWorld.sky.referenceClock = server.universeClock
|
||||
// shipWorld.sky.startFlying(true, true)
|
||||
shipWorld.eventLoop.start()
|
||||
enqueueWarp(WarpAlias.OwnShip)
|
||||
|
@ -4,13 +4,11 @@ import com.google.gson.JsonPrimitive
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.future.asCompletableFuture
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.vector.Vector3i
|
||||
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.WorldStorage
|
||||
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
|
||||
import ru.dbotthepony.kstarbound.util.Clock
|
||||
import ru.dbotthepony.kstarbound.util.ExceptionLogger
|
||||
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
|
||||
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
|
||||
import ru.dbotthepony.kstarbound.util.JVMClock
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
import ru.dbotthepony.kstarbound.world.UniversePos
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Supplier
|
||||
|
||||
sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
|
||||
init {
|
||||
@ -54,6 +47,17 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
||||
val chat = ChatHandler(this)
|
||||
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 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)
|
||||
|
||||
try {
|
||||
world.sky.referenceClock = universeClock
|
||||
world.eventLoop.start()
|
||||
world.prepare().await()
|
||||
} catch (err: Throwable) {
|
||||
@ -114,6 +119,9 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
||||
try {
|
||||
world.setProperty("ephemeral", JsonPrimitive(!config.persistent))
|
||||
|
||||
if (config.useUniverseClock)
|
||||
world.sky.referenceClock = universeClock
|
||||
|
||||
world.eventLoop.start()
|
||||
world.prepare().await()
|
||||
} catch (err: Throwable) {
|
||||
@ -177,17 +185,6 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
||||
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 {
|
||||
scheduleAtFixedRate(Runnable {
|
||||
channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds))
|
||||
@ -266,6 +263,8 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
||||
|
||||
private fun tickNormal() {
|
||||
try {
|
||||
// universeClock.nanos += Starbound.TIMESTEP_NANOS
|
||||
|
||||
channels.connections.forEach {
|
||||
try {
|
||||
it.tick()
|
||||
|
@ -15,6 +15,7 @@ import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
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.FIRST_RESERVED_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.TileColor
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@ -60,7 +62,13 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.resume
|
||||
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
|
||||
private set
|
||||
|
||||
@ -105,7 +113,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
// from not sufficiently generated neighbours
|
||||
|
||||
for (neighbour in pos.neighbours()) {
|
||||
val ticket = world.permanentChunkTicket(neighbour, state) ?: continue
|
||||
val ticket = world.permanentChunkTicket(neighbour, state).await() ?: continue
|
||||
neighbours.add(ticket)
|
||||
}
|
||||
|
||||
@ -147,10 +155,18 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
ChunkState.TERRAIN -> {
|
||||
if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) {
|
||||
// skip since no cells will be generated anyway
|
||||
cells.value.fill(AbstractCell.EMPTY)
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
cells[x, y].setStateQuiet(AbstractCell.EMPTY)
|
||||
}
|
||||
}
|
||||
|
||||
signalChunkContentsUpdated()
|
||||
} else {
|
||||
// tiles can be generated concurrently without any consequences
|
||||
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> {
|
||||
if (cells.isInitialized()) {
|
||||
return Object2DArray(cells.value)
|
||||
} else {
|
||||
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)
|
||||
}
|
||||
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { x, y -> cells[x, y].state }
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!cells.isInitialized()) {
|
||||
return DamageResult(TileDamageResult.NONE)
|
||||
}
|
||||
|
||||
val cell = cells.value[pos.x, pos.y]
|
||||
val cellState = cells[pos.x, pos.y]
|
||||
val cell = cellState.state
|
||||
|
||||
if (cell.isIndestructible || cell.tile(isBackground).material.value.isMeta) {
|
||||
return DamageResult(TileDamageResult.NONE)
|
||||
@ -414,7 +423,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
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 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)
|
||||
|
||||
if (health.isDead) {
|
||||
if (isBackground) {
|
||||
damagedTilesBackground.remove(pos)
|
||||
} else {
|
||||
damagedTilesForeground.remove(pos)
|
||||
}
|
||||
damagedCells.remove(pos)
|
||||
|
||||
val drops = ArrayList<ItemDescriptor>()
|
||||
|
||||
val copyHealth = health.copy()
|
||||
val mCell = cell.mutable()
|
||||
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.color = TileColor.DEFAULT
|
||||
mTile.hueShift = 0f
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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())
|
||||
health.reset()
|
||||
return DamageResult(result, copyHealth, cell)
|
||||
} else {
|
||||
if (isBackground) {
|
||||
damagedTilesBackground.add(pos)
|
||||
} else {
|
||||
damagedTilesForeground.add(pos)
|
||||
}
|
||||
|
||||
damagedCells.add(pos)
|
||||
return DamageResult(result, health, cell)
|
||||
}
|
||||
}
|
||||
|
||||
private val damagedTilesForeground = ObjectArraySet<Vector2i>()
|
||||
private val damagedTilesBackground = ObjectArraySet<Vector2i>()
|
||||
private val damagedCells = ObjectArraySet<Vector2i>()
|
||||
|
||||
fun tileDamagePackets(): List<TileDamageUpdatePacket> {
|
||||
val result = ArrayList<TileDamageUpdatePacket>()
|
||||
|
||||
if (tileHealthBackground.isInitialized()) {
|
||||
val tileHealthBackground = tileHealthBackground.value
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
val health = cells[x, y].backgroundHealth
|
||||
|
||||
for (x in 0 until CHUNK_SIZE) {
|
||||
for (y in 0 until CHUNK_SIZE) {
|
||||
val health = tileHealthBackground[x, y]
|
||||
|
||||
if (!health.isHealthy) {
|
||||
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, true, health))
|
||||
}
|
||||
if (!health.isHealthy) {
|
||||
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, true, health))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tileHealthForeground.isInitialized()) {
|
||||
val tileHealthForeground = tileHealthForeground.value
|
||||
val health2 = cells[x, y].foregroundHealth
|
||||
|
||||
for (x in 0 until CHUNK_SIZE) {
|
||||
for (y in 0 until CHUNK_SIZE) {
|
||||
val health = tileHealthForeground[x, y]
|
||||
|
||||
if (!health.isHealthy) {
|
||||
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, false, health))
|
||||
}
|
||||
if (!health2.isHealthy) {
|
||||
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, false, health2))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -529,24 +540,23 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
|
||||
super.tick()
|
||||
|
||||
if (cells.isInitialized() && (damagedTilesBackground.isNotEmpty() || damagedTilesForeground.isNotEmpty())) {
|
||||
val tileHealthBackground = tileHealthBackground.value
|
||||
val tileHealthForeground = tileHealthForeground.value
|
||||
val cells = cells.value
|
||||
damagedCells.removeIf { (x, y) ->
|
||||
val health = cells[x, y].foregroundHealth
|
||||
val health2 = cells[x, y].backgroundHealth
|
||||
|
||||
damagedTilesBackground.removeIf { (x, y) ->
|
||||
val health = tileHealthBackground[x, y]
|
||||
val result = !health.tick(cells[x, y].background.material.value.actualDamageTable)
|
||||
onTileHealthUpdate(x, y, true, health)
|
||||
result
|
||||
}
|
||||
var any = false
|
||||
|
||||
damagedTilesForeground.removeIf { (x, y) ->
|
||||
val health = tileHealthForeground[x, y]
|
||||
val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable)
|
||||
if (health.isTicking) {
|
||||
any = health.tick(cells[x, y].state.foreground.material.value.actualDamageTable) || any
|
||||
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> {
|
||||
if (cells.isInitialized()) {
|
||||
val cells = cells.value
|
||||
return Object2DArray(width, height) { a, b -> cells[a, b].toLegacyNet() }
|
||||
} else {
|
||||
return Object2DArray(width, height, LegacyNetworkCellState.NULL)
|
||||
}
|
||||
return Object2DArray(width, height) { a, b -> cells[a, b].state.toLegacyNet() }
|
||||
}
|
||||
|
||||
private fun prepareCells() {
|
||||
val cells = cells.value
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
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.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() {
|
||||
val cells = cells.value
|
||||
|
||||
for (x in 0 until width) {
|
||||
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) }
|
||||
|
||||
if (cell.liquid.isInfinite) {
|
||||
@ -675,7 +676,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
}
|
||||
|
||||
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() {
|
||||
val cells = cells.value
|
||||
|
||||
for (x in 0 until width) {
|
||||
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))
|
||||
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() {
|
||||
val cells = cells.value
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
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
|
||||
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.foreground.modifierHueShift = biome.hueShift(modify.foreground.modifier)
|
||||
|
||||
cells[x, y] = modify.immutable()
|
||||
cells[x, y].state = modify.immutable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,7 +348,7 @@ class ServerWorld private constructor(
|
||||
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)
|
||||
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),
|
||||
)
|
||||
|
||||
val region = permanentChunkTicket(spawnRect)
|
||||
val region = permanentChunkTicket(spawnRect).await()
|
||||
tickets.addAll(region)
|
||||
region.forEach { it.chunk.await() }
|
||||
|
||||
@ -417,32 +417,32 @@ class ServerWorld private constructor(
|
||||
return ServerChunk(this, pos)
|
||||
}
|
||||
|
||||
fun permanentChunkTicket(pos: ChunkPos, target: ChunkState = ChunkState.FULL): ServerChunk.ITicket? {
|
||||
return chunkMap.compute(pos)?.permanentTicket(target)
|
||||
fun permanentChunkTicket(pos: ChunkPos, target: ChunkState = ChunkState.FULL): CompletableFuture<ServerChunk.ITicket?> {
|
||||
return eventLoop.supplyAsync { chunkMap.compute(pos)?.permanentTicket(target) }
|
||||
}
|
||||
|
||||
fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITicket> {
|
||||
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
|
||||
fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): CompletableFuture<List<ServerChunk.ITicket>> {
|
||||
return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { permanentChunkTicket(it, target).get() } }
|
||||
}
|
||||
|
||||
fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITicket> {
|
||||
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
|
||||
fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): CompletableFuture<List<ServerChunk.ITicket>> {
|
||||
return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { permanentChunkTicket(it, target).get() } }
|
||||
}
|
||||
|
||||
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ChunkState = ChunkState.FULL): ServerChunk.ITimedTicket? {
|
||||
return chunkMap.compute(pos)?.temporaryTicket(time, target)
|
||||
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ChunkState = ChunkState.FULL): CompletableFuture<ServerChunk.ITimedTicket?> {
|
||||
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" }
|
||||
|
||||
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" }
|
||||
|
||||
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
|
||||
return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { temporaryChunkTicket(it, time, target).get() } }
|
||||
}
|
||||
|
||||
@JsonFactory
|
||||
|
@ -2,11 +2,14 @@ package ru.dbotthepony.kstarbound.server.world
|
||||
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrayList
|
||||
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.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
|
||||
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
|
||||
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
|
||||
import kotlinx.coroutines.future.await
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
@ -180,7 +183,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
|
||||
|
||||
for (pos in newTrackedChunks) {
|
||||
if (pos !in tickets) {
|
||||
val ticket = world.permanentChunkTicket(pos) ?: continue
|
||||
val ticket = world.permanentChunkTicket(pos).get() ?: continue
|
||||
val thisTicket = Ticket(ticket, pos)
|
||||
|
||||
tickets[pos] = thisTicket
|
||||
@ -208,6 +211,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
|
||||
}
|
||||
|
||||
val unseen = IntArrayList(entityVersions.keys)
|
||||
val changePackets = Int2ObjectOpenHashMap<Int2ObjectOpenHashMap<ByteArrayList>>()
|
||||
|
||||
for (entity in trackingEntities) {
|
||||
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))) {
|
||||
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
166
src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt
Normal file
166
src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package ru.dbotthepony.kstarbound.util
|
||||
|
||||
import ru.dbotthepony.kommons.util.JVMTimeSource
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
@ -56,7 +55,7 @@ class MailboxExecutorService(@Volatile var thread: Thread = Thread.currentThread
|
||||
@Volatile
|
||||
private var isTerminated = false
|
||||
|
||||
private val timeOrigin = JVMTimeSource()
|
||||
private val timeOrigin = JVMClock()
|
||||
|
||||
var exceptionHandler: Consumer<Throwable>? = null
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ru.dbotthepony.kstarbound.world
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||
import ru.dbotthepony.kommons.util.AABB
|
||||
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.OffsetCellAccess
|
||||
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 kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Чанк мира
|
||||
@ -24,7 +31,7 @@ import java.util.concurrent.CopyOnWriteArraySet
|
||||
*
|
||||
* Весь игровой мир будет измеряться в 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 pos: ChunkPos,
|
||||
) : ICellAccess {
|
||||
@ -61,58 +68,153 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
|
||||
val aabbd = aabb.toDoubleAABB()
|
||||
|
||||
// TODO: maybe fit them into "width" and "height" variables added recently?
|
||||
protected val cells = lazy {
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL)
|
||||
protected abstract val cells: Object2DArray<CellType>
|
||||
|
||||
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 {
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
|
||||
private val collisionsLock = Any()
|
||||
|
||||
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 {
|
||||
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
|
||||
abstract inner class ChunkCell(val x: Int, val y: Int) {
|
||||
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>) {
|
||||
val ours = cells.value
|
||||
val ours = cells
|
||||
source.checkSizeEquals(ours)
|
||||
|
||||
for (x 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 {
|
||||
if (!cells.isInitialized())
|
||||
return AbstractCell.NULL
|
||||
|
||||
return cells.value[x, y]
|
||||
return cells[x, y].state
|
||||
}
|
||||
|
||||
final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
|
||||
val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
|
||||
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)
|
||||
}
|
||||
|
||||
cells[x, y].state = cell.immutable()
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -16,12 +16,8 @@ private fun circulate(value: Int, bounds: Int): Int {
|
||||
}
|
||||
|
||||
/**
|
||||
* Сетка чанков идёт как и сетка тайлов.
|
||||
*
|
||||
* * Вправо у нас положительный X
|
||||
* * Влево у нас отрицательный X
|
||||
* * Вверх у нас положительный Y
|
||||
* * Вниз у нас отрицательный Y
|
||||
* Coordinate, representing direct positions of chunks in [World.ChunkMap], with some
|
||||
* helper methods and properties
|
||||
*/
|
||||
data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
|
||||
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
|
||||
|
@ -5,7 +5,6 @@ import ru.dbotthepony.kommons.io.map
|
||||
import ru.dbotthepony.kommons.math.linearInterpolation
|
||||
import ru.dbotthepony.kommons.util.getValue
|
||||
import ru.dbotthepony.kommons.util.setValue
|
||||
import ru.dbotthepony.kommons.util.value
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Globals
|
||||
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.networkedJson
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedVec2f
|
||||
import ru.dbotthepony.kstarbound.util.IClock
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
@ -71,6 +71,12 @@ class Sky() {
|
||||
var destination: SkyParameters? = null
|
||||
private set
|
||||
|
||||
var referenceClock: IClock? = null
|
||||
set(value) {
|
||||
field = value
|
||||
time = value?.seconds ?: time
|
||||
}
|
||||
|
||||
val speedupTime: Double get() {
|
||||
if (enterHyperspace) {
|
||||
return Globals.sky.hyperspaceSpeedupTime.coerceAtLeast(0.01)
|
||||
@ -187,7 +193,7 @@ class Sky() {
|
||||
|
||||
|
||||
fun tick(delta: Double = Starbound.TIMESTEP) {
|
||||
time += delta
|
||||
time = referenceClock?.seconds ?: (time + delta)
|
||||
flashTimer = (flashTimer - delta).coerceAtLeast(0.0)
|
||||
|
||||
if (flyingType != FlyingType.NONE) {
|
||||
|
@ -21,7 +21,7 @@ import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
|
||||
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
|
||||
import ru.dbotthepony.kstarbound.network.syncher.networkedData
|
||||
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.nextRange
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
@ -36,7 +36,7 @@ import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
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()
|
||||
abstract val entities: Map<UUID, Entity>
|
||||
abstract val ships: Map<UUID, Ship>
|
||||
|
@ -95,6 +95,9 @@ sealed class TileHealth() {
|
||||
damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent
|
||||
}
|
||||
|
||||
val isTicking: Boolean
|
||||
get() = !isHealthy && !isDead
|
||||
|
||||
fun tick(config: TileDamageConfig, delta: Double = Starbound.TIMESTEP): Boolean {
|
||||
if (isDead || isHealthy)
|
||||
return false
|
||||
|
@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.world.physics.CollisionType
|
||||
import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
|
||||
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Predicate
|
||||
@ -45,7 +46,7 @@ import java.util.random.RandomGenerator
|
||||
import java.util.stream.Stream
|
||||
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 foreground = TileView.Foreground(this)
|
||||
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 remove(x: Int, y: Int)
|
||||
|
||||
private val chunkCache = arrayOfNulls<Chunk<*, *>>(4)
|
||||
|
||||
operator fun get(pos: ChunkPos) = get(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 entities = Int2ObjectOpenHashMap<AbstractEntity>()
|
||||
val entityList = CopyOnWriteArrayList<AbstractEntity>()
|
||||
val entityIndex = SpatialIndex<AbstractEntity>(geometry)
|
||||
val dynamicEntities = ArrayList<DynamicEntity>()
|
||||
|
||||
@ -278,7 +278,19 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
}
|
||||
})).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())
|
||||
chunk.tick()
|
||||
@ -327,13 +339,16 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
}
|
||||
|
||||
fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> {
|
||||
val result = ArrayList<CollisionPoly>()
|
||||
val result = ObjectArrayList<CollisionPoly>() // no CME checks
|
||||
val tiles = aabb.encasingIntAABB()
|
||||
|
||||
for (x in tiles.mins.x .. tiles.maxs.x) {
|
||||
for (y in tiles.mins.y .. tiles.maxs.y) {
|
||||
getBlocksMarchingSquares(x, y, foreground, CollisionType.DYNAMIC, result)
|
||||
getBlockPlatforms(x, y, foreground, CollisionType.PLATFORM, result)
|
||||
val cx = geometry.x.cell(x)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()))
|
||||
}
|
||||
|
||||
fun chunkFromCell(x: Int, y: Int): ChunkPos {
|
||||
return ChunkPos(this.x.chunkFromCell(x), this.y.chunkFromCell(y))
|
||||
}
|
||||
|
||||
fun chunkFromCell(pos: IStruct2f): ChunkPos {
|
||||
return ChunkPos(x.chunkFromCell(pos.component1()), y.chunkFromCell(pos.component2()))
|
||||
}
|
||||
|
@ -34,6 +34,13 @@ data class MutableLiquidState(
|
||||
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 {
|
||||
return this
|
||||
}
|
||||
|
@ -130,6 +130,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
|
||||
|
||||
innerWorld = world
|
||||
world.entities[entityID] = this
|
||||
world.entityList.add(this)
|
||||
spatialEntry = world.entityIndex.Entry(this)
|
||||
onJoinWorld(world)
|
||||
}
|
||||
@ -140,6 +141,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
|
||||
|
||||
mailbox.shutdownNow()
|
||||
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
|
||||
world.entityList.remove(this)
|
||||
|
||||
try {
|
||||
onRemove(world, reason)
|
||||
|
@ -330,11 +330,8 @@ class ActorMovementController() : MovementController() {
|
||||
if (isOnGround) {
|
||||
groundMovementSustainTimer = GameTimer(maxGroundSustain)
|
||||
} else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) {
|
||||
val collideAny = localHitboxes
|
||||
.map { it + Vector2d(0.0, -groundCheckDistance) }
|
||||
.anyMatch {
|
||||
world.polyIntersects(it, { it.type >= CollisionType.PLATFORM })
|
||||
}
|
||||
val collideAny = computeLocalHitboxes()
|
||||
.any { world.polyIntersects(it + Vector2d(0.0, -groundCheckDistance), { it.type >= CollisionType.PLATFORM }) }
|
||||
|
||||
if (collideAny)
|
||||
groundMovementSustainTimer = GameTimer(0.0)
|
||||
|
@ -50,7 +50,7 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
|
||||
|
||||
override fun render(client: StarboundClient, layers: LayeredRenderer) {
|
||||
layers.add(RenderLayer.Overlay.point()) {
|
||||
val hitboxes = movement.localHitboxes.toList()
|
||||
val hitboxes = movement.computeLocalHitboxes()
|
||||
if (hitboxes.isEmpty()) return@add
|
||||
|
||||
hitboxes.forEach { it.render(client) }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package ru.dbotthepony.kstarbound.world.entities
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||
import ru.dbotthepony.kommons.io.DoubleValueCodec
|
||||
import ru.dbotthepony.kommons.io.FloatValueCodec
|
||||
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.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.times
|
||||
import ru.dbotthepony.kstarbound.Globals
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.MovementParameters
|
||||
import ru.dbotthepony.kstarbound.math.Interpolator
|
||||
@ -53,8 +55,21 @@ open class MovementController() {
|
||||
world0 = null
|
||||
}
|
||||
|
||||
val localHitboxes: Stream<Poly>
|
||||
get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } }
|
||||
fun computeLocalHitboxes(): List<Poly> {
|
||||
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 {
|
||||
return type !== CollisionType.NONE
|
||||
@ -102,7 +117,7 @@ open class MovementController() {
|
||||
fun updateFixtures() {
|
||||
val spatialEntry = spatialEntry ?: return
|
||||
fixturesChangeset++
|
||||
val localHitboxes = localHitboxes.toList()
|
||||
val localHitboxes = computeLocalHitboxes()
|
||||
|
||||
while (fixtures.size > localHitboxes.size) {
|
||||
fixtures.last().remove()
|
||||
@ -145,7 +160,8 @@ open class MovementController() {
|
||||
var appliedForceRegion: Boolean = false
|
||||
protected set
|
||||
|
||||
var movementParameters: MovementParameters = MovementParameters.EMPTY
|
||||
var movementParameters: MovementParameters = Globals.movementParameters
|
||||
protected set
|
||||
|
||||
var gravityMultiplier = 1.0
|
||||
var isGravityDisabled = false
|
||||
@ -267,12 +283,22 @@ open class MovementController() {
|
||||
val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) +
|
||||
(movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude
|
||||
|
||||
val localHitboxes = localHitboxes.toList()
|
||||
val aabb = localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get()
|
||||
val localHitboxes = computeLocalHitboxes()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@ -405,6 +431,8 @@ open class MovementController() {
|
||||
}
|
||||
}
|
||||
|
||||
protected data class BodyPair(val body: CollisionPoly, val distance: Double)
|
||||
|
||||
protected fun collisionSweep(
|
||||
body: Poly, staticBodies: List<CollisionPoly>,
|
||||
movement: Vector2d, ignorePlatforms: Boolean,
|
||||
@ -419,11 +447,13 @@ open class MovementController() {
|
||||
var totalCorrection = Vector2d.ZERO
|
||||
var movingCollisionId: Int? = null
|
||||
|
||||
val sorted = staticBodies.stream()
|
||||
.map { it to (it.poly.aabb.centre - sortCenter).lengthSquared }
|
||||
.sorted { o1, o2 -> o1.second.compareTo(o2.second) }
|
||||
.map { it.first }
|
||||
.toList()
|
||||
val sorted = ObjectArrayList<BodyPair>(staticBodies.size)
|
||||
|
||||
for (sbody in staticBodies) {
|
||||
sorted.add(BodyPair(sbody, (sbody.poly.aabb.centre - sortCenter).lengthSquared))
|
||||
}
|
||||
|
||||
sorted.sortWith { o1, o2 -> o1.distance.compareTo(o2.distance) }
|
||||
|
||||
if (slopeCorrection) {
|
||||
// Starbound: First try separating with our ground sliding cheat.
|
||||
@ -526,7 +556,7 @@ open class MovementController() {
|
||||
}
|
||||
|
||||
protected fun collisionSeparate(
|
||||
poly: Poly, staticBodies: List<CollisionPoly>,
|
||||
poly: Poly, staticBodies: List<BodyPair>,
|
||||
ignorePlatforms: Boolean, maximumPlatformCorrection: Double,
|
||||
upward: Boolean, separationTolerance: Double
|
||||
): CollisionSeparation {
|
||||
@ -534,7 +564,7 @@ open class MovementController() {
|
||||
var intersects = false
|
||||
var correctedPoly = poly
|
||||
|
||||
for (body in staticBodies) {
|
||||
for ((body) in staticBodies) {
|
||||
if (ignorePlatforms && body.type === CollisionType.PLATFORM)
|
||||
continue
|
||||
|
||||
@ -559,7 +589,7 @@ open class MovementController() {
|
||||
separation.solutionFound = true
|
||||
|
||||
if (intersects) {
|
||||
for (body in staticBodies) {
|
||||
for ((body) in staticBodies) {
|
||||
if (body.type === CollisionType.PLATFORM)
|
||||
continue
|
||||
|
||||
@ -576,6 +606,15 @@ open class MovementController() {
|
||||
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 {
|
||||
const val SEPARATION_STEPS = 3
|
||||
const val SEPARATION_TOLERANCE = 0.001
|
||||
|
@ -125,7 +125,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
|
||||
|
||||
try {
|
||||
currentMaterialSpaces.forEach { (p) ->
|
||||
tickets.add(world.permanentChunkTicket(ChunkPos(world.geometry.x.chunkFromCell(p.x), world.geometry.x.chunkFromCell(p.y)), ChunkState.EMPTY) ?: return@forEach)
|
||||
tickets.add(world.permanentChunkTicket(world.geometry.chunkFromCell(p.x, p.y), ChunkState.EMPTY).await() ?: return@forEach)
|
||||
}
|
||||
|
||||
tickets.forEach { it.chunk.await() }
|
||||
|
@ -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 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 {
|
||||
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 {
|
||||
|
@ -3,8 +3,8 @@ package ru.dbotthepony.kstarbound.world.physics
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
|
||||
data class CollisionPoly(
|
||||
val poly: Poly,
|
||||
val type: CollisionType,
|
||||
val poly: Poly = Poly.EMPTY,
|
||||
val type: CollisionType = CollisionType.NULL,
|
||||
val bounceFactor: Double = 0.0,
|
||||
val velocity: Vector2d = Vector2d.ZERO
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user