diff --git a/ADDITIONS.md b/ADDITIONS.md index 646a685f..8b77b1af 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -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` diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index e4e64947..8829a5ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -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 } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index efcd0599..c1aed984 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -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), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt index 009cf87e..f18f7bff 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt @@ -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 = 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, +) { + 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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt index 4839e736..fac38c8b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -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> = 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> = 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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index 709f7826..56ec4b34 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -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, ) { 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index 256732f5..c7bfe473 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -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() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt index 1c2bdd2c..0ec6b13e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ActiveItemStack.kt @@ -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) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index df1d6dbb..52e55192 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ToolItem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ToolItem.kt new file mode 100644 index 00000000..e78b8642 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ToolItem.kt @@ -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 + get() = listOf() + + val shieldPolys: Collection + get() = listOf() + + val forceRegions: Collection + get() = listOf() +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt index 73f37b02..d58d9644 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -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)) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt index 8e1ebe52..9ac2251a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt @@ -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) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index e51c7625..8df368db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt new file mode 100644 index 00000000..fcdfed16 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt new file mode 100644 index 00000000..5119f481 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/HitRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/HitRequestPacket.kt new file mode 100644 index 00000000..3aa333e3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/HitRequestPacket.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt index 108d292f..91cc4232 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt @@ -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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index b908641e..c402b68e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt index 0398f125..60607ced 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWorldStorage.kt @@ -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` ( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt index b0f29af1..aa93ea0b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt @@ -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 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt index 00255fd3..78440316 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 75917297..eb459a2c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 0df23a6a..80751753 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -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() 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt index bb3c5bb2..8245f23c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt @@ -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) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt index 4a40c8b4..16bcb4d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt @@ -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 = Predicate { true }): MutableList { + val entriesDirect = ObjectArrayList() + + iterate(line, visitor = { + if (filter.test(it)) entriesDirect.add(it) + }) + + return entriesDirect + } + fun any(rect: AABB, filter: Predicate = 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(rect, { visitor(it); KOptional() }, withEdges) } + fun iterate(line: Line2d, visitor: (AbstractEntity) -> Unit) { + walk(line) { visitor(it); KOptional() } + } + fun walk(rect: AABB, visitor: (AbstractEntity) -> KOptional, withEdges: Boolean = true): KOptional { val seen = IntAVLTreeSet() @@ -329,4 +344,9 @@ class EntityIndex(val geometry: WorldGeometry) { return KOptional() } + + fun walk(line: Line2d, visitor: (AbstractEntity) -> KOptional): KOptional { + // TODO: actually implement this + return walk(line.aabb, visitor) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt index b735da12..2b1699a5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index c9c72ed0..a42b920a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -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, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List> + 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index b36d2783..8f4a8429 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -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 { abstract val position: Vector2d @@ -81,6 +95,16 @@ abstract class AbstractEntity : Comparable { 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 { 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 + get() = listOf() + + /** + * Hitbox utilized to hitscan damage lines against + */ + open val damageHitbox: Collection + get() = listOf() + open fun onNetworkUpdate() { } @@ -176,12 +215,14 @@ abstract class AbstractEntity : Comparable { 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 { 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, val expiresAt: Double) + private val damageEvents = ObjectArrayList() + + /** + * 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 { + 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 { 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 + + 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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt index 855916f3..f7fa48a0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt @@ -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 get() { + val damageSources = ObjectArrayList() + val primaryHandItem = primaryHandItem + val secondaryHandItem = secondaryHandItem + + if (primaryHandItem is ToolItem) damageSources.addAll(primaryHandItem.damageSources) + if (secondaryHandItem is ToolItem) damageSources.addAll(secondaryHandItem.damageSources) + + return damageSources + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt index 043235d3..89699f2a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt @@ -680,7 +680,7 @@ class PlantEntity() : TileEntity() { moveSpaces() } - override fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { + override fun damageTileEntity(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { if (damageSpaces.isEmpty()) return false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt index 4f019423..0b9ac562 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -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 - abstract fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean + abstract fun damageTileEntity(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean private var needToUpdateSpaces = false private var needToUpdateRoots = false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 54114608..73c5d5f9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -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) : TileEntity(), ScriptedEntity { override fun deserialize(data: JsonObject) { @@ -314,7 +321,28 @@ open class WorldObject(val config: Registry.Entry) : 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 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) : 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 + // get() = listOf(volume) + protected open fun parametersUpdated() { parametersLazies.forEach { it.invalidate() } } @@ -629,7 +669,7 @@ open class WorldObject(val config: Registry.Entry) : 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) : TileEntit } } - override fun damage(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { + override fun damageTileEntity(damageSpaces: List, source: Vector2d, damage: TileDamage): Boolean { if (unbreakable) return false @@ -717,6 +757,44 @@ open class WorldObject(val config: Registry.Entry) : 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 { + 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 { return lua.invokeGlobal(fnName, *arguments) }