Сохранение Json структуры в KConcreteTypeAdapter

This commit is contained in:
DBotThePony 2022-12-30 15:12:55 +07:00
parent e5728e5ec9
commit d016aa807c
Signed by: DBot
GPG Key ID: DCC23B5715498507
9 changed files with 278 additions and 149 deletions

View File

@ -163,7 +163,7 @@ fun main() {
//client.world!!.parallax = Starbound.parallaxAccess["garden"] //client.world!!.parallax = Starbound.parallaxAccess["garden"]
for (i in 0 .. 16) { 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.position = Vector2d(600.0 + 16.0 + i, 721.0 + 48.0)
item.spawn() item.spawn()

View File

@ -458,7 +458,5 @@ object Starbound {
} }
} }
} }
items.values.stream().filter { it.currency != null }.forEach(::println)
} }
} }

View File

@ -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<Any>, interner: (String) -> String = String::intern): ImmutableList<Any> {
val builder = ImmutableList.builder<Any>()
for (v in input) {
when (v) {
is Map<*, *> -> builder.add(enrollMap(v as Map<String, Any>, interner))
is List<*> -> builder.add(enrollList(v as List<Any>, interner))
else -> builder.add((v as? String)?.let(interner) ?: v)
}
}
return builder.build()
}
/**
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun enrollMap(input: Map<String, Any>, interner: (String) -> String = String::intern): ImmutableMap<String, Any> {
val builder = ImmutableMap.builder<String, Any>()
for ((k, v) in input) {
when (v) {
is Map<*, *> -> builder.put(interner(k), enrollMap(v as Map<String, Any>))
is List<*> -> builder.put(interner(k), enrollList(v as List<Any>))
else -> builder.put(interner(k), (v as? String)?.let(interner) ?: v)
}
}
return builder.build()
}

View File

@ -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<Any> {
val flattened = ArrayList<Any>(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<String, Any> {
val flattened = Object2ObjectArrayMap<String, Any>()
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")
}
}

View File

@ -0,0 +1,34 @@
package ru.dbotthepony.kstarbound.defs
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
/**
* Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun flattenList(input: List<Any>): ArrayList<Any> {
val list = ArrayList<Any>(input.size)
for (v in input) {
when (v) {
is Map<*, *> -> list.add(flattenMap(v as Map<String, Any>))
is List<*> -> list.add(flattenList(v as List<Any>))
else -> list.add(v)
}
}
return list
}
fun flattenMap(input: Map<String, Any>): Object2ObjectArrayMap<String, Any> {
val map = Object2ObjectArrayMap<String, Any>()
for ((k, v) in input) {
when (v) {
is Map<*, *> -> map[k] = flattenMap(v as Map<String, Any>)
is List<*> -> map[k] = flattenList(v as List<Any>)
else -> map[k] = v
}
}
return map
}

View File

@ -1,124 +1,8 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap 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<Any> {
val flattened = ArrayList<Any>(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<String, Any> {
val flattened = Object2ObjectArrayMap<String, Any>()
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<Any>): ImmutableList<Any> {
val builder = ImmutableList.builder<Any>()
for (v in input) {
when (v) {
is Map<*, *> -> builder.add(enrollMap(v as Map<String, Any>))
is List<*> -> builder.add(enrollList(v as List<Any>))
else -> builder.add((v as? String)?.intern() ?: v)
}
}
return builder.build()
}
/**
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun enrollMap(input: Map<String, Any>): ImmutableMap<String, Any> {
val builder = ImmutableMap.builder<String, Any>()
for ((k, v) in input) {
when (v) {
is Map<*, *> -> builder.put(k.intern(), enrollMap(v as Map<String, Any>))
is List<*> -> builder.put(k.intern(), enrollList(v as List<Any>))
else -> builder.put(k.intern(), (v as? String)?.intern() ?: v)
}
}
return builder.build()
}
/**
* Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun flattenList(input: List<Any>): ArrayList<Any> {
val list = ArrayList<Any>(input.size)
for (v in input) {
when (v) {
is Map<*, *> -> list.add(flattenMap(v as Map<String, Any>))
is List<*> -> list.add(flattenList(v as List<Any>))
else -> list.add(v)
}
}
return list
}
fun flattenMap(input: Map<String, Any>): Object2ObjectArrayMap<String, Any> {
val map = Object2ObjectArrayMap<String, Any>()
for ((k, v) in input) {
when (v) {
is Map<*, *> -> map[k] = flattenMap(v as Map<String, Any>)
is List<*> -> map[k] = flattenList(v as List<Any>)
else -> map[k] = v
}
}
return map
}
/** /**
* Базовый класс описания прототипа игрового объекта * Базовый класс описания прототипа игрового объекта
* *

View File

@ -172,6 +172,13 @@ data class ItemDefinition(
* Lua скрипты для выполнения * Lua скрипты для выполнения
*/ */
val scripts: List<String> = listOf(), val scripts: List<String> = listOf(),
/**
* Прототип данного предмета, как JSON структура
*
* Имеет смысл только для Lua скриптов
*/
val json: Map<String, Any>,
) { ) {
data class FossilSetDescription( data class FossilSetDescription(
val price: Long = 0L, val price: Long = 0L,
@ -221,6 +228,8 @@ data class ItemDefinition(
.list(ItemDefinition::scripts, transformer = Starbound::readingFolderListTransformer) .list(ItemDefinition::scripts, transformer = Starbound::readingFolderListTransformer)
.storesJson()
.build() .build()
val FOSSIL_ADAPTER = KConcreteTypeAdapter.Builder(FossilSetDescription::class) val FOSSIL_ADAPTER = KConcreteTypeAdapter.Builder(FossilSetDescription::class)

View File

@ -218,12 +218,14 @@ data class RenderMatch(
val PIECE_ADAPTER = KConcreteTypeAdapter.Builder(Piece::class) val PIECE_ADAPTER = KConcreteTypeAdapter.Builder(Piece::class)
.plain(Piece::name) .plain(Piece::name)
.plain(Piece::offset) .plain(Piece::offset)
.build(asList = true) .inputAsList()
.build()
val MATCHER_ADAPTER = KConcreteTypeAdapter.Builder(Matcher::class) val MATCHER_ADAPTER = KConcreteTypeAdapter.Builder(Matcher::class)
.plain(Matcher::offset) .plain(Matcher::offset)
.plain(Matcher::ruleName) .plain(Matcher::ruleName)
.build(asList = true) .inputAsList()
.build()
} }
} }
@ -241,7 +243,8 @@ data class RenderMatchList(
val ADAPTER = KConcreteTypeAdapter.Builder(RenderMatchList::class) val ADAPTER = KConcreteTypeAdapter.Builder(RenderMatchList::class)
.plain(RenderMatchList::name) .plain(RenderMatchList::name)
.list(RenderMatchList::list) .list(RenderMatchList::list)
.build(asList = true) .inputAsList()
.build()
} }
} }

