Native world storage, featuring fast unique entity index, atomic updates and paletted tile storage (does not require integer IDs to be assigned to materials/modifiers/liquids)
This commit is contained in:
parent
72d6db0a17
commit
b4940ce8ca
@ -20,7 +20,7 @@ object VersionRegistry {
|
|||||||
return VersionedJson(name, currentVersion(name), contents)
|
return VersionedJson(name, currentVersion(name), contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrate(read: VersionedJson, identifier: String = read.id): JsonElement {
|
fun migrate(read: VersionedJson, identifier: String = read.id): JsonElement {
|
||||||
if (read.version != currentVersion(identifier)) {
|
if (read.version != currentVersion(identifier)) {
|
||||||
throw IllegalStateException("NYI: Migrating $identifier from ${read.version} to ${currentVersion(identifier)}")
|
throw IllegalStateException("NYI: Migrating $identifier from ${read.version} to ${currentVersion(identifier)}")
|
||||||
}
|
}
|
||||||
|
24
src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLUtils.kt
Normal file
24
src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLUtils.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.io
|
||||||
|
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
|
||||||
|
data class SavepointStatements(val begin: PreparedStatement, val commit: PreparedStatement, val rollback: PreparedStatement)
|
||||||
|
|
||||||
|
fun Connection.createSavepoint(name: String): SavepointStatements {
|
||||||
|
check('"' !in name) { "Invalid identifier: $name" }
|
||||||
|
|
||||||
|
val begin = prepareStatement("""
|
||||||
|
SAVEPOINT "$name"
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
val commit = prepareStatement("""
|
||||||
|
RELEASE SAVEPOINT "$name"
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
val rollback = prepareStatement("""
|
||||||
|
ROLLBACK TO SAVEPOINT "$name"
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
return SavepointStatements(begin, commit, rollback)
|
||||||
|
}
|
@ -28,6 +28,7 @@ import ru.dbotthepony.kstarbound.json.writeJsonElement
|
|||||||
import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated
|
import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated
|
||||||
import ru.dbotthepony.kstarbound.json.writeJsonObject
|
import ru.dbotthepony.kstarbound.json.writeJsonObject
|
||||||
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
|
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
|
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
|
||||||
@ -203,7 +204,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
|||||||
val fileName = location.pos.toString().replace(':', '_') + ".db"
|
val fileName = location.pos.toString().replace(':', '_') + ".db"
|
||||||
val file = File(universeFolder, fileName)
|
val file = File(universeFolder, fileName)
|
||||||
val firstTime = !file.exists()
|
val firstTime = !file.exists()
|
||||||
val storage = LegacyWorldStorage.SQL(file)
|
val storage = NativeLocalWorldStorage(file)
|
||||||
|
|
||||||
val world = try {
|
val world = try {
|
||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
@ -225,7 +226,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
|
|||||||
var i = 0
|
var i = 0
|
||||||
while (!file.renameTo(File(universeFolder, "$fileName-fail$i")) && ++i < 1000) {}
|
while (!file.renameTo(File(universeFolder, "$fileName-fail$i")) && ++i < 1000) {}
|
||||||
|
|
||||||
ServerWorld.create(this, WorldTemplate.create(location.pos, universe), LegacyWorldStorage.SQL(file), location)
|
ServerWorld.create(this, WorldTemplate.create(location.pos, universe), NativeLocalWorldStorage(file), location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import ru.dbotthepony.kommons.io.writeVarInt
|
|||||||
import ru.dbotthepony.kommons.util.xxhash32
|
import ru.dbotthepony.kommons.util.xxhash32
|
||||||
import ru.dbotthepony.kstarbound.Starbound
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
import ru.dbotthepony.kstarbound.VersionRegistry
|
import ru.dbotthepony.kstarbound.VersionRegistry
|
||||||
|
import ru.dbotthepony.kstarbound.defs.EntityType
|
||||||
import ru.dbotthepony.kstarbound.io.BTreeDB5
|
import ru.dbotthepony.kstarbound.io.BTreeDB5
|
||||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||||
import ru.dbotthepony.kstarbound.io.readVector2f
|
import ru.dbotthepony.kstarbound.io.readVector2f
|
||||||
@ -183,7 +184,29 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())
|
val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())
|
||||||
|
|
||||||
return load(key).thenApplyAsync(Function {
|
return load(key).thenApplyAsync(Function {
|
||||||
readEntities(pos, it ?: return@Function listOf())
|
it ?: return@Function listOf()
|
||||||
|
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
|
||||||
|
val i = reader.readVarInt()
|
||||||
|
val objects = ArrayList<AbstractEntity>()
|
||||||
|
|
||||||
|
for (i2 in 0 until i) {
|
||||||
|
val obj = VersionedJson(reader)
|
||||||
|
|
||||||
|
val type = EntityType.entries.firstOrNull { it.storeName == obj.id }
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
try {
|
||||||
|
objects.add(type.fromStorage(obj.content as JsonObject))
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
LOGGER.error("Unable to deserialize entity in chunk $pos", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.error("Unknown entity type in chunk $pos: ${obj.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
return@Function objects
|
||||||
}, Starbound.EXECUTOR)
|
}, Starbound.EXECUTOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +241,7 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
streamEntities.writeVarInt(entities.size)
|
streamEntities.writeVarInt(entities.size)
|
||||||
|
|
||||||
for (entity in entities) {
|
for (entity in entities) {
|
||||||
Starbound.storeJson {
|
Starbound.legacyStoreJson {
|
||||||
val data = JsonObject()
|
val data = JsonObject()
|
||||||
entity.serialize(data)
|
entity.serialize(data)
|
||||||
VersionRegistry.make(entity.type.storeName, data).write(streamEntities)
|
VersionRegistry.make(entity.type.storeName, data).write(streamEntities)
|
||||||
@ -335,17 +358,24 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Memory(private val get: (ByteKey) -> ByteArray?, private val set: (ByteKey, ByteArray) -> Unit) : LegacyWorldStorage() {
|
class Memory(private val get: (ByteKey) -> ByteArray?, private val set: (ByteKey, ByteArray) -> Unit) : LegacyWorldStorage() {
|
||||||
|
private val pending = HashMap<ByteKey, ByteArray>()
|
||||||
override val executor: Executor = Executor { it.run() }
|
override val executor: Executor = Executor { it.run() }
|
||||||
|
|
||||||
override fun load(at: ByteKey): CompletableFuture<ByteArray?> {
|
override fun load(at: ByteKey): CompletableFuture<ByteArray?> {
|
||||||
return CompletableFuture.completedFuture(get(at))
|
return CompletableFuture.completedFuture(pending[at] ?: get(at))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(at: ByteKey, value: ByteArray) {
|
override fun write(at: ByteKey, value: ByteArray) {
|
||||||
set(at, value)
|
// set(at, value)
|
||||||
|
pending[at] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {}
|
override fun close() {}
|
||||||
|
|
||||||
|
override fun commit() {
|
||||||
|
pending.entries.forEach { (k, v) -> set(k, v) }
|
||||||
|
pending.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DB5(private val database: BTreeDB5) : LegacyWorldStorage() {
|
class DB5(private val database: BTreeDB5) : LegacyWorldStorage() {
|
||||||
@ -363,6 +393,10 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
executor.execute { database.close() }
|
executor.execute { database.close() }
|
||||||
executor.wait(300L, TimeUnit.SECONDS)
|
executor.wait(300L, TimeUnit.SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun commit() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SQL(path: File) : LegacyWorldStorage() {
|
class SQL(path: File) : LegacyWorldStorage() {
|
||||||
@ -374,7 +408,6 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
val connection = connection
|
val connection = connection
|
||||||
|
|
||||||
cleaner = Starbound.CLEANER.register(this) {
|
cleaner = Starbound.CLEANER.register(this) {
|
||||||
/*connection.commit();*/
|
|
||||||
connection.close()
|
connection.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,6 +420,8 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
|"value" BLOB NOT NULL
|
|"value" BLOB NOT NULL
|
||||||
|)""".trimMargin())
|
|)""".trimMargin())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection.autoCommit = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// concurrent safety - load(), write() and close() are always called on one thread
|
// concurrent safety - load(), write() and close() are always called on one thread
|
||||||
@ -417,10 +452,13 @@ sealed class LegacyWorldStorage() : WorldStorage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
// carrier.execute { connection.commit() }
|
|
||||||
executor.execute { cleaner.clean() }
|
executor.execute { cleaner.clean() }
|
||||||
executor.wait(300L, TimeUnit.SECONDS)
|
executor.wait(300L, TimeUnit.SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun commit() {
|
||||||
|
executor.execute { connection.commit() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -0,0 +1,458 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.server.world
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
|
||||||
|
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
|
||||||
|
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2IntFunction
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
|
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||||
|
import ru.dbotthepony.kommons.io.readBinaryString
|
||||||
|
import ru.dbotthepony.kommons.io.readCollection
|
||||||
|
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||||
|
import ru.dbotthepony.kommons.io.writeCollection
|
||||||
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
|
import ru.dbotthepony.kstarbound.VersionRegistry
|
||||||
|
import ru.dbotthepony.kstarbound.defs.EntityType
|
||||||
|
import ru.dbotthepony.kstarbound.io.createSavepoint
|
||||||
|
import ru.dbotthepony.kstarbound.json.VersionedJson
|
||||||
|
import ru.dbotthepony.kstarbound.json.readJsonElement
|
||||||
|
import ru.dbotthepony.kstarbound.json.readJsonObjectInflated
|
||||||
|
import ru.dbotthepony.kstarbound.json.writeJsonElement
|
||||||
|
import ru.dbotthepony.kstarbound.json.writeJsonObjectDeflated
|
||||||
|
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||||
|
import ru.dbotthepony.kstarbound.math.vector.Vector2i
|
||||||
|
import ru.dbotthepony.kstarbound.util.CarriedExecutor
|
||||||
|
import ru.dbotthepony.kstarbound.util.supplyAsync
|
||||||
|
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||||
|
import ru.dbotthepony.kstarbound.world.ChunkState
|
||||||
|
import ru.dbotthepony.kstarbound.world.WorldGeometry
|
||||||
|
import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||||
|
import ru.dbotthepony.kstarbound.world.api.MutableCell
|
||||||
|
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.ref.Cleaner
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.sql.DriverManager
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.zip.Deflater
|
||||||
|
import java.util.zip.DeflaterOutputStream
|
||||||
|
import java.util.zip.Inflater
|
||||||
|
import java.util.zip.InflaterInputStream
|
||||||
|
|
||||||
|
class NativeLocalWorldStorage(file: File?) : WorldStorage() {
|
||||||
|
private val connection: Connection
|
||||||
|
private val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
|
||||||
|
private val cleaner: Cleaner.Cleanable
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (file == null) {
|
||||||
|
connection = DriverManager.getConnection("jdbc:sqlite:")
|
||||||
|
} else {
|
||||||
|
connection = DriverManager.getConnection("jdbc:sqlite:${file.absolutePath.replace('\\', '/')}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val connection = connection
|
||||||
|
|
||||||
|
cleaner = Starbound.CLEANER.register(this) {
|
||||||
|
connection.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun commit() {
|
||||||
|
executor.execute { connection.commit() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
executor.execute { connection.commit(); cleaner.clean() }
|
||||||
|
executor.wait(300L, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
connection.createStatement().use {
|
||||||
|
it.execute("PRAGMA journal_mode=WAL")
|
||||||
|
it.execute("PRAGMA synchronous=NORMAL")
|
||||||
|
|
||||||
|
it.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS "metadata" (
|
||||||
|
"key" VARCHAR NOT NULL PRIMARY KEY,
|
||||||
|
"version" INTEGER NOT NULL,
|
||||||
|
"data" BLOB NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
it.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS "cells" (
|
||||||
|
"x" INTEGER NOT NULL,
|
||||||
|
"y" INTEGER NOT NULL,
|
||||||
|
"generation_stage" INTEGER NOT NULL,
|
||||||
|
"version" INTEGER NOT NULL,
|
||||||
|
"data" BLOB NOT NULL,
|
||||||
|
PRIMARY KEY ("x", "y")
|
||||||
|
)
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
it.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS "entities" (
|
||||||
|
-- store chunks because rules for entities belonging to specific chunk might get different over time
|
||||||
|
"chunkX" INTEGER NOT NULL,
|
||||||
|
"chunkY" INTEGER NOT NULL,
|
||||||
|
"x" REAL NOT NULL,
|
||||||
|
"y" REAL NOT NULL,
|
||||||
|
"unique_id" VARCHAR,
|
||||||
|
"type" VARCHAR NOT NULL,
|
||||||
|
"version" INTEGER NOT NULL,
|
||||||
|
"data" BLOB NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
// Enforce unique-ness of unique entity IDs
|
||||||
|
// If another entity pops-up with same unique id, then it will overwrite entity in other chunk
|
||||||
|
// (unless colliding entities are both loaded into world's memory, which will cause runtime exception to be thrown;
|
||||||
|
// and no overwrite will happen)
|
||||||
|
it.execute("""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "entities_unique_id" ON "entities" ("unique_id")
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
it.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS "entities_chunk_pos" ON "entities" ("chunkX", "chunkY")
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.autoCommit = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private val readCells = connection.prepareStatement("""
|
||||||
|
SELECT "generation_stage", "version", "data" FROM "cells" WHERE "x" = ? AND "y" = ?
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
override fun loadCells(pos: ChunkPos): CompletableFuture<ChunkCells?> {
|
||||||
|
return executor.supplyAsync {
|
||||||
|
readCells.setInt(1, pos.x)
|
||||||
|
readCells.setInt(2, pos.y)
|
||||||
|
|
||||||
|
readCells.executeQuery().use {
|
||||||
|
if (!it.next())
|
||||||
|
return@supplyAsync null
|
||||||
|
|
||||||
|
val stage = ChunkState.entries[it.getInt(1)]
|
||||||
|
val version = it.getInt(2)
|
||||||
|
val data = it.getBytes(3)
|
||||||
|
|
||||||
|
val inflater = Inflater()
|
||||||
|
val stream = DataInputStream(BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(data), inflater, 0x10000), 0x40000))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val palette = PaletteSet()
|
||||||
|
palette.read(stream)
|
||||||
|
|
||||||
|
val width = stream.readInt()
|
||||||
|
val height = stream.readInt()
|
||||||
|
val array = Object2DArray.nulls<AbstractCell>(width, height)
|
||||||
|
|
||||||
|
for (x in 0 until width) {
|
||||||
|
for (y in 0 until height) {
|
||||||
|
array[x, y] = MutableCell().readNative(stream, palette, version).immutable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@supplyAsync ChunkCells(array as Object2DArray<out AbstractCell>, stage)
|
||||||
|
} finally {
|
||||||
|
inflater.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val readEntities = connection.prepareStatement("""
|
||||||
|
SELECT "type", "version", "data" FROM "entities" WHERE "chunkX" = ? AND "chunkY" = ?
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
override fun loadEntities(pos: ChunkPos): CompletableFuture<Collection<AbstractEntity>> {
|
||||||
|
return executor.supplyAsync {
|
||||||
|
readEntities.setInt(1, pos.x)
|
||||||
|
readEntities.setInt(2, pos.y)
|
||||||
|
|
||||||
|
val entities = ArrayList<AbstractEntity>()
|
||||||
|
|
||||||
|
readEntities.executeQuery().use {
|
||||||
|
while (it.next()) {
|
||||||
|
val rtype = it.getString(1)
|
||||||
|
val version = it.getInt(2)
|
||||||
|
val type = EntityType.entries.firstOrNull { it.storeName == rtype }
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
try {
|
||||||
|
val data = it.getBytes(3).readJsonObjectInflated()
|
||||||
|
entities.add(type.fromStorage(VersionRegistry.migrate(VersionedJson(rtype, version, data)) as JsonObject))
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
LOGGER.error("Unable to deserialize entity in chunk $pos", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.error("Unknown entity type $rtype in chunk $pos")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@supplyAsync entities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val readMetadata = connection.prepareStatement("""
|
||||||
|
SELECT "version", "data" FROM "metadata" WHERE "key" = ?
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
private val writeMetadata = connection.prepareStatement("""
|
||||||
|
REPLACE INTO "metadata" ("key", "version", "data") VALUES (?, ?, ?)
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
private fun readMetadata(key: String): Pair<Int, ByteArray>? {
|
||||||
|
readMetadata.setString(1, key)
|
||||||
|
|
||||||
|
return readMetadata.executeQuery().use {
|
||||||
|
if (it.next()) it.getInt(1) to it.getBytes(2) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeMetadata(key: String, version: Int, value: ByteArray) {
|
||||||
|
writeMetadata.setString(1, key)
|
||||||
|
writeMetadata.setInt(2, version)
|
||||||
|
writeMetadata.setBytes(3, value)
|
||||||
|
writeMetadata.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadMetadata(): CompletableFuture<Metadata?> {
|
||||||
|
return executor.supplyAsync {
|
||||||
|
val (version, metadata) = readMetadata("metadata") ?: return@supplyAsync null
|
||||||
|
|
||||||
|
val stream = DataInputStream(BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(metadata))))
|
||||||
|
|
||||||
|
val width = stream.readInt()
|
||||||
|
val height = stream.readInt()
|
||||||
|
val loopX = stream.readBoolean()
|
||||||
|
val loopY = stream.readBoolean()
|
||||||
|
val json = VersionedJson("WorldMetadata", version, stream.readJsonElement())
|
||||||
|
|
||||||
|
stream.close()
|
||||||
|
Metadata(WorldGeometry(Vector2i(width, height), loopX, loopY), json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val clearEntities = connection.prepareStatement("""
|
||||||
|
DELETE FROM "entities" WHERE "chunkX" = ? AND "chunkY" = ?
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
private val beginSaveEntities: PreparedStatement
|
||||||
|
private val finishSaveEntities: PreparedStatement
|
||||||
|
private val rollbackSaveEntities: PreparedStatement
|
||||||
|
|
||||||
|
init {
|
||||||
|
val (begin, commit, rollback) = connection.createSavepoint("save_entities")
|
||||||
|
beginSaveEntities = begin
|
||||||
|
finishSaveEntities = commit
|
||||||
|
rollbackSaveEntities = rollback
|
||||||
|
}
|
||||||
|
|
||||||
|
private val writeEntity = connection.prepareStatement("""
|
||||||
|
REPLACE INTO "entities" ("chunkX", "chunkY", "x", "y", "unique_id", "type", "version", "data")
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
override fun saveEntities(pos: ChunkPos, entities: Collection<AbstractEntity>) {
|
||||||
|
executor.execute {
|
||||||
|
beginSaveEntities.execute()
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearEntities.setInt(1, pos.x)
|
||||||
|
clearEntities.setInt(2, pos.y)
|
||||||
|
clearEntities.execute()
|
||||||
|
|
||||||
|
for (entity in entities) {
|
||||||
|
Starbound.storeJson {
|
||||||
|
val data = JsonObject()
|
||||||
|
entity.serialize(data)
|
||||||
|
|
||||||
|
writeEntity.setInt(1, pos.x)
|
||||||
|
writeEntity.setInt(2, pos.y)
|
||||||
|
writeEntity.setDouble(3, entity.position.x)
|
||||||
|
writeEntity.setDouble(4, entity.position.y)
|
||||||
|
writeEntity.setString(5, entity.uniqueID.get())
|
||||||
|
writeEntity.setString(6, entity.type.storeName)
|
||||||
|
writeEntity.setInt(7, VersionRegistry.currentVersion(entity.type.storeName))
|
||||||
|
writeEntity.setBytes(8, data.writeJsonObjectDeflated())
|
||||||
|
|
||||||
|
writeEntity.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishSaveEntities.execute()
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
rollbackSaveEntities.execute()
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Palette {
|
||||||
|
private val key2index = Object2IntOpenHashMap<String>()
|
||||||
|
private val index2key = Int2ObjectAVLTreeMap<String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
key2index.defaultReturnValue(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(value: String) {
|
||||||
|
index2key[key2index.computeIfAbsent(value, Object2IntFunction { key2index.size })] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(value: String): Int {
|
||||||
|
val get = key2index.getInt(value)
|
||||||
|
if (get == -1) throw NoSuchElementException("No such palette element with key '$value'")
|
||||||
|
return get
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(value: Int): String {
|
||||||
|
return index2key[value] ?: throw NoSuchElementException("No such palette element with index $value")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeKey(stream: DataOutputStream, key: String) {
|
||||||
|
if (key2index.size <= 256) {
|
||||||
|
stream.writeByte(get(key))
|
||||||
|
} else {
|
||||||
|
stream.writeShort(get(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readKey(stream: DataInputStream): String {
|
||||||
|
if (key2index.size <= 256) {
|
||||||
|
return get(stream.readUnsignedByte())
|
||||||
|
} else {
|
||||||
|
return get(stream.readUnsignedShort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(stream: DataOutputStream) {
|
||||||
|
stream.writeCollection(index2key.values) { writeBinaryString(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun read(stream: DataInputStream) {
|
||||||
|
key2index.clear()
|
||||||
|
index2key.clear()
|
||||||
|
|
||||||
|
for (key in stream.readCollection { readBinaryString() }) {
|
||||||
|
if (key in key2index)
|
||||||
|
throw IllegalStateException("Duplicate palette key $key")
|
||||||
|
|
||||||
|
index2key[key2index.computeIfAbsent(key, Object2IntFunction { key2index.size })] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaletteSet {
|
||||||
|
val tiles = Palette()
|
||||||
|
val mods = Palette()
|
||||||
|
val liquids = Palette()
|
||||||
|
|
||||||
|
fun write(stream: DataOutputStream) {
|
||||||
|
tiles.write(stream)
|
||||||
|
mods.write(stream)
|
||||||
|
liquids.write(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun read(stream: DataInputStream) {
|
||||||
|
tiles.read(stream)
|
||||||
|
mods.read(stream)
|
||||||
|
liquids.read(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val writeCells = connection.prepareStatement("""
|
||||||
|
REPLACE INTO "cells" ("x", "y", "generation_stage", "version", "data")
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
override fun saveCells(pos: ChunkPos, data: ChunkCells) {
|
||||||
|
executor.execute {
|
||||||
|
val palette = PaletteSet()
|
||||||
|
|
||||||
|
for (x in 0 until data.cells.columns) {
|
||||||
|
for (y in 0 until data.cells.rows) {
|
||||||
|
data.cells[x, y].prepare(palette)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deflater = Deflater()
|
||||||
|
val buff = FastByteArrayOutputStream()
|
||||||
|
val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(buff, deflater, 0x10000), 0x40000))
|
||||||
|
|
||||||
|
try {
|
||||||
|
palette.write(stream)
|
||||||
|
|
||||||
|
stream.writeInt(data.cells.columns)
|
||||||
|
stream.writeInt(data.cells.rows)
|
||||||
|
|
||||||
|
for (x in 0 until data.cells.columns) {
|
||||||
|
for (y in 0 until data.cells.rows) {
|
||||||
|
data.cells[x, y].writeNative(stream, palette)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
writeCells.setInt(1, pos.x)
|
||||||
|
writeCells.setInt(2, pos.y)
|
||||||
|
writeCells.setInt(3, data.state.ordinal)
|
||||||
|
writeCells.setInt(4, 0)
|
||||||
|
writeCells.setBytes(5, buff.array.copyOf(buff.length))
|
||||||
|
writeCells.execute()
|
||||||
|
} finally {
|
||||||
|
deflater.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveMetadata(data: Metadata) {
|
||||||
|
executor.execute {
|
||||||
|
val buff = FastByteArrayOutputStream()
|
||||||
|
val stream = DataOutputStream(BufferedOutputStream(DeflaterOutputStream(buff, Deflater(), 0x10000), 0x40000))
|
||||||
|
|
||||||
|
stream.writeInt(data.geometry.size.x)
|
||||||
|
stream.writeInt(data.geometry.size.y)
|
||||||
|
stream.writeBoolean(data.geometry.loopX)
|
||||||
|
stream.writeBoolean(data.geometry.loopY)
|
||||||
|
stream.writeJsonElement(data.data.content)
|
||||||
|
|
||||||
|
stream.close()
|
||||||
|
writeMetadata("metadata", data.data.version ?: 0, buff.array.copyOf(buff.length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val findUniqueEntity = connection.prepareStatement("""
|
||||||
|
SELECT "chunkX", "chunkY", "x", "y" FROM "entities" WHERE "unique_id" = ?
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
override fun findUniqueEntity(identifier: String): CompletableFuture<UniqueEntitySearchResult?> {
|
||||||
|
return executor.supplyAsync {
|
||||||
|
findUniqueEntity.setString(1, identifier)
|
||||||
|
|
||||||
|
findUniqueEntity.executeQuery().use {
|
||||||
|
if (it.next()) {
|
||||||
|
UniqueEntitySearchResult(ChunkPos(it.getInt(1), it.getInt(2)), Vector2d(it.getDouble(3), it.getDouble(4)))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOGGER = LogManager.getLogger()
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
package ru.dbotthepony.kstarbound.server.world
|
|
||||||
|
|
||||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
|
||||||
import ru.dbotthepony.kommons.io.ByteKey
|
|
||||||
import ru.dbotthepony.kommons.util.KOptional
|
|
||||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
|
||||||
import ru.dbotthepony.kstarbound.world.ChunkState
|
|
||||||
import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
|
||||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
|
|
||||||
class NativeWorldStorage() : WorldStorage() {
|
|
||||||
override fun loadCells(pos: ChunkPos): CompletableFuture<ChunkCells?> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadEntities(pos: ChunkPos): CompletableFuture<Collection<AbstractEntity>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadMetadata(): CompletableFuture<Metadata?> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveEntities(pos: ChunkPos, entities: Collection<AbstractEntity>) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveCells(pos: ChunkPos, data: ChunkCells) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveMetadata(data: Metadata) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findUniqueEntity(identifier: String): CompletableFuture<UniqueEntitySearchResult?> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -538,7 +538,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
|
|
||||||
private var savedCellState = -1
|
private var savedCellState = -1
|
||||||
|
|
||||||
private fun writeToStorage(): Collection<AbstractEntity> {
|
private fun writeToStorage(isUnloadingWorld: Boolean = false): Collection<AbstractEntity> {
|
||||||
if (!cells.isInitialized() || state <= ChunkState.EMPTY)
|
if (!cells.isInitialized() || state <= ChunkState.EMPTY)
|
||||||
return emptyList()
|
return emptyList()
|
||||||
|
|
||||||
@ -548,10 +548,15 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
filter = Predicate { !it.isRemote && aabbd.isInside(it.position) })
|
filter = Predicate { !it.isRemote && aabbd.isInside(it.position) })
|
||||||
|
|
||||||
if (!isLoadingFromDisk) {
|
if (!isLoadingFromDisk) {
|
||||||
if (savedCellState != cellChangeset)
|
if (savedCellState != cellChangeset) {
|
||||||
world.storage.saveCells(pos, WorldStorage.ChunkCells(copyCells(), state))
|
world.storage.saveCells(pos, WorldStorage.ChunkCells(copyCells(), state))
|
||||||
|
savedCellState = cellChangeset
|
||||||
|
}
|
||||||
|
|
||||||
world.storage.saveEntities(pos, unloadable.filter { it.isPersistent })
|
world.storage.saveEntities(pos, unloadable.filter { it.isPersistent })
|
||||||
|
|
||||||
|
if (!isUnloadingWorld)
|
||||||
|
world.storage.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
return unloadable
|
return unloadable
|
||||||
@ -565,7 +570,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
}
|
}
|
||||||
}, 5L, 30L, TimeUnit.SECONDS)
|
}, 5L, 30L, TimeUnit.SECONDS)
|
||||||
|
|
||||||
fun unload() {
|
fun unload(isUnloadingWorld: Boolean = false) {
|
||||||
if (isUnloaded)
|
if (isUnloaded)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -576,7 +581,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
world.chunkMap.remove(pos)
|
world.chunkMap.remove(pos)
|
||||||
flushTask.cancel(false)
|
flushTask.cancel(false)
|
||||||
|
|
||||||
writeToStorage().forEach {
|
writeToStorage(isUnloadingWorld).forEach {
|
||||||
it.remove(AbstractEntity.RemovalReason.UNLOADED)
|
it.remove(AbstractEntity.RemovalReason.UNLOADED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,9 +145,11 @@ class ServerWorld private constructor(
|
|||||||
storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata))))
|
storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var uncleanShutdown = false
|
||||||
|
|
||||||
override val eventLoop = object : BlockableEventLoop("Server World $worldID") {
|
override val eventLoop = object : BlockableEventLoop("Server World $worldID") {
|
||||||
init {
|
init {
|
||||||
isDaemon = true
|
isDaemon = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performShutdown() {
|
override fun performShutdown() {
|
||||||
@ -159,11 +161,15 @@ class ServerWorld private constructor(
|
|||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkMap.chunks().forEach {
|
if (!uncleanShutdown) {
|
||||||
|
for (chunk in chunkMap.chunks()) {
|
||||||
try {
|
try {
|
||||||
it.unload()
|
chunk.unload(isUnloadingWorld = true)
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
LOGGER.error("Exception while saving chunk ${it.pos}", err)
|
LOGGER.error("Exception while saving chunk ${chunk.pos}", err)
|
||||||
|
uncleanShutdown = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +179,11 @@ class ServerWorld private constructor(
|
|||||||
it.client.enqueueWarp(WarpAlias.Return)
|
it.client.enqueueWarp(WarpAlias.Return)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!uncleanShutdown) {
|
||||||
saveMetadata()
|
saveMetadata()
|
||||||
|
storage.commit()
|
||||||
|
}
|
||||||
|
|
||||||
storage.close()
|
storage.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -423,6 +433,7 @@ class ServerWorld private constructor(
|
|||||||
}
|
}
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
LOGGER.fatal("Exception in world tick loop", err)
|
LOGGER.fatal("Exception in world tick loop", err)
|
||||||
|
uncleanShutdown = true
|
||||||
eventLoop.shutdown()
|
eventLoop.shutdown()
|
||||||
|
|
||||||
if (worldID is WorldID.ShipWorld) {
|
if (worldID is WorldID.ShipWorld) {
|
||||||
|
@ -37,36 +37,10 @@ abstract class WorldStorage : Closeable {
|
|||||||
data class UniqueEntitySearchResult(val chunk: ChunkPos, val pos: Vector2d)
|
data class UniqueEntitySearchResult(val chunk: ChunkPos, val pos: Vector2d)
|
||||||
abstract fun findUniqueEntity(identifier: String): CompletableFuture<UniqueEntitySearchResult?>
|
abstract fun findUniqueEntity(identifier: String): CompletableFuture<UniqueEntitySearchResult?>
|
||||||
|
|
||||||
protected fun readEntities(pos: ChunkPos, data: ByteArray): List<AbstractEntity> {
|
/**
|
||||||
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data))))
|
* closes storage, discarding any un[commit]ted data
|
||||||
val i = reader.readVarInt()
|
*/
|
||||||
val objects = ArrayList<AbstractEntity>()
|
override fun close() {}
|
||||||
|
|
||||||
for (i2 in 0 until i) {
|
abstract fun commit()
|
||||||
val obj = VersionedJson(reader)
|
|
||||||
|
|
||||||
val type = EntityType.entries.firstOrNull { it.storeName == obj.id }
|
|
||||||
|
|
||||||
if (type != null) {
|
|
||||||
try {
|
|
||||||
objects.add(type.fromStorage(obj.content as JsonObject))
|
|
||||||
} catch (err: Throwable) {
|
|
||||||
LOGGER.error("Unable to deserialize entity in chunk $pos", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGGER.error("Unknown entity type in chunk $pos: ${obj.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.close()
|
|
||||||
return objects
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val LOGGER = LogManager.getLogger()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import ru.dbotthepony.kstarbound.Starbound
|
|||||||
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
|
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
|
||||||
import ru.dbotthepony.kstarbound.math.vector.Vector2i
|
import ru.dbotthepony.kstarbound.math.vector.Vector2i
|
||||||
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
|
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import ru.dbotthepony.kstarbound.world.physics.CollisionType
|
import ru.dbotthepony.kstarbound.world.physics.CollisionType
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
@ -74,6 +75,30 @@ sealed class AbstractCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun prepare(palette: NativeLocalWorldStorage.PaletteSet) {
|
||||||
|
foreground.prepare(palette)
|
||||||
|
background.prepare(palette)
|
||||||
|
liquid.prepare(palette)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeNative(stream: DataOutputStream, palette: NativeLocalWorldStorage.PaletteSet) {
|
||||||
|
foreground.writeNative(stream, palette)
|
||||||
|
background.writeNative(stream, palette)
|
||||||
|
liquid.writeNative(stream, palette)
|
||||||
|
|
||||||
|
stream.writeShort(dungeonId)
|
||||||
|
stream.writeShort(blockBiome)
|
||||||
|
stream.writeShort(envBiome)
|
||||||
|
stream.writeBoolean(biomeTransition)
|
||||||
|
|
||||||
|
if (rootSource == null) {
|
||||||
|
stream.writeBoolean(false)
|
||||||
|
} else {
|
||||||
|
stream.writeBoolean(true)
|
||||||
|
stream.writeStruct2i(rootSource!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun skip(stream: DataInputStream) {
|
fun skip(stream: DataInputStream) {
|
||||||
AbstractTileState.skip(stream)
|
AbstractTileState.skip(stream)
|
||||||
|
@ -5,6 +5,7 @@ import ru.dbotthepony.kstarbound.Registry
|
|||||||
import ru.dbotthepony.kstarbound.Starbound
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
||||||
import ru.dbotthepony.kstarbound.network.LegacyNetworkLiquidState
|
import ru.dbotthepony.kstarbound.network.LegacyNetworkLiquidState
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
|
|
||||||
@ -32,6 +33,17 @@ sealed class AbstractLiquidState {
|
|||||||
stream.writeBoolean(isInfinite)
|
stream.writeBoolean(isInfinite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun writeNative(stream: DataOutputStream, palette: NativeLocalWorldStorage.PaletteSet) {
|
||||||
|
palette.liquids.writeKey(stream, state.key)
|
||||||
|
stream.writeFloat(level)
|
||||||
|
stream.writeFloat(pressure)
|
||||||
|
stream.writeBoolean(isInfinite)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepare(palette: NativeLocalWorldStorage.PaletteSet) {
|
||||||
|
palette.liquids.add(state.key)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun skip(stream: DataInputStream) {
|
fun skip(stream: DataInputStream) {
|
||||||
stream.skipNBytes(1 + 4 + 4 + 1)
|
stream.skipNBytes(1 + 4 + 4 + 1)
|
||||||
|
@ -7,6 +7,7 @@ import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
|||||||
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
|
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
|
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
|
||||||
import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState
|
import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
|
|
||||||
@ -46,6 +47,20 @@ sealed class AbstractTileState {
|
|||||||
stream.writeByte(byteModifierHueShift())
|
stream.writeByte(byteModifierHueShift())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun writeNative(stream: DataOutputStream, palette: NativeLocalWorldStorage.PaletteSet) {
|
||||||
|
palette.tiles.writeKey(stream, material.key)
|
||||||
|
stream.writeFloat(hueShift)
|
||||||
|
stream.writeByte(color.ordinal)
|
||||||
|
|
||||||
|
palette.mods.writeKey(stream, modifier.key)
|
||||||
|
stream.writeFloat(modifierHueShift)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepare(palette: NativeLocalWorldStorage.PaletteSet) {
|
||||||
|
palette.tiles.add(material.key)
|
||||||
|
palette.mods.add(modifier.key)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun skip(stream: DataInputStream) {
|
fun skip(stream: DataInputStream) {
|
||||||
stream.skipNBytes(2 + 1 + 1 + 2 + 1)
|
stream.skipNBytes(2 + 1 + 1 + 2 + 1)
|
||||||
|
@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.world.api
|
|||||||
import ru.dbotthepony.kstarbound.io.readVector2i
|
import ru.dbotthepony.kstarbound.io.readVector2i
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
|
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
|
||||||
import ru.dbotthepony.kstarbound.math.vector.Vector2i
|
import ru.dbotthepony.kstarbound.math.vector.Vector2i
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
|
|
||||||
data class MutableCell(
|
data class MutableCell(
|
||||||
@ -47,6 +48,26 @@ data class MutableCell(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun readNative(stream: DataInputStream, palette: NativeLocalWorldStorage.PaletteSet, version: Int): MutableCell {
|
||||||
|
foreground.readNative(stream, palette, version)
|
||||||
|
background.readNative(stream, palette, version)
|
||||||
|
liquid.readNative(stream, palette, version)
|
||||||
|
|
||||||
|
dungeonId = stream.readUnsignedShort()
|
||||||
|
blockBiome = stream.readUnsignedShort()
|
||||||
|
envBiome = stream.readUnsignedShort()
|
||||||
|
|
||||||
|
biomeTransition = stream.readBoolean()
|
||||||
|
|
||||||
|
if (stream.readBoolean()) {
|
||||||
|
rootSource = stream.readVector2i()
|
||||||
|
} else {
|
||||||
|
rootSource = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
override fun tile(background: Boolean): MutableTileState {
|
override fun tile(background: Boolean): MutableTileState {
|
||||||
if (background)
|
if (background)
|
||||||
return this.background
|
return this.background
|
||||||
|
@ -4,6 +4,7 @@ import ru.dbotthepony.kstarbound.Registries
|
|||||||
import ru.dbotthepony.kstarbound.Registry
|
import ru.dbotthepony.kstarbound.Registry
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
|
|
||||||
data class MutableLiquidState(
|
data class MutableLiquidState(
|
||||||
@ -27,6 +28,14 @@ data class MutableLiquidState(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun readNative(stream: DataInputStream, palette: NativeLocalWorldStorage.PaletteSet, version: Int): MutableLiquidState {
|
||||||
|
state = Registries.liquid[palette.liquids.readKey(stream)] ?: BuiltinMetaMaterials.NO_LIQUID
|
||||||
|
level = stream.readFloat()
|
||||||
|
pressure = stream.readFloat()
|
||||||
|
isInfinite = stream.readBoolean()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
state = BuiltinMetaMaterials.NO_LIQUID
|
state = BuiltinMetaMaterials.NO_LIQUID
|
||||||
level = 0f
|
level = 0f
|
||||||
|
@ -5,6 +5,7 @@ import ru.dbotthepony.kstarbound.Registry
|
|||||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
|
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
|
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
|
||||||
|
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
|
|
||||||
data class MutableTileState(
|
data class MutableTileState(
|
||||||
@ -66,8 +67,21 @@ data class MutableTileState(
|
|||||||
material = Registries.tiles[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY
|
material = Registries.tiles[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY
|
||||||
setHueShift(stream.readUnsignedByte())
|
setHueShift(stream.readUnsignedByte())
|
||||||
color = TileColor.of(stream.readUnsignedByte())
|
color = TileColor.of(stream.readUnsignedByte())
|
||||||
|
|
||||||
modifier = Registries.tileModifiers[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY_MOD
|
modifier = Registries.tileModifiers[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY_MOD
|
||||||
setModHueShift(stream.readUnsignedByte())
|
setModHueShift(stream.readUnsignedByte())
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readNative(stream: DataInputStream, palette: NativeLocalWorldStorage.PaletteSet, version: Int): MutableTileState {
|
||||||
|
material = Registries.tiles[palette.tiles.readKey(stream)] ?: BuiltinMetaMaterials.EMPTY
|
||||||
|
hueShift = stream.readFloat()
|
||||||
|
color = TileColor.of(stream.readUnsignedByte())
|
||||||
|
|
||||||
|
modifier = Registries.tileModifiers[palette.mods.readKey(stream)] ?: BuiltinMetaMaterials.EMPTY_MOD
|
||||||
|
modifierHueShift = stream.readFloat()
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user