Damage handling (not tested because entire system is stupid)

This commit is contained in:
DBotThePony 2024-04-25 16:13:35 +07:00
parent c0c63b9240
commit ba0db89dcf
Signed by: DBot
GPG Key ID: DCC23B5715498507
32 changed files with 851 additions and 113 deletions

View File

@ -96,6 +96,8 @@ val color: TileColor = TileColor.DEFAULT
## Scripting
* In DamageSource, `sourceEntityId` combination with `rayCheck` has been fixed, and check for tile collision between victim and inflictor (this entity), not between victim and attacker (`sourceEntityId`)
#### Random
* Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point
* Removed `random:addEntropy`

View File

@ -434,9 +434,9 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
return JsonPath.query(jsonPath).get(json)
}
fun loadJsonAsset(path: JsonElement, relative: String): JsonElement? {
fun loadJsonAsset(path: JsonElement, relative: String): JsonElement {
if (path is JsonPrimitive) {
return loadJsonAsset(AssetPathStack.relativeTo(relative, path.asString))
return loadJsonAsset(AssetPathStack.relativeTo(relative, path.asString)) ?: JsonNull.INSTANCE
} else {
return path
}

View File

@ -22,6 +22,9 @@ import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
@ -51,8 +54,8 @@ class ClientWorld(
throw RuntimeException("unreachable code")
}
override val isClient: Boolean
get() = true
override val connectionID: Int
get() = client.activeConnection?.connectionID ?: throw IllegalStateException("ClientWorld exists without active connection")
val renderRegionWidth = determineChunkSize(geometry.size.x)
val renderRegionHeight = determineChunkSize(geometry.size.y)
@ -324,6 +327,18 @@ class ClientWorld(
TODO("Not yet implemented")
}
override fun networkDamageNotification(notification: DamageNotificationPacket) {
client.activeConnection?.send(notification)
}
override fun networkHitRequest(data: HitRequestPacket) {
client.activeConnection?.send(data)
}
override fun networkDamageRequest(data: DamageRequestPacket) {
client.activeConnection?.send(data)
}
companion object {
val ring = listOf(
Vector2i(0, 0),

View File

@ -9,17 +9,21 @@ import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readEnumStupid
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readMVariant2
import ru.dbotthepony.kstarbound.io.readNullableDouble
import ru.dbotthepony.kstarbound.io.readNullableString
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeEnumStupid
import ru.dbotthepony.kstarbound.io.writeMVariant2
import ru.dbotthepony.kstarbound.io.writeNullable
import ru.dbotthepony.kstarbound.io.writeStruct2d
@ -29,6 +33,8 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
@ -139,6 +145,80 @@ data class TouchDamage(
val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
)
@JsonFactory
data class DamageNotification(
val sourceEntityId: Int,
val targetEntityId: Int,
val position: Vector2d,
val damageDealt: Double,
val healthLost: Double,
val hitType: HitType,
val damageSourceKind: String,
val targetMaterialKind: String
) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
if (isLegacy) stream.readSignedVarInt() else stream.readInt(),
if (isLegacy) stream.readSignedVarInt() else stream.readInt(),
Vector2d(stream.readDouble(0.01, isLegacy), stream.readDouble(0.01, isLegacy)),
stream.readDouble(isLegacy),
stream.readDouble(isLegacy),
HitType.entries[stream.readEnumStupid(isLegacy)],
stream.readInternedString(),
stream.readInternedString(),
)
fun isAttacker(entity: AbstractEntity): Boolean = entity.entityID == sourceEntityId
fun isVictim(entity: AbstractEntity): Boolean = entity.entityID == targetEntityId
fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) stream.writeSignedVarInt(sourceEntityId) else stream.writeInt(sourceEntityId)
if (isLegacy) stream.writeSignedVarInt(targetEntityId) else stream.writeInt(targetEntityId)
stream.writeDouble(position.x, 0.01, isLegacy)
stream.writeDouble(position.y, 0.01, isLegacy)
stream.writeDouble(damageDealt, isLegacy)
stream.writeDouble(healthLost, isLegacy)
stream.writeEnumStupid(hitType.ordinal, isLegacy)
stream.writeBinaryString(damageSourceKind)
stream.writeBinaryString(targetMaterialKind)
}
}
@JsonFactory
data class DamageData(
val hitType: HitType,
val damageType: DamageType,
val damage: Double,
val knockback: Vector2d,
val sourceEntityId: Int,
val inflictorEntityId: Int = 0,
val kind: String,
val statusEffects: Collection<EphemeralStatusEffect>,
) {
constructor(stream: DataInputStream, isLegacy: Boolean, inflictorEntityId: Int) : this(
HitType.entries[stream.readEnumStupid(isLegacy)],
DamageType.entries[stream.readUnsignedByte()],
stream.readDouble(isLegacy),
stream.readVector2d(isLegacy),
stream.readInt(),
inflictorEntityId, // DamageData is written inside packets which specify inflictor by themselves
stream.readInternedString(),
stream.readCollection { EphemeralStatusEffect(stream, isLegacy) }
)
fun isAttacker(entity: AbstractEntity): Boolean = entity.entityID == sourceEntityId
fun isInflictor(entity: AbstractEntity): Boolean = entity.entityID == inflictorEntityId
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeEnumStupid(hitType.ordinal, isLegacy)
stream.writeByte(damageType.ordinal)
stream.writeDouble(damage, isLegacy)
stream.writeStruct2d(knockback, isLegacy)
stream.writeInt(sourceEntityId)
stream.writeBinaryString(kind)
stream.writeCollection(statusEffects) { it.write(this, isLegacy) }
}
}
// this shit is a complete mess, because in original code DamageSource::toJson() method
// will create json structure which will not be readable by DamageSource's constructor
// (will always throw an exception)
@ -172,6 +252,27 @@ data class DamageSource(
stream.readBoolean()
)
fun intersect(other: Poly): Boolean {
return damageArea.map({ it.intersect(other) != null }, { other.intersect(it) != null })
}
operator fun plus(offset: Vector2d): DamageSource {
return copy(damageArea = damageArea.flatMap({ it + offset }, { it + offset }))
}
fun knockbackMomentum(geometry: WorldGeometry, targetCenter: Vector2d): Vector2d {
if (knockback.isRight) {
return knockback.right()
} else {
val knockback = knockback.left()
if (knockback == 0.0)
return Vector2d.ZERO
return damageArea.map({ geometry.diff(targetCenter, it.centre).unitVector * knockback }, { it.difference.unitVector * knockback })
}
}
data class JsonData(
val poly: Poly? = null,
val line: Line2d? = null,

View File

@ -5,6 +5,8 @@ import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
@ -34,6 +36,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
@ -72,7 +75,7 @@ data class ObjectDefinition(
val soundEffectRangeMultiplier: Double = 1.0,
val price: Long = 1L,
val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
val touchDamage: JsonReference.Object = JsonReference.Object(null, null, null),
val touchDamage: JsonElement,
val minimumLiquidLevel: Float? = null,
val maximumLiquidLevel: Float? = null,
val liquidCheckInterval: Float = 0.5f,
@ -141,8 +144,7 @@ data class ObjectDefinition(
val soundEffectRangeMultiplier: Double = 1.0,
val price: Long = 1L,
val statusEffects: ImmutableList<Either<String, StatModifier>> = ImmutableList.of(),
//val touchDamage: TouchDamage,
val touchDamage: JsonReference.Object = JsonReference.Object(null, null, null),
val touchDamage: JsonElement = JsonNull.INSTANCE,
val minimumLiquidLevel: Float? = null,
val maximumLiquidLevel: Float? = null,
val liquidCheckInterval: Float = 0.5f,
@ -243,7 +245,7 @@ data class ObjectDefinition(
soundEffectRangeMultiplier = basic.soundEffectRangeMultiplier,
price = basic.price,
statusEffects = basic.statusEffects,
touchDamage = basic.touchDamage,
touchDamage = Starbound.loadJsonAsset(basic.touchDamage, AssetPathStack.last()),
minimumLiquidLevel = basic.minimumLiquidLevel,
maximumLiquidLevel = basic.maximumLiquidLevel,
liquidCheckInterval = basic.liquidCheckInterval,

View File

@ -4,6 +4,8 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
@ -35,6 +37,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
import kotlin.math.PI
@ -60,7 +63,7 @@ data class ObjectOrientation(
val lightPosition: Vector2i,
val beamAngle: Double,
val statusEffectArea: Vector2d?,
val touchDamage: JsonReference.Object?,
val touchDamage: JsonElement,
val particleEmitters: ArrayList<ParticleEmissionEntry>,
) {
fun placementValid(world: World<*, *>, position: Vector2i, ignoreProtectedDungeons: Boolean = false): Boolean {
@ -285,7 +288,7 @@ data class ObjectOrientation(
val lightPosition = obj["lightPosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO
val beamAngle = obj.get("beamAngle", 0.0) / 180.0 * PI
val statusEffectArea = obj["statusEffectArea"]?.let { vectorsd.fromJsonTree(it) }
val touchDamage = obj["touchDamage"]?.let { objectRefs.fromJsonTree(it) }
val touchDamage = Starbound.loadJsonAsset(obj["touchDamage"] ?: JsonNull.INSTANCE, AssetPathStack.last())
val emitters = ArrayList<ParticleEmissionEntry>()

View File

@ -9,12 +9,14 @@ import ru.dbotthepony.kommons.io.readDouble
import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readInt
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeInt
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.io.writeStruct2i
@ -304,3 +306,19 @@ fun OutputStream.writeByteArray(array: ByteArrayList) {
fun OutputStream.writeByteArray(array: FastByteArrayOutputStream) {
writeByteArray(array.array, 0, array.length)
}
fun OutputStream.writeDouble(value: Double, precision: Double, isLegacy: Boolean) {
if (isLegacy) {
writeSignedVarLong((value / precision).toLong())
} else {
writeDouble(value)
}
}
fun InputStream.readDouble(precision: Double, isLegacy: Boolean): Double {
if (isLegacy) {
return readSignedVarLong() * precision
} else {
return readDouble()
}
}

View File

@ -26,7 +26,7 @@ import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.physics.Poly
class ActiveItemStack(entry: ItemRegistry.Entry, config: JsonObject, parameters: JsonObject, size: Long) : ItemStack(entry, config, parameters, size), NetworkedStatefulItemStack.Stateful {
class ActiveItemStack(entry: ItemRegistry.Entry, config: JsonObject, parameters: JsonObject, size: Long) : ItemStack(entry, config, parameters, size), ToolItem {
override val networkElement = NetworkedGroup()
val animator: Animator
@ -61,11 +61,11 @@ class ActiveItemStack(entry: ItemRegistry.Entry, config: JsonObject, parameters:
var armAngle by networkedFixedPoint2(0.01).also { networkElement.add(it) }
var facingDirection by networkedData(KOptional(), Direction.CODEC.koptional()).also { networkElement.add(it) }
val damageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkElement.add(it) }
override val damageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkElement.add(it) }
val itemDamageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkElement.add(it) }
val shieldPolys = NetworkedList(Poly.CODEC, Poly.LEGACY_CODEC).also { networkElement.add(it) }
override val shieldPolys = NetworkedList(Poly.CODEC, Poly.LEGACY_CODEC).also { networkElement.add(it) }
val itemShieldPolys = NetworkedList(Poly.CODEC, Poly.LEGACY_CODEC).also { networkElement.add(it) }
val forceRegions = NetworkedList(PhysicsForceRegion.CODEC, PhysicsForceRegion.LEGACY_CODEC).also { networkElement.add(it) }
override val forceRegions = NetworkedList(PhysicsForceRegion.CODEC, PhysicsForceRegion.LEGACY_CODEC).also { networkElement.add(it) }
val itemForceRegions = NetworkedList(PhysicsForceRegion.CODEC, PhysicsForceRegion.LEGACY_CODEC).also { networkElement.add(it) }
val scriptedAnimationParameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkElement.add(it, false) }

View File

@ -39,6 +39,7 @@ import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.valueOf
@ -80,6 +81,9 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
}
}
open val networkElement: NetworkedElement?
get() = null
var parameters: JsonObject = parameters
protected set

View File

@ -0,0 +1,16 @@
package ru.dbotthepony.kstarbound.item
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion
import ru.dbotthepony.kstarbound.world.physics.Poly
interface ToolItem {
val damageSources: Collection<DamageSource>
get() = listOf()
val shieldPolys: Collection<Poly>
get() = listOf()
val forceRegions: Collection<PhysicsForceRegion>
get() = listOf()
}

View File

@ -240,11 +240,11 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
}
table["setDamageSources"] = luaFunction { sources: Table? ->
self.damageSources.clear()
self.customDamageSources.clear()
if (sources != null) {
for ((_, v) in sources) {
self.damageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java))
self.customDamageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java))
}
}
}

View File

@ -14,6 +14,8 @@ import ru.dbotthepony.kstarbound.json.getAdapter
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
private operator fun Vector2d.compareTo(other: Vector2d): Int {
var cmp = x.compareTo(other.x)
@ -35,6 +37,13 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
val center: Vector2d
get() = p0 + difference * 0.5
val aabb: AABB get() {
return AABB(
Vector2d(min(p0.x, p1.x), min(p0.y, p1.y)),
Vector2d(max(p0.x, p1.x), max(p0.y, p1.y)),
)
}
operator fun plus(other: IStruct2d): Line2d {
return Line2d(p0 + other, p1 + other)
}

View File

@ -19,6 +19,9 @@ import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
@ -477,9 +480,9 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::EntityDestroyPacket)
LEGACY.add(::EntityInteractPacket)
LEGACY.add(::EntityInteractResultPacket)
LEGACY.skip("HitRequest")
LEGACY.skip("DamageRequest")
LEGACY.skip("DamageNotification")
LEGACY.add(HitRequestPacket::read)
LEGACY.add(DamageRequestPacket::read)
LEGACY.add(::DamageNotificationPacket)
LEGACY.skip("EntityMessage")
LEGACY.skip("EntityMessageResponse")
LEGACY.add(::UpdateWorldPropertiesPacket)

View File

@ -0,0 +1,45 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.DamageNotification
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker
import java.io.DataInputStream
import java.io.DataOutputStream
// Why does this specify source, when DamageNotification already contains it?????????????
class DamageNotificationPacket(val source: Int, val notification: DamageNotification) : IServerPacket, IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), DamageNotification(stream, isLegacy))
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(source)
notification.write(stream, isLegacy)
}
fun shouldNetworkTo(client: ServerWorldTracker): Boolean {
return source in client.client.entityIDRange ||
notification.targetEntityId in client.client.entityIDRange ||
client.isTracking(source) ||
client.isTracking(notification.position)
}
override fun play(connection: ServerConnection) {
connection.enqueue {
pushRemoteDamageNotification(this@DamageNotificationPacket)
for (client in clients) {
if (client.client !== connection && shouldNetworkTo(client)) {
client.send(this@DamageNotificationPacket)
}
}
}
}
override fun play(connection: ClientConnection) {
connection.enqueue {
world?.pushRemoteDamageNotification(this@DamageNotificationPacket)
}
}
}

