diff --git a/gradle.properties b/gradle.properties index d7d159d6..6042f32b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.13.1 +kommonsVersion=2.14.0 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index a75f1189..791bce53 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -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() private set - var actorMovementParameters = ActorMovementParameters() + var actorMovementParameters by Delegates.notNull() private set - var movementParameters = MovementParameters() + var movementParameters by Delegates.notNull() private set var client by Delegates.notNull() @@ -77,6 +78,9 @@ object Globals { var worldServer by Delegates.notNull() private set + var itemDrop by Delegates.notNull() + private set + var currencies by Delegates.notNull>() 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)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index bc6e77d0..65472d9d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -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("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val dungeons = Registry("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) } - private fun key(mapper: (T) -> String): (T) -> Pair { - return { mapper.invoke(it) to null } + private fun key(mapper: (T) -> String): (T) -> Pair> { + return { mapper.invoke(it) to KOptional() } } - private fun key(mapper: (T) -> String, mapperInt: (T) -> Int): (T) -> Pair { - return { mapper.invoke(it) to mapperInt.invoke(it) } + private fun key(mapper: (T) -> String, mapperInt: (T) -> Int?): (T) -> Pair> { + return { mapper.invoke(it) to KOptional(mapperInt.invoke(it)) } } fun validate(): CompletableFuture { @@ -106,7 +107,7 @@ object Registries { private inline fun loadRegistry( registry: Registry, files: List, - noinline keyProvider: (T) -> Pair, + noinline keyProvider: (T) -> Pair>, noinline after: (T, IStarboundFile) -> Unit = { _, _ -> } ): List> { 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index f5d5ea38..dfbe8bf1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -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(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 @@ -119,7 +120,7 @@ class Registry(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 @@ -180,7 +181,14 @@ class Registry(val name: String) { return valid } - fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry { + fun add( + key: String, + value: T, + json: JsonElement = JsonNull.INSTANCE, + file: IStarboundFile? = null, + id: KOptional = KOptional(), + isBuiltin: Boolean = false + ): Entry { require(key != "") { "Adding $name with empty name (empty name is reserved)" } lock.withLock { @@ -188,120 +196,38 @@ class Registry(val name: String) { LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } - 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 ?: ""})") + } + } + + 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 { - 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 ?: ""})") - } - - if (id in idsInternal) { - LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: ""})") - } - - 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 { - 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 ; old def originate from ${keysInternal[key]?.file ?: ""})") - } - - 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 { - 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 ; old def originate from ${keysInternal[key]?.file ?: ""})") + 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 ; old def originate from ${idsInternal[id]?.file ?: ""})") - } - - 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 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt index 43f5c84b..0b473997 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ChunkCellsPacket.kt @@ -16,7 +16,7 @@ import java.io.DataOutputStream class ChunkCellsPacket(val pos: ChunkPos, val data: List) : IClientPacket { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() }) - constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList(CHUNK_SIZE * CHUNK_SIZE).also { + constructor(chunk: Chunk<*, *, *>) : this(chunk.pos, ArrayList(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()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt index a1f0f74e..7a42f3de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientChunk.kt @@ -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(world, pos) { +class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos) { + inner class ChunkCell(x: Int, y: Int) : Chunk.ChunkCell(x, y) + + override val cells: Object2DArray = Object2DArray(width, height, ::ChunkCell) + override val state: ChunkState get() = ChunkState.FULL diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index d806b8fd..e426260f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -279,7 +279,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar val tickets = ArrayList() 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index d9e3c3c3..4e2bbe53 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -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 { + fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set { 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 { + fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set { 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() + val result = ObjectArraySet() // 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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDropConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDropConfig.kt new file mode 100644 index 00000000..7dda8569 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDropConfig.kt @@ -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)) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index 3f62646e..0a343318 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -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() - new.addAll(occupySpaces) + // new.addAll(occupySpaces) new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages)) occupySpaces = new.build() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index c0173f89..378d3721 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index bd0c7ae8..078ee84d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 3e56bd00..abcc4c5b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -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 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)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index ab79a977..2fe88d93 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -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") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/GiveItemPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/GiveItemPacket.kt new file mode 100644 index 00000000..62e8b4ed --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/GiveItemPacket.kt @@ -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") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/RequestDropPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/RequestDropPacket.kt new file mode 100644 index 00000000..7ad85179 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/RequestDropPacket.kt @@ -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())) + } + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt index 6bf1c5c8..02c9165c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt @@ -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 > networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java)) +fun > networkedEnum(values: List, value: E = values.first()) = BasicNetworkedElement(value, VarIntValueCodec.map({ values[this] }, { ordinal })) + +// networks enum as a int32_t on legacy protocol +fun > networkedEnumSortOfStupid(value: E): BasicNetworkedElement { + 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 > networkedEnumSortOfStupid(values: List, value: E = values.first()): BasicNetworkedElement { + 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 > networkedEnumStupid(value: E): BasicNetworkedElement { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt index 06d80f34..cc010e10 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt @@ -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 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt index 5d52e46b..ad5797de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt @@ -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) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index e3d07081..76ad74ab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 0b8fe2ac..bafb23a0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -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>() 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 9e4afb7f..014b6d4c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -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(world, pos) { +class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(world, pos) { + inner class ChunkCell(x: Int, y: Int) : Chunk.ChunkCell(x, y) { + + } + + override val cells: Object2DArray = Object2DArray(width, height, ::ChunkCell) + override var state: ChunkState = ChunkState.FRESH private set @@ -105,7 +113,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { 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 { - 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() 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() - private val damagedTilesBackground = ObjectArraySet() + private val damagedCells = ObjectArraySet() fun tileDamagePackets(): List { val result = ArrayList() - 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 + 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 { - 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 { + return eventLoop.supplyAsync { chunkMap.compute(pos)?.permanentTicket(target) } } - fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): List { - return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() + fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): CompletableFuture> { + return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { permanentChunkTicket(it, target).get() } } } - fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): List { - return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() + fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): CompletableFuture> { + 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 { + return eventLoop.supplyAsync { chunkMap.compute(pos)?.temporaryTicket(time, target) } } - fun temporaryChunkTicket(region: AABBi, time: Int, target: ChunkState = ChunkState.FULL): List { + fun temporaryChunkTicket(region: AABBi, time: Int, target: ChunkState = ChunkState.FULL): CompletableFuture> { 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 { + fun temporaryChunkTicket(region: AABB, time: Int, target: ChunkState = ChunkState.FULL): CompletableFuture> { 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index e8d1b25e..5d44b253 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -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>() 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)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt deleted file mode 100644 index 8e6df071..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clock.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt new file mode 100644 index 00000000..e11f2611 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Clocks.kt @@ -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 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt deleted file mode 100644 index d193baea..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt +++ /dev/null @@ -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 - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt index 251245b1..3a444f91 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt @@ -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? = null diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index f3d7fef0..d2fe6eb2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -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, This : Chunk>( +abstract class Chunk, This : Chunk, CellType : Chunk.ChunkCell>( val world: WorldType, val pos: ChunkPos, ) : ICellAccess { @@ -61,58 +68,153 @@ abstract class Chunk, This : Chunk + + private var hasDirtyCollisions = false + + // bulk mark collision dirty of neighbour chunks + protected fun signalChunkContentsUpdated() { + val signalPositions = ArrayList() + + 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) { + 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(2) // no CME checks } fun loadCells(source: Object2DArray) { - 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 } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt index aa9567c8..d8518681 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkPos.kt @@ -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 { constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt index ce0964cc..0848e1fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt index 4cf942d7..4d8c89af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/SystemWorld.kt @@ -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 abstract val ships: Map diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt index 41fb8b70..0d2bf721 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 2dd421ec..29ac42ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -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, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess { +abstract class World, ChunkType : Chunk>(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, ChunkType : Chunk abstract fun remove(x: Int, y: Int) - private val chunkCache = arrayOfNulls>(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, ChunkType : Chunk() + val entityList = CopyOnWriteArrayList() val entityIndex = SpatialIndex(geometry) val dynamicEntities = ArrayList() @@ -278,7 +278,19 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk { - val result = ArrayList() + val result = ObjectArrayList() // 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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt index 452b6966..a34d51a2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt @@ -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())) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt index 15631768..3e71a190 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt @@ -34,6 +34,13 @@ data class MutableLiquidState( isInfinite = false } + fun setInfinite(state: Registry.Entry, pressure: Float) { + level = 1f + this.state = state + this.pressure = pressure + this.isInfinite = true + } + override fun mutable(): MutableLiquidState { return this } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index 049a092a..7eae5499 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -130,6 +130,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt index ac037a86..a4ccdf5c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/DynamicEntity.kt @@ -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) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt new file mode 100644 index 00000000..131b9ed7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index 64607e9a..ec0adfd7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -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 - get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } } + fun computeLocalHitboxes(): List { + val poly = movementParameters.collisionPoly ?: return listOf() + + if (poly.isLeft) { + return listOf(poly.left().rotate(rotation) + position) + } else { + val build = ObjectArrayList(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(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, 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(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, + poly: Poly, staticBodies: List, 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt index 96020206..b9b0e3cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 2d9d0824..ca3a0911 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -178,7 +178,7 @@ open class WorldObject(val config: Registry.Entry) : 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) : 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, source: Vector2d, damage: TileDamage): Boolean { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt index 73625606..ea6b2259 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionPoly.kt @@ -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 )