View File

@ -4,8 +4,12 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.Interner import com.google.common.collect.Interner
import com.google.common.collect.Interners 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.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.internal.bind.TypeAdapters import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken 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 it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound 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.getValue
import ru.dbotthepony.kstarbound.setValue import ru.dbotthepony.kstarbound.setValue
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import kotlin.jvm.internal.DefaultConstructorMarker import kotlin.jvm.internal.DefaultConstructorMarker
import kotlin.reflect.* import kotlin.reflect.*
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.full.isSupertypeOf import kotlin.reflect.full.isSupertypeOf
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
@ -173,8 +181,9 @@ private data class PackedProperty<Clazz : Any, T>(
class KConcreteTypeAdapter<T : Any> private constructor( class KConcreteTypeAdapter<T : Any> private constructor(
val bound: KClass<T>, val bound: KClass<T>,
private val types: ImmutableList<PackedProperty<T, *>>, private val types: ImmutableList<PackedProperty<T, *>>,
private val asJsonArray: Boolean = false, val asJsonArray: Boolean,
val stringInterner: Interner<String> val stringInterner: Interner<String>,
val storesJson: Boolean
) : TypeAdapter<T>() { ) : TypeAdapter<T>() {
private val mapped = Object2IntArrayMap<String>() private val mapped = Object2IntArrayMap<String>()
private val loggedMisses = ObjectArraySet<String>() private val loggedMisses = ObjectArraySet<String>()
@ -193,10 +202,17 @@ class KConcreteTypeAdapter<T : Any> private constructor(
* Обычный конструктор класса (без флагов "значения по умолчанию") * Обычный конструктор класса (без флагов "значения по умолчанию")
*/ */
private val regularFactory: KFunction<T> = bound.constructors.firstOrNull first@{ private val regularFactory: KFunction<T> = bound.constructors.firstOrNull first@{
if (it.parameters.size == types.size) { var requiredSize = types.size
val iterator = types.iterator()
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 nextParam = iterator.next()
val a = param.type val a = param.type
@ -207,6 +223,20 @@ class KConcreteTypeAdapter<T : Any> 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 return@first true
} }
@ -217,14 +247,20 @@ class KConcreteTypeAdapter<T : Any> private constructor(
* Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию * Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию
*/ */
private val syntheticFactory: Constructor<T>? = try { private val syntheticFactory: Constructor<T>? = try {
bound.java.getDeclaredConstructor(*types.map { (it.returnType.classifier as KClass<*>).java }.also { val typelist = types.map { (it.returnType.classifier as KClass<*>).java }.toMutableList()
it as MutableList
for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1)) if (storesJson)
it.add(Int::class.java) if (asJsonArray)
typelist.add(List::class.java)
else
typelist.add(Map::class.java)
it.add(DefaultConstructorMarker::class.java) for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1))
}.toTypedArray()) typelist.add(Int::class.java)
typelist.add(DefaultConstructorMarker::class.java)
bound.java.getDeclaredConstructor(*typelist.toTypedArray())
} catch(_: NoSuchMethodException) { } catch(_: NoSuchMethodException) {
null null
} }
@ -270,30 +306,51 @@ class KConcreteTypeAdapter<T : Any> private constructor(
} }
override fun read(reader: JsonReader): T { override fun read(reader: JsonReader): T {
if (asJsonArray) {
reader.beginArray()
} else {
reader.beginObject()
}
// таблица присутствия значений (если значение true то на i было значение внутри json) // таблица присутствия значений (если значение true то на i было значение внутри json)
val presentValues = BooleanArray(types.size) val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0))
val readValues = arrayOfNulls<Any>(types.size) val readValues = arrayOfNulls<Any>(types.size + (if (storesJson) 1 else 0))
if (storesJson)
presentValues[presentValues.size - 1] = true
@Suppress("name_shadowing")
var reader = reader
// Если нам необходимо читать объект как набор данных массива, то давай // Если нам необходимо читать объект как набор данных массива, то давай
if (asJsonArray) { if (asJsonArray) {
val iterator = types.iterator() val iterator = types.iterator()
var fieldId = 0 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<Any>, stringInterner::intern)
}
reader.beginArray()
while (reader.peek() != JsonToken.END_ARRAY) { while (reader.peek() != JsonToken.END_ARRAY) {
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
val name = fieldId.toString() val name = fieldId.toString()
if (loggedMisses.add(name)) { if (loggedMisses.add(name)) {
if (currentSymbolicName == null) { if (storesJson) {
LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") 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 { } 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<T : Any> private constructor(
} }
// иначе - читаем как json object // иначе - читаем как json object
} else { } 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<String, Any>, stringInterner::intern)
}
reader.beginObject()
while (reader.peek() != JsonToken.END_OBJECT) { while (reader.peek() != JsonToken.END_OBJECT) {
val name = reader.nextName() val name = reader.nextName()
val fieldId = mapped.getInt(name) val fieldId = mapped.getInt(name)
if (fieldId == -1) { if (fieldId == -1) {
if (loggedMisses.add(name)) { if (loggedMisses.add(name)) {
if (currentSymbolicName == null) { if (storesJson) {
LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") 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 { } 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<T : Any> private constructor(
private val types = ArrayList<PackedProperty<T, *>>() private val types = ArrayList<PackedProperty<T, *>>()
var stringInterner: Interner<String> = Interners.newWeakInterner() var stringInterner: Interner<String> = Interners.newWeakInterner()
/**
* Принимает ли класс *последним* аргументом JSON объект
*
* На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList]
*
* Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] первым аргументом,
* иначе поиск конструктора завершится неудчаей
*/
var storesJson = false
fun storesJson(): Builder<T> {
storesJson = true
return this
}
fun specifyStringInterner(interner: Interner<String>): Builder<T> { fun specifyStringInterner(interner: Interner<String>): Builder<T> {
stringInterner = interner stringInterner = interner
return this return this
@ -573,12 +666,25 @@ class KConcreteTypeAdapter<T : Any> private constructor(
return this return this
} }
fun build(asList: Boolean = false): KConcreteTypeAdapter<T> { var asList = false
fun inputAsMap(): Builder<T> {
asList = false
return this
}
fun inputAsList(): Builder<T> {
asList = true
return this
}
fun build(): KConcreteTypeAdapter<T> {
return KConcreteTypeAdapter( return KConcreteTypeAdapter(
bound = clazz, bound = clazz,
types = ImmutableList.copyOf(types), types = ImmutableList.copyOf(types),
asJsonArray = asList, asJsonArray = asList,
stringInterner = stringInterner stringInterner = stringInterner,
storesJson = storesJson
) )
} }
} }