View File

@ -0,0 +1,42 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.DamageData
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class DamageRequestPacket(val inflictor: Int, val target: Int, val request: DamageData) : IServerPacket, IClientPacket {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(inflictor)
stream.writeInt(target)
request.write(stream, isLegacy)
}
val destinationConnection: Int
get() = Connection.connectionForEntityID(target)
override fun play(connection: ServerConnection) {
connection.enqueue {
pushRemoteDamageRequest(this@DamageRequestPacket)
}
}
override fun play(connection: ClientConnection) {
connection.enqueue {
world?.pushRemoteDamageRequest(this@DamageRequestPacket)
}
}
companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): DamageRequestPacket {
val inflictor = stream.readInt()
val target = stream.readInt()
val data = DamageData(stream, isLegacy, inflictor)
return DamageRequestPacket(inflictor, target, data)
}
}
}

View File

@ -0,0 +1,42 @@
package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.DamageData
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class HitRequestPacket(val inflictor: Int, val target: Int, val request: DamageData) : IServerPacket, IClientPacket {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeInt(inflictor)
stream.writeInt(target)
request.write(stream, isLegacy)
}
val destinationConnection: Int
get() = Connection.connectionForEntityID(inflictor)
override fun play(connection: ServerConnection) {
connection.enqueue {
pushRemoteHitRequest(this@HitRequestPacket)
}
}
override fun play(connection: ClientConnection) {
connection.enqueue {
world?.pushRemoteHitRequest(this@HitRequestPacket)
}
}
companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): HitRequestPacket {
val inflictor = stream.readInt()
val target = stream.readInt()
val data = DamageData(stream, isLegacy, inflictor)
return HitRequestPacket(inflictor, target, data)
}
}
}

