diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt index ad5ce847..096e1488 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt @@ -51,6 +51,10 @@ object VersionRegistry { return migrate(read, name) } + fun load(reader: JsonElement): JsonElement { + return migrate(VersionedJson.fromJson(reader)) + } + private val adapter by lazy { Starbound.gson.getAdapter(VersionedJson::class.java) } fun load(): Future<*> { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLSavepoint.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLSavepoint.kt new file mode 100644 index 00000000..ab03772d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLSavepoint.kt @@ -0,0 +1,44 @@ +package ru.dbotthepony.kstarbound.io + +import java.io.Closeable +import java.sql.Connection +import java.sql.PreparedStatement + +class SQLSavepoint(connection: Connection, name: String) : Closeable { + private val begin: PreparedStatement + private val finish: PreparedStatement + private val rollback: PreparedStatement + + init { + check('"' !in name) { "Invalid identifier: $name" } + + begin = connection.prepareStatement(""" + SAVEPOINT "$name" + """.trimIndent()) + + finish = connection.prepareStatement(""" + RELEASE SAVEPOINT "$name" + """.trimIndent()) + + rollback = connection. prepareStatement(""" + ROLLBACK TO SAVEPOINT "$name" + """.trimIndent()) + } + + fun execute(block: () -> Unit) { + try { + begin.execute() + block.invoke() + finish.execute() + } catch (err: Throwable) { + rollback.execute() + throw err + } + } + + override fun close() { + begin.close() + finish.close() + rollback.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLUtils.kt deleted file mode 100644 index e79d36a0..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/SQLUtils.kt +++ /dev/null @@ -1,24 +0,0 @@ -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) -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedJson.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedJson.kt index d069069f..536f9d10 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedJson.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/VersionedJson.kt @@ -40,5 +40,9 @@ data class VersionedJson(val id: String, val version: Int?, val content: JsonEle companion object { private val adapter by lazy { Starbound.gson.getAdapter() } + + fun fromJson(data: JsonElement): VersionedJson { + return adapter.fromJsonTree(data) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt index efb71e54..a4624021 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.server.world +import com.google.gson.JsonArray import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.io.FastByteArrayInputStream @@ -15,12 +16,13 @@ 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.io.SQLSavepoint import ru.dbotthepony.kstarbound.json.VersionedJson +import ru.dbotthepony.kstarbound.json.readJsonArrayInflated import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonObjectInflated +import ru.dbotthepony.kstarbound.json.writeJsonArrayDeflated 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 @@ -39,7 +41,6 @@ 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 @@ -96,28 +97,26 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() { it.execute(""" CREATE TABLE IF NOT EXISTS "entities" ( + "x" INTEGER NOT NULL, + "y" INTEGER NOT NULL, + "data" BLOB NOT NULL, + PRIMARY KEY ("x", "y") + ) + """.trimIndent()) + + it.execute(""" + CREATE TABLE IF NOT EXISTS "unique_entities" ( + "unique_id" BLOB NOT NULL PRIMARY KEY, -- 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 + "y" REAL 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") + CREATE INDEX IF NOT EXISTS "entities_chunk_pos" ON "unique_entities" ("chunkX", "chunkY") """.trimIndent()) } @@ -167,7 +166,7 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() { } private val readEntities = connection.prepareStatement(""" - SELECT "type", "version", "data" FROM "entities" WHERE "chunkX" = ? AND "chunkY" = ? + SELECT "data" FROM "entities" WHERE "x" = ? AND "y" = ? """.trimIndent()) override fun loadEntities(pos: ChunkPos): CompletableFuture> { @@ -178,20 +177,22 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() { val entities = ArrayList() 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 (it.next()) { + val data = it.getBytes(1).readJsonArrayInflated() - 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) + for (entry in data) { + val versioned = VersionedJson.fromJson(entry) + val type = EntityType.entries.firstOrNull { it.storeName == versioned.id } + + if (type != null) { + try { + entities.add(type.fromStorage(VersionRegistry.migrate(versioned) as JsonObject)) + } catch (err: Throwable) { + LOGGER.error("Unable to deserialize entity in chunk $pos", err) + } + } else { + LOGGER.error("Unknown entity type ${versioned.id} in chunk $pos") } - } else { - LOGGER.error("Unknown entity type $rtype in chunk $pos") } } } @@ -240,57 +241,53 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() { } } - private val clearEntities = connection.prepareStatement(""" - DELETE FROM "entities" WHERE "chunkX" = ? AND "chunkY" = ? + private val clearUniqueEntities = connection.prepareStatement(""" + DELETE FROM "unique_entities" WHERE "chunkX" = ? AND "chunkY" = ? """.trimIndent()) - private val beginSaveEntities: PreparedStatement - private val finishSaveEntities: PreparedStatement - private val rollbackSaveEntities: PreparedStatement + private val entitiesSavepoint = SQLSavepoint(connection, "save_entities") - init { - val (begin, commit, rollback) = connection.createSavepoint("save_entities") - beginSaveEntities = begin - finishSaveEntities = commit - rollbackSaveEntities = rollback - } + private val writeEntities = connection.prepareStatement(""" + REPLACE INTO "entities" ("x", "y", "data") + VALUES (?, ?, ?) + """.trimIndent()) - private val writeEntity = connection.prepareStatement(""" - REPLACE INTO "entities" ("chunkX", "chunkY", "x", "y", "unique_id", "type", "version", "data") - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + private val writeUniqueEntity = connection.prepareStatement(""" + REPLACE INTO "unique_entities" ("unique_id", "chunkX", "chunkY", "x", "y") + VALUES (?, ?, ?, ?, ?) """.trimIndent()) override fun saveEntities(pos: ChunkPos, entities: Collection) { executor.execute { - beginSaveEntities.execute() + entitiesSavepoint.execute { + clearUniqueEntities.setInt(1, pos.x) + clearUniqueEntities.setInt(2, pos.y) + clearUniqueEntities.execute() - try { - clearEntities.setInt(1, pos.x) - clearEntities.setInt(2, pos.y) - clearEntities.execute() + val storeData = JsonArray() for (entity in entities) { Starbound.storeJson { val data = JsonObject() entity.serialize(data) + storeData.add(VersionRegistry.make(entity.type.storeName, data).toJson()) - 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()) + if (entity.uniqueID.get() != null) { + writeUniqueEntity.setString(1, entity.uniqueID.get()) + writeUniqueEntity.setInt(2, pos.x) + writeUniqueEntity.setInt(3, pos.y) + writeUniqueEntity.setDouble(4, entity.position.x) + writeUniqueEntity.setDouble(5, entity.position.y) - writeEntity.execute() + writeUniqueEntity.execute() + } } } - finishSaveEntities.execute() - } catch (err: Throwable) { - rollbackSaveEntities.execute() - throw err + writeEntities.setInt(1, pos.x) + writeEntities.setInt(2, pos.y) + writeEntities.setBytes(3, storeData.writeJsonArrayDeflated()) + writeEntities.execute() } } } @@ -430,7 +427,7 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() { } private val findUniqueEntity = connection.prepareStatement(""" - SELECT "chunkX", "chunkY", "x", "y" FROM "entities" WHERE "unique_id" = ? + SELECT "chunkX", "chunkY", "x", "y" FROM "unique_entities" WHERE "unique_id" = ? """.trimIndent()) override fun findUniqueEntity(identifier: String): CompletableFuture {