From ba0db89dcf858f31fe63f8e57a139b8a6365c86e Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Thu, 25 Apr 2024 16:13:35 +0700
Subject: [PATCH] Damage handling (not tested because entire system is stupid)

---
 ADDITIONS.md                                  |   2 +
 .../ru/dbotthepony/kstarbound/Starbound.kt    |   4 +-
 .../kstarbound/client/world/ClientWorld.kt    |  19 +-
 .../ru/dbotthepony/kstarbound/defs/Damage.kt  | 101 ++++++++
 .../defs/object/ObjectDefinition.kt           |  10 +-
 .../defs/object/ObjectOrientation.kt          |   7 +-
 .../ru/dbotthepony/kstarbound/io/Streams.kt   |  18 ++
 .../kstarbound/item/ActiveItemStack.kt        |   8 +-
 .../dbotthepony/kstarbound/item/ItemStack.kt  |   4 +
 .../dbotthepony/kstarbound/item/ToolItem.kt   |  16 ++
 .../lua/bindings/WorldObjectBindings.kt       |   4 +-
 .../ru/dbotthepony/kstarbound/math/Line2d.kt  |   9 +
 .../kstarbound/network/PacketRegistry.kt      |   9 +-
 .../packets/DamageNotificationPacket.kt       |  45 ++++
 .../network/packets/DamageRequestPacket.kt    |  42 +++
 .../network/packets/HitRequestPacket.kt       |  42 +++
 .../syncher/NetworkedStatefulItemStack.kt     |  96 +++----
 .../kstarbound/server/StarboundServer.kt      |   5 +-
 .../server/world/LegacyWorldStorage.kt        |   1 -
 .../server/world/ServerSystemWorld.kt         |   5 +-
 .../kstarbound/server/world/ServerUniverse.kt |   7 +-
 .../kstarbound/server/world/ServerWorld.kt    |  33 ++-
 .../server/world/ServerWorldTracker.kt        |  11 +-
 .../kstarbound/util/BlockableEventLoop.kt     |   4 +-
 .../kstarbound/world/EntityIndex.kt           |  20 ++
 .../kstarbound/world/Raycasting.kt            |  31 ++-
 .../ru/dbotthepony/kstarbound/world/World.kt  |  65 ++++-
 .../world/entities/AbstractEntity.kt          | 241 +++++++++++++++++-
 .../world/entities/HumanoidActorEntity.kt     |  14 +
 .../world/entities/tile/PlantEntity.kt        |   2 +-
 .../world/entities/tile/TileEntity.kt         |   3 +-
 .../world/entities/tile/WorldObject.kt        |  86 ++++++-
 32 files changed, 851 insertions(+), 113 deletions(-)
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/item/ToolItem.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageNotificationPacket.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/DamageRequestPacket.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/HitRequestPacket.kt

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<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,
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<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,
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<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>()
 
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<DamageSource>
+		get() = listOf()
+
+	val shieldPolys: Collection<Poly>
+		get() = listOf()
+
+	val forceRegions: Collection<PhysicsForceRegion>
+		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<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)
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<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)
+	}
 }
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<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()
 
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<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) {
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<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
+	}
 }
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<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
+	override fun damageTileEntity(damageSpaces: List<Vector2i>, 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<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
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<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)
 	}