Semi-fix microdungeons overlap

This commit is contained in:
DBotThePony 2024-04-28 18:18:00 +07:00
parent d141bb64b0
commit 0854baa986
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 98 additions and 49 deletions

View File

@ -135,10 +135,10 @@ val color: TileColor = TileColor.DEFAULT
## Deterministic world generation ## Deterministic world generation
In new engine, entirety of world generation is made deterministic. What this means that given one world seed, engine will In new engine, entirety of world generation is made deterministic. What this means that given one world seed, engine will
generate _exactly the same_ world each time it is requested to generate one (given prototype definitions which influence generate _exactly the same_ (on best effort*) world each time it is requested to generate one (given prototype definitions which influence
world generation are the same between generations). world generation are the same between generations).
To put it simply, when you visit a planet on your friend's server, it is _guaranteed_ that in your singleplayer To put it simply, when you visit a planet on your friend's server, it is expected* that in your singleplayer
or on other server, given same set of mods installed (and both players are using new engine server or new engine client), or on other server, given same set of mods installed (and both players are using new engine server or new engine client),
you will get exactly the same planet as you saw before. you will get exactly the same planet as you saw before.
@ -159,6 +159,13 @@ there is `seed` specified for such world `/instance_worlds.config`. And since va
If you are mod creator, **PLEASE** update your mod(s), and remove `seed` from your dungeon worlds! If you are mod creator, **PLEASE** update your mod(s), and remove `seed` from your dungeon worlds!
Both new and old engines will provide random seed for you if you don't specify one inside `/instance_worlds.config`. Both new and old engines will provide random seed for you if you don't specify one inside `/instance_worlds.config`.
*On best effort - due to how worldgen code flow is structured, engine _may_ rearrange generation events, which can yield
_slightly_ different results from execution to execution,
such as one microdungeon taking precedence over another microdungeon
if they happen to generate in proximity on chunk border (one dungeon generated in chunk A, second generated in chunk B,
and they happened to overlap each other),
and which one gets placed is determined by who finishes generating first.
--------------- ---------------
## Behavior ## Behavior

View File

@ -94,8 +94,8 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
// entities themselves to be removed // entities themselves to be removed
private val tileEntitiesToRemove = HashSet<TileEntity>(2048, 0.5f) private val tileEntitiesToRemove = HashSet<TileEntity>(2048, 0.5f)
private val touchedTiles = HashSet<Vector2i>(16384, 0.5f) val touchedTiles = HashSet<Vector2i>(16384, 0.5f)
private val protectTile = HashSet<Vector2i>(16384, 0.5f) val protectTile = HashSet<Vector2i>(16384, 0.5f)
private val boundingBoxes = ArrayList<AABBi>() private val boundingBoxes = ArrayList<AABBi>()
@ -624,20 +624,6 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
} }
} }
}.await() }.await()
if (targetChunkState != ChunkState.FULL) {
// and finally, schedule chunks to be loaded into FULL state
// this way, dungeons won't get cut off when chunks being saved
// to disk because of dungeon bleeding into neighbour chunks who
// never get promoted further
// But this might trigger cascading world generation
// (dungeon generates another dungeon, and another, and so on),
// tough, so need to take care!
for (box in boundingBoxes) {
// specify timer as 0 so ticket gets removed on next world tick
parent.temporaryChunkTicket(box, 0, ChunkState.FULL)
}
}
} finally { } finally {
tickets.forEach { it.cancel() } tickets.forEach { it.cancel() }
} }

View File

