diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt index 618960a7..59226fcc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt @@ -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 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] diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt index 12d065ec..4ae059df 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonWriter.kt @@ -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.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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt index da2aa966..76371460 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/CelestialRequestPacket.kt @@ -31,9 +31,5 @@ class CelestialRequestPacket(val requests: Collection override fun play(connection: ServerConnection) { connection.pushCelestialRequests(requests) - - connection.scope.launch { - - } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index bb1b48ba..79c83d41 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -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() + val planets = HashMap>() + + for (system in systems) { + systemParameters[system.location] = server.universe.parameters(system) ?: continue + + val systemPlanets = HashMap() + planets[system.location] = systemPlanets + + for (planetPos in server.universe.children(system)) { + val parameters = server.universe.parameters(planetPos) ?: continue + val satelliteMap = HashMap() + + 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index d738cab6..2165318a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt index a4735932..0cb7881e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt @@ -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() - private val closeables = ArrayList() + 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, val constellations: Set>) { + 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, 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>() + + // 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>() + + 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 { + 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>() + + // 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>() + + 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 { + 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 { + val chunk = getChunk(pos).await() + return chunk.systems.map { UniversePos(Vector3i(it.x, it.y, it.z)) } + } + + override suspend fun chunkConstellations(pos: Vector2i): List> { + val chunk = getChunk(pos).await() + return chunk.constellations.toList() } override suspend fun children(pos: UniversePos): List { - 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?): List { val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null - val futures = ArrayList>>() + val futures = ArrayList>>() for (pos in chunkPositions(region)) { - val f = getChunkFuture(pos).thenApply { - it.map> { + val f = getChunk(pos).thenApply { + if (copy == null) { val result = ArrayList() - 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>() + + for (system in it.systems) { + innerFutures.add(getSystem(system)) + } + + CompletableFuture.allOf(*innerFutures.toTypedArray()) + .thenApply { + val result = ArrayList() + + 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 scanSystems(region: AABBi, callback: suspend (UniversePos) -> KOptional): KOptional { 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> { - val futures = ArrayList>>>() + val futures = ArrayList>>>() for (pos in chunkPositions(region)) { - val f = getChunkFuture(pos).thenApply { - it.map>> { 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>> = 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> { - 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 { + val random = random(staticRandom64(chunkPos.x, chunkPos.y, "ChunkIndexMix")) + val region = chunkRegion(chunkPos) + + return CompletableFuture.supplyAsync(Supplier { + val constellationCandidates = ArrayList() + val systems = ArrayList() + + 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("", random.nextInt(0, 10).toString()) + .replace("", paddedNumber(random.nextInt(0, 100), 2)) + .replace("", paddedNumber(random.nextInt(0, 1000), 3)) + .replace("", 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, 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): List> { + if (candidates.size <= 2 || random.nextDouble() > generationInformation.constellationProbability) return listOf() + + val constellations = ArrayList>() + val constellationPoints = ObjectArrayList() + val constellationLines = ArrayList() + + 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)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt deleted file mode 100644 index 508421e6..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt +++ /dev/null @@ -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> - - 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> { - 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> { - 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> { - val random = random(staticRandom64(chunkPos.component1(), chunkPos.component2(), "ChunkIndexMix")) - val region = universe.chunkRegion(chunkPos) - - return CompletableFuture.supplyAsync { - val constellationCandidates = ArrayList() - 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("", random.nextInt(0, 10).toString()) - .replace("", paddedNumber(random.nextInt(0, 100), 2)) - .replace("", paddedNumber(random.nextInt(0, 1000), 3)) - .replace("", 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() - - 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() - 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): List> { - if (candidates.size <= 2 || random.nextDouble() > universe.generationInformation.constellationProbability) return listOf() - - val constellations = ArrayList>() - val constellationPoints = ObjectArrayList() - val constellationLines = ArrayList() - - 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() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt index 9fc1c96d..da7a6a62 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/CarriedExecutor.kt @@ -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() + private val queue = ConcurrentLinkedDeque() 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt index edfab2d8..be1cff55 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Universe.kt @@ -37,6 +37,9 @@ abstract class Universe { abstract suspend fun hasChildren(pos: UniversePos): Boolean abstract suspend fun children(pos: UniversePos): List + abstract suspend fun chunkSystems(pos: Vector2i): List + abstract suspend fun chunkConstellations(pos: Vector2i): List> + /** * Returns false if part or all of the specified region is not loaded. This * is only relevant for calls to scanSystems and scanConstellationLines, and