diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index 6aa8e5ea..cfbf11ad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -58,6 +58,9 @@ object GlobalDefaults { var bushDamage by Delegates.notNull() private set + var tileDamage by Delegates.notNull() + private set + var sky by Delegates.notNull() private set @@ -120,6 +123,7 @@ object GlobalDefaults { tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage)) + tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index be76c2f6..d08b9734 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -837,7 +837,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { private fun renderWorld(world: ClientWorld) { updateViewportParams() - world.think() + world.tick() stack.clear(Matrix3f.identity()) @@ -911,7 +911,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { GLFW.glfwPollEvents() if (world != null && Starbound.initialized) - world.think() + world.tick() activeConnection?.flush() return true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index edd0407d..6c5a4422 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -308,7 +308,7 @@ class ClientWorld( } } - override fun thinkInner() { + override fun tickInner() { } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt index f44cc4ef..1a92b33d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt @@ -20,7 +20,37 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import kotlin.collections.HashMap -data class AssetReference(val path: String?, val fullPath: String?, val value: V?, val json: JsonElement?) { +class AssetReference { + constructor(value: V?) { + lazy = lazy { value } + path = null + fullPath = null + json = null + } + + constructor(value: () -> V?) { + lazy = lazy { value() } + path = null + fullPath = null + json = null + } + + constructor(path: String?, fullPath: String?, value: V?, json: JsonElement?) { + this.path = path + this.fullPath = fullPath + this.lazy = lazy { value } + this.json = json + } + + val path: String? + val fullPath: String? + val json: JsonElement? + + val value: V? + get() = lazy.value + + private val lazy: Lazy + companion object : TypeAdapterFactory { val EMPTY = AssetReference(null, null, null, null) 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 8af719db..624bcb91 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.defs.tile import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap import it.unimi.dsi.fastutil.objects.Object2DoubleMap import it.unimi.dsi.fastutil.objects.Object2DoubleMaps import ru.dbotthepony.kstarbound.Registries @@ -19,13 +20,13 @@ object BuiltinMetaMaterials { renderParameters = RenderParameters.META, isMeta = true, collisionKind = collisionType, - damageConfig = TileDamageConfig( - damageFactors = Object2DoubleMaps.emptyMap(), - damageRecovery = 1.0, + damageTable = AssetReference(TileDamageConfig( + damageFactors = ImmutableMap.of(), + damageRecovery = Double.MAX_VALUE, maximumEffectTime = 0.0, totalHealth = Double.MAX_VALUE, harvestLevel = Int.MAX_VALUE, - ) + )) )) /** diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamage.kt new file mode 100644 index 00000000..89d0fcb4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamage.kt @@ -0,0 +1,26 @@ +package ru.dbotthepony.kstarbound.defs.tile + +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import java.io.DataInputStream +import java.io.DataOutputStream + +data class TileDamage(val type: TileDamageType = TileDamageType.PROTECTED, val amount: Double = 0.0, val harvestLevel: Int = 1) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + TileDamageType.entries[stream.readUnsignedByte()], + if (isLegacy) stream.readFloat().toDouble() else stream.readDouble(), + if (isLegacy) stream.readInt() else stream.readVarInt() + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(type.ordinal) + + if (isLegacy) { + stream.writeFloat(amount.toFloat()) + stream.writeInt(harvestLevel) + } else { + stream.writeDouble(amount) + stream.writeVarInt(harvestLevel) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt index 70d1d4d9..af2fe1e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt @@ -1,14 +1,37 @@ package ru.dbotthepony.kstarbound.defs.tile +import com.google.common.collect.ImmutableMap import it.unimi.dsi.fastutil.objects.Object2DoubleMap import it.unimi.dsi.fastutil.objects.Object2DoubleMaps +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory data class TileDamageConfig( - val damageFactors: Object2DoubleMap = Object2DoubleMaps.emptyMap(), + val damageFactors: ImmutableMap = ImmutableMap.of(), val damageRecovery: Double = 1.0, val maximumEffectTime: Double = 1.5, val totalHealth: Double = 1.0, val harvestLevel: Int = 1, -) +) { + val damageFactorsMapped: ImmutableMap = damageFactors.entries.stream().map { + var find = TileDamageType.entries.firstOrNull { e -> e.match(it.key) } + + if (find == null) { + LOGGER.error("Unknown tile damage type ${it.key}!") + find = TileDamageType.PROTECTED + } + + find to it.value + }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + + fun damageDone(damage: TileDamage): Double { + return (damageFactorsMapped[damage.type] ?: 1.0) * damage.amount + } + + companion object { + val EMPTY = TileDamageConfig() + + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt new file mode 100644 index 00000000..a87aba05 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt @@ -0,0 +1,7 @@ +package ru.dbotthepony.kstarbound.defs.tile + +enum class TileDamageResult { + NONE, + PROTECTED, + NORMAL; +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt new file mode 100644 index 00000000..13c1b71f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt @@ -0,0 +1,20 @@ +package ru.dbotthepony.kstarbound.defs.tile + +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class TileDamageType(override val jsonName: String) : IStringSerializable { + // Damage done that will not actually kill the target + PROTECTED("protected"), + // Best at chopping down trees, things made of wood, etc. + PLANT("plantish"), + // For digging / drilling through materials + BLOCK("blockish"), + // Gravity gun etc + BEAM("beamish"), + // Penetrating damage done passivly by explosions. + EXPLOSIVE("explosive"), + // Can melt certain block types + FIRE("fire"), + // Can "till" certain materials into others + TILLING("tilling"); +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index 378eed22..923fdb47 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs.tile import com.google.common.collect.ImmutableList import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.defs.IThingWithDescription @@ -25,8 +26,11 @@ data class TileDefinition( val category: String, - @JsonFlat - val damageConfig: TileDamageConfig, + @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) + val damageTable: AssetReference = AssetReference(GlobalDefaults::tileDamage), + + val health: Double? = null, + val requiredHarvestLevel: Int? = null, @JsonFlat val descriptionData: ThingDescription, @@ -40,4 +44,18 @@ data class TileDefinition( override val renderTemplate: AssetReference, override val renderParameters: RenderParameters, -) : IRenderableTile, IThingWithDescription by descriptionData +) : IRenderableTile, IThingWithDescription by descriptionData { + val actualDamageTable: TileDamageConfig by lazy { + val dmg = damageTable.value ?: TileDamageConfig.EMPTY + + return@lazy if (health == null && requiredHarvestLevel == null) { + dmg + } else if (health != null && requiredHarvestLevel != null) { + dmg.copy(totalHealth = health, harvestLevel = requiredHarvestLevel) + } else if (health != null) { + dmg.copy(totalHealth = health) + } else { + dmg.copy(harvestLevel = requiredHarvestLevel!!) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt index 42468772..35aade55 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt @@ -247,7 +247,7 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument } private fun materialHealth(context: ExecutionContext, arguments: ArgumentIterator) { - context.returnBuffer.setTo(lookupStrict(Registries.tiles, arguments.nextAny()).value.damageConfig.totalHealth) + context.returnBuffer.setTo(lookupStrict(Registries.tiles, arguments.nextAny()).value.actualDamageTable.totalHealth) } private fun liquidName(context: ExecutionContext, arguments: ArgumentIterator) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index ed436944..8ec29183 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -39,11 +39,13 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPac import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.DamageTileGroupPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket @@ -411,7 +413,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(LegacyTileArrayUpdatePacket::read) LEGACY.add(LegacyTileUpdatePacket::read) LEGACY.skip("TileLiquidUpdate") - LEGACY.skip("TileDamageUpdate") + LEGACY.add(::TileDamageUpdatePacket) LEGACY.skip("TileModificationFailure") LEGACY.skip("GiveItem") LEGACY.skip("EnvironmentUpdate") @@ -424,7 +426,7 @@ class PacketRegistry(val isLegacy: Boolean) { // Packets sent world client -> world server LEGACY.skip("ModifyTileList") - LEGACY.skip("DamageTileGroup") + LEGACY.add(::DamageTileGroupPacket) LEGACY.skip("CollectLiquid") LEGACY.skip("RequestDrop") LEGACY.skip("SpawnEntity") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt new file mode 100644 index 00000000..ba235847 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/TileDamageUpdatePacket.kt @@ -0,0 +1,22 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.world.TileHealth +import java.io.DataInputStream +import java.io.DataOutputStream + +class TileDamageUpdatePacket(val x: Int, val y: Int, val isBackground: Boolean, val health: TileHealth) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readInt(), stream.readBoolean(), TileHealth(stream, isLegacy)) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeInt(x) + stream.writeInt(y) + stream.writeBoolean(isBackground) + health.write(stream, isLegacy) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt new file mode 100644 index 00000000..a11cc7d1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DamageTileGroupPacket.kt @@ -0,0 +1,49 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readVector2d +import ru.dbotthepony.kommons.io.readVector2f +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.io.writeStruct2i +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream + +class DamageTileGroupPacket(val tiles: Collection, val isBackground: Boolean, val sourcePosition: Vector2d, val damage: TileDamage, val source: Int?) : IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readCollection { readVector2i() }, + stream.readBoolean(), + if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d(), + TileDamage(stream, isLegacy), + if (stream.readBoolean()) stream.readInt() else null + ) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeCollection(tiles) { writeStruct2i(it) } + stream.writeBoolean(isBackground) + + if (isLegacy) + stream.writeStruct2f(sourcePosition.toFloatVector()) + else + stream.writeStruct2d(sourcePosition) + + damage.write(stream, isLegacy) + stream.writeBoolean(source != null) + + if (source != null) + stream.writeInt(source) + } + + override fun play(connection: ServerConnection) { + connection.enqueue { + damageTiles(tiles, isBackground, sourcePosition, damage) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index f7ef2830..7bbc86af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -26,11 +26,13 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpda import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener +import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity @@ -98,10 +100,15 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn override fun onEntityAdded(entity: AbstractEntity) {} override fun onEntityRemoved(entity: AbstractEntity) {} - val dirtyCells = ObjectArraySet() - override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { - dirtyCells.add(Vector2i(x, y)) + if (pos !in pendingSend) { + send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet())) + } + } + + override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) { + // let's hope nothing bad happens from referencing live data + send(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, isBackground, health)) } } @@ -194,6 +201,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn entityVersions.defaultReturnValue(-1L) } + fun isTracking(pos: ChunkPos): Boolean { + return pos in tickets + } + fun tickWorld() { val world = world!! @@ -222,18 +233,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn pendingSend.remove(pos) ticket.ticket.cancel() itr.remove() - } else { - if (ticket.dirtyCells.isNotEmpty()) { - val chunk = world.chunkMap[ticket.pos] ?: continue - - for (tilePos in ticket.dirtyCells) { - if (isLegacy) { - send(LegacyTileUpdatePacket(pos.tile + tilePos, chunk.getCell(tilePos).toLegacyNet())) - } - } - - ticket.dirtyCells.clear() - } } } @@ -256,6 +255,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn if (isLegacy) { send(LegacyTileArrayUpdatePacket(chunk)) + chunk.tileDamagePackets().forEach { send(it) } } else { send(ChunkCellsPacket(chunk)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index d37a14f0..27c24899 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -6,9 +6,12 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -21,6 +24,7 @@ import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener +import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.api.ImmutableCell @@ -152,7 +156,7 @@ class ServerWorld private constructor( if (isClosed.get()) return false try { - think() + tick() return true } catch (err: Throwable) { LOGGER.fatal("Exception in world tick loop", err) @@ -168,7 +172,22 @@ class ServerWorld private constructor( return Thread.currentThread() === thread } - override fun thinkInner() { + fun damageTiles(positions: Collection, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { + if (damage.amount <= 0.0) + return TileDamageResult.NONE + + val actualPositions = positions.stream().map { geometry.wrap(it) }.distinct().toList() + var topMost = TileDamageResult.NONE + + for (pos in actualPositions) { + val chunk = chunkMap[geometry.chunkFromCell(pos)] ?: continue + topMost = topMost.coerceAtLeast(chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source)) + } + + return topMost + } + + override fun tickInner() { val packet = StepUpdatePacket(ticks) internalPlayers.forEach { @@ -311,6 +330,11 @@ class ServerWorld private constructor( temporary.forEach { it.listener?.onCellChanges(x, y, cell) } } + override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) { + permanent.forEach { it.listener?.onTileHealthUpdate(x, y, isBackground, health) } + temporary.forEach { it.listener?.onTileHealthUpdate(x, y, isBackground, health) } + } + abstract inner class AbstractTicket : ITicket { final override val id: Int = nextTicketID.getAndIncrement() final override val pos: ChunkPos @@ -362,11 +386,12 @@ class ServerWorld private constructor( isCanceled = true chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) } loadFuture?.cancel(false) - onCancel() + listener = null + cancel0() } } - protected abstract fun onCancel() + protected abstract fun cancel0() final override val chunk: ServerChunk? get() = chunkMap[pos] @@ -387,7 +412,7 @@ class ServerWorld private constructor( init() } - override fun onCancel() { + override fun cancel0() { permanent.remove(this) } } @@ -403,7 +428,7 @@ class ServerWorld private constructor( init() } - override fun onCancel() { + override fun cancel0() { temporary.remove(this) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 3fb8201a..1df97fd0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -6,7 +6,12 @@ import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult +import ru.dbotthepony.kstarbound.defs.tile.TileDamageType import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState +import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ImmutableCell @@ -15,7 +20,6 @@ import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity import ru.dbotthepony.kstarbound.world.entities.TileEntity -import java.util.concurrent.CompletableFuture import kotlin.concurrent.withLock /** @@ -67,6 +71,49 @@ abstract class Chunk, This : Chunk TileHealth() } + } + + protected val tileHealthBackground = lazy { + Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth() } + } + + fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { + if (!cells.isInitialized()) { + return TileDamageResult.NONE + } + + val tile = cells.value[pos.x, pos.y] + + if (tile.isIndestructible || tile.tile(isBackground).material.isBuiltin) { + return TileDamageResult.NONE + } + + var damage = damage + var result = TileDamageResult.NORMAL + + if (tile.dungeonId in world.protectedDungeonIDs) { + damage = damage.copy(type = TileDamageType.PROTECTED) + result = TileDamageResult.PROTECTED + } + + val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y] + health.damage(tile.tile(isBackground).material.value.actualDamageTable, sourcePosition, damage) + subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) } + + if (isBackground) { + damagedTilesBackground.add(pos) + } else { + damagedTilesForeground.add(pos) + } + + return result + } + + protected val damagedTilesForeground = ObjectArraySet() + protected val damagedTilesBackground = ObjectArraySet() + fun legacyNetworkCells(): Object2DArray { if (cells.isInitialized()) { val cells = cells.value @@ -76,6 +123,40 @@ abstract class Chunk, This : Chunk { + val result = ArrayList() + + if (tileHealthBackground.isInitialized()) { + val tileHealthBackground = tileHealthBackground.value + + 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 (tileHealthForeground.isInitialized()) { + val tileHealthForeground = tileHealthForeground.value + + 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)) + } + } + } + } + + return result + } + fun loadCells(source: Object2DArray) { val ours = cells.value source.checkSizeEquals(ours) @@ -254,8 +335,26 @@ abstract class Chunk, This : Chunk + val health = tileHealthBackground[x, y] + val result = !health.tick(cells[x, y].background.material.value.actualDamageTable) + subscribers.forEach { it.onTileHealthUpdate(x, y, true, health) } + result + } + + damagedTilesForeground.removeIf { (x, y) -> + val health = tileHealthForeground[x, y] + val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable) + subscribers.forEach { it.onTileHealthUpdate(x, y, false, health) } + result + } + } } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/IChunkListener.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/IChunkListener.kt index 970943b0..66dac075 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/IChunkListener.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/IChunkListener.kt @@ -3,16 +3,9 @@ package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity -fun interface IEntityAdditionListener { - fun onEntityAdded(entity: AbstractEntity) +interface IChunkListener { + fun onEntityAdded(entity: AbstractEntity) {} + fun onEntityRemoved(entity: AbstractEntity) {} + fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {} + fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {} } - -fun interface IEntityRemovalListener { - fun onEntityRemoved(entity: AbstractEntity) -} - -fun interface ICellChangeListener { - fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) -} - -interface IChunkListener : IEntityAdditionListener, IEntityRemovalListener, ICellChangeListener diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt new file mode 100644 index 00000000..7c1ca216 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt @@ -0,0 +1,122 @@ +package ru.dbotthepony.kstarbound.world + +import ru.dbotthepony.kommons.io.readVector2d +import ru.dbotthepony.kommons.io.readVector2f +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kommons.io.writeStruct2f +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig +import ru.dbotthepony.kstarbound.defs.tile.TileDamageType +import java.io.DataInputStream +import java.io.DataOutputStream + +class TileHealth() { + constructor(stream: DataInputStream, isLegacy: Boolean) : this() { + read(stream, isLegacy) + } + + var isHarvested: Boolean = false + private set + var damageSource: Vector2d = Vector2d.ZERO + private set + var damageType: TileDamageType = TileDamageType.PROTECTED + private set + var damagePercent: Double = 0.0 + private set + var damageEffectTimeFactor: Double = 0.0 + private set + var damageEffectPercentage: Double = 0.0 + private set + + fun copy(): TileHealth { + val copy = TileHealth() + + copy.isHarvested = isHarvested + copy.damageSource = damageSource + copy.damageType = damageType + copy.damagePercent = damagePercent + copy.damageEffectTimeFactor = damageEffectTimeFactor + copy.damageEffectPercentage = damageEffectPercentage + + return copy + } + + val isHealthy: Boolean + get() = damagePercent <= 0.0 + + val isDead: Boolean + get() = damagePercent >= 1.0 && damageType != TileDamageType.PROTECTED + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + stream.writeFloat(damagePercent.toFloat()) + stream.writeFloat(damageEffectTimeFactor.toFloat()) + stream.writeBoolean(isHarvested) + stream.writeStruct2f(damageSource.toFloatVector()) + } else { + stream.writeDouble(damagePercent) + stream.writeDouble(damageEffectTimeFactor) + stream.writeBoolean(isHarvested) + stream.writeStruct2d(damageSource) + } + + stream.writeByte(damageType.ordinal) + } + + fun read(stream: DataInputStream, isLegacy: Boolean) { + if (isLegacy) { + damagePercent = stream.readFloat().toDouble() + damageEffectTimeFactor = stream.readFloat().toDouble() + isHarvested = stream.readBoolean() + damageSource = stream.readVector2f().toDoubleVector() + } else { + damagePercent = stream.readDouble() + damageEffectTimeFactor = stream.readDouble() + isHarvested = stream.readBoolean() + damageSource = stream.readVector2d() + } + + damageType = TileDamageType.entries[stream.readUnsignedByte()] + damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent + } + + fun reset() { + isHarvested = false + damageSource = Vector2d.ZERO + damageType = TileDamageType.PROTECTED + damagePercent = 0.0 + damageEffectTimeFactor = 0.0 + } + + fun damage(config: TileDamageConfig, source: Vector2d, damage: TileDamage) { + val actualDamage = config.damageDone(damage) / config.totalHealth + damagePercent = (damagePercent + actualDamage).coerceAtMost(1.0) + isHarvested = damage.harvestLevel >= config.harvestLevel + damageSource = source + damageType = damage.type + + if (actualDamage > 0.0) + damageEffectTimeFactor = config.maximumEffectTime + + damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent + } + + fun tick(config: TileDamageConfig, delta: Double = Starbound.TIMESTEP): Boolean { + if (isDead || isHealthy) + return false + + damagePercent -= config.damageRecovery * delta / config.totalHealth + damageEffectTimeFactor -= delta + + if (damagePercent <= 0.0) { + damagePercent = 0.0 + damageEffectTimeFactor = 0.0 + damageType = TileDamageType.PROTECTED + } + + damageEffectPercentage = damageEffectTimeFactor.coerceIn(0.0, 1.0) * damagePercent + return damagePercent > 0.0 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 0319d31b..63d63155 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.* @@ -264,7 +265,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk