Universe migrated to sqlite database

This commit is contained in:
DBotThePony 2024-04-23 22:46:09 +07:00
parent 9797202af2
commit 60fb895fe8
Signed by: DBot
GPG Key ID: DCC23B5715498507
9 changed files with 652 additions and 495 deletions

View File

@ -10,21 +10,44 @@ import com.google.gson.JsonSyntaxException
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readString
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.readInternedString
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.EOFException
import java.io.InputStream
import java.io.Reader
import java.util.LinkedList
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream
fun ByteArray.readJsonElement(): JsonElement = DataInputStream(FastByteArrayInputStream(this)).readJsonElement()
fun ByteArray.readJsonObject(): JsonObject = DataInputStream(FastByteArrayInputStream(this)).readJsonObject()
fun ByteArray.readJsonArray(): JsonArray = DataInputStream(FastByteArrayInputStream(this)).readJsonArray()
private fun <T> ByteArray.callRead(inflate: Boolean, callable: DataInputStream.() -> T): T {
val stream = FastByteArrayInputStream(this)
if (inflate) {
val data = DataInputStream(BufferedInputStream(InflaterInputStream(stream)))
val t = callable(data)
data.close()
return t
} else {
return callable(DataInputStream(stream))
}
}
fun ByteArray.readJsonElement(): JsonElement = callRead(false) { readJsonElement() }
fun ByteArray.readJsonObject(): JsonObject = callRead(false) { readJsonObject() }
fun ByteArray.readJsonArray(): JsonArray = callRead(false) { readJsonArray() }
fun ByteArray.readJsonElementInflated(): JsonElement = callRead(true) { readJsonElement() }
fun ByteArray.readJsonObjectInflated(): JsonObject = callRead(true) { readJsonObject() }
fun ByteArray.readJsonArrayInflated(): JsonArray = callRead(true) { readJsonArray() }
/**
* Позволяет читать двоичный JSON прямиком в [JsonElement]

View File

@ -10,13 +10,33 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.zip.DeflaterOutputStream
import kotlin.math.absoluteValue
fun JsonElement.writeJsonElement(): ByteArray = FastByteArrayOutputStream().let { DataOutputStream(it).writeJsonElement(this); it.array.copyOf(it.length) }
fun JsonObject.writeJsonObject(): ByteArray = FastByteArrayOutputStream().let { DataOutputStream(it).writeJsonObject(this); it.array.copyOf(it.length) }
fun JsonArray.writeJsonArray(): ByteArray = FastByteArrayOutputStream().let { DataOutputStream(it).writeJsonArray(this); it.array.copyOf(it.length) }
private fun <T> T.callWrite(deflate: Boolean, callable: DataOutputStream.(T) -> Unit): ByteArray {
val stream = FastByteArrayOutputStream()
if (deflate) {
val data = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(stream)))
callable(data, this)
data.close()
} else {
callable(DataOutputStream(stream), this)
}
return stream.array.copyOf(stream.length)
}
fun JsonElement.writeJsonElement(): ByteArray = callWrite(false) { writeJsonElement(it) }
fun JsonObject.writeJsonObject(): ByteArray = callWrite(false) { writeJsonObject(it) }
fun JsonArray.writeJsonArray(): ByteArray = callWrite(false) { writeJsonArray(it) }
fun JsonElement.writeJsonElementDeflated(): ByteArray = callWrite(true) { writeJsonElement(it) }
fun JsonObject.writeJsonObjectDeflated(): ByteArray = callWrite(true) { writeJsonObject(it) }
fun JsonArray.writeJsonArrayDeflated(): ByteArray = callWrite(true) { writeJsonArray(it) }
fun DataOutputStream.writeJsonElement(value: JsonElement) {
when (value) {

View File

@ -31,9 +31,5 @@ class CelestialRequestPacket(val requests: Collection<Either<Vector2i, Vector3i>
override fun play(connection: ServerConnection) {
connection.pushCelestialRequests(requests)
connection.scope.launch {
}
}
}
}

View File

@ -39,10 +39,10 @@ import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.HashMap
import java.util.UUID
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import kotlin.collections.HashMap
import kotlin.properties.Delegates
// serverside part of connection
@ -393,7 +393,33 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
for (request in requests) {
if (request.isLeft) {
val chunkPos = request.left()
responses.add(Either.left(server.universe.getChunk(chunkPos)?.toNetwork() ?: continue))
val constellations = server.universe.chunkConstellations(chunkPos)
val systems = server.universe.chunkSystems(chunkPos)
val systemParameters = HashMap<Vector3i, CelestialParameters>()
val planets = HashMap<Vector3i, HashMap<Int, CelestialResponsePacket.PlanetData>>()
for (system in systems) {
systemParameters[system.location] = server.universe.parameters(system) ?: continue
val systemPlanets = HashMap<Int, CelestialResponsePacket.PlanetData>()
planets[system.location] = systemPlanets
for (planetPos in server.universe.children(system)) {
val parameters = server.universe.parameters(planetPos) ?: continue
val satelliteMap = HashMap<Int, CelestialParameters>()
for (satellitePos in server.universe.children(planetPos)) {
satelliteMap[satellitePos.satelliteOrbit] = server.universe.parameters(satellitePos) ?: continue
}
systemPlanets[planetPos.planetOrbit] = CelestialResponsePacket.PlanetData(parameters, satelliteMap)
}
}
responses.add(Either.left(CelestialResponsePacket.ChunkData(
chunkPos, constellations, systemParameters, planets
)))
} else {
val systemPos = UniversePos(request.right())
val map = HashMap<Int, CelestialResponsePacket.PlanetData>()

View File

@ -309,6 +309,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
scheduleWithFixedDelay(Runnable {
database.commit()
universe.flush()
}, Globals.universeServer.universeStorageInterval, Globals.universeServer.universeStorageInterval, TimeUnit.MILLISECONDS)
scheduleAtFixedRate(Runnable {

View File

@ -1,13 +1,27 @@
package ru.dbotthepony.kstarbound.server.world
import com.github.benmanes.caffeine.cache.AsyncCacheLoader
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import ru.dbotthepony.kommons.collect.chainOptionalFutures
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2i
@ -16,106 +30,354 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialConfig
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonArray
import ru.dbotthepony.kstarbound.json.readJsonArrayInflated
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonElementInflated
import ru.dbotthepony.kstarbound.json.writeJsonArray
import ru.dbotthepony.kstarbound.json.writeJsonArrayDeflated
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.binnedChoice
import ru.dbotthepony.kstarbound.util.paddedNumber
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable
import java.io.File
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.function.Supplier
import java.util.random.RandomGenerator
import kotlin.collections.ArrayList
class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeable {
constructor() : this(null) {
sources.add(NativeUniverseSource(null, this).generator)
}
constructor(folder: File) : this(null) {
val nativeFile = File(folder, "universe.kchunks")
val legacyFile = File(folder, "universe.chunks")
val native = if (!nativeFile.exists()) {
NativeUniverseSource(BTreeDB6.create(nativeFile, sync = false), this)
} else {
NativeUniverseSource(BTreeDB6(nativeFile, sync = false), this)
}
val legacy = if (legacyFile.exists()) {
LegacyUniverseSource(BTreeDB5(legacyFile))
} else {
null
}
sources.add(native.reader)
if (legacy != null) sources.add(legacy)
sources.add(native.generator)
closeables.add(native)
if (legacy != null) closeables.add(legacy)
}
class ServerUniverse(folder: File? = null) : Universe(), Closeable {
override val baseInformation: CelestialBaseInformation
get() = Globals.celestialBaseInformation
val generationInformation: CelestialConfig
get() = Globals.celestialConfig
private val sources = ArrayList<UniverseSource>()
private val closeables = ArrayList<Closeable>()
private val database: Connection
private val legacyDatabase: BTreeDB5?
private val isMemory = folder == null
init {
if (folder == null) {
// in-memory database
database = DriverManager.getConnection("jdbc:sqlite:")
legacyDatabase = null
} else {
val nativeFile = File(folder, "universe-chunks.db")
val legacyFile = File(folder, "universe.chunks")
database = DriverManager.getConnection("jdbc:sqlite:${nativeFile.absolutePath.replace('\\', '/')}")
if (legacyFile.exists()) {
legacyDatabase = BTreeDB5(legacyFile)
} else {
legacyDatabase = null
}
}
database.createStatement().use {
it.execute("""
CREATE TABLE IF NOT EXISTS `chunk` (
`x` INTEGER NOT NULL,
`y` INTEGER NOT NULL,
`systems` BLOB NOT NULL, -- binary json array representing 3D coordinates
`constellations` BLOB NOT NULL, -- binary json array representing pairs of 2D coordinates
PRIMARY KEY(`x`, `y`)
)
""".trimIndent())
it.execute("""
CREATE TABLE IF NOT EXISTS `system` (
`x` INTEGER NOT NULL,
`y` INTEGER NOT NULL,
`z` INTEGER NOT NULL,
`parameters` BLOB NOT NULL,
`planets` BLOB NOT NULL,
PRIMARY KEY(`x`, `y`, `z`)
)
""".trimIndent())
}
database.autoCommit = false
}
private val carrier = CarriedExecutor(Starbound.IO_EXECUTOR)
private val scope = CoroutineScope(carrier.asCoroutineDispatcher() + SupervisorJob())
private val selectChunk = database.prepareStatement("SELECT `systems`, `constellations` FROM `chunk` WHERE `x` = ? AND `y` = ?")
private val selectSystem = database.prepareStatement("SELECT `parameters`, `planets` FROM `system` WHERE `x` = ? AND `y` = ? AND `z` = ?")
private val insertChunk = database.prepareStatement("REPLACE INTO `chunk` (`x`, `y`, `systems`, `constellations`) VALUES (?, ?, ?, ?)")
private val insertSystem = database.prepareStatement("REPLACE INTO `system` (`x`, `y`, `z`, `parameters`, `planets`) VALUES (?, ?, ?, ?, ?)")
private data class Chunk(val x: Int, val y: Int, val systems: Set<Vector3i>, val constellations: Set<Pair<Vector2i, Vector2i>>) {
constructor(x: Int, y: Int, data: ResultSet) : this(
x, y,
data.getBytes(1).readJsonArrayInflated().map { Vector3i(it.asJsonArray[0].asInt, it.asJsonArray[1].asInt, it.asJsonArray[2].asInt) }.toSet(),
data.getBytes(2).readJsonArrayInflated().map {
val a = it.asJsonArray[0].asJsonArray
val b = it.asJsonArray[1].asJsonArray
Vector2i(a[0].asInt, a[1].asInt) to Vector2i(b[0].asInt, b[1].asInt)
}.toSet()
)
fun write(statement: PreparedStatement) {
statement.setInt(1, x)
statement.setInt(2, y)
statement.setBytes(3, systems.stream()
.map { jsonArrayOf(it.x, it.y, it.z) }
.collect(JsonArrayCollector)
.writeJsonArrayDeflated())
statement.setBytes(4, constellations.stream().map {
jsonArrayOf(jsonArrayOf(it.first.x, it.first.y), jsonArrayOf(it.second.x, it.second.y))
}.collect(JsonArrayCollector).writeJsonArrayDeflated())
statement.execute()
}
}
private data class System(val x: Int, val y: Int, val z: Int, val parameters: CelestialParameters, val planets: Map<Pair<Int, Int>, CelestialParameters>) {
constructor(x: Int, y: Int, z: Int, data: ResultSet) : this(
x, y, z,
Starbound.gson.fromJson(data.getBytes(1).readJsonElementInflated())!!,
data.getBytes(2).readJsonArrayInflated().associate {
it as JsonArray
(it[0].asInt to it[1].asInt) to Starbound.gson.fromJson(it[2])!!
}
)
fun parameters(pos: UniversePos): CelestialParameters? {
if (pos.isSystem) {
return parameters
} else {
return planets[pos.planetOrbit to pos.satelliteOrbit]
}
}
fun write(statement: PreparedStatement) {
statement.setInt(1, x)
statement.setInt(2, y)
statement.setInt(3, z)
statement.setBytes(4, Starbound.gson.toJsonTree(parameters).writeJsonElementDeflated())
statement.setBytes(5, planets.entries.stream()
.map { jsonArrayOf(it.key.first, it.key.second, it.value) }
.collect(JsonArrayCollector).writeJsonArrayDeflated())
statement.execute()
}
}
// first, chunks in process of loading/generating must not be evicted
private val chunkFutures = ConcurrentHashMap<Vector2i, CompletableFuture<Chunk>>()
// then, once chunk is loaded, it is put into cache, where it may get evicted
private val chunksCache = Caffeine.newBuilder()
.maximumSize(4096L)
.executor(Starbound.EXECUTOR)
.build<Vector2i, CompletableFuture<Chunk>>()
private suspend fun getChunk0(pos: Vector2i): Chunk {
selectChunk.setInt(1, pos.x)
selectChunk.setInt(2, pos.y)
val existing = selectChunk.executeQuery().use {
if (it.next()) {
Chunk(pos.x, pos.y, it)
} else {
null
}
}
if (existing != null) {
chunkFutures.remove(pos)
return existing
}
// TODO
// load legacy chunk here
val generated = generateChunk(pos).await()
generated.write(insertChunk)
chunkFutures.remove(pos)
return generated
}
private fun getChunk(pos: Vector2i): CompletableFuture<Chunk> {
return chunksCache.get(pos) {
chunkFutures.computeIfAbsent(it) {
scope.async { getChunk0(it) }.asCompletableFuture()
}
}
}
// first, systems in process of loading/generating must not be evicted
private val systemFutures = ConcurrentHashMap<Vector3i, CompletableFuture<System?>>()
// then, once system is loaded, it is put into cache, where it may get evicted
private val systemCache = Caffeine.newBuilder()
.maximumSize(2048L)
.executor(Starbound.EXECUTOR)
.build<Vector3i, CompletableFuture<System?>>()
private fun loadSystem(pos: Vector3i): System? {
selectSystem.setInt(1, pos.x)
selectSystem.setInt(2, pos.y)
selectSystem.setInt(3, pos.z)
return selectSystem.executeQuery().use {
if (it.next()) {
System(pos.x, pos.y, pos.z, it)
} else {
null
}
}
}
private suspend fun loadOrComputeSystem(pos: Vector3i): System? {
val existing = loadSystem(pos)
if (existing != null) {
// hit, system already exists
systemFutures.remove(pos)
return existing
}
// lets try to get chunk this system is in
// if chunk doesn't exist, it will be generated, along all systems in it
val chunk = getChunk(world2chunk(Vector2i(pos))).await()
// once chunk has been generated, try again
// if nothing is there, then system does not exist
if (pos !in chunk.systems)
return null
return loadSystem(pos)
}
private fun getSystem(pos: Vector3i): CompletableFuture<System?> {
return systemCache.get(pos) {
systemFutures.computeIfAbsent(it) {
scope.async { loadOrComputeSystem(it) }.asCompletableFuture()
}
}
}
override suspend fun parameters(pos: UniversePos): CelestialParameters? {
return getChunk(pos)?.parameters(pos)
return getSystem(pos.location).await()?.parameters(pos)
}
override suspend fun hasChildren(pos: UniversePos): Boolean {
val system = getChunk(pos)?.systems?.get(pos.location) ?: return false
if (pos.isSatellite)
return false
if (pos.isSystem)
val system = getSystem(pos.location).await() ?: return false
if (pos.isSystem) {
return system.planets.isNotEmpty()
else if (pos.isPlanet)
return system.planets[pos.orbitNumber]?.satellites?.isNotEmpty() ?: false
} else {
return system.planets.keys.any { it.first == pos.planetOrbit && it.second != 0 }
}
}
return false
override suspend fun chunkSystems(pos: Vector2i): List<UniversePos> {
val chunk = getChunk(pos).await()
return chunk.systems.map { UniversePos(Vector3i(it.x, it.y, it.z)) }
}
override suspend fun chunkConstellations(pos: Vector2i): List<Pair<Vector2i, Vector2i>> {
val chunk = getChunk(pos).await()
return chunk.constellations.toList()
}
override suspend fun children(pos: UniversePos): List<UniversePos> {
val chunk = getChunk(pos) ?: return emptyList()
val system = chunk.systems[pos.location] ?: return listOf()
if (pos.isSatellite)
return emptyList()
if (pos.isSystem)
return system.planets.keys.intStream().mapToObj { UniversePos(pos.location, it) }.toList()
else if (pos.isPlanet)
return system.planets[pos.planetOrbit]?.satellites?.keys?.intStream()?.mapToObj { UniversePos(pos.location, pos.planetOrbit, it) }?.toList() ?: listOf()
val system = getSystem(pos.location).await() ?: return emptyList()
return listOf()
if (pos.isSystem) {
val keys = IntArraySet()
for ((key, s) in system.planets.keys) {
if (s == 0)
keys.add(key)
}
return keys.map { pos.child(it) }
} else {
val keys = IntArraySet()
for ((f, key) in system.planets.keys) {
if (f == pos.planetOrbit && key != 0)
keys.add(key)
}
return keys.map { pos.child(it) }
}
}
override suspend fun findSystems(region: AABBi, includedTypes: Set<String>?): List<UniversePos> {
val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null
val futures = ArrayList<CompletableFuture<List<UniversePos>>>()
val futures = ArrayList<CompletableFuture<ArrayList<UniversePos>>>()
for (pos in chunkPositions(region)) {
val f = getChunkFuture(pos).thenApply {
it.map<List<UniversePos>> {
val f = getChunk(pos).thenApply {
if (copy == null) {
val result = ArrayList<UniversePos>()
if (copy == null) {
for (system in it.systems.keys) {
result.add(UniversePos(system))
}
} else {
for ((system, params) in it.systems) {
if (params.parameters.parameters.get("typeName", "") in copy) {
result.add(UniversePos(system))
}
}
for (system in it.systems) {
result.add(UniversePos(system))
}
result
}.orElse(listOf())
}
CompletableFuture.completedFuture(result)
} else {
val innerFutures = ArrayList<CompletableFuture<System?>>()
for (system in it.systems) {
innerFutures.add(getSystem(system))
}
CompletableFuture.allOf(*innerFutures.toTypedArray())
.thenApply {
val result = ArrayList<UniversePos>()
for (systemF in innerFutures) {
val system = systemF.get() ?: continue
if (system.parameters.parameters.get("typeName", "") in copy) {
result.add(UniversePos(Vector3i(system.x, system.y, system.z)))
}
}
result
}
}
}.thenCompose { it }
futures.add(f)
}
@ -126,9 +388,9 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
override suspend fun <T> scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional<T>): KOptional<T> {
for (pos in chunkPositions(region)) {
val chunk = getChunk(pos) ?: continue
val chunk = getChunk(pos).await()
for (system in chunk.systems.keys) {
for (system in chunk.systems) {
val result = callback(UniversePos(system))
if (result.isPresent) {
@ -144,7 +406,7 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
require(tries > 0) { "Non-positive amount of tries: $tries" }
require(range > 0) { "Non-positive range: $range" }
val random = random(seed ?: System.nanoTime())
val random = random(seed ?: java.lang.System.nanoTime())
val rect = AABBi(Vector2i(-range, -range), Vector2i(range, range))
for (i in 0 until tries) {
@ -190,11 +452,11 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
}
override suspend fun scanConstellationLines(region: AABBi, aggressive: Boolean): List<Pair<Vector2i, Vector2i>> {
val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>()
val futures = ArrayList<CompletableFuture<ObjectArrayList<Pair<Vector2i, Vector2i>>>>()
for (pos in chunkPositions(region)) {
val f = getChunkFuture(pos).thenApply {
it.map<List<Pair<Vector2i, Vector2i>>> { ObjectArrayList(it.constellations) }.orElse(listOf())
val f = getChunk(pos).thenApply {
ObjectArrayList(it.constellations)
}
futures.add(f)
@ -209,38 +471,242 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
}
override fun close() {
closeables.forEach { it.close() }
scope.cancel()
carrier.execute {
legacyDatabase?.close()
database.commit()
database.close()
}
carrier.wait(300L, TimeUnit.SECONDS)
}
// Edge case: we load so many chunks AND try to load same chunk twice that it gets loaded/generated twice
// shouldn't cause actual issues though
private val chunkCache: Cache<Vector2i, CompletableFuture<KOptional<UniverseChunk>>> = Caffeine
.newBuilder()
.expireAfterAccess(Duration.ofMinutes(10L))
.maximumSize(1024L)
.softValues()
.scheduler(Starbound)
.executor(Starbound.EXECUTOR)
.build()
private val systemPerlin = AbstractPerlinNoise.of(generationInformation.systemTypePerlin)
fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {
return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } }
}
suspend fun getChunk(pos: UniversePos): UniverseChunk? {
return getChunk(world2chunk(Vector2i(pos.location)))
}
suspend fun getChunk(pos: Vector2i): UniverseChunk? {
val get = getChunkFuture(pos).await()
if (get.isPresent) {
return get.value
} else {
return null
init {
if (!systemPerlin.hasSeedSpecified) {
systemPerlin.init(staticRandom64("SystemTypePerlin"))
}
}
override val region: AABBi = AABBi(Vector2i(baseInformation.xyCoordRange.x, baseInformation.xyCoordRange.x), Vector2i(baseInformation.xyCoordRange.y, baseInformation.xyCoordRange.y))
fun flush() {
if (!isMemory) {
database.commit()
}
}
private fun generateChunk(chunkPos: Vector2i): CompletableFuture<Chunk> {
val random = random(staticRandom64(chunkPos.x, chunkPos.y, "ChunkIndexMix"))
val region = chunkRegion(chunkPos)
return CompletableFuture.supplyAsync(Supplier {
val constellationCandidates = ArrayList<Vector2i>()
val systems = ArrayList<Vector3i>()
for (x in region.mins.x until region.maxs.x) {
for (y in region.mins.y until region.maxs.y) {
if (random.nextFloat() < generationInformation.systemProbability) {
val z = random.nextInt(baseInformation.zCoordRange.x, baseInformation.zCoordRange.y)
val pos = Vector3i(x, y, z)
val system = generateSystem(random, pos) ?: continue
systems.add(pos)
systemCache.put(pos, CompletableFuture.completedFuture(system))
carrier.executePriority { system.write(insertSystem) }
if (
system.parameters.parameters.get("constellationCapable", true) &&
system.parameters.parameters.get("magnitude", 0.0) >= generationInformation.minimumConstellationMagnitude
) {
constellationCandidates.add(Vector2i(x, y))
}
}
}
}
Chunk(chunkPos.x, chunkPos.y, ObjectOpenHashSet(systems), ObjectOpenHashSet(generateConstellations(random, constellationCandidates)))
}, Starbound.EXECUTOR)
}
private fun generateSystem(random: RandomGenerator, location: Vector3i): System? {
val typeSelector = systemPerlin[location.x.toDouble(), location.y.toDouble()]
val type = generationInformation.systemTypeBins
.stream()
.binnedChoice(typeSelector).orElse("")
if (type.isBlank())
return null
val system = generationInformation.systemTypes[type]!!
val systemPos = UniversePos(location)
val systemSeed = random.nextLong()
val prefix = Globals.celestialNames.systemPrefixNames.sample(random).orElse("")
val mid = Globals.celestialNames.systemNames.sample(random).orElse("missingsystemname $location")
val suffix = Globals.celestialNames.systemSuffixNames.sample(random).orElse("")
val systemName = "$prefix $mid $suffix".trim()
.replace("<onedigit>", random.nextInt(0, 10).toString())
.replace("<twodigit>", paddedNumber(random.nextInt(0, 100), 2))
.replace("<threedigit>", paddedNumber(random.nextInt(0, 1000), 3))
.replace("<fourdigit>", paddedNumber(random.nextInt(0, 10000), 4))
val systemParams = CelestialParameters(
systemPos,
systemSeed,
systemName,
mergeJson(system.baseParameters.deepCopy(), system.variationParameters.random(random))
)
if ("typeName" !in systemParams.parameters) {
systemParams.parameters["typeName"] = system.typeName
}
if ("constellationCapable" !in systemParams.parameters) {
systemParams.parameters["constellationCapable"] = system.constellationCapable
}
val planets = HashMap<Pair<Int, Int>, CelestialParameters>()
for (planetOrbitIndex in 1 .. baseInformation.planetOrbitalLevels) {
// this looks dumb at first, but then it makes sense
// in celestial.config, you define orbital region, where planets
// of only specific type appear.
val systemOrbitRegion = system.orbitRegions
.stream()
.filter { planetOrbitIndex in it.orbitRange.x .. it.orbitRange.y }
.findFirst().orElse(null) ?: continue
if (systemOrbitRegion.bodyProbability > random.nextDouble()) continue
val planetaryTypeO = systemOrbitRegion.planetaryTypes.sample(random).flatMap { KOptional.ofNullable(generationInformation.planetaryTypes[it]) }
if (!planetaryTypeO.isPresent) continue
val planetaryType = planetaryTypeO.value
val planetCoordinate = UniversePos(location, planetOrbitIndex)
val planetSeed = random.nextLong()
val planetName = "$systemName ${Globals.celestialNames.planetarySuffixes[planetOrbitIndex]}"
planets[planetOrbitIndex to 0] = CelestialParameters(
planetCoordinate,
planetSeed,
planetName,
mergeJson(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random))
)
var satelliteCount = 0
val maxSatelliteCount = planetaryType.maxSatelliteCount ?: baseInformation.satelliteOrbitalLevels
if (maxSatelliteCount > 0) {
for (satelliteOrbitIndex in 1 .. baseInformation.satelliteOrbitalLevels) {
if (random.nextDouble() < planetaryType.satelliteProbability) {
val satelliteTypeO = systemOrbitRegion.satelliteTypes.sample(random).flatMap { KOptional.ofNullable(generationInformation.satelliteTypes[it]) }
if (!satelliteTypeO.isPresent) continue
val satelliteType = satelliteTypeO.value
val satelliteSeed = random.nextLong()
val satelliteName = "$planetName ${Globals.celestialNames.satelliteSuffixes[satelliteCount]}"
val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex)
val merge = JsonObject()
mergeJson(merge, satelliteType.baseParameters)
mergeJson(merge, satelliteType.variationParameters.random(random))
if (systemOrbitRegion.regionName in satelliteType.orbitParameters) {
mergeJson(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random))
}
planets[planetOrbitIndex to satelliteOrbitIndex] = CelestialParameters(
satelliteCoordinate,
satelliteSeed,
satelliteName,
merge
)
if (++satelliteCount >= maxSatelliteCount)
break
}
}
}
}
return System(location.x, location.y, location.z, systemParams, planets)
}
private fun generateConstellations(random: RandomGenerator, candidates: List<Vector2i>): List<Pair<Vector2i, Vector2i>> {
if (candidates.size <= 2 || random.nextDouble() > generationInformation.constellationProbability) return listOf()
val constellations = ArrayList<Pair<Vector2i, Vector2i>>()
val constellationPoints = ObjectArrayList<Vector2i>()
val constellationLines = ArrayList<Line2d>()
val target = random.nextInt(generationInformation.constellationLineCountRange.x, generationInformation.constellationLineCountRange.y)
var tries = 0
while (constellationLines.size < target && ++tries < generationInformation.constellationMaxTries) {
val start = if (constellationPoints.isEmpty)
candidates.random(random)
else
constellationPoints.random(random)
val end = candidates.random(random)
if (start == end) continue
val proposed = Line2d(start.toDoubleVector(), end.toDoubleVector())
val proposedReversed = proposed.reverse()
if (proposed in constellationLines || proposedReversed in constellationLines) continue
if (start.distance(end) !in generationInformation.minimumConstellationLineLength .. generationInformation.maximumConstellationLineLength) continue
var valid = true
for (existingLine in constellationLines) {
val intersection = proposed.intersect(existingLine)
if (
intersection.intersects &&
intersection.point != proposed.p0 &&
intersection.point != proposed.p1
) {
valid = false
break
}
if (
proposed != existingLine &&
proposed.distanceTo(existingLine.p0) < generationInformation.minimumConstellationLineCloseness
) {
valid = false
break
}
if (
proposed != existingLine.reverse() &&
proposed.distanceTo(existingLine.p1) < generationInformation.minimumConstellationLineCloseness
) {
valid = false
break
}
}
if (valid) {
// TODO: Original engine generates "single" constellation
// out of multiple (probably disconnected) point pairs.
// Should we do the same? It just doesn't seem to make much sense
// Side effect: It is now possible to have chunks where only two stars are connected (one line)
// Original game engine requires at least two lines to be present to form a constellation
constellations.add(start to end)
constellationLines.add(proposed)
constellationPoints.add(start)
constellationPoints.add(end)
}
}
return constellations
}
override val region: AABBi = AABBi(Vector2i(baseInformation.xyCoordRange.x, baseInformation.xyCoordRange.x), Vector2i(baseInformation.xyCoordRange.y, baseInformation.xyCoordRange.y))
}

View File

@ -1,387 +0,0 @@
package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.binnedChoice
import ru.dbotthepony.kstarbound.util.paddedNumber
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.Closeable
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
import java.util.random.RandomGenerator
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream
sealed class UniverseSource {
abstract fun getChunk(chunkPos: IStruct2i): CompletableFuture<KOptional<UniverseChunk>>
open fun saveChunk(chunkPos: IStruct2i, data: UniverseChunk): Boolean {
return false
}
}
// legacy chunks are indexed by... 8 byte keys?
// quite strange.
private fun key(chunkPos: IStruct2i): ByteKey {
val (x, y) = chunkPos
val bytes = ByteArray(8)
bytes[0] = ((x ushr 24) and 0xFF).toByte()
bytes[1] = ((x ushr 16) and 0xFF).toByte()
bytes[2] = ((x ushr 8) and 0xFF).toByte()
bytes[3] = ((x ushr 0) and 0xFF).toByte()
bytes[4 + 0] = ((y ushr 24) and 0xFF).toByte()
bytes[4 + 1] = ((y ushr 16) and 0xFF).toByte()
bytes[4 + 2] = ((y ushr 8) and 0xFF).toByte()
bytes[4 + 3] = ((y ushr 0) and 0xFF).toByte()
return ByteKey.wrap(bytes)
}
class LegacyUniverseSource(private val db: BTreeDB5) : UniverseSource(), Closeable {
private val carried = CarriedExecutor(Starbound.IO_EXECUTOR)
override fun close() {
carried.execute { db.close() }
carried.wait(Int.MAX_VALUE.toLong(), TimeUnit.MILLISECONDS)
}
override fun getChunk(chunkPos: IStruct2i): CompletableFuture<KOptional<UniverseChunk>> {
val key = key(chunkPos)
return CompletableFuture.supplyAsync(Supplier { db.read(key) }, carried).thenApplyAsync {
it.map {
val stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(it)))
UniverseChunk(Vector2i(chunkPos))
}
}
}
}
class NativeUniverseSource(private val db: BTreeDB6?, private val universe: ServerUniverse) : Closeable {
private inner class Reader : UniverseSource() {
override fun getChunk(chunkPos: IStruct2i): CompletableFuture<KOptional<UniverseChunk>> {
if (db == null) return CompletableFuture.completedFuture(KOptional())
val key = key(chunkPos)
return CompletableFuture.supplyAsync(Supplier {
db.read(key)
}, carrier).thenApplyAsync {
it.flatMap {
val data = DataInputStream(BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(it)))).readJsonElement()
val expected = Vector2i(chunkPos)
try {
val chunk = Starbound.gson.fromJson(data, UniverseChunk::class.java)
if (expected == chunk.chunkPos) {
KOptional(chunk)
} else {
throw IllegalArgumentException("Universe chunk at $chunkPos has ${chunk.chunkPos} stored as position!")
}
} catch (err: Throwable) {
LOGGER.error("Error while deserializing universe chunk from disk storage at $chunkPos, it will be regenerated", err)
KOptional()
}
}
}
}
override fun saveChunk(chunkPos: IStruct2i, data: UniverseChunk): Boolean {
storeChunk(chunkPos, data)
return true
}
}
// generator will generate different systems than vanilla
// because of different code flow and ability to use different random number generator
private inner class Generator : UniverseSource() {
override fun getChunk(chunkPos: IStruct2i): CompletableFuture<KOptional<UniverseChunk>> {
val random = random(staticRandom64(chunkPos.component1(), chunkPos.component2(), "ChunkIndexMix"))
val region = universe.chunkRegion(chunkPos)
return CompletableFuture.supplyAsync {
val constellationCandidates = ArrayList<Vector2i>()
val chunk = UniverseChunk(Vector2i(chunkPos))
for (x in region.mins.x until region.maxs.x) {
for (y in region.mins.y until region.maxs.y) {
if (random.nextFloat() < universe.generationInformation.systemProbability) {
val z = random.nextInt(universe.baseInformation.zCoordRange.x, universe.baseInformation.zCoordRange.y)
val pos = Vector3i(x, y, z)
val system = generateSystem(random, pos) ?: continue
chunk.systems[pos] = system
if (
system.parameters.parameters.get("constellationCapable", true) &&
system.parameters.parameters.get("magnitude", 0.0) >= universe.generationInformation.minimumConstellationMagnitude
) {
constellationCandidates.add(Vector2i(x, y))
}
}
}
}
for (pair in generateConstellations(random, constellationCandidates)) {
chunk.constellations.add(pair)
}
storeChunk(chunkPos, chunk)
KOptional(chunk)
}
}
private val systemPerlin = AbstractPerlinNoise.of(universe.generationInformation.systemTypePerlin)
init {
if (!systemPerlin.hasSeedSpecified) {
systemPerlin.init(staticRandom64("SystemTypePerlin"))
}
}
private fun generateSystem(random: RandomGenerator, location: Vector3i): UniverseChunk.System? {
val typeSelector = systemPerlin[location.x.toDouble(), location.y.toDouble()]
val type = universe.generationInformation.systemTypeBins
.stream()
.binnedChoice(typeSelector).orElse("")
if (type.isBlank())
return null
val system = universe.generationInformation.systemTypes[type]!!
val systemPos = UniversePos(location)
val systemSeed = random.nextLong()
val prefix = Globals.celestialNames.systemPrefixNames.sample(random).orElse("")
val mid = Globals.celestialNames.systemNames.sample(random).orElse("missingsystemname $location")
val suffix = Globals.celestialNames.systemSuffixNames.sample(random).orElse("")
val systemName = "$prefix $mid $suffix".trim()
.replace("<onedigit>", random.nextInt(0, 10).toString())
.replace("<twodigit>", paddedNumber(random.nextInt(0, 100), 2))
.replace("<threedigit>", paddedNumber(random.nextInt(0, 1000), 3))
.replace("<fourdigit>", paddedNumber(random.nextInt(0, 10000), 4))
val systemParams = CelestialParameters(
systemPos,
systemSeed,
systemName,
mergeJson(system.baseParameters.deepCopy(), system.variationParameters.random(random))
)
if ("typeName" !in systemParams.parameters) {
systemParams.parameters["typeName"] = system.typeName
}
if ("constellationCapable" !in systemParams.parameters) {
systemParams.parameters["constellationCapable"] = system.constellationCapable
}
val planets = Int2ObjectArrayMap<UniverseChunk.Planet>()
for (planetOrbitIndex in 1 .. universe.baseInformation.planetOrbitalLevels) {
// this looks dumb at first, but then it makes sense
// in celestial.config, you define orbital region, where planets
// of only specific type appear.
val systemOrbitRegion = system.orbitRegions
.stream()
.filter { planetOrbitIndex in it.orbitRange.x .. it.orbitRange.y }
.findFirst().orElse(null) ?: continue
if (systemOrbitRegion.bodyProbability > random.nextDouble()) continue
val planetaryTypeO = systemOrbitRegion.planetaryTypes.sample(random).flatMap { KOptional.ofNullable(universe.generationInformation.planetaryTypes[it]) }
if (!planetaryTypeO.isPresent) continue
val planetaryType = planetaryTypeO.value
val planetCoordinate = UniversePos(location, planetOrbitIndex)
val planetSeed = random.nextLong()
val planetName = "$systemName ${Globals.celestialNames.planetarySuffixes[planetOrbitIndex]}"
val planetParams = CelestialParameters(
planetCoordinate,
planetSeed,
planetName,
mergeJson(planetaryType.baseParameters.deepCopy(), planetaryType.variationParameters.random(random))
)
val satellites = Int2ObjectArrayMap<CelestialParameters>()
val maxSatelliteCount = planetaryType.maxSatelliteCount ?: universe.baseInformation.satelliteOrbitalLevels
if (maxSatelliteCount > 0) {
for (satelliteOrbitIndex in 1 .. universe.baseInformation.satelliteOrbitalLevels) {
if (random.nextDouble() < planetaryType.satelliteProbability) {
val satelliteTypeO = systemOrbitRegion.satelliteTypes.sample(random).flatMap { KOptional.ofNullable(universe.generationInformation.satelliteTypes[it]) }
if (!satelliteTypeO.isPresent) continue
val satelliteType = satelliteTypeO.value
val satelliteSeed = random.nextLong()
val satelliteName = "$planetName ${Globals.celestialNames.satelliteSuffixes[satellites.size]}"
val satelliteCoordinate = UniversePos(location, planetOrbitIndex, satelliteOrbitIndex)
val merge = JsonObject()
mergeJson(merge, satelliteType.baseParameters)
mergeJson(merge, satelliteType.variationParameters.random(random))
if (systemOrbitRegion.regionName in satelliteType.orbitParameters) {
mergeJson(merge, satelliteType.orbitParameters[systemOrbitRegion.regionName]!!.random(random))
}
satellites[satelliteOrbitIndex] = CelestialParameters(
satelliteCoordinate,
satelliteSeed,
satelliteName,
merge
)
if (satellites.size >= maxSatelliteCount)
break
}
}
}
planets[planetOrbitIndex] = UniverseChunk.Planet(planetParams, satellites)
}
return UniverseChunk.System(systemParams, planets)
}
private fun generateConstellations(random: RandomGenerator, candidates: List<Vector2i>): List<Pair<Vector2i, Vector2i>> {
if (candidates.size <= 2 || random.nextDouble() > universe.generationInformation.constellationProbability) return listOf()
val constellations = ArrayList<Pair<Vector2i, Vector2i>>()
val constellationPoints = ObjectArrayList<Vector2i>()
val constellationLines = ArrayList<Line2d>()
val target = random.nextInt(universe.generationInformation.constellationLineCountRange.x, universe.generationInformation.constellationLineCountRange.y)
var tries = 0
while (constellationLines.size < target && ++tries < universe.generationInformation.constellationMaxTries) {
val start = if (constellationPoints.isEmpty)
candidates.random(random)
else
constellationPoints.random(random)
val end = candidates.random(random)
if (start == end) continue
val proposed = Line2d(start.toDoubleVector(), end.toDoubleVector())
val proposedReversed = proposed.reverse()
if (proposed in constellationLines || proposedReversed in constellationLines) continue
if (start.distance(end) !in universe.generationInformation.minimumConstellationLineLength .. universe.generationInformation.maximumConstellationLineLength) continue
var valid = true
for (existingLine in constellationLines) {
val intersection = proposed.intersect(existingLine)
if (
intersection.intersects &&
intersection.point != proposed.p0 &&
intersection.point != proposed.p1
) {
valid = false
break
}
if (
proposed != existingLine &&
proposed.distanceTo(existingLine.p0) < universe.generationInformation.minimumConstellationLineCloseness
) {
valid = false
break
}
if (
proposed != existingLine.reverse() &&
proposed.distanceTo(existingLine.p1) < universe.generationInformation.minimumConstellationLineCloseness
) {
valid = false
break
}
}
if (valid) {
// TODO: Original engine generates "single" constellation
// TODO: out of multiple (probably disconnected) point pairs.
// TODO: Should we do the same? It just doesn't seem to make much sense
// Side effect: It is now possible to have chunks where only two stars are connected (one line)
// Original game engine requires at least two lines to be present to form a constellation
constellations.add(start to end)
constellationLines.add(proposed)
constellationPoints.add(start)
constellationPoints.add(end)
}
}
return constellations
}
}
private fun storeChunk(chunkPos: IStruct2i, chunk: UniverseChunk) {
if (db == null) return
val key = key(chunkPos)
try {
val data = Starbound.gson.toJsonTree(chunk)
val binaryData = FastByteArrayOutputStream()
val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(binaryData, Deflater(2))))
stream.writeJsonElement(data)
stream.close()
carrier.execute {
db.write(key, binaryData.array, 0, binaryData.length)
}
} catch (err: Throwable) {
LOGGER.error("Error while saving universe chunk at $chunkPos, it will not persist", err)
}
}
private val carrier = CarriedExecutor(Starbound.IO_EXECUTOR)
val reader: UniverseSource = Reader()
val generator: UniverseSource = Generator()
override fun close() {
carrier.execute { db?.close() }
carrier.wait(Int.MAX_VALUE.toLong(), TimeUnit.MILLISECONDS)
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.util
import java.lang.ref.Reference
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
@ -8,7 +9,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.LockSupport
class CarriedExecutor(private val parent: Executor) : Executor, Runnable {
private val queue = ConcurrentLinkedQueue<Runnable>()
private val queue = ConcurrentLinkedDeque<Runnable>()
private val isCarried = AtomicBoolean()
override fun execute(command: Runnable) {
@ -19,6 +20,14 @@ class CarriedExecutor(private val parent: Executor) : Executor, Runnable {
}
}
fun executePriority(command: Runnable) {
queue.addFirst(command)
if (isCarried.compareAndSet(false, true)) {
parent.execute(this)
}
}
override fun run() {
while (true) {
var next = queue.poll()

View File

@ -37,6 +37,9 @@ abstract class Universe {
abstract suspend fun hasChildren(pos: UniversePos): Boolean
abstract suspend fun children(pos: UniversePos): List<UniversePos>
abstract suspend fun chunkSystems(pos: Vector2i): List<UniversePos>
abstract suspend fun chunkConstellations(pos: Vector2i): List<Pair<Vector2i, Vector2i>>
/**
* Returns false if part or all of the specified region is not loaded. This
* is only relevant for calls to scanSystems and scanConstellationLines, and