Paced tile damage/modifications, to root out evil actors

This commit is contained in:
DBotThePony 2024-04-20 22:38:13 +07:00
parent f89afb80bb
commit 5c13567fed
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 103 additions and 14 deletions

View File

@ -42,8 +42,6 @@ class DamageTileGroupPacket(val tiles: Collection<Vector2i>, val isBackground: B
} }
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
connection.enqueue { connection.tracker?.damageTiles(tiles, isBackground, sourcePosition, damage)
damageTiles(tiles, isBackground, sourcePosition, damage)
}
} }
} }

View File

@ -21,13 +21,7 @@ class ModifyTileListPacket(val modifications: Collection<Pair<Vector2i, TileModi
} }
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
val inWorld = connection.enqueue { val inWorld = connection.tracker?.modifyTiles(modifications, allowEntityOverlap) != null
val unapplied = applyTileModifications(modifications, allowEntityOverlap)
if (unapplied.isNotEmpty()) {
connection.send(TileModificationFailurePacket(unapplied))
}
}
if (!inWorld) { if (!inWorld) {
connection.send(TileModificationFailurePacket(modifications)) connection.send(TileModificationFailurePacket(modifications))

View File

@ -8,6 +8,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
@ -36,6 +37,7 @@ import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.Pacer
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
@ -159,7 +161,10 @@ class ServerWorld private constructor(
override val isClient: Boolean override val isClient: Boolean
get() = false get() = false
fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { /**
* this method does not block if pacer is null (safe to use with runBlocking {})
*/
suspend fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null, pacer: Pacer? = null): TileDamageResult {
if (damage.amount <= 0.0) if (damage.amount <= 0.0)
return TileDamageResult.NONE return TileDamageResult.NONE
@ -188,6 +193,7 @@ class ServerWorld private constructor(
.filter { p -> actualPositions.any { it.first == p } } .filter { p -> actualPositions.any { it.first == p } }
.toList() .toList()
pacer?.consume(10)
val broken = entity.damage(occupySpaces, sourcePosition, damage) val broken = entity.damage(occupySpaces, sourcePosition, damage)
if (source != null && broken) { if (source != null && broken) {
@ -205,6 +211,7 @@ class ServerWorld private constructor(
// entity. // entity.
if (tileEntityResult == TileDamageResult.NONE || damage.type.isPenetrating) { if (tileEntityResult == TileDamageResult.NONE || damage.type.isPenetrating) {
chunk ?: continue chunk ?: continue
pacer?.consume()
val (result, health, tile) = chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source) val (result, health, tile) = chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source)
topMost = topMost.coerceAtLeast(result) topMost = topMost.coerceAtLeast(result)
@ -225,6 +232,10 @@ class ServerWorld private constructor(
} }
override fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean): List<Pair<Vector2i, TileModification>> { override fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean): List<Pair<Vector2i, TileModification>> {
return runBlocking { applyTileModifications(modifications, allowEntityOverlap, ignoreTileProtection, null) }
}
suspend fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false, pacer: Pacer?): List<Pair<Vector2i, TileModification>> {
val unapplied = ArrayList(modifications) val unapplied = ArrayList(modifications)
var size: Int var size: Int
@ -239,6 +250,7 @@ class ServerWorld private constructor(
continue continue
if (modification.allowed(this, pos, allowEntityOverlap)) { if (modification.allowed(this, pos, allowEntityOverlap)) {
pacer?.consume()
modification.apply(this, pos, allowEntityOverlap) modification.apply(this, pos, allowEntityOverlap)
itr.remove() itr.remove()
} }

View File

@ -3,13 +3,17 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.future.await import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
@ -18,6 +22,7 @@ import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.defs.SpawnTarget import ru.dbotthepony.kstarbound.defs.SpawnTarget
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.world.FlyingType import ru.dbotthepony.kstarbound.defs.world.FlyingType
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
@ -28,12 +33,15 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.EnvironmentUpdatePa
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileModificationFailurePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.Pacer
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
@ -55,6 +63,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
private val tickets = HashMap<ChunkPos, Ticket>() private val tickets = HashMap<ChunkPos, Ticket>()
private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>() private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
private val entityVersions = Int2LongOpenHashMap() private val entityVersions = Int2LongOpenHashMap()
private val scope = CoroutineScope(world.eventLoop.coroutines + SupervisorJob())
init { init {
entityVersions.defaultReturnValue(-1L) entityVersions.defaultReturnValue(-1L)
@ -66,6 +75,47 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
client.worldID = world.worldID client.worldID = world.worldID
} }
private data class DamageTileEntry(val positions: Collection<Vector2i>, val isBackground: Boolean, val sourcePosition: Vector2d, val damage: TileDamage, val source: AbstractEntity? = null)
private val damageTilesQueue = Channel<DamageTileEntry>(64) // 64 pending tile damages should be enough
private val tileModificationBudget = Pacer.actionsPerSecond(actions = 512, handicap = 2048) // TODO: make this configurable
private val modifyTilesQueue = Channel<Pair<Collection<Pair<Vector2i, TileModification>>, Boolean>>(64)
private suspend fun damageTilesLoop() {
while (true) {
val (positions, isBackground, sourcePosition, damage, source) = damageTilesQueue.receive()
world.damageTiles(positions, isBackground, sourcePosition, damage, source, tileModificationBudget)
}
}
fun damageTiles(positions: Collection<Vector2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null) {
damageTilesQueue.trySend(DamageTileEntry(positions, isBackground, sourcePosition, damage, source))
}
private suspend fun modifyTilesLoop() {
while (true) {
val (modifications, allowEntityOverlap) = modifyTilesQueue.receive()
try {
val unapplied = world.applyTileModifications(modifications, allowEntityOverlap, pacer = tileModificationBudget)
if (unapplied.isNotEmpty()) {
client.send(TileModificationFailurePacket(unapplied))
}
} catch (err: CancellationException) {
client.send(TileModificationFailurePacket(modifications))
}
}
}
fun modifyTiles(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean) {
modifyTilesQueue.trySend(modifications to allowEntityOverlap)
}
init {
scope.launch { damageTilesLoop() }
scope.launch { modifyTilesLoop() }
}
fun send(packet: IPacket) = client.send(packet) fun send(packet: IPacket) = client.send(packet)
// packets which interact with world must be // packets which interact with world must be
@ -301,6 +351,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
// this handles case where player is removed from world and // this handles case where player is removed from world and
// instantly added back because new world rejected us // instantly added back because new world rejected us
world.eventLoop.execute { remove0() } world.eventLoop.execute { remove0() }
damageTilesQueue.close()
modifyTilesQueue.close()
scope.cancel()
} }
} }

View File

@ -0,0 +1,31 @@
package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay
/**
* Allows to perform up to [maxForward] actions per given time window,
* otherwise pauses execution
*/
class Pacer(val maxForward: Int, val delayBetween: Long) {
private val maxForwardNanos = maxForward * delayBetween
private var currentTime = System.nanoTime() - maxForwardNanos
suspend fun consume(actions: Int = 1) {
require(actions >= 1) { "Invalid amount of actions to consume: $actions" }
val time = System.nanoTime()
if (time - currentTime > maxForwardNanos)
currentTime = time - maxForwardNanos
currentTime += delayBetween * (actions - 1)
val diff = (currentTime - time) / 1_000_000L
currentTime += delayBetween
if (diff > 0L) delay(diff)
}
companion object {
fun actionsPerSecond(actions: Int, handicap: Int = 0): Pacer {
return Pacer(handicap, 1_000_000_000L / actions)
}
}
}