Universe migrated to sqlite database
This commit is contained in:
parent
9797202af2
commit
60fb895fe8
@ -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]
|
||||
|
@ -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) {
|
||||
|
@ -31,9 +31,5 @@ class CelestialRequestPacket(val requests: Collection<Either<Vector2i, Vector3i>
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
connection.pushCelestialRequests(requests)
|
||||
|
||||
connection.scope.launch {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user