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) {
connection.enqueue {
damageTiles(tiles, isBackground, sourcePosition, damage)
}
connection.tracker?.damageTiles(tiles, isBackground, sourcePosition, damage)
}
}

View File

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

View File

@ -8,6 +8,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABB
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.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.Pacer
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
@ -159,7 +161,10 @@ class ServerWorld private constructor(
override val isClient: Boolean
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)
return TileDamageResult.NONE
@ -188,6 +193,7 @@ class ServerWorld private constructor(
.filter { p -> actualPositions.any { it.first == p } }
.toList()
pacer?.consume(10)
val broken = entity.damage(occupySpaces, sourcePosition, damage)
if (source != null && broken) {
@ -205,6 +211,7 @@ class ServerWorld private constructor(
// entity.
if (tileEntityResult == TileDamageResult.NONE || damage.type.isPenetrating) {
chunk ?: continue
pacer?.consume()
val (result, health, tile) = chunk.damageTile(pos - chunk.pos.tile, isBackground, sourcePosition, damage, source)
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>> {
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)
var size: Int
@ -239,6 +250,7 @@ class ServerWorld private constructor(
continue
if (modification.allowed(this, pos, allowEntityOverlap)) {
pacer?.consume()
modification.apply(this, pos, allowEntityOverlap)
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.ints.Int2LongOpenHashMap
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.IntArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import kotlinx.coroutines.future.await
import kotlinx.coroutines.CancellationException
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 ru.dbotthepony.kstarbound.math.vector.Vector2d
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.WarpAction
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.world.FlyingType
import ru.dbotthepony.kstarbound.network.IPacket
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.LegacyTileUpdatePacket
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.WorldStopPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.Pacer
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
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 tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
private val entityVersions = Int2LongOpenHashMap()
private val scope = CoroutineScope(world.eventLoop.coroutines + SupervisorJob())
init {
entityVersions.defaultReturnValue(-1L)
@ -66,6 +75,47 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
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)
// 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
// instantly added back because new world rejected us
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)
}
}
}