@ -1,20 +1,14 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.gson.JsonArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.guava.immutableList import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries
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
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -37,10 +31,8 @@ import ru.dbotthepony.kstarbound.defs.world.Biome
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.util.ExecutionTimePacer
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64 import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
@ -48,7 +40,6 @@ import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
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.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
@ -58,20 +49,16 @@ import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.function.Supplier import java.util.function.Supplier
import java.util.random.RandomGenerator
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.math.min
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
override var state: ChunkState = ChunkState.FRESH override var state: ChunkState = ChunkState.FRESH
@ -772,22 +759,33 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY)) if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY))
continue continue
// this is quite ugly code flow, but we should try to avoid double-walking val placed = world.queueMicrodungeonPlacement(pos.x, pos.y) {
// over all dungeon tiles (DungeonTile#canPlace already checks for absence of other dungeons on their place, // this is quite ugly code flow, but we should try to avoid double-walking
// so we only need to tell DungeonPart to not force-place) // over all dungeon tiles (DungeonTile#canPlace already checks for absence of other dungeons on their place,
if (anchor.canPlace(pos.x, pos.y, world, false)) { // so we only need to tell DungeonPart to not force-place)
try { if (it.measureAndSuspend { anchor.canPlace(pos.x, pos.y, world, false) }) {
dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await() try {
} catch (err: Throwable) { val dungeonWorld = dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID, commit = false).await()
LOGGER.error("Error while generating microdungeon ${dungeon.key.left()} at $pos", err) val placementIsFree = dungeonWorld.touchedTiles.all { world.getCell(it).dungeonId == NO_DUNGEON_ID }
if (placementIsFree) {
dungeonWorld.commit()
} else {
LOGGER.debug("Dungeons overlap somewhere around {} after built new dungeon, not placing {}", pos, dungeon.key.left())
}
} catch (err: Throwable) {
LOGGER.error("Error while generating microdungeon ${dungeon.key.left()} at $pos", err)
}
return@queueMicrodungeonPlacement true
} }
break return@queueMicrodungeonPlacement false
} }
// some breathing room for other code, since placement checking is performance intense operation if (placed) {
if (!world.isInPreparation && world.clients.isNotEmpty()) break
delay(min(60L, anchor.reader.size.x * anchor.reader.size.y / 240L)) }
} }
} }
} }

View File

@ -9,6 +9,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.async 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.runBlocking 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
@ -43,6 +44,7 @@ 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.ActionPacer import ru.dbotthepony.kstarbound.util.ActionPacer
import ru.dbotthepony.kstarbound.util.ExecutionTimePacer
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
@ -52,11 +54,14 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.PriorityQueue
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
class ServerWorld private constructor( class ServerWorld private constructor(
val server: StarboundServer, val server: StarboundServer,
@ -177,6 +182,45 @@ class ServerWorld private constructor(
}, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) }, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
} }
private var placementTaskID = 0L
private data class PlacementElement<T>(val x: Int, val y: Int, val id: Long, val callback: suspend (ExecutionTimePacer) -> T, val future: Continuation<T>) : Comparable<PlacementElement<*>> {
override fun compareTo(other: PlacementElement<*>): Int {
var cmp = x.compareTo(other.x)
if (cmp == 0) cmp = y.compareTo(other.y)
if (cmp == 0) cmp = id.compareTo(other.id)
return cmp
}
}
// not Channel because we must be able to select elements by priority
private val placementQueue = PriorityQueue<PlacementElement<*>>()
private var placementQueueIsActive = false
private val placementPacer = ExecutionTimePacer(5_000_000L, 16L)
private suspend fun placementQueueLoop() {
placementQueueIsActive = true
while (placementQueue.isNotEmpty()) {
val next = placementQueue.remove() as PlacementElement<Any?>
next.future.resumeWith(Result.success(next.callback(if (isInPreparation || clients.isEmpty()) ExecutionTimePacer.UNLIMITED else placementPacer)))
}
placementQueueIsActive = false
}
// this is used for scheduling and resolving microdungeon placement
// tries to early-resolve artifacts like this: https://i.dbotthepony.ru/2024/04/28/gb6GdbLox7.png
suspend fun <T> queueMicrodungeonPlacement(x: Int, y: Int, callback: suspend (ExecutionTimePacer) -> T): T {
return suspendCoroutine {
placementQueue.add(PlacementElement(x, y, placementTaskID++, callback, it))
if (!placementQueueIsActive) {
eventLoop.scope.launch { placementQueueLoop() }
}
}
}
override fun toString(): String { override fun toString(): String {
return "Server World $worldID" return "Server World $worldID"
} }

View File

@ -2,13 +2,27 @@ package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class ExecutionTimePacer(private val budget: Long, private val pause: Long) { class ExecutionTimePacer(val budget: Long, val pause: Long) {
private var origin = System.nanoTime() var elapsed = 0L
suspend fun measureAndSuspend() { suspend inline fun <T> measureAndSuspend(block: () -> T): T {
if (System.nanoTime() - origin >= budget) { if (budget <= 0L || pause <= 0L) {
delay(pause) return block()
origin = System.nanoTime()
} }
val origin = System.nanoTime()
val result = block()
elapsed += System.nanoTime() - origin
if (elapsed >= budget) {
elapsed = 0
delay(pause)
}
return result
}
companion object {
val UNLIMITED = ExecutionTimePacer(0L, 0L)
} }
} }