Damage handling (not tested because entire system is stupid)
This commit is contained in:
parent
c0c63b9240
commit
ba0db89dcf
@ -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`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>()
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
||||
|
16
src/main/kotlin/ru/dbotthepony/kstarbound/item/ToolItem.kt
Normal file
16
src/main/kotlin/ru/dbotthepony/kstarbound/item/ToolItem.kt
Normal 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()
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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` (
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user