From d016aa807cb027bc58638c3f9dfdd84ccc28c59a Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 30 Dec 2022 15:12:55 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20Json=20=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D1=8B=20=D0=B2=20KConcreteTypeAdapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 2 +- .../ru/dbotthepony/kstarbound/Starbound.kt | 2 - .../kstarbound/defs/ImmutableEnroller.kt | 38 +++++ .../kstarbound/defs/JsonFlattener.kt | 57 +++++++ .../kstarbound/defs/MutableFlattener.kt | 34 ++++ .../kstarbound/defs/RawPrototype.kt | 116 ------------- .../kstarbound/defs/item/ItemDefinition.kt | 9 + .../kstarbound/defs/tile/RenderTemplate.kt | 9 +- .../kstarbound/io/KConcreteTypeAdapter.kt | 160 +++++++++++++++--- 9 files changed, 278 insertions(+), 149 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFlattener.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/MutableFlattener.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 66aff778..dce62ae5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -163,7 +163,7 @@ fun main() { //client.world!!.parallax = Starbound.parallaxAccess["garden"] for (i in 0 .. 16) { - val item = ItemEntity(client.world!!, Starbound.itemAccess["brain"]!!) + val item = ItemEntity(client.world!!, Starbound.itemAccess["money"]!!) item.position = Vector2d(600.0 + 16.0 + i, 721.0 + 48.0) item.spawn() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index f937cf1e..2df232ef 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -458,7 +458,5 @@ object Starbound { } } } - - items.values.stream().filter { it.currency != null }.forEach(::println) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt new file mode 100644 index 00000000..7654fe5d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ImmutableEnroller.kt @@ -0,0 +1,38 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap + +/** + * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов + */ +fun enrollList(input: List, interner: (String) -> String = String::intern): ImmutableList { + val builder = ImmutableList.builder() + + for (v in input) { + when (v) { + is Map<*, *> -> builder.add(enrollMap(v as Map, interner)) + is List<*> -> builder.add(enrollList(v as List, interner)) + else -> builder.add((v as? String)?.let(interner) ?: v) + } + } + + return builder.build() +} + +/** + * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов + */ +fun enrollMap(input: Map, interner: (String) -> String = String::intern): ImmutableMap { + val builder = ImmutableMap.builder() + + for ((k, v) in input) { + when (v) { + is Map<*, *> -> builder.put(interner(k), enrollMap(v as Map)) + is List<*> -> builder.put(interner(k), enrollList(v as List)) + else -> builder.put(interner(k), (v as? String)?.let(interner) ?: v) + } + } + + return builder.build() +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFlattener.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFlattener.kt new file mode 100644 index 00000000..df9efb3b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFlattener.kt @@ -0,0 +1,57 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap + +private fun flattenJsonPrimitive(input: JsonPrimitive): Any { + if (input.isNumber) { + return input.asNumber + } else if (input.isString) { + return input.asString.intern() + } else { + return input.asBoolean + } +} + +private fun flattenJsonArray(input: JsonArray): ArrayList { + val flattened = ArrayList(input.size()) + + for (v in input) { + when (v) { + is JsonObject -> flattened.add(flattenJsonObject(v)) + is JsonArray -> flattened.add(flattenJsonArray(v)) + is JsonPrimitive -> flattened.add(flattenJsonPrimitive(v)) + // is JsonNull -> baked.add(null) + } + } + + return flattened +} + +private fun flattenJsonObject(input: JsonObject): Object2ObjectArrayMap { + val flattened = Object2ObjectArrayMap() + + for ((k, v) in input.entrySet()) { + when (v) { + is JsonObject -> flattened[k] = flattenJsonObject(v) + is JsonArray -> flattened[k] = flattenJsonArray(v) + is JsonPrimitive -> flattened[k] = flattenJsonPrimitive(v) + } + } + + return flattened +} + +fun flattenJsonElement(input: JsonElement): Any? { + return when (input) { + is JsonObject -> flattenJsonObject(input) + is JsonArray -> flattenJsonArray(input) + is JsonPrimitive -> flattenJsonPrimitive(input) + is JsonNull -> null + else -> throw IllegalArgumentException("Unknown argument $input") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MutableFlattener.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MutableFlattener.kt new file mode 100644 index 00000000..c9d3fbb7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MutableFlattener.kt @@ -0,0 +1,34 @@ +package ru.dbotthepony.kstarbound.defs + +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap + +/** + * Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов + */ +fun flattenList(input: List): ArrayList { + val list = ArrayList(input.size) + + for (v in input) { + when (v) { + is Map<*, *> -> list.add(flattenMap(v as Map)) + is List<*> -> list.add(flattenList(v as List)) + else -> list.add(v) + } + } + + return list +} + +fun flattenMap(input: Map): Object2ObjectArrayMap { + val map = Object2ObjectArrayMap() + + for ((k, v) in input) { + when (v) { + is Map<*, *> -> map[k] = flattenMap(v as Map) + is List<*> -> map[k] = flattenList(v as List) + else -> map[k] = v + } + } + + return map +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt index c2cfdf7b..1417bf1b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RawPrototype.kt @@ -1,124 +1,8 @@ package ru.dbotthepony.kstarbound.defs -import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap -import com.google.gson.* import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap -private fun flattenJsonPrimitive(input: JsonPrimitive): Any { - if (input.isNumber) { - return input.asNumber - } else if (input.isString) { - return input.asString.intern() - } else { - return input.asBoolean - } -} - -private fun flattenJsonArray(input: JsonArray): ArrayList { - val flattened = ArrayList(input.size()) - - for (v in input) { - when (v) { - is JsonObject -> flattened.add(flattenJsonObject(v)) - is JsonArray -> flattened.add(flattenJsonArray(v)) - is JsonPrimitive -> flattened.add(flattenJsonPrimitive(v)) - // is JsonNull -> baked.add(null) - } - } - - return flattened -} - -private fun flattenJsonObject(input: JsonObject): Object2ObjectArrayMap { - val flattened = Object2ObjectArrayMap() - - for ((k, v) in input.entrySet()) { - when (v) { - is JsonObject -> flattened[k] = flattenJsonObject(v) - is JsonArray -> flattened[k] = flattenJsonArray(v) - is JsonPrimitive -> flattened[k] = flattenJsonPrimitive(v) - } - } - - return flattened -} - -fun flattenJsonElement(input: JsonElement): Any? { - return when (input) { - is JsonObject -> flattenJsonObject(input) - is JsonArray -> flattenJsonArray(input) - is JsonPrimitive -> flattenJsonPrimitive(input) - is JsonNull -> null - else -> throw IllegalArgumentException("Unknown argument $input") - } -} - -/** - * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов - */ -fun enrollList(input: List): ImmutableList { - val builder = ImmutableList.builder() - - for (v in input) { - when (v) { - is Map<*, *> -> builder.add(enrollMap(v as Map)) - is List<*> -> builder.add(enrollList(v as List)) - else -> builder.add((v as? String)?.intern() ?: v) - } - } - - return builder.build() -} - -/** - * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов - */ -fun enrollMap(input: Map): ImmutableMap { - val builder = ImmutableMap.builder() - - for ((k, v) in input) { - when (v) { - is Map<*, *> -> builder.put(k.intern(), enrollMap(v as Map)) - is List<*> -> builder.put(k.intern(), enrollList(v as List)) - else -> builder.put(k.intern(), (v as? String)?.intern() ?: v) - } - } - - return builder.build() -} - -/** - * Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов - */ -fun flattenList(input: List): ArrayList { - val list = ArrayList(input.size) - - for (v in input) { - when (v) { - is Map<*, *> -> list.add(flattenMap(v as Map)) - is List<*> -> list.add(flattenList(v as List)) - else -> list.add(v) - } - } - - return list -} - -fun flattenMap(input: Map): Object2ObjectArrayMap { - val map = Object2ObjectArrayMap() - - for ((k, v) in input) { - when (v) { - is Map<*, *> -> map[k] = flattenMap(v as Map) - is List<*> -> map[k] = flattenList(v as List) - else -> map[k] = v - } - } - - return map -} - /** * Базовый класс описания прототипа игрового объекта * diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt index 6c6152b6..18b881c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt @@ -172,6 +172,13 @@ data class ItemDefinition( * Lua скрипты для выполнения */ val scripts: List = listOf(), + + /** + * Прототип данного предмета, как JSON структура + * + * Имеет смысл только для Lua скриптов + */ + val json: Map, ) { data class FossilSetDescription( val price: Long = 0L, @@ -221,6 +228,8 @@ data class ItemDefinition( .list(ItemDefinition::scripts, transformer = Starbound::readingFolderListTransformer) + .storesJson() + .build() val FOSSIL_ADAPTER = KConcreteTypeAdapter.Builder(FossilSetDescription::class) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt index 95042e2c..1a48368e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -218,12 +218,14 @@ data class RenderMatch( val PIECE_ADAPTER = KConcreteTypeAdapter.Builder(Piece::class) .plain(Piece::name) .plain(Piece::offset) - .build(asList = true) + .inputAsList() + .build() val MATCHER_ADAPTER = KConcreteTypeAdapter.Builder(Matcher::class) .plain(Matcher::offset) .plain(Matcher::ruleName) - .build(asList = true) + .inputAsList() + .build() } } @@ -241,7 +243,8 @@ data class RenderMatchList( val ADAPTER = KConcreteTypeAdapter.Builder(RenderMatchList::class) .plain(RenderMatchList::name) .list(RenderMatchList::list) - .build(asList = true) + .inputAsList() + .build() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt index 6f79459a..0b6dad23 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt @@ -4,8 +4,12 @@ import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.Interner import com.google.common.collect.Interners +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParseException import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter +import com.google.gson.internal.bind.JsonTreeReader import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken @@ -14,11 +18,15 @@ import it.unimi.dsi.fastutil.objects.Object2IntArrayMap import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.enrollList +import ru.dbotthepony.kstarbound.defs.enrollMap +import ru.dbotthepony.kstarbound.defs.flattenJsonElement import ru.dbotthepony.kstarbound.getValue import ru.dbotthepony.kstarbound.setValue import java.lang.reflect.Constructor import kotlin.jvm.internal.DefaultConstructorMarker import kotlin.reflect.* +import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSupertypeOf import kotlin.reflect.full.memberProperties @@ -173,8 +181,9 @@ private data class PackedProperty( class KConcreteTypeAdapter private constructor( val bound: KClass, private val types: ImmutableList>, - private val asJsonArray: Boolean = false, - val stringInterner: Interner + val asJsonArray: Boolean, + val stringInterner: Interner, + val storesJson: Boolean ) : TypeAdapter() { private val mapped = Object2IntArrayMap() private val loggedMisses = ObjectArraySet() @@ -193,10 +202,17 @@ class KConcreteTypeAdapter private constructor( * Обычный конструктор класса (без флагов "значения по умолчанию") */ private val regularFactory: KFunction = bound.constructors.firstOrNull first@{ - if (it.parameters.size == types.size) { - val iterator = types.iterator() + var requiredSize = types.size - for (param in it.parameters) { + if (storesJson) + requiredSize++ + + if (it.parameters.size == requiredSize) { + val iterator = types.iterator() + val factoryIterator = it.parameters.iterator() + + while (factoryIterator.hasNext() && iterator.hasNext()) { + val param = factoryIterator.next() val nextParam = iterator.next() val a = param.type @@ -207,6 +223,20 @@ class KConcreteTypeAdapter private constructor( } } + if (storesJson) { + val nextParam = factoryIterator.next() + + if (asJsonArray) { + if (!(nextParam.type.classifier as KClass<*>).isSubclassOf(List::class)) { + return@first false + } + } else { + if (!(nextParam.type.classifier as KClass<*>).isSubclassOf(Map::class)) { + return@first false + } + } + } + return@first true } @@ -217,14 +247,20 @@ class KConcreteTypeAdapter private constructor( * Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию */ private val syntheticFactory: Constructor? = try { - bound.java.getDeclaredConstructor(*types.map { (it.returnType.classifier as KClass<*>).java }.also { - it as MutableList + val typelist = types.map { (it.returnType.classifier as KClass<*>).java }.toMutableList() - for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1)) - it.add(Int::class.java) + if (storesJson) + if (asJsonArray) + typelist.add(List::class.java) + else + typelist.add(Map::class.java) - it.add(DefaultConstructorMarker::class.java) - }.toTypedArray()) + for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1)) + typelist.add(Int::class.java) + + typelist.add(DefaultConstructorMarker::class.java) + + bound.java.getDeclaredConstructor(*typelist.toTypedArray()) } catch(_: NoSuchMethodException) { null } @@ -270,30 +306,51 @@ class KConcreteTypeAdapter private constructor( } override fun read(reader: JsonReader): T { - if (asJsonArray) { - reader.beginArray() - } else { - reader.beginObject() - } - // таблица присутствия значений (если значение true то на i было значение внутри json) - val presentValues = BooleanArray(types.size) - val readValues = arrayOfNulls(types.size) + val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0)) + val readValues = arrayOfNulls(types.size + (if (storesJson) 1 else 0)) + + if (storesJson) + presentValues[presentValues.size - 1] = true + + @Suppress("name_shadowing") + var reader = reader // Если нам необходимо читать объект как набор данных массива, то давай if (asJsonArray) { val iterator = types.iterator() var fieldId = 0 + if (storesJson) { + val readArray = TypeAdapters.JSON_ELEMENT.read(reader) + + if (readArray !is JsonArray) { + throw JsonParseException("Expected JSON element to be an Array, ${readArray::class.qualifiedName} given") + } + + reader = JsonTreeReader(readArray) + readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray) as List, stringInterner::intern) + } + + reader.beginArray() + while (reader.peek() != JsonToken.END_ARRAY) { if (!iterator.hasNext()) { val name = fieldId.toString() if (loggedMisses.add(name)) { - if (currentSymbolicName == null) { - LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") + if (storesJson) { + if (currentSymbolicName == null) { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field, it will be only visible to Lua scripts") + } else { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field, it will be only visible to Lua scripts (reading: $currentSymbolicName)") + } } else { - LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field (reading: $currentSymbolicName)") + if (currentSymbolicName == null) { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") + } else { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field (reading: $currentSymbolicName)") + } } } @@ -317,16 +374,37 @@ class KConcreteTypeAdapter private constructor( } // иначе - читаем как json object } else { + if (storesJson) { + val readMap = TypeAdapters.JSON_ELEMENT.read(reader) + + if (readMap !is JsonObject) { + throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given") + } + + reader = JsonTreeReader(readMap) + readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map, stringInterner::intern) + } + + reader.beginObject() + while (reader.peek() != JsonToken.END_OBJECT) { val name = reader.nextName() val fieldId = mapped.getInt(name) if (fieldId == -1) { if (loggedMisses.add(name)) { - if (currentSymbolicName == null) { - LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") + if (storesJson) { + if (currentSymbolicName == null) { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field, it will be only visible to Lua scripts") + } else { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field, it will be only visible to Lua scripts (reading: $currentSymbolicName)") + } } else { - LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field (reading: $currentSymbolicName)") + if (currentSymbolicName == null) { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") + } else { + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field (reading: $currentSymbolicName)") + } } } @@ -439,6 +517,21 @@ class KConcreteTypeAdapter private constructor( private val types = ArrayList>() var stringInterner: Interner = Interners.newWeakInterner() + /** + * Принимает ли класс *последним* аргументом JSON объект + * + * На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList] + * + * Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] первым аргументом, + * иначе поиск конструктора завершится неудчаей + */ + var storesJson = false + + fun storesJson(): Builder { + storesJson = true + return this + } + fun specifyStringInterner(interner: Interner): Builder { stringInterner = interner return this @@ -573,12 +666,25 @@ class KConcreteTypeAdapter private constructor( return this } - fun build(asList: Boolean = false): KConcreteTypeAdapter { + var asList = false + + fun inputAsMap(): Builder { + asList = false + return this + } + + fun inputAsList(): Builder { + asList = true + return this + } + + fun build(): KConcreteTypeAdapter { return KConcreteTypeAdapter( bound = clazz, types = ImmutableList.copyOf(types), asJsonArray = asList, - stringInterner = stringInterner + stringInterner = stringInterner, + storesJson = storesJson ) } }