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>() var bushDamage by Delegates.notNull<TileDamageConfig>()
private set private set
var tileDamage by Delegates.notNull<TileDamageConfig>()
private set
var sky by Delegates.notNull<SkyGlobalConfig>() var sky by Delegates.notNull<SkyGlobalConfig>()
private set private set
@ -120,6 +123,7 @@ object GlobalDefaults {
tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/grassDamage.config", ::grassDamage))
tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage))
tasks.add(load("/plants/bushDamage.config", ::bushDamage)) 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("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter()))
tasks.add(load("/currencies.config", ::currencies, 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) { private fun renderWorld(world: ClientWorld) {
updateViewportParams() updateViewportParams()
world.think() world.tick()
stack.clear(Matrix3f.identity()) stack.clear(Matrix3f.identity())
@ -911,7 +911,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
GLFW.glfwPollEvents() GLFW.glfwPollEvents()
if (world != null && Starbound.initialized) if (world != null && Starbound.initialized)
world.think() world.tick()
activeConnection?.flush() activeConnection?.flush()
return true 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 java.util.concurrent.ConcurrentHashMap
import kotlin.collections.HashMap 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 { companion object : TypeAdapterFactory {
val EMPTY = AssetReference(null, null, null, null) val EMPTY = AssetReference(null, null, null, null)

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.defs.tile package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList 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.Object2DoubleMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
@ -19,13 +20,13 @@ object BuiltinMetaMaterials {
renderParameters = RenderParameters.META, renderParameters = RenderParameters.META,
isMeta = true, isMeta = true,
collisionKind = collisionType, collisionKind = collisionType,
damageConfig = TileDamageConfig( damageTable = AssetReference(TileDamageConfig(
damageFactors = Object2DoubleMaps.emptyMap(), damageFactors = ImmutableMap.of(),
damageRecovery = 1.0, damageRecovery = Double.MAX_VALUE,
maximumEffectTime = 0.0, maximumEffectTime = 0.0,
totalHealth = Double.MAX_VALUE, totalHealth = Double.MAX_VALUE,
harvestLevel = Int.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 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.Object2DoubleMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
data class TileDamageConfig( data class TileDamageConfig(
val damageFactors: Object2DoubleMap<String> = Object2DoubleMaps.emptyMap(), val damageFactors: ImmutableMap<String, Double> = ImmutableMap.of(),
val damageRecovery: Double = 1.0, val damageRecovery: Double = 1.0,
val maximumEffectTime: Double = 1.5, val maximumEffectTime: Double = 1.5,
val totalHealth: Double = 1.0, val totalHealth: Double = 1.0,
val harvestLevel: Int = 1, 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 com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
@ -25,8 +26,11 @@ data class TileDefinition(
val category: String, val category: String,
@JsonFlat @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
val damageConfig: TileDamageConfig, val damageTable: AssetReference<TileDamageConfig> = AssetReference(GlobalDefaults::tileDamage),
val health: Double? = null,
val requiredHarvestLevel: Int? = null,
@JsonFlat @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
@ -40,4 +44,18 @@ data class TileDefinition(
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate>,
override val renderParameters: RenderParameters, 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) { 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) { 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.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerInfoPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket 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.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket 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.FindUniqueEntityPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket
@ -411,7 +413,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(LegacyTileArrayUpdatePacket::read) LEGACY.add(LegacyTileArrayUpdatePacket::read)
LEGACY.add(LegacyTileUpdatePacket::read) LEGACY.add(LegacyTileUpdatePacket::read)
LEGACY.skip("TileLiquidUpdate") LEGACY.skip("TileLiquidUpdate")
LEGACY.skip("TileDamageUpdate") LEGACY.add(::TileDamageUpdatePacket)
LEGACY.skip("TileModificationFailure") LEGACY.skip("TileModificationFailure")
LEGACY.skip("GiveItem") LEGACY.skip("GiveItem")
LEGACY.skip("EnvironmentUpdate") LEGACY.skip("EnvironmentUpdate")
@ -424,7 +426,7 @@ class PacketRegistry(val isLegacy: Boolean) {
// Packets sent world client -> world server // Packets sent world client -> world server
LEGACY.skip("ModifyTileList") LEGACY.skip("ModifyTileList")
LEGACY.skip("DamageTileGroup") LEGACY.add(::DamageTileGroupPacket)
LEGACY.skip("CollectLiquid") LEGACY.skip("CollectLiquid")
LEGACY.skip("RequestDrop") LEGACY.skip("RequestDrop")
LEGACY.skip("SpawnEntity") 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.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket 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.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity 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 onEntityAdded(entity: AbstractEntity) {}
override fun onEntityRemoved(entity: AbstractEntity) {} override fun onEntityRemoved(entity: AbstractEntity) {}
val dirtyCells = ObjectArraySet<Vector2i>()
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { 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) entityVersions.defaultReturnValue(-1L)
} }
fun isTracking(pos: ChunkPos): Boolean {
return pos in tickets
}
fun tickWorld() { fun tickWorld() {
val world = world!! val world = world!!
@ -222,18 +233,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
pendingSend.remove(pos) pendingSend.remove(pos)
ticket.ticket.cancel() ticket.ticket.cancel()
itr.remove() 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) { if (isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk)) send(LegacyTileArrayUpdatePacket(chunk))
chunk.tileDamagePackets().forEach { send(it) }
} else { } else {
send(ChunkCellsPacket(chunk)) 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.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket 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.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
@ -152,7 +156,7 @@ class ServerWorld private constructor(
if (isClosed.get()) return false if (isClosed.get()) return false
try { try {
think() tick()
return true return true
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Exception in world tick loop", err) LOGGER.fatal("Exception in world tick loop", err)
@ -168,7 +172,22 @@ class ServerWorld private constructor(
return Thread.currentThread() === thread 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) val packet = StepUpdatePacket(ticks)
internalPlayers.forEach { internalPlayers.forEach {
@ -311,6 +330,11 @@ class ServerWorld private constructor(
temporary.forEach { it.listener?.onCellChanges(x, y, cell) } 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 { abstract inner class AbstractTicket : ITicket {
final override val id: Int = nextTicketID.getAndIncrement() final override val id: Int = nextTicketID.getAndIncrement()
final override val pos: ChunkPos final override val pos: ChunkPos
@ -362,11 +386,12 @@ class ServerWorld private constructor(
isCanceled = true isCanceled = true
chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) } chunk?.entities?.forEach { e -> listener?.onEntityRemoved(e) }
loadFuture?.cancel(false) loadFuture?.cancel(false)
onCancel() listener = null
cancel0()
} }
} }
protected abstract fun onCancel() protected abstract fun cancel0()
final override val chunk: ServerChunk? final override val chunk: ServerChunk?
get() = chunkMap[pos] get() = chunkMap[pos]
@ -387,7 +412,7 @@ class ServerWorld private constructor(
init() init()
} }
override fun onCancel() { override fun cancel0() {
permanent.remove(this) permanent.remove(this)
} }
} }
@ -403,7 +428,7 @@ class ServerWorld private constructor(
init() init()
} }
override fun onCancel() { override fun cancel0() {
temporary.remove(this) 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.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d 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.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell 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.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.TileEntity import ru.dbotthepony.kstarbound.world.entities.TileEntity
import java.util.concurrent.CompletableFuture
import kotlin.concurrent.withLock 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) 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> { fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
if (cells.isInitialized()) { if (cells.isInitialized()) {
val cells = cells.value 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>) { fun loadCells(source: Object2DArray<out AbstractCell>) {
val ours = cells.value val ours = cells.value
source.checkSizeEquals(ours) 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 { 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.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
fun interface IEntityAdditionListener { interface IChunkListener {
fun onEntityAdded(entity: AbstractEntity) 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.AABB
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.* 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()}" } check(isSameThread()) { "Trying to access $this from ${Thread.currentThread()}" }
} }
fun think() { fun tick() {
try { try {
ticks++ ticks++
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
@ -276,17 +277,17 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
for (chunk in chunkMap) { for (chunk in chunkMap) {
chunk.think() chunk.tick()
} }
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
thinkInner() tickInner()
} catch(err: Throwable) { } catch(err: Throwable) {
throw RuntimeException("Ticking world $this", err) throw RuntimeException("Ticking world $this", err)
} }
} }
protected abstract fun thinkInner() protected abstract fun tickInner()
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
override fun close() { 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) 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) { fun write(stream: DataOutputStream) {
foreground.write(stream) foreground.write(stream)
background.write(stream) background.write(stream)