Tile damaging?

This commit is contained in:
DBotThePony 2024-03-28 10:15:43 +07:00
parent a5192bc551
commit bf5710542e
Signed by: DBot
GPG Key ID: DCC23B5715498507
21 changed files with 504 additions and 55 deletions

View File

@ -58,6 +58,9 @@ object GlobalDefaults {
var bushDamage by Delegates.notNull<TileDamageConfig>()
private set
var tileDamage by Delegates.notNull<TileDamageConfig>()
private set
var sky by Delegates.notNull<SkyGlobalConfig>()
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()))

View File

@ -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

View File

@ -308,7 +308,7 @@ class ClientWorld(
}
}
override fun thinkInner() {
override fun tickInner() {
}

View File

@ -20,7 +20,37 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.HashMap
data class AssetReference<V>(val path: String?, val fullPath: String?, val value: V?, val json: JsonElement?) {
class AssetReference<V> {
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<V?>
companion object : TypeAdapterFactory {
val EMPTY = AssetReference(null, null, null, null)

View File

@ -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,
)
))
))
/**

View File

@ -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)
}
}
}

View File

@ -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<String> = Object2DoubleMaps.emptyMap(),
val damageFactors: ImmutableMap<String, Double> = ImmutableMap.of(),
val damageRecovery: Double = 1.0,
val maximumEffectTime: Double = 1.5,
val totalHealth: Double = 1.0,
val harvestLevel: Int = 1,
)
) {
val damageFactorsMapped: ImmutableMap<TileDamageType, Double> = 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()
}
}

View File

@ -0,0 +1,7 @@
package ru.dbotthepony.kstarbound.defs.tile
enum class TileDamageResult {
NONE,
PROTECTED,
NORMAL;
}

View File

@ -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");
}

View File

@ -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<TileDamageConfig> = 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<RenderTemplate>,
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!!)
}
}
}

View File

@ -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) {

View File

@ -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")

View File

@ -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")
}
}

View File

@ -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<Vector2i>, 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)
}
}
}

View File

@ -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<Vector2i>()
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))
}

View File

@ -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<IStruct2i>, 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)
}

View File

@ -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<WorldType : World<WorldType, This>, This : Chunk<WorldType,
Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL)
}
protected val tileHealthForeground = lazy {
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> 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<Vector2i>()
protected val damagedTilesBackground = ObjectArraySet<Vector2i>()
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
if (cells.isInitialized()) {
val cells = cells.value
@ -76,6 +123,40 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
}
}
fun tileDamagePackets(): List<TileDamageUpdatePacket> {
val result = ArrayList<TileDamageUpdatePacket>()
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<out AbstractCell>) {
val ours = cells.value
source.checkSizeEquals(ours)
@ -254,8 +335,26 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
}
}
open fun think() {
open fun tick() {
if (cells.isInitialized() && (damagedTilesBackground.isNotEmpty() || damagedTilesForeground.isNotEmpty())) {
val tileHealthBackground = tileHealthBackground.value
val tileHealthForeground = tileHealthForeground.value
val cells = cells.value
damagedTilesBackground.removeIf { (x, y) ->
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 {

View File

@ -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

View File

@ -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
}
}

View File

@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
check(isSameThread()) { "Trying to access $this from ${Thread.currentThread()}" }
}
fun think() {
fun tick() {
try {
ticks++
mailbox.executeQueuedTasks()
@ -276,17 +277,17 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
mailbox.executeQueuedTasks()
for (chunk in chunkMap) {
chunk.think()
chunk.tick()
}
mailbox.executeQueuedTasks()
thinkInner()
tickInner()
} catch(err: Throwable) {
throw RuntimeException("Ticking world $this", err)
}
}
protected abstract fun thinkInner()
protected abstract fun tickInner()
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
override fun close() {

View File

@ -25,6 +25,13 @@ sealed class AbstractCell {
return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId)
}
fun tile(background: Boolean): AbstractTileState {
if (background)
return this.background
else
return this.foreground
}
fun write(stream: DataOutputStream) {
foreground.write(stream)
background.write(stream)