This commit is contained in:
DBotThePony 2022-12-31 13:44:15 +07:00
parent 2805717ded
commit 3e5195cbc1
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 63 additions and 145 deletions

View File

@ -6,6 +6,7 @@ import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
private fun flattenJsonPrimitive(input: JsonPrimitive): Any { private fun flattenJsonPrimitive(input: JsonPrimitive): Any {
if (input.isNumber) { if (input.isNumber) {
@ -32,8 +33,8 @@ private fun flattenJsonArray(input: JsonArray): ArrayList<Any> {
return flattened return flattened
} }
private fun flattenJsonObject(input: JsonObject): Object2ObjectArrayMap<String, Any> { private fun flattenJsonObject(input: JsonObject): MutableMap<String, Any> {
val flattened = Object2ObjectArrayMap<String, Any>() val flattened = Object2ObjectOpenHashMap<String, Any>()
for ((k, v) in input.entrySet()) { for ((k, v) in input.entrySet()) {
when (v) { when (v) {
@ -55,3 +56,7 @@ fun flattenJsonElement(input: JsonElement): Any? {
else -> throw IllegalArgumentException("Unknown argument $input") else -> throw IllegalArgumentException("Unknown argument $input")
} }
} }
fun flattenJsonElement(input: JsonObject) = flattenJsonObject(input)
fun flattenJsonElement(input: JsonArray) = flattenJsonArray(input)
fun flattenJsonElement(input: JsonPrimitive) = flattenJsonPrimitive(input)

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder
/** /**
* Базовый класс описания прототипа игрового объекта * Базовый класс описания прототипа игрового объекта
@ -12,10 +13,15 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
* Если границы поля зависят от других полей, то проверка такого поля должна осуществляться уже при самой * Если границы поля зависят от других полей, то проверка такого поля должна осуществляться уже при самой
* сборке прототипа. * сборке прототипа.
*/ */
abstract class RawPrototype<RAW : RawPrototype<RAW, ASSEMBLED>, ASSEMBLED : AssembledPrototype<ASSEMBLED, RAW>> { abstract class RawPrototype<RAW : RawPrototype<RAW, ASSEMBLED>, ASSEMBLED : AssembledPrototype<ASSEMBLED, RAW>> : INativeJsonHolder {
val json = Object2ObjectArrayMap<String, Any>() val json = Object2ObjectArrayMap<String, Any>()
fun enroll() = enrollMap(json) fun enroll() = enrollMap(json)
abstract fun assemble(directory: String = ""): ASSEMBLED abstract fun assemble(directory: String = ""): ASSEMBLED
override fun acceptJson(json: MutableMap<String, Any>) {
this.json.clear()
this.json.putAll(json)
}
} }
/** /**

View File

@ -7,9 +7,9 @@ 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.* import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.io.json.ConfigurableTypeAdapter
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.registerTypeAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kstarbound.util.NotNullVar
import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.Color
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -95,7 +95,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
} }
companion object { companion object {
val ADAPTER = ConfigurableTypeAdapter( val ADAPTER = BuilderAdapter.Builder(
::ConfigurableProjectile, ::ConfigurableProjectile,
ConfigurableProjectile::projectileName, ConfigurableProjectile::projectileName,
ConfigurableProjectile::physics, ConfigurableProjectile::physics,
@ -118,16 +118,16 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
ConfigurableProjectile::piercing, ConfigurableProjectile::piercing,
ConfigurableProjectile::speed, ConfigurableProjectile::speed,
ConfigurableProjectile::power, ConfigurableProjectile::power,
) ).build()
fun registerGson(gson: GsonBuilder) { fun registerGson(gson: GsonBuilder) {
gson.registerTypeAdapter(ConfigurableProjectile::class.java, ADAPTER) gson.registerTypeAdapter(ADAPTER)
gson.registerTypeAdapter(ProjectilePhysics::class.java, CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe()) gson.registerTypeAdapter(CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
gson.registerTypeAdapter(ActionConfig::class.java, ActionConfig.ADAPTER) gson.registerTypeAdapter(ActionConfig.ADAPTER)
gson.registerTypeAdapter(ActionProjectile::class.java, ActionProjectile.ADAPTER) gson.registerTypeAdapter(ActionProjectile.ADAPTER)
gson.registerTypeAdapter(ActionSound::class.java, ActionSound.ADAPTER) gson.registerTypeAdapter(ActionSound.ADAPTER)
gson.registerTypeAdapter(ActionLoop::class.java, ActionLoop.ADAPTER) gson.registerTypeAdapter(ActionLoop.ADAPTER)
gson.registerTypeAdapter(ActionActions::class.java, ActionActions.ADAPTER) gson.registerTypeAdapter(ActionActions.ADAPTER)
} }
} }
} }

View File

@ -2,19 +2,46 @@ package ru.dbotthepony.kstarbound.io.json
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
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.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.flattenJsonElement
import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kstarbound.util.NotNullVar
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
/**
* Данный интерфейс имеет один единственный метод: [acceptJson]
*
* Используется в связке с [BuilderAdapter] для классов, которым необходимо хранить оригинальную JSON структуру
*/
interface IJsonHolder {
/**
* Выставляет [JsonObject], который является источником данных для данной структуры
*/
fun acceptJson(json: JsonObject)
}
/**
* Для классов, которые хотят принимать Java'вские [Map] напрямую, как оригинальную JSON структуру
*/
interface INativeJsonHolder : IJsonHolder {
override fun acceptJson(json: JsonObject) {
acceptJson(flattenJsonElement(json))
}
fun acceptJson(json: MutableMap<String, Any>)
}
@Suppress("FunctionName") @Suppress("FunctionName")
fun <T : Any> BuilderAdapter(factory: () -> T, vararg fields: KMutableProperty1<T, *>): BuilderAdapter<T> { fun <T : Any> BuilderAdapter(factory: () -> T, vararg fields: KMutableProperty1<T, *>): BuilderAdapter<T> {
val builder = BuilderAdapter.Builder(factory) val builder = BuilderAdapter.Builder(factory)
@ -57,13 +84,22 @@ class BuilderAdapter<T : Any> private constructor(
} }
override fun read(reader: JsonReader): T { override fun read(reader: JsonReader): T {
@Suppress("name_shadowing")
var reader = reader
val missing = ObjectArraySet<WrappedProperty<T, *>>() val missing = ObjectArraySet<WrappedProperty<T, *>>()
missing.addAll(properties.values) missing.addAll(properties.values)
reader.beginObject()
val instance = factory.invoke() val instance = factory.invoke()
if (instance is IJsonHolder) {
val obj = TypeAdapters.JSON_ELEMENT.read(reader)
reader = JsonTreeReader(obj)
instance.acceptJson(obj.asJsonObject)
}
reader.beginObject()
while (reader.hasNext()) { while (reader.hasNext()) {
val name = reader.nextName() val name = reader.nextName()
@ -247,6 +283,6 @@ class BuilderAdapter<T : Any> private constructor(
} }
companion object { companion object {
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java) private val LOGGER = LogManager.getLogger()
} }
} }

View File

@ -1,129 +0,0 @@
package ru.dbotthepony.kstarbound.io.json
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.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.RawPrototype
import ru.dbotthepony.kstarbound.defs.flattenJsonElement
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.isSuperclassOf
/**
* Kotlin property aware adapter with arbitrary structure writer
*/
class ConfigurableTypeAdapter<T : RawPrototype<*, *>>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() {
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>()
// потому что returnType медленный
private val mappedFieldsReturnTypes = Object2ObjectArrayMap<String, KType>()
private val loggedMisses = ObjectArraySet<String>()
init {
for (field in fields) {
// потому что в котлине нет понятия KProperty который не имеет getter'а, только setter
require(mappedFields.put(field.name, field as KMutableProperty1<T, in Any?>) == null) { "${field.name} is defined twice" }
mappedFieldsReturnTypes[field.name] = field.returnType
}
}
val fields: Array<KMutableProperty1<T, in Any?>> get() {
val iterator = mappedFields.values.iterator()
return Array(mappedFields.size) { iterator.next() }
}
override fun write(writer: JsonWriter, value: T) {
TODO("Not yet implemented")
}
override fun read(reader: JsonReader): T? {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return null
}
reader.beginObject()
val instance = factory.invoke()
while (reader.hasNext()) {
val name = reader.nextName()
val field = mappedFields[name]
if (field != null) {
try {
val peek = reader.peek()
val expectedType = mappedFieldsReturnTypes[name]!!
if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) {
throw IllegalArgumentException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls")
} else if (peek == JsonToken.NULL) {
field.set(instance, null)
reader.nextNull()
} else {
val classifier = expectedType.classifier
if (classifier is KClass<*>) {
if (classifier.isSuperclassOf(Float::class)) {
val read = reader.nextDouble()
instance.json[name] = read
field.set(instance, read.toFloat())
} else if (classifier.isSuperclassOf(Double::class)) {
val read = reader.nextDouble()
instance.json[name] = read
field.set(instance, read)
} else if (classifier.isSuperclassOf(Int::class)) {
val read = reader.nextInt()
instance.json[name] = read
field.set(instance, read)
} else if (classifier.isSuperclassOf(Long::class)) {
val read = reader.nextLong()
instance.json[name] = read
field.set(instance, read)
} else if (classifier.isSuperclassOf(String::class)) {
val read = reader.nextString()
instance.json[name] = read
field.set(instance, read)
} else if (classifier.isSuperclassOf(Boolean::class)) {
val read = reader.nextBoolean()
instance.json[name] = read
field.set(instance, read)
} else {
val readElement = TypeAdapters.JSON_ELEMENT.read(reader)
instance.json[name] = flattenJsonElement(readElement)
field.set(instance, Starbound.gson.fromJson(readElement, classifier.java))
}
} else {
throw IllegalStateException("Expected ${field.name} classifier to be KClass, got $classifier")
}
}
} catch(err: Throwable) {
throw JsonSyntaxException(
"Reading property ${field.name} of ${instance::class.qualifiedName} near ${reader.path}",
err
)
}
} else {
instance.json[name] = flattenJsonElement(TypeAdapters.JSON_ELEMENT.read(reader))
if (!loggedMisses.contains(name)) {
loggedMisses.add(name)
LOGGER.warn("{} has no property for storing {}, this value will be visible to Lua scripts only", instance::class.qualifiedName, name)
}
}
}
reader.endObject()
return instance
}
companion object {
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
}
}