KConcreteTypeAdapter test

This commit is contained in:
DBotThePony 2022-08-26 16:29:37 +07:00
parent 0866edcb74
commit e2b17f5761
Signed by: DBot
GPG Key ID: DCC23B5715498507
7 changed files with 522 additions and 22 deletions

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import org.apache.logging.log4j.LogManager
import org.lwjgl.Version
@ -8,6 +9,7 @@ import ru.dbotthepony.kbox2d.api.*
import ru.dbotthepony.kbox2d.collision.shapes.CircleShape
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.MaterialModifier
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.defs.projectile.ProjectilePhysics
import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef
@ -39,6 +41,34 @@ fun main() {
//return
}
if (true) {
val input = "{\n" +
" \"modId\" : 26,\n" +
" \"modName\" : \"aegisalt\",\n" +
" \"itemDrop\" : \"aegisaltore\",\n" +
" \"description\" : \"Aegisalt.\",\n" +
" \"health\" : 5,\n" +
" \"harvestLevel\" : 5,\n" +
" \"breaksWithTile\" : true,\n" +
"\n" +
" \"miningSounds\" : [ \"/sfx/tools/pickaxe_ore.ogg\", \"/sfx/tools/pickaxe_ore2.ogg\" ],\n" +
" \"miningParticle\" : \"orespark\",\n" +
"\n" +
" \"renderTemplate\" : \"/tiles/classicmaterialtemplate.config\",\n" +
" \"renderParameters\" : {\n" +
" \"texture\" : \"aegisalt.png\",\n" +
" \"variants\" : 8,\n" +
" \"multiColored\" : false,\n" +
" \"zLevel\" : 0\n" +
" }\n" +
"}\n"
val json = Starbound.gson.fromJson(input, MaterialModifier::class.java)
println(json)
return
}
val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world"))
//val db = BTreeDB(File("world.world"))

View File

@ -71,6 +71,7 @@ object Starbound : IVFS {
.also(DungeonWorldDef::registerGson)
.also(ParallaxPrototype::registerGson)
.also(JsonFunction::registerGson)
.also(MaterialModifier::registerGson)
.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe())
@ -226,21 +227,19 @@ object Starbound : IVFS {
private fun loadTileMaterials(callback: (String) -> Unit) {
for (fs in fileSystems) {
for (listedFile in fs.listAllFiles("tiles/materials")) {
if (listedFile.endsWith(".material")) {
try {
callback("Loading $listedFile")
for (listedFile in fs.listAllFilesWithExtension("material")) {
try {
callback("Loading $listedFile")
val tileDef = TileDefinitionBuilder.fromJson(JsonParser.parseReader(getReader(listedFile)) as JsonObject).build("/tiles/materials")
val tileDef = TileDefinitionBuilder.fromJson(JsonParser.parseReader(getReader(listedFile)) as JsonObject).build("/tiles/materials")
check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" }
check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" }
tilesByMaterialID[tileDef.materialId] = tileDef
tiles[tileDef.materialName] = tileDef
} catch (err: Throwable) {
//throw TileDefLoadingException("Loading tile file $listedFile", err)
LOGGER.error("Loading tile file $listedFile", err)
}
check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" }
check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" }
tilesByMaterialID[tileDef.materialId] = tileDef
tiles[tileDef.materialName] = tileDef
} catch (err: Throwable) {
//throw TileDefLoadingException("Loading tile file $listedFile", err)
LOGGER.error("Loading tile file $listedFile", err)
}
}
}
@ -298,4 +297,10 @@ object Starbound : IVFS {
}
}
}
private fun loadMaterialModifiers(callback: (String) -> Unit) {
for (fs in fileSystems) {
}
}
}

View File

@ -125,7 +125,7 @@ class StarboundClient : AutoCloseable {
val gl = GLStateTracker()
var world: ClientWorld? = ClientWorld(this, 0L, 94)
var world: ClientWorld? = ClientWorld(this, 0L, 0)
fun ensureSameThread() = gl.ensureSameThread()

View File

@ -0,0 +1,36 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter
data class MaterialModifier(
val modId: Int,
val modName: String,
val itemDrop: String,
val description: String,
val health: Int,
val harvestLevel: Int,
val breaksWithTile: Boolean,
val miningSounds: List<String>,
val miningParticle: String,
val renderTemplate: String,
) {
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(MaterialModifier::class)
.plain(MaterialModifier::modId)
.plain(MaterialModifier::modName)
.plain(MaterialModifier::itemDrop)
.plain(MaterialModifier::description)
.plain(MaterialModifier::health)
.plain(MaterialModifier::harvestLevel)
.plain(MaterialModifier::breaksWithTile)
.list(MaterialModifier::miningSounds, String::class.java)
.plain(MaterialModifier::miningParticle)
.plain(MaterialModifier::renderTemplate)
.build()
fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(MaterialModifier::class.java, ADAPTER)
}
}
}

