Сохранение 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"]
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()

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
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<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 скрипты для выполнения
*/
val scripts: List<String> = listOf(),
/**
* Прототип данного предмета, как JSON структура
*
* Имеет смысл только для Lua скриптов
*/
val json: Map<String, Any>,
) {
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)

View File

@ -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()
}
}

View File

@ -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<Clazz : Any, T>(
class KConcreteTypeAdapter<T : Any> private constructor(
val bound: KClass<T>,
private val types: ImmutableList<PackedProperty<T, *>>,
private val asJsonArray: Boolean = false,
val stringInterner: Interner<String>
val asJsonArray: Boolean,
val stringInterner: Interner<String>,
val storesJson: Boolean
) : TypeAdapter<T>() {
private val mapped = Object2IntArrayMap<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@{
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<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
}
@ -217,14 +247,20 @@ class KConcreteTypeAdapter<T : Any> private constructor(
* Синтетический конструктор класса, который создаётся Kotlin'ном, для создания классов со значениями по умолчанию
*/
private val syntheticFactory: Constructor<T>? = 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<T : Any> 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<Any>(types.size)
val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0))
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) {
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<Any>, 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<T : Any> 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<String, Any>, 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<T : Any> private constructor(
private val types = ArrayList<PackedProperty<T, *>>()
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> {
stringInterner = interner
return this
@ -573,12 +666,25 @@ class KConcreteTypeAdapter<T : Any> private constructor(
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(
bound = clazz,
types = ImmutableList.copyOf(types),
asJsonArray = asList,
stringInterner = stringInterner
stringInterner = stringInterner,
storesJson = storesJson
)
}
}