From e2b17f5761bd68470f853c02f7c5b939d9dac81f Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 26 Aug 2022 16:29:37 +0700 Subject: [PATCH] KConcreteTypeAdapter test --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 30 ++ .../ru/dbotthepony/kstarbound/Starbound.kt | 31 +- .../kstarbound/client/StarboundClient.kt | 2 +- .../kstarbound/defs/MaterialModifiers.kt | 36 ++ .../kstarbound/defs/TileDefinition.kt | 17 + .../kstarbound/io/KConcreteTypeAdapter.kt | 400 ++++++++++++++++++ .../dbotthepony/kstarbound/io/KTypeAdapter.kt | 28 +- 7 files changed, 522 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 2b3bbb0d..35a1009b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound +import com.google.gson.JsonElement import com.google.gson.JsonObject import org.apache.logging.log4j.LogManager import org.lwjgl.Version @@ -8,6 +9,7 @@ import ru.dbotthepony.kbox2d.api.* import ru.dbotthepony.kbox2d.collision.shapes.CircleShape import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.defs.MaterialModifier import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.projectile.ProjectilePhysics import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef @@ -39,6 +41,34 @@ fun main() { //return } + if (true) { + val input = "{\n" + + " \"modId\" : 26,\n" + + " \"modName\" : \"aegisalt\",\n" + + " \"itemDrop\" : \"aegisaltore\",\n" + + " \"description\" : \"Aegisalt.\",\n" + + " \"health\" : 5,\n" + + " \"harvestLevel\" : 5,\n" + + " \"breaksWithTile\" : true,\n" + + "\n" + + " \"miningSounds\" : [ \"/sfx/tools/pickaxe_ore.ogg\", \"/sfx/tools/pickaxe_ore2.ogg\" ],\n" + + " \"miningParticle\" : \"orespark\",\n" + + "\n" + + " \"renderTemplate\" : \"/tiles/classicmaterialtemplate.config\",\n" + + " \"renderParameters\" : {\n" + + " \"texture\" : \"aegisalt.png\",\n" + + " \"variants\" : 8,\n" + + " \"multiColored\" : false,\n" + + " \"zLevel\" : 0\n" + + " }\n" + + "}\n" + + val json = Starbound.gson.fromJson(input, MaterialModifier::class.java) + + println(json) + return + } + val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) //val db = BTreeDB(File("world.world")) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index e394e7dc..334314ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -71,6 +71,7 @@ object Starbound : IVFS { .also(DungeonWorldDef::registerGson) .also(ParallaxPrototype::registerGson) .also(JsonFunction::registerGson) + .also(MaterialModifier::registerGson) .registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) @@ -226,21 +227,19 @@ object Starbound : IVFS { private fun loadTileMaterials(callback: (String) -> Unit) { for (fs in fileSystems) { - for (listedFile in fs.listAllFiles("tiles/materials")) { - if (listedFile.endsWith(".material")) { - try { - callback("Loading $listedFile") + for (listedFile in fs.listAllFilesWithExtension("material")) { + try { + callback("Loading $listedFile") - val tileDef = TileDefinitionBuilder.fromJson(JsonParser.parseReader(getReader(listedFile)) as JsonObject).build("/tiles/materials") + val tileDef = TileDefinitionBuilder.fromJson(JsonParser.parseReader(getReader(listedFile)) as JsonObject).build("/tiles/materials") - check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" } - check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" } - tilesByMaterialID[tileDef.materialId] = tileDef - tiles[tileDef.materialName] = tileDef - } catch (err: Throwable) { - //throw TileDefLoadingException("Loading tile file $listedFile", err) - LOGGER.error("Loading tile file $listedFile", err) - } + check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" } + check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" } + tilesByMaterialID[tileDef.materialId] = tileDef + tiles[tileDef.materialName] = tileDef + } catch (err: Throwable) { + //throw TileDefLoadingException("Loading tile file $listedFile", err) + LOGGER.error("Loading tile file $listedFile", err) } } } @@ -298,4 +297,10 @@ object Starbound : IVFS { } } } + + private fun loadMaterialModifiers(callback: (String) -> Unit) { + for (fs in fileSystems) { + + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 9ac78823..2e48ac17 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -125,7 +125,7 @@ class StarboundClient : AutoCloseable { val gl = GLStateTracker() - var world: ClientWorld? = ClientWorld(this, 0L, 94) + var world: ClientWorld? = ClientWorld(this, 0L, 0) fun ensureSameThread() = gl.ensureSameThread() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt new file mode 100644 index 00000000..76458758 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt @@ -0,0 +1,36 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.gson.GsonBuilder +import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter + +data class MaterialModifier( + val modId: Int, + val modName: String, + val itemDrop: String, + val description: String, + val health: Int, + val harvestLevel: Int, + val breaksWithTile: Boolean, + val miningSounds: List, + val miningParticle: String, + val renderTemplate: String, +) { + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(MaterialModifier::class) + .plain(MaterialModifier::modId) + .plain(MaterialModifier::modName) + .plain(MaterialModifier::itemDrop) + .plain(MaterialModifier::description) + .plain(MaterialModifier::health) + .plain(MaterialModifier::harvestLevel) + .plain(MaterialModifier::breaksWithTile) + .list(MaterialModifier::miningSounds, String::class.java) + .plain(MaterialModifier::miningParticle) + .plain(MaterialModifier::renderTemplate) + .build() + + fun registerGson(gsonBuilder: GsonBuilder) { + gsonBuilder.registerTypeAdapter(MaterialModifier::class.java, ADAPTER) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt index 5a8ae028..8cff6210 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt @@ -2,9 +2,14 @@ package ru.dbotthepony.kstarbound.defs import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap +import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.internal.bind.TypeAdapters +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.world.ITileGetter @@ -482,6 +487,18 @@ data class TileRenderTemplate( companion object { val map = HashMap() + fun register(builder: GsonBuilder) { + builder.registerTypeAdapter(TileRenderTemplate::class.java, object : TypeAdapter() { + override fun write(out: JsonWriter?, value: TileRenderTemplate?) { + TODO("Not yet implemented") + } + + override fun read(`in`: JsonReader): TileRenderTemplate { + return fromJson(TypeAdapters.JSON_ELEMENT.read(`in`) as JsonObject) + } + }) + } + fun load(path: String): TileRenderTemplate { return map.computeIfAbsent(path) { try { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt new file mode 100644 index 00000000..3c8c0556 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt @@ -0,0 +1,400 @@ +package ru.dbotthepony.kstarbound.io + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonElement +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.internal.bind.TypeAdapters +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Starbound +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.isSuperclassOf +import kotlin.reflect.full.isSupertypeOf + +private class PassthroughAdapter(private val bound: Class) : TypeAdapter() { + override fun write(out: JsonWriter, value: T?) { + Starbound.gson.toJson(Starbound.gson.toJsonTree(value, bound) as JsonElement, out) + } + + override fun read(reader: JsonReader): T? { + return Starbound.gson.fromJson(reader, bound) + } +} + +private fun resolveBound(bound: Class): TypeAdapter? { + return when (bound) { + Float::class.java -> TypeAdapters.FLOAT as TypeAdapter + Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter + String::class.java -> TypeAdapters.STRING as TypeAdapter + Int::class.java -> TypeAdapters.INTEGER as TypeAdapter + Long::class.java -> TypeAdapters.LONG as TypeAdapter + Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter + else -> Starbound.gson.getAdapter(bound) + } +} + +class ListAdapter(private val bound: Class) : TypeAdapter>() { + private val resolvedBound by lazy { + resolveBound(bound) + } + + override fun write(out: JsonWriter, value: List) { + out.beginArray() + + val resolvedBound = resolvedBound + + if (resolvedBound != null) { + for (v in value) { + resolvedBound.write(out, v) + } + } else { + for (v in value) { + Starbound.gson.toJson(Starbound.gson.toJsonTree(v, bound) as JsonElement, out) + } + } + + out.endArray() + } + + override fun read(reader: JsonReader): List { + reader.beginArray() + + val builder = ImmutableList.builder() + val resolvedBound = resolvedBound + + if (resolvedBound != null) { + while (reader.peek() != JsonToken.END_ARRAY) { + val readObject = resolvedBound.read(reader) ?: throw JsonSyntaxException("List does not accept nulls") + builder.add(readObject as T) + } + } else { + while (reader.peek() != JsonToken.END_ARRAY) { + val readObject = Starbound.gson.fromJson(reader, bound) ?: throw JsonSyntaxException("List does not accept nulls") + builder.add(readObject) + } + } + + reader.endArray() + + return builder.build() + } +} + +class MapAdapter(private val boundKey: Class, private val boundValue: Class) : TypeAdapter>() { + private val resolvedKey by lazy { + resolveBound(boundKey) + } + + private val resolvedValue by lazy { + resolveBound(boundValue) + } + + override fun write(out: JsonWriter, value: Map) { + out.beginArray() + + val resolvedKey = resolvedKey + val resolvedValue = resolvedValue + + if (resolvedKey != null && resolvedValue != null) { + for ((k, v) in value) { + out.beginArray() + resolvedKey.write(out, k) + resolvedValue.write(out, v) + out.endArray() + } + } else if (resolvedKey != null) { + for ((k, v) in value) { + out.beginArray() + resolvedKey.write(out, k) + Starbound.gson.toJson(Starbound.gson.toJsonTree(v, boundValue) as JsonElement, out) + out.endArray() + } + } else if (resolvedValue != null) { + for ((k, v) in value) { + out.beginArray() + Starbound.gson.toJson(Starbound.gson.toJsonTree(k, boundKey) as JsonElement, out) + resolvedValue.write(out, v) + out.endArray() + } + } else { + for ((k, v) in value) { + out.beginArray() + Starbound.gson.toJson(Starbound.gson.toJsonTree(k, boundKey) as JsonElement, out) + Starbound.gson.toJson(Starbound.gson.toJsonTree(v, boundValue) as JsonElement, out) + out.endArray() + } + } + + out.endArray() + } + + override fun read(reader: JsonReader): Map { + reader.beginArray() + + val builder = ImmutableMap.builder() + + val resolvedKey = resolvedKey + val resolvedValue = resolvedValue + + if (resolvedKey != null && resolvedValue != null) { + while (reader.peek() != JsonToken.END_ARRAY) { + reader.beginArray() + builder.put(resolvedKey.read(reader), resolvedValue.read(reader)) + reader.endArray() + } + } else if (resolvedKey != null) { + while (reader.peek() != JsonToken.END_ARRAY) { + reader.beginArray() + builder.put(resolvedKey.read(reader), Starbound.gson.fromJson(reader, boundValue)) + reader.endArray() + } + } else if (resolvedValue != null) { + while (reader.peek() != JsonToken.END_ARRAY) { + reader.beginArray() + builder.put(Starbound.gson.fromJson(reader, boundKey), resolvedValue.read(reader)) + reader.endArray() + } + } else { + while (reader.peek() != JsonToken.END_ARRAY) { + reader.beginArray() + builder.put(Starbound.gson.fromJson(reader, boundKey), Starbound.gson.fromJson(reader, boundValue)) + reader.endArray() + } + } + + reader.endArray() + + return builder.build() + } +} + +class StringMapAdapter(private val bound: Class) : TypeAdapter>() { + private val resolvedBound by lazy { + resolveBound(bound) + } + + override fun write(out: JsonWriter, value: Map) { + val resolvedBound = resolvedBound + + out.beginObject() + + if (resolvedBound != null) { + for ((k, v) in value) { + out.name(k) + resolvedBound.write(out, v) + } + } else { + for ((k, v) in value) { + out.name(k) + Starbound.gson.toJson(Starbound.gson.toJsonTree(v, bound) as JsonElement, out) + } + } + + out.endObject() + } + + override fun read(reader: JsonReader): Map { + val builder = ImmutableMap.builder() + + reader.beginObject() + + val resolvedBound = resolvedBound + + if (resolvedBound != null) { + while (reader.peek() != JsonToken.END_OBJECT) { + builder.put(reader.nextName(), resolvedBound.read(reader)) + } + } else { + while (reader.peek() != JsonToken.END_OBJECT) { + builder.put(reader.nextName(), Starbound.gson.fromJson(reader, bound)) + } + } + + reader.endObject() + + return builder.build() + } +} + +/** + * TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов"). + */ +class KConcreteTypeAdapter( + val bound: KClass, + val types: ImmutableList, TypeAdapter<*>>> +) : TypeAdapter() { + private val returnTypeCache = Object2ObjectArrayMap, KType>() + private val mapped = Object2IntArrayMap() + + private val loggedMisses = ObjectArraySet() + + init { + for ((field) in types) { + returnTypeCache[field] = field.returnType + } + + mapped.defaultReturnValue(-1) + + for ((i, pair) in types.withIndex()) { + mapped[pair.first.name] = i + } + } + + private val factory: KFunction = bound.constructors.firstOrNull first@{ + if (it.parameters.size == types.size) { + val iterator = types.iterator() + + for (param in it.parameters) { + val nextParam = iterator.next() + + val a = param.type + val b = nextParam.first.returnType + + if (!a.isSupertypeOf(b) || a.isMarkedNullable != b.isMarkedNullable) { + return@first false + } + } + + return@first true + } + + return@first false + } ?: throw NoSuchElementException("Unable to determine constructor for ${bound.qualifiedName} matching (${types.joinToString(", ")})") + + override fun write(out: JsonWriter, value: T) { + out.beginObject() + + for ((field, adapter) in types) { + out.name(field.name) + (adapter as TypeAdapter).write(out, (field as KProperty1).get(value)) + } + + out.endObject() + } + + override fun read(reader: JsonReader): T { + reader.beginObject() + + val readValues = arrayOfNulls(types.size) + + while (reader.peek() != JsonToken.END_OBJECT) { + val name = reader.nextName() + val fieldId = mapped.getInt(name) + + if (fieldId == -1) { + if (loggedMisses.add(name)) { + LOGGER.warn("Skipping JSON field with name $name because ${bound.qualifiedName} has no such field") + } + + reader.skipValue() + } else { + val (field, adapter) = types[fieldId] + + try { + readValues[fieldId] = adapter.read(reader) + } catch(err: Throwable) { + throw JsonSyntaxException("Exception reading field ${field.name}", err) + } + } + } + + for ((i, pair) in types.withIndex()) { + val (field) = pair + + if (readValues[i] == null && !returnTypeCache[field]!!.isMarkedNullable) { + throw JsonSyntaxException("Field ${field.name} does not accept nulls") + } + } + + reader.endObject() + + return factory.call(*readValues as Array) + } + + class Builder(val clazz: KClass) { + private val types = ArrayList, TypeAdapter<*>>>() + + /** + * Добавляет поле без generic типов + */ + fun plain(field: KProperty1): Builder { + val returnType = field.returnType + val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${field.name}!") + + if (classifier.isSuperclassOf(Float::class)) { + types.add(field to TypeAdapters.FLOAT) + } else if (classifier.isSuperclassOf(Double::class)) { + types.add(field to TypeAdapters.DOUBLE) + } else if (classifier.isSuperclassOf(Int::class)) { + types.add(field to TypeAdapters.INTEGER) + } else if (classifier.isSuperclassOf(Long::class)) { + types.add(field to TypeAdapters.LONG) + } else if (classifier.isSuperclassOf(String::class)) { + types.add(field to TypeAdapters.STRING) + } else if (classifier.isSuperclassOf(Boolean::class)) { + types.add(field to TypeAdapters.BOOLEAN) + } else { + types.add(field to PassthroughAdapter(classifier.java)) + } + + return this + } + + /** + * Добавляет поле, которое содержит список значений V (без null). + * + * Список неизменяем (создаётся объект [ImmutableList]) + */ + fun list(field: KProperty1>, type: Class): Builder { + types.add(field to ListAdapter(type)) + return this + } + + /** + * Добавляет поле, которое содержит список значений V (без null). + * + * Список неизменяем (создаётся объект [ImmutableList]) + */ + fun list(field: KProperty1>, type: KClass): Builder { + return this.list(field, type.java) + } + + /** + * Добавляет поле-таблицу, которое кодируется как [[key, value], [key, value], ...] + * + * Таблица неизменяема (создаётся объект [ImmutableMap]) + */ + fun map(field: KProperty1>, keyType: Class, valueType: Class): Builder { + types.add(field to MapAdapter(keyType, valueType)) + return this + } + + /** + * Добавляет поле-таблицу, которое кодируется как {"a": value, "b": value, ...} + * + * Таблица неизменяема (создаётся объект [ImmutableMap]) + */ + fun map(field: KProperty1>, valueType: Class): Builder { + types.add(field to StringMapAdapter(valueType)) + return this + } + + fun build(): KConcreteTypeAdapter { + return KConcreteTypeAdapter(clazz, ImmutableList.copyOf(types)) + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt index 67c7e7ac..622eaf49 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.io +import com.google.common.collect.ImmutableList import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.internal.bind.TypeAdapters @@ -17,7 +18,14 @@ import kotlin.reflect.KType import kotlin.reflect.full.isSuperclassOf /** - * Kotlin property aware adapter + * Kotlin property aware adapter. + * + * Создаёт пустые классы, а после наполняет их данными, что подходит для builder'ов с очень + * большим количеством возможных данных внутри. + * + * + * + * Подходит для игровых структур которые могут быть "разобраны" и пересобраны. */ class KTypeAdapter(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { private val mappedFields = Object2ObjectArrayMap>() @@ -35,9 +43,14 @@ class KTypeAdapter(val factory: () -> T, vararg fields: KMutableProperty1> get() { - val iterator = mappedFields.values.iterator() - return Array(mappedFields.size) { iterator.next() } + val fields: List> by lazy { + return@lazy ImmutableList.builder>().let { + for (v in mappedFields.values.iterator()) { + it.add(v) + } + + it.build() + } } fun ignoreProperty(vararg value: String): KTypeAdapter { @@ -81,7 +94,7 @@ class KTypeAdapter(val factory: () -> T, vararg fields: KMutableProperty1(val factory: () -> T, vararg fields: KMutableProperty1(val factory: () -> T, vararg fields: KMutableProperty1