diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 35a1009b..09b0b7fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,31 +1,17 @@ package ru.dbotthepony.kstarbound -import com.google.gson.JsonElement -import com.google.gson.JsonObject import org.apache.logging.log4j.LogManager import org.lwjgl.Version import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose -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 +import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.io.* -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos -import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.PlayerEntity -import ru.dbotthepony.kstarbound.world.entities.projectile.Projectile import ru.dbotthepony.kvector.vector.ndouble.Vector2d import java.io.ByteArrayInputStream import java.io.DataInputStream import java.io.File -import java.io.InputStream -import java.util.* import java.util.zip.Inflater private val LOGGER = LogManager.getLogger() @@ -41,34 +27,6 @@ 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")) @@ -133,6 +91,34 @@ fun main() { val ent = PlayerEntity(client.world!!) Starbound.onInitialize { + 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@onInitialize + } + var find = 0L var set = 0L var parse = 0L diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 334314ed..914a260d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -9,6 +9,9 @@ import ru.dbotthepony.kstarbound.api.getPathFilename import ru.dbotthepony.kstarbound.api.getPathFolder import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.projectile.* +import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier +import ru.dbotthepony.kstarbound.defs.tile.RenderParameters +import ru.dbotthepony.kstarbound.defs.tile.RenderTemplate import ru.dbotthepony.kstarbound.defs.world.SkyParameters import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef import ru.dbotthepony.kstarbound.io.* @@ -72,6 +75,8 @@ object Starbound : IVFS { .also(ParallaxPrototype::registerGson) .also(JsonFunction::registerGson) .also(MaterialModifier::registerGson) + .also(RenderParameters::registerGson) + .also(RenderTemplate::registerGson) .registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt similarity index 74% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt index 76458758..6947ab04 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MaterialModifiers.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.defs +package ru.dbotthepony.kstarbound.defs.tile import com.google.gson.GsonBuilder import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter @@ -11,9 +11,10 @@ data class MaterialModifier( val health: Int, val harvestLevel: Int, val breaksWithTile: Boolean, - val miningSounds: List<String>, - val miningParticle: String, - val renderTemplate: String, + val miningSounds: List<String> = listOf(), + val miningParticle: String? = null, + val renderTemplate: RenderTemplate, + val renderParameters: RenderParameters ) { companion object { val ADAPTER = KConcreteTypeAdapter.Builder(MaterialModifier::class) @@ -26,7 +27,8 @@ data class MaterialModifier( .plain(MaterialModifier::breaksWithTile) .list(MaterialModifier::miningSounds, String::class.java) .plain(MaterialModifier::miningParticle) - .plain(MaterialModifier::renderTemplate) + .plain(MaterialModifier::renderTemplate, RenderTemplate.CACHE) + .plain(MaterialModifier::renderParameters) .build() fun registerGson(gsonBuilder: GsonBuilder) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt new file mode 100644 index 00000000..915f5dfb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt @@ -0,0 +1,26 @@ +package ru.dbotthepony.kstarbound.defs.tile + +import com.google.gson.GsonBuilder +import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter + +data class RenderParameters( + val texture: String, + val variants: Int, + val multiColored: Boolean, + val zLevel: Int, +) { + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderParameters::class) + .plain( + RenderParameters::texture, + RenderParameters::variants, + RenderParameters::multiColored, + RenderParameters::zLevel, + ) + .build() + + fun registerGson(gsonBuilder: GsonBuilder) { + gsonBuilder.registerTypeAdapter(RenderParameters::class.java, ADAPTER) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt new file mode 100644 index 00000000..63377beb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -0,0 +1,186 @@ +package ru.dbotthepony.kstarbound.defs.tile + +import com.google.gson.GsonBuilder +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter +import ru.dbotthepony.kstarbound.world.ITileGetter +import ru.dbotthepony.kvector.vector.nint.Vector2i +import java.util.concurrent.ConcurrentHashMap + +data class RenderPiece( + val texture: String? = null, + val textureSize: Vector2i, + val texturePosition: Vector2i, + val colorStride: Vector2i? = null, + val variantStride: Vector2i? = null, +) { + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderPiece::class) + .plain( + RenderPiece::texture, + RenderPiece::textureSize, + RenderPiece::texturePosition, + RenderPiece::colorStride, + RenderPiece::variantStride, + ) + .build() + } +} + +data class RenderRule( + val type: String, + val matchHue: Boolean = false, + val inverse: Boolean = false, +) { + private fun doTest(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + return when (type) { + "EqualsSelf" -> getter[thisPos + offsetPos]?.def == thisRef + "Connects" -> getter[thisPos + offsetPos] != null + + else -> false + } + } + + fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + if (inverse) { + return !doTest(getter, thisRef, thisPos, offsetPos) + } + + return doTest(getter, thisRef, thisPos, offsetPos) + } + + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderRule::class) + .plain( + RenderRule::type, + RenderRule::matchHue, + RenderRule::inverse, + ) + .build() + } +} + +data class RenderRuleList( + val entries: List<RenderRule> +) { + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderRuleList::class) + .list(RenderRuleList::entries, RenderRule::class) + .build() + } +} + +data class RenderMatch( + val pieces: List<Piece>? = null, + val matchAllPoints: List<Matcher>? = null, + val subMatches: List<RenderMatch>? = null, + val haltOnMatch: Boolean = false +) { + data class Piece( + val name: String, + val point: Vector2i + ) + + data class Matcher( + val point: Vector2i, + val ruleName: String + ) + + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderMatch::class) + .list(RenderMatch::pieces, Piece::class) + .list(RenderMatch::matchAllPoints, Matcher::class) + .list(RenderMatch::subMatches, RenderMatch::class) + .plain(RenderMatch::haltOnMatch) + .build() + + val PIECE_ADAPTER = KConcreteTypeAdapter.Builder(Piece::class) + .plain(Piece::name) + .plain(Piece::point) + .build(asList = true) + + val MATCHER_ADAPTER = KConcreteTypeAdapter.Builder(Matcher::class) + .plain(Matcher::point) + .plain(Matcher::ruleName) + .build(asList = true) + } +} + +data class RenderMatchList( + val name: String, + val list: List<RenderMatch> +) { + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderMatchList::class) + .plain(RenderMatchList::name) + .list(RenderMatchList::list, RenderMatch::class) + .build(asList = true) + } +} + +data class RenderTemplate( + val pieces: Map<String, RenderPiece>, + val representativePiece: String, + val matches: List<RenderMatchList> +) { + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(RenderTemplate::class) + .map(RenderTemplate::pieces, RenderPiece::class.java) + .plain(RenderTemplate::representativePiece) + .list(RenderTemplate::matches, RenderMatchList::class.java) + .build() + + fun registerGson(gsonBuilder: GsonBuilder) { + RenderPiece.ADAPTER.register(gsonBuilder) + RenderRule.ADAPTER.register(gsonBuilder) + RenderRuleList.ADAPTER.register(gsonBuilder) + RenderMatch.ADAPTER.register(gsonBuilder) + RenderMatch.PIECE_ADAPTER.register(gsonBuilder) + RenderMatch.MATCHER_ADAPTER.register(gsonBuilder) + RenderMatchList.ADAPTER.register(gsonBuilder) + + ADAPTER.register(gsonBuilder) + } + + private val cache = ConcurrentHashMap<String, RenderTemplate>() + private val _readingFolder = ThreadLocal<String>() + + /** + * Случит переменной для указания из какой папки происходит чтение в данном потоке + */ + var readingFolder: String? + get() = _readingFolder.get() + set(value) { _readingFolder.set(value) } + + val CACHE = object : TypeAdapter<RenderTemplate>() { + override fun write(out: JsonWriter, value: RenderTemplate) { + ADAPTER.write(out, value) + } + + override fun read(reader: JsonReader): RenderTemplate { + if (reader.peek() != JsonToken.STRING) { + throw JsonSyntaxException("Expected string as input for render template cache retriever") + } + + var path = reader.nextString() + + if (path[0] != '/') { + // относительный путь + + val readingFolder = readingFolder ?: throw NullPointerException("Currently read folder is not specified") + path = "$readingFolder/$path" + } + + return cache.computeIfAbsent(path) { + return@computeIfAbsent Starbound.gson.fromJson(Starbound.getReader(it), RenderTemplate::class.java) + } + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt index 3c8c0556..1ba47d5e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt @@ -2,7 +2,7 @@ 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.GsonBuilder import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.internal.bind.TypeAdapters @@ -14,24 +14,15 @@ 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 java.lang.reflect.Constructor +import kotlin.jvm.internal.DefaultConstructorMarker +import kotlin.reflect.* import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSupertypeOf +import kotlin.reflect.full.memberProperties -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>? { +@Suppress("unchecked_cast") +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> @@ -53,14 +44,8 @@ class ListAdapter<T>(private val bound: Class<T>) : TypeAdapter<List<T>>() { 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) - } + for (v in value) { + resolvedBound.write(out, v) } out.endArray() @@ -72,16 +57,9 @@ class ListAdapter<T>(private val bound: Class<T>) : TypeAdapter<List<T>>() { 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) - } + while (reader.peek() != JsonToken.END_ARRAY) { + val readObject = resolvedBound.read(reader) ?: throw JsonSyntaxException("List does not accept nulls") + builder.add(readObject as T) } reader.endArray() @@ -105,34 +83,11 @@ class MapAdapter<K, V>(private val boundKey: Class<K>, private val boundValue: C 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() - } + for ((k, v) in value) { + out.beginArray() + resolvedKey.write(out, k) + resolvedValue.write(out, v) + out.endArray() } out.endArray() @@ -146,30 +101,10 @@ class MapAdapter<K, V>(private val boundKey: Class<K>, private val boundValue: C 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() - } + while (reader.peek() != JsonToken.END_ARRAY) { + reader.beginArray() + builder.put(resolvedKey.read(reader), resolvedValue.read(reader)) + reader.endArray() } reader.endArray() @@ -188,16 +123,9 @@ class StringMapAdapter<V>(private val bound: Class<V>) : TypeAdapter<Map<String, 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) - } + for ((k, v) in value) { + out.name(k) + resolvedBound.write(out, v) } out.endObject() @@ -210,14 +138,8 @@ class StringMapAdapter<V>(private val bound: Class<V>) : TypeAdapter<Map<String, 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)) - } + while (reader.peek() != JsonToken.END_OBJECT) { + builder.put(reader.nextName(), resolvedBound.read(reader)) } reader.endObject() @@ -226,13 +148,30 @@ class StringMapAdapter<V>(private val bound: Class<V>) : TypeAdapter<Map<String, } } +private class LazyTypeProvider<T : Any?>(private val bound: Class<T>) : TypeAdapter<T>() { + private val resolved by lazy { resolveBound(bound) } + + override fun write(out: JsonWriter, value: T) { + resolved.write(out, value) + } + + override fun read(`in`: JsonReader): T { + return resolved.read(`in`) + } +} + /** * TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов"). */ class KConcreteTypeAdapter<T : Any>( val bound: KClass<T>, - val types: ImmutableList<Pair<KProperty1<T, *>, TypeAdapter<*>>> + val types: ImmutableList<Pair<KProperty1<T, *>, TypeAdapter<*>>>, + val asList: Boolean = false ) : TypeAdapter<T>() { + fun register(gsonBuilder: GsonBuilder) { + gsonBuilder.registerTypeAdapter(bound.java, this) + } + private val returnTypeCache = Object2ObjectArrayMap<KProperty1<T, *>, KType>() private val mapped = Object2IntArrayMap<String>() @@ -250,7 +189,7 @@ class KConcreteTypeAdapter<T : Any>( } } - private val factory: KFunction<T> = bound.constructors.firstOrNull first@{ + private val regularFactory: KFunction<T> = bound.constructors.firstOrNull first@{ if (it.parameters.size == types.size) { val iterator = types.iterator() @@ -271,11 +210,50 @@ class KConcreteTypeAdapter<T : Any>( return@first false } ?: throw NoSuchElementException("Unable to determine constructor for ${bound.qualifiedName} matching (${types.joinToString(", ")})") + private val syntheticFactory: Constructor<T>? = try { + bound.java.getDeclaredConstructor(*types.map { (it.first.returnType.classifier as KClass<*>).java }.also { + it as MutableList + it.add(Int::class.java) + it.add(DefaultConstructorMarker::class.java) + }.toTypedArray()) + } catch(_: NoSuchMethodException) { + null + } + + private val syntheticPrimitives: Array<Any?>? + + init { + if (syntheticFactory == null) { + syntheticPrimitives = null + } else { + syntheticPrimitives = arrayOfNulls(syntheticFactory.parameters.size) + + for ((i, param) in syntheticFactory.parameters.withIndex()) { + val type = param.parameterizedType as? Class<*> ?: continue + + if (type.isPrimitive) { + syntheticPrimitives[i] = when (type.name) { + "boolean" -> false + "int" -> 0 + "long" -> 0L + "short" -> (0).toShort() + "byte" -> (0).toByte() + "char" -> (0).toChar() + "float" -> 0f + "double" -> 0.0 + else -> throw IllegalArgumentException("mamma mia: ${type.name}") + } + } + } + } + } + override fun write(out: JsonWriter, value: T) { out.beginObject() for ((field, adapter) in types) { out.name(field.name) + @Suppress("unchecked_cast") (adapter as TypeAdapter<Any>).write(out, (field as KProperty1<T, Any>).get(value)) } @@ -283,79 +261,211 @@ class KConcreteTypeAdapter<T : Any>( } override fun read(reader: JsonReader): T { - reader.beginObject() + if (asList) { + reader.beginArray() + } else { + reader.beginObject() + } + val presentValues = BooleanArray(types.size) val readValues = arrayOfNulls<Any>(types.size) - while (reader.peek() != JsonToken.END_OBJECT) { - val name = reader.nextName() - val fieldId = mapped.getInt(name) + if (asList) { + val iterator = types.iterator() + var fieldId = 0 - if (fieldId == -1) { - if (loggedMisses.add(name)) { - LOGGER.warn("Skipping JSON field with name $name because ${bound.qualifiedName} has no such field") + while (reader.peek() != JsonToken.END_ARRAY) { + if (!iterator.hasNext()) { + val name = fieldId.toString() + + if (loggedMisses.add(name)) { + LOGGER.warn("Skipping JSON field with name $name because ${bound.qualifiedName} has no such field") + } + + reader.skipValue() + fieldId++ + + continue } - reader.skipValue() - } else { - val (field, adapter) = types[fieldId] + val (field, adapter) = iterator.next() try { readValues[fieldId] = adapter.read(reader) + presentValues[fieldId] = true } catch(err: Throwable) { throw JsonSyntaxException("Exception reading field ${field.name}", err) } + + fieldId++ + } + } else { + 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) + presentValues[fieldId] = true + } 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") + for (i in readValues.indices) { + if (readValues[i] is String) { + readValues[i] = (readValues[i] as String).intern() } } - reader.endObject() + if (asList) { + reader.endArray() + } else { + reader.endObject() + } - return factory.call(*readValues as Array<out Any>) + if (syntheticFactory == null || presentValues.all { it }) { + for ((i, pair) in types.withIndex()) { + val (field) = pair + + if (readValues[i] == null) { + if (!returnTypeCache[field]!!.isMarkedNullable) { + throw JsonSyntaxException("Field ${field.name} does not accept nulls") + } + + if (!regularFactory.parameters[i].isOptional && !presentValues[i]) { + throw JsonSyntaxException("Field ${field.name} must be defined (even just as null)") + } + } + } + + @Suppress("unchecked_cast") + return regularFactory.call(*readValues as Array<out Any>) + } else { + val ints = if (presentValues.size % 31 == 0) presentValues.size / 31 else presentValues.size / 31 + 1 + val copied = readValues.copyOf(readValues.size + ints + 1) + var intIndex = readValues.size + var target = 0 + var targetMove = 0 + + for (bool in presentValues) { + if (!bool) { + target = target.or(1.shl(targetMove)) + } + + targetMove++ + + if (targetMove >= 32) { + copied[intIndex++] = target + target = 0 + targetMove = 0 + } + } + + if (targetMove != 0) { + copied[intIndex] = target + } + + val syntheticPrimitives = syntheticPrimitives!! + + for ((i, pair) in types.withIndex()) { + if (copied[i] != null) { + continue + } + + val (field) = pair + val param = regularFactory.parameters[i] + + if (!param.isOptional && !presentValues[i]) { + throw JsonSyntaxException("Field ${field.name} is missing") + } + + if (returnTypeCache[field]!!.isMarkedNullable) { + continue + } + + if (param.isOptional && !presentValues[i]) { + copied[i] = syntheticPrimitives[i] ?: throw NullPointerException("HOW $i") + continue + } + + throw JsonSyntaxException("Field ${field.name} does not accept nulls") + } + + return syntheticFactory.newInstance(*copied) + } } 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}!") + fun <V> plain(field: KProperty1<T, V>, adapter: TypeAdapter<V>): Builder<T> { + types.add(field to adapter) + return this + } - 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)) + /** + * Добавляет поле(я) без generic типов и без преобразователей + */ + fun plain(vararg fields: KProperty1<T, *>): Builder<T> { + for (field in fields) { + 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 LazyTypeProvider(classifier.java)) + } } return this } + /** + * Добавляет поле(я) без generic типов. + * + * Если поле с данным именем не найдено, кидается [NoSuchElementException] + */ + fun plain(vararg fields: String): Builder<T> { + val members = clazz.memberProperties + + return this.plain(*Array(fields.size) { + val field = fields[it] + return@Array members.firstOrNull { it.name == field } ?: throw NoSuchElementException("Unable to find field $field") + }) + } + /** * Добавляет поле, которое содержит список значений V (без null). * * Список неизменяем (создаётся объект [ImmutableList]) */ - fun <V> list(field: KProperty1<T, List<V>>, type: Class<V>): Builder<T> { + fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>): Builder<T> { types.add(field to ListAdapter(type)) return this } @@ -365,7 +475,7 @@ class KConcreteTypeAdapter<T : Any>( * * Список неизменяем (создаётся объект [ImmutableList]) */ - fun <V : Any> list(field: KProperty1<T, List<V>>, type: KClass<V>): Builder<T> { + fun <V : Any> list(field: KProperty1<T, List<V>?>, type: KClass<V>): Builder<T> { return this.list(field, type.java) } @@ -379,6 +489,16 @@ class KConcreteTypeAdapter<T : Any>( return this } + /** + * Добавляет поле-таблицу, которое кодируется как [[key, value], [key, value], ...] + * + * Таблица неизменяема (создаётся объект [ImmutableMap]) + */ + fun <K : Any, V : Any> map(field: KProperty1<T, Map<K, V>>, keyType: KClass<K>, valueType: KClass<V>): Builder<T> { + types.add(field to MapAdapter(keyType.java, valueType.java)) + return this + } + /** * Добавляет поле-таблицу, которое кодируется как {"a": value, "b": value, ...} * @@ -389,8 +509,18 @@ class KConcreteTypeAdapter<T : Any>( return this } - fun build(): KConcreteTypeAdapter<T> { - return KConcreteTypeAdapter(clazz, ImmutableList.copyOf(types)) + /** + * Добавляет поле-таблицу, которое кодируется как {"a": value, "b": value, ...} + * + * Таблица неизменяема (создаётся объект [ImmutableMap]) + */ + fun <V : Any> map(field: KProperty1<T, Map<String, V>>, valueType: KClass<V>): Builder<T> { + types.add(field to StringMapAdapter(valueType.java)) + return this + } + + fun build(asList: Boolean = false): KConcreteTypeAdapter<T> { + return KConcreteTypeAdapter(clazz, ImmutableList.copyOf(types), asList = asList) } }