View File

@ -2,9 +2,14 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.world.ITileGetter
@ -482,6 +487,18 @@ data class TileRenderTemplate(
companion object {
val map = HashMap<String, TileRenderTemplate>()
fun register(builder: GsonBuilder) {
builder.registerTypeAdapter(TileRenderTemplate::class.java, object : TypeAdapter<TileRenderTemplate>() {
override fun write(out: JsonWriter?, value: TileRenderTemplate?) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): TileRenderTemplate {
return fromJson(TypeAdapters.JSON_ELEMENT.read(`in`) as JsonObject)
}
})
}
fun load(path: String): TileRenderTemplate {
return map.computeIfAbsent(path) {
try {

View File

@ -0,0 +1,400 @@
package ru.dbotthepony.kstarbound.io
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonElement
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.Object2IntArrayMap
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 kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.full.isSupertypeOf
private class PassthroughAdapter<T : Any?>(private val bound: Class<T>) : TypeAdapter<T?>() {
override fun write(out: JsonWriter, value: T?) {
Starbound.gson.toJson(Starbound.gson.toJsonTree(value, bound) as JsonElement, out)
}
override fun read(reader: JsonReader): T? {
return Starbound.gson.fromJson(reader, bound)
}
}
private fun <T> resolveBound(bound: Class<T>): TypeAdapter<T>? {
return when (bound) {
Float::class.java -> TypeAdapters.FLOAT as TypeAdapter<T>
Double::class.java -> TypeAdapters.DOUBLE as TypeAdapter<T>
String::class.java -> TypeAdapters.STRING as TypeAdapter<T>
Int::class.java -> TypeAdapters.INTEGER as TypeAdapter<T>
Long::class.java -> TypeAdapters.LONG as TypeAdapter<T>
Boolean::class.java -> TypeAdapters.BOOLEAN as TypeAdapter<T>
else -> Starbound.gson.getAdapter(bound)
}
}
class ListAdapter<T>(private val bound: Class<T>) : TypeAdapter<List<T>>() {
private val resolvedBound by lazy {
resolveBound(bound)
}
override fun write(out: JsonWriter, value: List<T>) {
out.beginArray()
val resolvedBound = resolvedBound
if (resolvedBound != null) {
for (v in value) {
resolvedBound.write(out, v)
}
} else {
for (v in value) {
Starbound.gson.toJson(Starbound.gson.toJsonTree(v, bound) as JsonElement, out)
}
}
out.endArray()
}
override fun read(reader: JsonReader): List<T> {
reader.beginArray()
val builder = ImmutableList.builder<T>()
val resolvedBound = resolvedBound
if (resolvedBound != null) {
while (reader.peek() != JsonToken.END_ARRAY) {
val readObject = resolvedBound.read(reader) ?: throw JsonSyntaxException("List does not accept nulls")
builder.add(readObject as T)
}
} else {
while (reader.peek() != JsonToken.END_ARRAY) {
val readObject = Starbound.gson.fromJson<T>(reader, bound) ?: throw JsonSyntaxException("List does not accept nulls")
builder.add(readObject)
}
}
reader.endArray()
return builder.build()
}
}
class MapAdapter<K, V>(private val boundKey: Class<K>, private val boundValue: Class<V>) : TypeAdapter<Map<K, V>>() {
private val resolvedKey by lazy {
resolveBound(boundKey)
}
private val resolvedValue by lazy {
resolveBound(boundValue)
}
override fun write(out: JsonWriter, value: Map<K, V>) {
out.beginArray()
val resolvedKey = resolvedKey
val resolvedValue = resolvedValue
if (resolvedKey != null && resolvedValue != null) {
for ((k, v) in value) {
out.beginArray()
resolvedKey.write(out, k)
resolvedValue.write(out, v)
out.endArray()
}
} else if (resolvedKey != null) {
for ((k, v) in value) {
out.beginArray()
resolvedKey.write(out, k)
Starbound.gson.toJson(Starbound.gson.toJsonTree(v, boundValue) as JsonElement, out)
out.endArray()
}
} else if (resolvedValue != null) {
for ((k, v) in value) {
out.beginArray()
Starbound.gson.toJson(Starbound.gson.toJsonTree(k, boundKey) as JsonElement, out)
resolvedValue.write(out, v)
out.endArray()
}
} else {
for ((k, v) in value) {
out.beginArray()
Starbound.gson.toJson(Starbound.gson.toJsonTree(k, boundKey) as JsonElement, out)
Starbound.gson.toJson(Starbound.gson.toJsonTree(v, boundValue) as JsonElement, out)
out.endArray()
}
}
out.endArray()
}
override fun read(reader: JsonReader): Map<K, V> {
reader.beginArray()
val builder = ImmutableMap.builder<K, V>()
val resolvedKey = resolvedKey
val resolvedValue = resolvedValue
if (resolvedKey != null && resolvedValue != null) {
while (reader.peek() != JsonToken.END_ARRAY) {
reader.beginArray()
builder.put(resolvedKey.read(reader), resolvedValue.read(reader))
reader.endArray()
}
} else if (resolvedKey != null) {
while (reader.peek() != JsonToken.END_ARRAY) {
reader.beginArray()
builder.put(resolvedKey.read(reader), Starbound.gson.fromJson(reader, boundValue))
reader.endArray()
}
} else if (resolvedValue != null) {
while (reader.peek() != JsonToken.END_ARRAY) {
reader.beginArray()
builder.put(Starbound.gson.fromJson(reader, boundKey), resolvedValue.read(reader))
reader.endArray()
}
} else {
while (reader.peek() != JsonToken.END_ARRAY) {
reader.beginArray()
builder.put(Starbound.gson.fromJson(reader, boundKey), Starbound.gson.fromJson(reader, boundValue))
reader.endArray()
}
}
reader.endArray()
return builder.build()
}
}
class StringMapAdapter<V>(private val bound: Class<V>) : TypeAdapter<Map<String, V>>() {
private val resolvedBound by lazy {
resolveBound(bound)
}
override fun write(out: JsonWriter, value: Map<String, V>) {
val resolvedBound = resolvedBound
out.beginObject()
if (resolvedBound != null) {
for ((k, v) in value) {
out.name(k)
resolvedBound.write(out, v)
}
} else {
for ((k, v) in value) {
out.name(k)
Starbound.gson.toJson(Starbound.gson.toJsonTree(v, bound) as JsonElement, out)
}
}
out.endObject()
}
override fun read(reader: JsonReader): Map<String, V> {
val builder = ImmutableMap.builder<String, V>()
reader.beginObject()
val resolvedBound = resolvedBound
if (resolvedBound != null) {
while (reader.peek() != JsonToken.END_OBJECT) {
builder.put(reader.nextName(), resolvedBound.read(reader))
}
} else {
while (reader.peek() != JsonToken.END_OBJECT) {
builder.put(reader.nextName(), Starbound.gson.fromJson(reader, bound))
}
}
reader.endObject()
return builder.build()
}
}
/**
* TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов").
*/
class KConcreteTypeAdapter<T : Any>(
val bound: KClass<T>,
val types: ImmutableList<Pair<KProperty1<T, *>, TypeAdapter<*>>>
) : TypeAdapter<T>() {
private val returnTypeCache = Object2ObjectArrayMap<KProperty1<T, *>, KType>()
private val mapped = Object2IntArrayMap<String>()
private val loggedMisses = ObjectArraySet<String>()
init {
for ((field) in types) {
returnTypeCache[field] = field.returnType
}
mapped.defaultReturnValue(-1)
for ((i, pair) in types.withIndex()) {
mapped[pair.first.name] = i
}
}
private val factory: KFunction<T> = bound.constructors.firstOrNull first@{
if (it.parameters.size == types.size) {
val iterator = types.iterator()
for (param in it.parameters) {
val nextParam = iterator.next()
val a = param.type
val b = nextParam.first.returnType
if (!a.isSupertypeOf(b) || a.isMarkedNullable != b.isMarkedNullable) {
return@first false
}
}
return@first true
}
return@first false
} ?: throw NoSuchElementException("Unable to determine constructor for ${bound.qualifiedName} matching (${types.joinToString(", ")})")
override fun write(out: JsonWriter, value: T) {
out.beginObject()
for ((field, adapter) in types) {
out.name(field.name)
(adapter as TypeAdapter<Any>).write(out, (field as KProperty1<T, Any>).get(value))
}
out.endObject()
}
override fun read(reader: JsonReader): T {
reader.beginObject()
val readValues = arrayOfNulls<Any>(types.size)
while (reader.peek() != JsonToken.END_OBJECT) {
val name = reader.nextName()
val fieldId = mapped.getInt(name)
if (fieldId == -1) {
if (loggedMisses.add(name)) {
LOGGER.warn("Skipping JSON field with name $name because ${bound.qualifiedName} has no such field")
}
reader.skipValue()
} else {
val (field, adapter) = types[fieldId]
try {
readValues[fieldId] = adapter.read(reader)
} catch(err: Throwable) {
throw JsonSyntaxException("Exception reading field ${field.name}", err)
}
}
}
for ((i, pair) in types.withIndex()) {
val (field) = pair
if (readValues[i] == null && !returnTypeCache[field]!!.isMarkedNullable) {
throw JsonSyntaxException("Field ${field.name} does not accept nulls")
}
}
reader.endObject()
return factory.call(*readValues as Array<out Any>)
}
class Builder<T : Any>(val clazz: KClass<T>) {
private val types = ArrayList<Pair<KProperty1<T, *>, TypeAdapter<*>>>()
/**
* Добавляет поле без generic типов
*/
fun plain(field: KProperty1<T, *>): Builder<T> {
val returnType = field.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${field.name}!")
if (classifier.isSuperclassOf(Float::class)) {
types.add(field to TypeAdapters.FLOAT)
} else if (classifier.isSuperclassOf(Double::class)) {
types.add(field to TypeAdapters.DOUBLE)
} else if (classifier.isSuperclassOf(Int::class)) {
types.add(field to TypeAdapters.INTEGER)
} else if (classifier.isSuperclassOf(Long::class)) {
types.add(field to TypeAdapters.LONG)
} else if (classifier.isSuperclassOf(String::class)) {
types.add(field to TypeAdapters.STRING)
} else if (classifier.isSuperclassOf(Boolean::class)) {
types.add(field to TypeAdapters.BOOLEAN)
} else {
types.add(field to PassthroughAdapter(classifier.java))
}
return this
}
/**
* Добавляет поле, которое содержит список значений V (без null).
*
* Список неизменяем (создаётся объект [ImmutableList])
*/
fun <V> list(field: KProperty1<T, List<V>>, type: Class<V>): Builder<T> {
types.add(field to ListAdapter(type))
return this
}
/**
* Добавляет поле, которое содержит список значений V (без null).
*
* Список неизменяем (создаётся объект [ImmutableList])
*/
fun <V : Any> list(field: KProperty1<T, List<V>>, type: KClass<V>): Builder<T> {
return this.list(field, type.java)
}
/**
* Добавляет поле-таблицу, которое кодируется как [[key, value], [key, value], ...]
*
* Таблица неизменяема (создаётся объект [ImmutableMap])
*/
fun <K, V> map(field: KProperty1<T, Map<K, V>>, keyType: Class<K>, valueType: Class<V>): Builder<T> {
types.add(field to MapAdapter(keyType, valueType))
return this
}
/**
* Добавляет поле-таблицу, которое кодируется как {"a": value, "b": value, ...}
*
* Таблица неизменяема (создаётся объект [ImmutableMap])
*/
fun <V> map(field: KProperty1<T, Map<String, V>>, valueType: Class<V>): Builder<T> {
types.add(field to StringMapAdapter(valueType))
return this
}
fun build(): KConcreteTypeAdapter<T> {
return KConcreteTypeAdapter(clazz, ImmutableList.copyOf(types))
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.io
import com.google.common.collect.ImmutableList
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.TypeAdapters
@ -17,7 +18,14 @@ import kotlin.reflect.KType
import kotlin.reflect.full.isSuperclassOf
/**
* Kotlin property aware adapter
* Kotlin property aware adapter.
*
* Создаёт пустые классы, а после наполняет их данными, что подходит для builder'ов с очень
* большим количеством возможных данных внутри.
*
*
*
* Подходит для игровых структур которые могут быть "разобраны" и пересобраны.
*/
class KTypeAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() {
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>()
@ -35,9 +43,14 @@ class KTypeAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T,
}
}
val fields: Array<KMutableProperty1<T, in Any?>> get() {
val iterator = mappedFields.values.iterator()
return Array(mappedFields.size) { iterator.next() }
val fields: List<KMutableProperty1<T, in Any?>> by lazy {
return@lazy ImmutableList.builder<KMutableProperty1<T, in Any?>>().let {
for (v in mappedFields.values.iterator()) {
it.add(v)
}
it.build()
}
}
fun ignoreProperty(vararg value: String): KTypeAdapter<T> {
@ -81,7 +94,7 @@ class KTypeAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T,
val expectedType = mappedFieldsReturnTypes[name]!!
if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) {
throw IllegalArgumentException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls")
throw NullPointerException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls")
} else if (peek == JsonToken.NULL) {
field.set(instance, null)
reader.nextNull()
@ -108,8 +121,7 @@ class KTypeAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T,
val read = reader.nextBoolean()
field.set(instance, read)
} else {
val readElement = TypeAdapters.JSON_ELEMENT.read(reader)
field.set(instance, Starbound.gson.fromJson(readElement, classifier.java))
field.set(instance, Starbound.gson.fromJson(reader, classifier.java))
}
} else {
throw IllegalStateException("Expected ${field.name} classifier to be KClass, got $classifier")
@ -140,4 +152,4 @@ class KTypeAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T,
companion object {
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
}
}
}