Paced tile damage/modifications, to root out evil actors
This commit is contained in:
parent
f89afb80bb
commit
5c13567fed
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
src/main/kotlin/ru/dbotthepony/kstarbound/util/Pacer.kt
Normal file
31
src/main/kotlin/ru/dbotthepony/kstarbound/util/Pacer.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user