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.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
private fun flattenJsonPrimitive(input: JsonPrimitive): Any {
if (input.isNumber) {
@ -32,8 +33,8 @@ private fun flattenJsonArray(input: JsonArray): ArrayList<Any> {
return flattened
}
private fun flattenJsonObject(input: JsonObject): Object2ObjectArrayMap<String, Any> {
val flattened = Object2ObjectArrayMap<String, Any>()
private fun flattenJsonObject(input: JsonObject): MutableMap<String, Any> {
val flattened = Object2ObjectOpenHashMap<String, Any>()
for ((k, v) in input.entrySet()) {
when (v) {
@ -55,3 +56,7 @@ fun flattenJsonElement(input: JsonElement): Any? {
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 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>()
fun enroll() = enrollMap(json)
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 ru.dbotthepony.kstarbound.Starbound
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.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.registerTypeAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar
import ru.dbotthepony.kvector.vector.Color
import java.util.concurrent.ConcurrentHashMap
@ -95,7 +95,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
}
companion object {
val ADAPTER = ConfigurableTypeAdapter(
val ADAPTER = BuilderAdapter.Builder(
::ConfigurableProjectile,
ConfigurableProjectile::projectileName,
ConfigurableProjectile::physics,
@ -118,16 +118,16 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
ConfigurableProjectile::piercing,
ConfigurableProjectile::speed,
ConfigurableProjectile::power,
)
).build()
fun registerGson(gson: GsonBuilder) {
gson.registerTypeAdapter(ConfigurableProjectile::class.java, ADAPTER)
gson.registerTypeAdapter(ProjectilePhysics::class.java, CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
gson.registerTypeAdapter(ActionConfig::class.java, ActionConfig.ADAPTER)
gson.registerTypeAdapter(ActionProjectile::class.java, ActionProjectile.ADAPTER)
gson.registerTypeAdapter(ActionSound::class.java, ActionSound.ADAPTER)
gson.registerTypeAdapter(ActionLoop::class.java, ActionLoop.ADAPTER)
gson.registerTypeAdapter(ActionActions::class.java, ActionActions.ADAPTER)
gson.registerTypeAdapter(ADAPTER)
gson.registerTypeAdapter(CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
gson.registerTypeAdapter(ActionConfig.ADAPTER)
gson.registerTypeAdapter(ActionProjectile.ADAPTER)
gson.registerTypeAdapter(ActionSound.ADAPTER)
gson.registerTypeAdapter(ActionLoop.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.ImmutableSet
import com.google.gson.JsonObject
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
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.flattenJsonElement
import ru.dbotthepony.kstarbound.util.NotNullVar
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
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")
fun <T : Any> BuilderAdapter(factory: () -> T, vararg fields: KMutableProperty1<T, *>): BuilderAdapter<T> {
val builder = BuilderAdapter.Builder(factory)
@ -57,13 +84,22 @@ class BuilderAdapter<T : Any> private constructor(
}
override fun read(reader: JsonReader): T {
@Suppress("name_shadowing")
var reader = reader
val missing = ObjectArraySet<WrappedProperty<T, *>>()
missing.addAll(properties.values)
reader.beginObject()
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()) {
val name = reader.nextName()
@ -247,6 +283,6 @@ class BuilderAdapter<T : Any> private constructor(
}
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)
}
}