View File

@ -6,55 +6,49 @@ import java.io.DataOutputStream
import java.util.function.LongSupplier
class NetworkedStatefulItemStack(value: ItemStack = ItemStack.EMPTY) : NetworkedItemStack(value) {
interface Stateful {
val networkElement: NetworkedElement
}
private var isInterpolating = false
private var extrapolation = 0.0
override fun enableInterpolation(extrapolation: Double) {
isInterpolating = true
this.extrapolation = extrapolation
(itemStack as? Stateful)?.networkElement?.enableInterpolation(extrapolation)
itemStack.networkElement?.enableInterpolation(extrapolation)
}
override fun disableInterpolation() {
isInterpolating = false
(itemStack as? Stateful)?.networkElement?.disableInterpolation()
itemStack.networkElement?.disableInterpolation()
}
override fun specifyVersioner(versionCounter: LongSupplier) {
super.specifyVersioner(versionCounter)
(itemStack as? Stateful)?.networkElement?.disableInterpolation()
itemStack.networkElement?.disableInterpolation()
}
override fun hasChangedSince(version: Long): Boolean {
return super.hasChangedSince(version) || (get() as? Stateful)?.networkElement?.hasChangedSince(version) == true
return super.hasChangedSince(version) || itemStack.networkElement?.hasChangedSince(version) == true
}
override fun accept(t: ItemStack) {
super.accept(t)
if (t is Stateful) {
if (versionCounter != null) {
t.networkElement.specifyVersioner(versionCounter!!)
}
if (versionCounter != null) {
t.networkElement?.specifyVersioner(versionCounter!!)
}
if (isInterpolating) {
t.networkElement.enableInterpolation(extrapolation)
} else {
t.networkElement.disableInterpolation()
}
if (isInterpolating) {
t.networkElement?.enableInterpolation(extrapolation)
} else {
t.networkElement?.disableInterpolation()
}
}
override fun readBlankDelta(interpolationDelay: Double) {
(itemStack as? Stateful)?.networkElement?.readBlankDelta(interpolationDelay)
itemStack.networkElement?.readBlankDelta(interpolationDelay)
}
override fun tickInterpolation(delta: Double) {
(itemStack as? Stateful)?.networkElement?.tickInterpolation(delta)
itemStack.networkElement?.tickInterpolation(delta)
}
override fun toString(): String {
@ -66,27 +60,20 @@ class NetworkedStatefulItemStack(value: ItemStack = ItemStack.EMPTY) : Networked
val stack = itemStack
if (stack is Stateful) {
val versionCounter = versionCounter
val versionCounter = versionCounter
if (versionCounter != null)
stack.networkElement.specifyVersioner(versionCounter)
if (versionCounter != null)
stack.networkElement?.specifyVersioner(versionCounter)
if (isInterpolating)
stack.networkElement.enableInterpolation(extrapolation)
if (isInterpolating)
stack.networkElement?.enableInterpolation(extrapolation)
stack.networkElement.readInitial(data, isLegacy)
}
stack.networkElement?.readInitial(data, isLegacy)
}
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
super.writeInitial(data, isLegacy)
val stack = itemStack
if (stack is Stateful) {
stack.networkElement.writeInitial(data, isLegacy)
}
itemStack.networkElement?.writeInitial(data, isLegacy)
}
override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
@ -98,37 +85,34 @@ class NetworkedStatefulItemStack(value: ItemStack = ItemStack.EMPTY) : Networked
super.readInitial(data, isLegacy)
val stack = itemStack
val versionCounter = versionCounter
if (stack is Stateful) {
val versionCounter = versionCounter
if (versionCounter != null) {
stack.networkElement?.specifyVersioner(versionCounter)
}
if (versionCounter != null) {
stack.networkElement.specifyVersioner(versionCounter)
}
if (isInterpolating) {
stack.networkElement.enableInterpolation(extrapolation)
}
if (isInterpolating) {
stack.networkElement?.enableInterpolation(extrapolation)
}
}
2 -> {
val stack = itemStack
val stack = itemStack.networkElement
if (stack is Stateful) {
stack.networkElement.readInitial(data, isLegacy)
if (stack != null) {
stack.readInitial(data, isLegacy)
} else {
throw IllegalStateException("Remote and Local disagree whenever ItemStack has networked state (local item: $stack)")
throw IllegalStateException("Remote and Local disagree whenever ItemStack has networked state (local item: $itemStack)")
}
}
3 -> {
val stack = itemStack
val stack = itemStack.networkElement
if (stack is Stateful) {
stack.networkElement.readDelta(data, interpolationDelay, isLegacy)
if (stack != null) {
stack.readDelta(data, interpolationDelay, isLegacy)
} else {
throw IllegalStateException("Remote and Local disagree whenever ItemStack has networked state (local item: $stack)")
throw IllegalStateException("Remote and Local disagree whenever ItemStack has networked state (local item: $itemStack)")
}
}
@ -142,18 +126,18 @@ class NetworkedStatefulItemStack(value: ItemStack = ItemStack.EMPTY) : Networked
data.writeByte(1)
super.writeInitial(data, isLegacy)
val stack = itemStack
val stack = itemStack.networkElement
if (stack is Stateful) {
if (stack != null) {
data.writeByte(2)
stack.networkElement.writeInitial(data, isLegacy)
stack.writeInitial(data, isLegacy)
}
} else {
val stack = itemStack
val stack = itemStack.networkElement
if (stack is Stateful && stack.networkElement.hasChangedSince(remoteVersion)) {
if (stack?.hasChangedSince(remoteVersion) == true) {
data.writeByte(3)
stack.networkElement.writeDelta(data, remoteVersion, isLegacy)
stack.writeDelta(data, remoteVersion, isLegacy)
}
}

View File

@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import java.io.File
import java.lang.ref.Cleaner
import java.sql.DriverManager
import java.util.UUID
import java.util.concurrent.CompletableFuture
@ -67,10 +68,10 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
private val database = DriverManager.getConnection("jdbc:sqlite:${File(universeFolder, "universe.db").absolutePath.replace('\\', '/')}")
private val databaseCleanable = Starbound.CLEANER.register(this, database::close)
init {
database.createStatement().use {
it.execute("PRAGMA locking_mode=EXCLUSIVE")
it.execute("PRAGMA journal_mode=WAL")
it.execute("CREATE TABLE IF NOT EXISTS `metadata` (`key` VARCHAR NOT NULL PRIMARY KEY, `value` BLOB NOT NULL)")
it.execute("CREATE TABLE IF NOT EXISTS `universe_flags` (`flag` VARCHAR NOT NULL PRIMARY KEY)")
@ -423,7 +424,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
}
database.commit()
database.close()
databaseCleanable.clean()
universe.close()
close0()
}

View File

@ -214,7 +214,6 @@ sealed class LegacyWorldStorage() : WorldStorage() {
}
connection.createStatement().use {
it.execute("PRAGMA locking_mode=EXCLUSIVE")
it.execute("PRAGMA journal_mode=WAL")
it.execute("""CREATE TABLE IF NOT EXISTS `data` (

View File

@ -563,7 +563,6 @@ class ServerSystemWorld : SystemWorld {
LOGGER.warn("Tried to create system world at $location, but nothing is there")
return null
} else {
LOGGER.info("Creating new System World at $location")
val world = ServerSystemWorld(server, location)
try {
@ -578,13 +577,12 @@ class ServerSystemWorld : SystemWorld {
throw err
}
LOGGER.info("Created new System World at $location")
return world
}
}
suspend fun load(server: StarboundServer, location: Vector3i, data: JsonElement): ServerSystemWorld {
LOGGER.info("Loading System World at $location")
val load = Starbound.gson.fromJson(data, JsonData::class.java)
if (load.location != location) {
@ -604,6 +602,7 @@ class ServerSystemWorld : SystemWorld {
throw err
}
LOGGER.info("Loaded System World at $location")
return world
}
}

View File

@ -46,6 +46,7 @@ import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable
import java.io.File
import java.lang.ref.Cleaner.Cleanable
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
@ -68,17 +69,20 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
private val database: Connection
private val legacyDatabase: BTreeDB5?
private val isMemory = folder == null
private val databaseCleanable: Cleanable
init {
if (folder == null) {
// in-memory database
database = DriverManager.getConnection("jdbc:sqlite:")
legacyDatabase = null
databaseCleanable = Starbound.CLEANER.register(this, database::close)
} else {
val nativeFile = File(folder, "universe-chunks.db")
val legacyFile = File(folder, "universe.chunks")
database = DriverManager.getConnection("jdbc:sqlite:${nativeFile.absolutePath.replace('\\', '/')}")
databaseCleanable = Starbound.CLEANER.register(this, database::close)
if (legacyFile.exists()) {
legacyDatabase = BTreeDB5(legacyFile)
@ -88,7 +92,6 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
}
database.createStatement().use {
it.execute("PRAGMA locking_mode=EXCLUSIVE")
it.execute("PRAGMA journal_mode=WAL")
it.execute("""
@ -495,7 +498,7 @@ class ServerUniverse(folder: File? = null) : Universe(), Closeable {
carrier.execute {
legacyDatabase?.close()
database.commit()
database.close()
databaseCleanable.clean()
}
carrier.wait(300L, TimeUnit.SECONDS)

View File

@ -31,6 +31,9 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket
@ -183,8 +186,8 @@ class ServerWorld private constructor(
private val wireProcessor = LegacyWireProcessor(this)
override val isClient: Boolean
get() = false
override val connectionID: Int
get() = 0
/**
* this method does not block if pacer is null (safe to use with runBlocking {})
@ -254,7 +257,7 @@ class ServerWorld private constructor(
.toList()
pacer?.consume(10)
val broken = entity.damage(occupySpaces, sourcePosition, actualDamage)
val broken = entity.damageTileEntity(occupySpaces, sourcePosition, actualDamage)
if (source != null && broken) {
source.receiveMessage("tileEntityBroken", jsonArrayOf(
@ -470,10 +473,10 @@ class ServerWorld private constructor(
val random = if (hint == null) random(template.seed) else random()
try {
LOGGER.info("Trying to find player spawn position...")
LOGGER.debug("Trying to find player spawn position...")
var pos = hint ?: CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble())
var previous = pos
LOGGER.info("Trying to find player spawn position near $pos...")
LOGGER.debug("Trying to find player spawn position near {}...", pos)
for (t in 0 until Globals.worldServer.playerStartRegionMaximumTries) {
var foundGround = false
@ -520,7 +523,7 @@ class ServerWorld private constructor(
region.forEach { it.chunk.await() }
if (!anyCellSatisfies(spawnRect) { tx, ty, tcell -> tcell.foreground.material.value.collisionKind != CollisionType.NONE } && spawnRect.maxs.y < geometry.size.y) {
LOGGER.info("Found appropriate spawn position at $pos")
LOGGER.debug("Found appropriate spawn position at {}", pos)
return pos
}
@ -531,7 +534,7 @@ class ServerWorld private constructor(
pos = CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble())
if (previous != pos) {
LOGGER.info("Still trying to find player spawn position near $pos...")
LOGGER.debug("Still trying to find player spawn position near {}...", pos)
previous = pos
} else {
break
@ -581,6 +584,22 @@ class ServerWorld private constructor(
return eventLoop.supplyAsync { geometry.region2Chunks(region).mapNotNull { temporaryChunkTicket(it, time, target).get() } }
}
override fun networkDamageNotification(notification: DamageNotificationPacket) {
for (client in clients) {
if (notification.shouldNetworkTo(client)) {
client.client.send(notification)
}
}
}
override fun networkHitRequest(data: HitRequestPacket) {
server.channels.connectionByID(data.destinationConnection)?.send(data)
}
override fun networkDamageRequest(data: DamageRequestPacket) {
server.channels.connectionByID(data.destinationConnection)?.send(data)
}
@JsonFactory
data class MetadataJson(
val playerStart: Vector2d,

View File

@ -51,6 +51,7 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
import kotlin.math.roundToInt
// couples ServerWorld and ServerConnection together,
// allowing ServerConnection client to track ServerWorld state
@ -191,10 +192,18 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
return pos in tickets
}
fun isTracking(pos: Vector2d): Boolean {
return currentlyTrackingRegions.any { it.isInside(pos.x.roundToInt(), pos.y.roundToInt()) }
}
fun isTracking(entity: AbstractEntity): Boolean {
return entityVersions.containsKey(entity.entityID)
}
fun isTracking(entity: Int): Boolean {
return entityVersions.containsKey(entity)
}
fun forget(entity: AbstractEntity, reason: AbstractEntity.RemovalReason) {
val version = entityVersions.remove(entity.entityID)
@ -312,7 +321,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
val trackingEntities = ObjectAVLTreeSet<AbstractEntity>()
for (region in trackingRegions) {
trackingEntities.addAll(world.entityIndex.query(region.toDoubleAABB(), filter = { it.connectionID != client.connectionID }))
trackingEntities.addAll(world.entityIndex.query(region.toDoubleAABB(), filter = { it.visibleToRemotes && it.connectionID != client.connectionID }))
}
val unseen = IntArrayList(entityVersions.keys)

View File

@ -304,11 +304,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
final override fun shutdown() {
if (!isShutdown) {
LOGGER.info("$name shutdown initiated")
isShutdown = true
if (currentThread() === this || state == State.NEW) {
LOGGER.info("Shutdown initiated")
while (eventLoopIteration()) {}
while (scheduledQueue.isNotEmpty()) {
@ -330,6 +329,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
isRunning = false
}
} else {
LOGGER.info("$name shutdown initiated")
// wake up thread
LockSupport.unpark(this)
}

View File

@ -9,6 +9,7 @@ import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
@ -263,6 +264,16 @@ class EntityIndex(val geometry: WorldGeometry) {
return entriesDirect
}
fun query(line: Line2d, filter: Predicate<in AbstractEntity> = Predicate { true }): MutableList<AbstractEntity> {
val entriesDirect = ObjectArrayList<AbstractEntity>()
iterate(line, visitor = {
if (filter.test(it)) entriesDirect.add(it)
})
return entriesDirect
}
fun any(rect: AABB, filter: Predicate<in AbstractEntity> = Predicate { true }, withEdges: Boolean = true): Boolean {
return walk(rect, withEdges = withEdges, visitor = {
if (filter.test(it)) KOptional(true) else KOptional()
@ -301,6 +312,10 @@ class EntityIndex(val geometry: WorldGeometry) {
walk<Unit>(rect, { visitor(it); KOptional() }, withEdges)
}
fun iterate(line: Line2d, visitor: (AbstractEntity) -> Unit) {
walk<Unit>(line) { visitor(it); KOptional() }
}
fun <V> walk(rect: AABB, visitor: (AbstractEntity) -> KOptional<V>, withEdges: Boolean = true): KOptional<V> {
val seen = IntAVLTreeSet()
@ -329,4 +344,9 @@ class EntityIndex(val geometry: WorldGeometry) {
return KOptional()
}
fun <V> walk(line: Line2d, visitor: (AbstractEntity) -> KOptional<V>): KOptional<V> {
// TODO: actually implement this
return walk(line.aabb, visitor)
}
}

View File

@ -55,10 +55,35 @@ fun interface TileRayFilter {
* [x] and [y] are wrapped around positions
*/
fun test(cell: AbstractCell, fraction: Double, x: Int, y: Int, normal: RayDirection, borderX: Double, borderY: Double): RayFilterResult
}
val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE }
val NonEmptyFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.of(!state.foreground.material.value.collisionKind.isEmpty) }
object Never : TileRayFilter {
override fun test(
cell: AbstractCell,
fraction: Double,
x: Int,
y: Int,
normal: RayDirection,
borderX: Double,
borderY: Double
): RayFilterResult {
return RayFilterResult.CONTINUE
}
}
object Solid : TileRayFilter {
override fun test(
cell: AbstractCell,
fraction: Double,
x: Int,
y: Int,
normal: RayDirection,
borderX: Double,
borderY: Double
): RayFilterResult {
return RayFilterResult.of(cell.foreground.material.value.collisionKind.isSolidCollision)
}
}
}
fun ICellAccess.castRay(startPos: Vector2d, direction: Vector2d, length: Double, filter: TileRayFilter) = castRay(startPos, startPos + direction * length, filter)

View File

@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
@ -28,12 +27,14 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
@ -231,9 +232,15 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
*/
val random: RandomGenerator = random()
abstract val isClient: Boolean
val isClient: Boolean
get() = connectionID != 0
val isServer: Boolean
get() = !isClient
get() = connectionID == 0
/**
* 0 means server, anything else represent client's connection ID
*/
abstract val connectionID: Int
// generic lock
val lock = ReentrantLock()
@ -332,6 +339,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
CompletableFuture.allOf(*tasks.toTypedArray()).join()
}
// FIXME: this will throw an exception if entity is removing another entity
// which haven't ticked yet
entityList.forEach {
try {
if (it.isInWorld) // entities might remove other entities during tick
@ -514,6 +523,54 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
*/
abstract fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List<Pair<Vector2i, TileModification>>
fun addDamageNotification(notification: DamageNotificationPacket) {
pushRemoteDamageNotification(notification)
networkDamageNotification(notification)
}
fun addHitRequest(data: HitRequestPacket) {
if (data.destinationConnection == connectionID)
pushRemoteHitRequest(data)
else
networkHitRequest(data)
}
fun addDamageRequest(data: DamageRequestPacket) {
if (data.destinationConnection == connectionID)
pushRemoteDamageRequest(data)
else
networkDamageRequest(data)
}
open fun pushRemoteDamageNotification(notification: DamageNotificationPacket) {
val entity = entities[notification.source]
if (entity != null && !entity.isRemote && entity.entityID != notification.notification.targetEntityId) {
entity.damagedOther(notification)
}
}
fun pushRemoteHitRequest(data: HitRequestPacket) {
require(data.destinationConnection == connectionID) { "RemoteDamageRequest routed to wrong DamageManager" }
val inflictor = entities[data.inflictor] ?: return
check(!inflictor.isRemote)
inflictor.hitOther(data)
}
fun pushRemoteDamageRequest(data: DamageRequestPacket) {
require(data.destinationConnection == connectionID) { "RemoteDamageRequest routed to wrong DamageManager" }
val target = entities[data.target] ?: return
check(!target.isRemote)
for (notification in target.experienceDamage(data)) {
addDamageNotification(DamageNotificationPacket(data.request.sourceEntityId, notification))
}
}
protected abstract fun networkDamageNotification(notification: DamageNotificationPacket)
protected abstract fun networkHitRequest(data: HitRequestPacket)
protected abstract fun networkDamageRequest(data: DamageRequestPacket)
companion object {
private val LOGGER = LogManager.getLogger()

View File

@ -5,20 +5,30 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.DamageData
import ru.dbotthepony.kstarbound.defs.DamageNotification
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.DamageType
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.HitType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -27,9 +37,13 @@ import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.EntityIndex
import ru.dbotthepony.kstarbound.world.TileRayFilter
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream
import java.util.function.Consumer
import java.util.function.Predicate
abstract class AbstractEntity : Comparable<AbstractEntity> {
abstract val position: Vector2d
@ -81,6 +95,16 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
abstract val type: EntityType
// in original engine call this "masterOnly" and condition is inverted
/**
* Whenever this entity should be networked to remote(s) (client -> server or server -> clients)
*
* If this value is changed once entity has been spawned in world
* entity tracker behavior is undefined
*/
open val visibleToRemotes: Boolean
get() = true
var isEphemeral: Boolean = false
protected set
@ -138,6 +162,21 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
open val collisionArea: AABB
get() = AABB.NEVER
/**
* in local coordinates because shoddy-ness (plural shoddies)
*
* in all seriousness though, these might get created by Lua code and they
* want damage source to be "pinned" to location where it was created
*/
open val damageSources: Collection<DamageSource>
get() = listOf()
/**
* Hitbox utilized to hitscan damage lines against
*/
open val damageHitbox: Collection<Poly>
get() = listOf()
open fun onNetworkUpdate() {
}
@ -176,12 +215,14 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
spatialEntry = world.entityIndex.Entry(this)
onJoinWorld(world)
if (world is ClientWorld && !isRemote) {
val connection = world.client.activeConnection
// TODO: incomplete
connection?.send(EntityCreatePacket(type, writeNetwork(connection.isLegacy), networkGroup.write(0L, connection.isLegacy).first, entityID))
} else if (world is ServerWorld) {
world.clients.forEach { it.evaluateShouldTrack(this) }
if (visibleToRemotes) {
if (world is ClientWorld && !isRemote) {
val connection = world.client.activeConnection
// TODO: incomplete
connection?.send(EntityCreatePacket(type, writeNetwork(connection.isLegacy), networkGroup.write(0L, connection.isLegacy).first, entityID))
} else if (world is ServerWorld) {
world.clients.forEach { it.evaluateShouldTrack(this) }
}
}
}
@ -225,12 +266,200 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
var isRemote: Boolean = false
private fun isDamageAuthoritative(target: AbstractEntity): Boolean {
// Damage manager is authoritative if either one of the entities is
// masterOnly, OR the manager is server-side and both entities are
// server-side master entities, OR the damage manager is server-side and both
// entities are different clients, OR if the manager is client-side and the
// source is client-side master and the target is server-side master, OR if
// the manager is client-side and the target is client-side master.
//
// This means that PvE and EvP are both decided on the player doing the
// hitting or getting hit, and PvP is decided on the server, except for
// master-only entities whose interactions are always decided on the machine
// they are residing on.
val causeClient = connectionID
val targetClient = target.connectionID
val thisID = world.connectionID
if (!visibleToRemotes || !target.visibleToRemotes) {
return true
} else if (causeClient == 0 && targetClient == 0) {
return thisID == 0
} else if (causeClient != 0 && targetClient != 0 && causeClient != targetClient) {
return thisID == 0
} else if (targetClient == 0) {
return causeClient == thisID
} else {
return targetClient == thisID
}
}
/**
* Called in narrow phase when determining which entities can be hit
*/
fun canBeHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): Boolean {
if (!source.team.canDamage(team.get(), attacker === this)) {
return false
}
if (source.rayCheck) {
val damageSource = inflictor ?: attacker ?: return true // nothing to check against
// Original ray check is utterly broken, because it checks for line of sight between this entity and attacker entity,
// and not the inflictor. What this means, is that if you throw a grenade, and grenade is sourced to Player, then
// when grenade hits someone, rayCheck will check for line of sight between victim and Player (attacker), and not between victim and grenade (inflictor)
if (source.damageArea.isLeft) {
val poly = source.damageArea.left()
val overlap = damageSource.metaBoundingBox.overlap(poly.aabb)
// TODO: this should check for tile physics geometry, and not cell spaces
// Also, this seems to be flawed, since it casts ray from overlap center to damage source position
// So it is possible, in some setups, to attack through 1 tile wide walls
if (!overlap.isEmpty && world.castRay(overlap.centre, damageSource.position, TileRayFilter.Solid).hitTile != null) {
return false
}
} else {
val line = source.damageArea.right()
for (poly in damageHitbox) {
val intersect = poly.intersect(line)
// TODO: this should check for tile physics geometry, and not cell spaces
if (intersect != null && intersect.second.point != null && world.castRay(line.p0, intersect.second.point!!, TileRayFilter.Solid).hitTile != null) {
return false
}
}
}
}
return true
}
/**
* Called in broad phase when determining which entities can be hit
*/
open fun potentiallyCanBeHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): Boolean {
return false
}
/**
* Called in narrow phase when entity is determined to be hittable by [potentiallyCanBeHit] and [canBeHit]
*/
open fun queryHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): HitType? {
return null
}
private data class DamageEvent(val group: Either<String, Int>, val expiresAt: Double)
private val damageEvents = ObjectArrayList<DamageEvent>()
/**
* Called on local entities (isRemote = false) when getting hit.
*
* Returns list of damage notifications, which are used for both displaying damage
* numbers and notifying entities they have damaged something
*/
open fun experienceDamage(damage: DamageRequestPacket): List<DamageNotification> {
return listOf()
}
/**
* Called on local entities (isRemote = false) when this entity has hit something (possibly oneself)
*
* Called only for inflictors
*/
open fun hitOther(damage: HitRequestPacket) {}
/**
* Called on local entities (isRemote = false) when this entity has damaged something
* (possibly oneself, when entity has generated [DamageNotification])
*
* Called only for attackers.
*/
open fun damagedOther(notification: DamageNotificationPacket) {}
open fun tick(delta: Double) {
mailbox.executeQueuedTasks()
if (networkGroup.upstream.isInterpolating) {
networkGroup.upstream.tickInterpolation(delta)
}
damageEvents.removeIf { it.expiresAt <= world.sky.time }
val world = world
// It took a good amount of pondering around original code base, but here is how it works:
// A - Player
// B - Arrow
// C - Target
// When A fires B, B registers its damage source with sourceEntityId = A
// When B reaches C, B receives hitOther() event, while C receives applyDamage() event
// if C is vulnerable to damage specified, it will generate one, or more DamageNotifications
// which, in turn, will trigger A's damagedOther() event
//
// This presents us with next problems:
// * A will be completely unaware when hitting invulnerable entities
// (such as hitting NPCs with shield raised, and damage fully negated)
// * B will be unaware, if it actually damaged C, B only knows it has tried to damage C.
// But we also must keep in mind that "projectiles" and similar entities remove themselves
// once they hit something, so they won't live to see applyDamage() to be called on remote entities
for (bsource in damageSources) {
var source = bsource
if (source.trackSourceEntity) {
source += position
}
val attacker = world.entities[source.sourceEntityId] ?: this
val predicate = Predicate<AbstractEntity> { it !== this && isDamageAuthoritative(it) && it.potentiallyCanBeHit(source, attacker, this) && it.canBeHit(source, attacker, this) }
val hitEntities = source.damageArea.map({ world.entityIndex.query(it.aabb, predicate) }, { world.entityIndex.query(it, predicate) })
for (entity in hitEntities) {
val hitType = entity.queryHit(source, attacker, this) ?: return
val eventGroup: Either<String, Int>
if (source.damageRepeatGroup != null) {
eventGroup = Either.left(source.damageRepeatGroup!!)
} else {
// Original engine checks against attacker and not against inflictor
// TODO: allow mods to specify whenever invulnerability frames should be per-inflictor (new behavior) or per-attacker (old behavior)
// If both attacker and inflictor are nulls, then it means damage comes from environment
// but shouldn't it be impossible? Example - lava doesn't damage directly, "on fire" is what damages entities
eventGroup = Either.right(entityID)
}
if (entity.damageEvents.any { it.group == eventGroup }) {
return
}
val invulnerabilityFrames = source.damageRepeatTimeout ?: 1.0
entity.damageEvents.add(DamageEvent(eventGroup, world.sky.time + invulnerabilityFrames))
val data = DamageData(
hitType,
source.damageType,
source.damage,
source.knockbackMomentum(world.geometry, position),
source.sourceEntityId,
entityID,
source.damageSourceKind,
source.statusEffects
)
// I tried my best to make something more sane than what is
// in original sources, but after 20 hours of tinkering
// (trying to make it meaningful AND not deviate from original behavior), I gave up.
//
// At least we won't be as unoptimized as original code
world.addHitRequest(HitRequestPacket(entityID, entity.entityID, data))
if (data.damageType != DamageType.NO_DAMAGE)
world.addDamageRequest(DamageRequestPacket(entityID, entity.entityID, data))
}
}
}
open fun render(client: StarboundClient, layers: LayeredRenderer) {

View File

@ -1,10 +1,13 @@
package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.item.ToolItem
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
@ -52,4 +55,15 @@ abstract class HumanoidActorEntity() : ActorEntity() {
var chestCosmeticItem by armorNetworkGroup.add(networkedItem())
var legsCosmeticItem by armorNetworkGroup.add(networkedItem())
var backCosmeticItem by armorNetworkGroup.add(networkedItem())
override val damageSources: List<DamageSource> get() {
val damageSources = ObjectArrayList<DamageSource>()
val primaryHandItem = primaryHandItem
val secondaryHandItem = secondaryHandItem
if (primaryHandItem is ToolItem) damageSources.addAll(primaryHandItem.damageSources)
if (secondaryHandItem is ToolItem) damageSources.addAll(secondaryHandItem.damageSources)
return damageSources
}
}

View File

@ -680,7 +680,7 @@ class PlantEntity() : TileEntity() {
moveSpaces()
}
override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
override fun damageTileEntity(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
if (damageSpaces.isEmpty())
return false

View File

@ -24,7 +24,6 @@ import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import kotlin.math.PI
/**
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
@ -93,7 +92,7 @@ abstract class TileEntity : AbstractEntity() {
*/
abstract val roots: Collection<Vector2i>
abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
abstract fun damageTileEntity(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
private var needToUpdateSpaces = false
private var needToUpdateRoots = false

View File

@ -10,6 +10,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.Table
@ -33,9 +34,12 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.DamageData
import ru.dbotthepony.kstarbound.defs.DamageNotification
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.HitType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
@ -44,7 +48,6 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.io.Vector2iCodec
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream
@ -76,6 +79,7 @@ import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.server.world.LegacyWireProcessor
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.asStringOrNull
@ -83,13 +87,16 @@ import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream
import java.util.Collections
import java.util.HashMap
import java.util.random.RandomGenerator
import kotlin.math.min
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity {
override fun deserialize(data: JsonObject) {
@ -314,7 +321,28 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
val offeredQuests = NetworkedList(QuestArcDescriptor.CODEC, QuestArcDescriptor.LEGACY_CODEC).also { networkGroup.upstream.add(it) }
val turnInQuests = NetworkedList(InternedStringCodec).also { networkGroup.upstream.add(it) }
val damageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkGroup.upstream.add(it) }
val customDamageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkGroup.upstream.add(it) }
private val damageSources0 = ManualLazy {
val sources = ObjectArrayList(customDamageSources)
val orientation = orientation
if (orientation != null) {
val touchDamageConfig = mergeJson(config.value.touchDamage.deepCopy(), orientation.touchDamage)
if (!touchDamageConfig.isJsonNull) {
sources.add(Starbound.gson.fromJson(touchDamageConfig, DamageSource::class.java).copy(sourceEntityId = entityID, team = team.get()))
}
}
sources
}.also { orientationLazies.add(it) }
init {
customDamageSources.addListener(Runnable { damageSources0.invalidate() })
}
override val damageSources: List<DamageSource> by damageSources0
// don't interpolate scripted animation parameters
val scriptedAnimationParameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkGroup.upstream.add(it, false) }
@ -377,6 +405,18 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
}
}
val volume by ManualLazy {
if (occupySpaces.isEmpty()) {
Poly(AABB(position, position + Vector2d.POSITIVE_XY))
} else {
Poly.quickhull(occupySpaces.map { it.toDoubleVector() })
}
}.also { orientationLazies.add(it); spacesLazies.add(it) }
// this will cause false negative when we have material spaces and rayCheck is used
//override val damageHitbox: Collection<Poly>
// get() = listOf(volume)
protected open fun parametersUpdated() {
parametersLazies.forEach { it.invalidate() }
}
@ -629,7 +669,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason)
val doSmash = health <= 0.0 || lookupProperty("smashOnBreak") { JsonPrimitive(config.value.smashOnBreak) }.asBoolean
val doSmash = config.value.smashable && (health <= 0.0 || lookupProperty("smashOnBreak") { JsonPrimitive(config.value.smashOnBreak) }.asBoolean)
fun spawnRandomItems(poolName: String, optionsName: String, seedName: String): Boolean {
val dropPool = lookupProperty(poolName) { JsonPrimitive("") }.asString
@ -700,7 +740,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
}
}
override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
override fun damageTileEntity(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
if (unbreakable)
return false
@ -717,6 +757,44 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return tileHealth.isDead
}
override fun potentiallyCanBeHit(source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity?): Boolean {
return health >= 0.0 && config.value.smashable && !unbreakable
}
override fun queryHit(source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity?): HitType? {
if (source.intersect(volume))
return HitType.HIT
return null
}
override fun experienceDamage(damage: DamageRequestPacket): List<DamageNotification> {
if (!config.value.smashable || health <= 0.0 || unbreakable)
return emptyList()
val dmg = min(health, damage.request.damage)
health -= dmg
// if you don't play Duke Nukem 3D
if (health <= 0.0) {
// you
remove(RemovalReason.DYING)
}
return listOf(
DamageNotification(
damage.request.sourceEntityId,
entityID,
position,
damage.request.damage,
dmg,
if (health <= 0.0) HitType.KILL else HitType.HIT,
damage.request.kind,
config.value.damageMaterialKind
)
)
}
override fun callScript(fnName: String, vararg arguments: Any?): Array<Any?> {
return lua.invokeGlobal(fnName, *arguments)
}