From 2d8e3a7ff5f415ba1ea76b2c705e5fa060efa14b Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Sun, 22 Jan 2023 20:29:48 +0700
Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BC=D0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kotlin/ru/dbotthepony/kstarbound/Main.kt  |  3 ++
 .../ru/dbotthepony/kstarbound/Starbound.kt    |  5 +-
 .../io/json/builder/BuilderAdapter.kt         |  9 +++-
 .../io/json/builder/FactoryAdapter.kt         | 13 +++++-
 .../factory/ImmutableArrayMapTypeAdapter.kt   | 37 +++++++++++++++
 .../ImmutableCollectionAdapterFactory.kt      | 42 +++++++++++++++++
 .../json/factory/ImmutableListTypeAdapter.kt  | 45 ++++++++++++++++++
 .../json/factory/ImmutableMapTypeAdapter.kt   | 35 ++++++++++++++
 .../json/factory/ImmutableSetTypeAdapter.kt   | 46 +++++++++++++++++++
 .../io/json/util/LazyTypeProvider.kt          |  4 +-
 10 files changed, 234 insertions(+), 5 deletions(-)
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableArrayMapTypeAdapter.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableCollectionAdapterFactory.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableListTypeAdapter.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableMapTypeAdapter.kt
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableSetTypeAdapter.kt

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index 81a59540..dfa59cf5 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -1,10 +1,13 @@
 package ru.dbotthepony.kstarbound
 
+import com.google.common.collect.ImmutableList
+import com.google.gson.GsonBuilder
 import org.apache.logging.log4j.LogManager
 import org.lwjgl.Version
 import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
 import ru.dbotthepony.kstarbound.client.StarboundClient
 import ru.dbotthepony.kstarbound.io.*
+import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory
 import ru.dbotthepony.kstarbound.world.ChunkPos
 import ru.dbotthepony.kstarbound.world.entities.ItemEntity
 import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
index 4ab1c574..b4ad84b1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
@@ -46,6 +46,7 @@ import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
 import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter
+import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory
 import ru.dbotthepony.kstarbound.math.*
 import ru.dbotthepony.kvector.vector.Color
 import java.io.*
@@ -145,7 +146,9 @@ object Starbound {
 		.setDateFormat(DateFormat.LONG)
 		.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
 		.setPrettyPrinting()
-		.registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe())
+		.registerTypeAdapter(ColorTypeAdapter.nullSafe())
+
+		.registerTypeAdapterFactory(ImmutableCollectionAdapterFactory)
 
 		// чтоб строки всегда intern'ились
 		.registerTypeAdapter(NULLABLE_STRING_ADAPTER)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt
index 6218f386..b099da1e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt
@@ -2,11 +2,14 @@ package ru.dbotthepony.kstarbound.io.json.builder
 
 import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableSet
+import com.google.gson.Gson
 import com.google.gson.JsonObject
 import com.google.gson.JsonSyntaxException
 import com.google.gson.TypeAdapter
+import com.google.gson.TypeAdapterFactory
 import com.google.gson.internal.bind.JsonTreeReader
 import com.google.gson.internal.bind.TypeAdapters
+import com.google.gson.reflect.TypeToken
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonToken
 import com.google.gson.stream.JsonWriter
@@ -241,13 +244,17 @@ class BuilderAdapter<T : Any> private constructor(
 		var mustBePresent: Boolean? = null
 	}
 
-	class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) {
+	class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapterFactory {
 		private val properties = ArrayList<WrappedProperty<T, *>>()
 		private val flatProperties = ArrayList<WrappedProperty<T, *>>()
 		private val ignoreKeys = ObjectArraySet<String>()
 		var extraPropertiesAreFatal = false
 		var logMisses: Boolean? = null
 
+		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
+			return null
+		}
+
 		/**
 		 * Являются ли "лишние" ключи в JSON структуре ошибкой.
 		 *
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt
index 456921ca..19a19fe4 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt
@@ -2,13 +2,16 @@ package ru.dbotthepony.kstarbound.io.json.builder
 
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
+import com.google.gson.Gson
 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.TypeAdapterFactory
 import com.google.gson.internal.bind.JsonTreeReader
 import com.google.gson.internal.bind.TypeAdapters
+import com.google.gson.reflect.TypeToken
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonToken
 import com.google.gson.stream.JsonWriter
@@ -382,7 +385,7 @@ class FactoryAdapter<T : Any> private constructor(
 	/**
 	 * Позволяет построить класс [FactoryAdapter] на основе заданных параметров
 	 */
-	class Builder<T : Any>(val clazz: KClass<T>) {
+	class Builder<T : Any>(val clazz: KClass<T>) : TypeAdapterFactory {
 		constructor(clazz: KClass<T>, vararg fields: KProperty1<T, *>) : this(clazz) {
 			for (field in fields) {
 				auto(field)
@@ -391,6 +394,14 @@ class FactoryAdapter<T : Any> private constructor(
 
 		private val types = ArrayList<PackedProperty<T, *>>()
 
+		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
+			if (type.rawType == clazz.java) {
+				return build() as TypeAdapter<T>
+			}
+
+			return null
+		}
+
 		/**
 		 * Принимает ли класс *последним* аргументом JSON структуру
 		 *
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableArrayMapTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableArrayMapTypeAdapter.kt
new file mode 100644
index 00000000..52a60081
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableArrayMapTypeAdapter.kt
@@ -0,0 +1,37 @@
+package ru.dbotthepony.kstarbound.io.json.factory
+
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
+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
+
+class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() {
+	override fun write(out: JsonWriter, value: ImmutableMap<K, V>) {
+		out.beginArray()
+
+		for ((k, v) in value) {
+			keyAdapter.write(out, k)
+			elementAdapter.write(out, v)
+		}
+
+		out.endArray()
+	}
+
+	override fun read(reader: JsonReader): ImmutableMap<K, V> {
+		reader.beginArray()
+
+		val builder = ImmutableMap.Builder<K, V>()
+
+		while (reader.peek() != JsonToken.END_OBJECT) {
+			builder.put(
+				keyAdapter.read(reader) ?: throw JsonSyntaxException("Nulls as keys are not allowed, near ${reader.path}"),
+				elementAdapter.read(reader) ?: throw JsonSyntaxException("Nulls as values are not allowed, near ${reader.path}"))
+		}
+
+		reader.endArray()
+		return builder.build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableCollectionAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableCollectionAdapterFactory.kt
new file mode 100644
index 00000000..4a716806
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableCollectionAdapterFactory.kt
@@ -0,0 +1,42 @@
+package ru.dbotthepony.kstarbound.io.json.factory
+
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableSet
+import com.google.gson.Gson
+import com.google.gson.TypeAdapter
+import com.google.gson.TypeAdapterFactory
+import com.google.gson.internal.`$Gson$Types`
+import com.google.gson.reflect.TypeToken
+
+@Suppress("unchecked_cast")
+object ImmutableCollectionAdapterFactory : TypeAdapterFactory {
+	override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
+		when (type.rawType) {
+			ImmutableList::class.java -> {
+				val elementType = `$Gson$Types`.getCollectionElementType(type.type, type.rawType)
+				return ImmutableListTypeAdapter(gson.getAdapter(TypeToken.get(elementType))) as TypeAdapter<T>
+			}
+
+			ImmutableSet::class.java -> {
+				val elementType = `$Gson$Types`.getCollectionElementType(type.type, type.rawType)
+				return ImmutableSetTypeAdapter(gson.getAdapter(TypeToken.get(elementType))) as TypeAdapter<T>
+			}
+
+			ImmutableMap::class.java -> {
+				val (elementType0, elementType1) = `$Gson$Types`.getMapKeyAndValueTypes(type.type, type.rawType)
+
+				if (`$Gson$Types`.getRawType(elementType0) == String::class.java) {
+					return ImmutableMapTypeAdapter(gson.getAdapter(TypeToken.get(elementType1))) as TypeAdapter<T>
+				}
+
+				return ImmutableArrayMapTypeAdapter(
+					gson.getAdapter(TypeToken.get(elementType0)),
+					gson.getAdapter(TypeToken.get(elementType1))
+				) as TypeAdapter<T>
+			}
+
+			else -> return null
+		}
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableListTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableListTypeAdapter.kt
new file mode 100644
index 00000000..0db51916
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableListTypeAdapter.kt
@@ -0,0 +1,45 @@
+package ru.dbotthepony.kstarbound.io.json.factory
+
+import com.google.common.collect.ImmutableList
+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
+
+class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableList<E>>() {
+	override fun write(out: JsonWriter, value: ImmutableList<E>) {
+		if (value.size == 1) {
+			elementAdapter.write(out, value[0])
+			return
+		}
+
+		out.beginArray()
+
+		for (v in value) {
+			elementAdapter.write(out, v)
+		}
+
+		out.endArray()
+	}
+
+	override fun read(reader: JsonReader): ImmutableList<E> {
+		if (reader.peek() != JsonToken.BEGIN_ARRAY) {
+			// не массив, возможно упрощение структуры "a": [value] -> "a": value
+			return ImmutableList.of(elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls"))
+		}
+
+		reader.beginArray()
+
+		val builder = ImmutableList.Builder<E>()
+
+		while (reader.peek() != JsonToken.END_ARRAY) {
+			val readObject = elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls")
+			builder.add(readObject)
+		}
+
+		reader.endArray()
+
+		return builder.build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableMapTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableMapTypeAdapter.kt
new file mode 100644
index 00000000..58bc816d
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableMapTypeAdapter.kt
@@ -0,0 +1,35 @@
+package ru.dbotthepony.kstarbound.io.json.factory
+
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
+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
+
+class ImmutableMapTypeAdapter<V>(val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<String, V>>() {
+	override fun write(out: JsonWriter, value: ImmutableMap<String, V>) {
+		out.beginObject()
+
+		for ((k, v) in value) {
+			out.name(k)
+			elementAdapter.write(out, v)
+		}
+
+		out.endObject()
+	}
+
+	override fun read(reader: JsonReader): ImmutableMap<String, V> {
+		reader.beginObject()
+
+		val builder = ImmutableMap.Builder<String, V>()
+
+		while (reader.peek() != JsonToken.END_OBJECT) {
+			builder.put(reader.nextName(), elementAdapter.read(reader) ?: throw JsonSyntaxException("Nulls are not allowed, near ${reader.path}"))
+		}
+
+		reader.endObject()
+		return builder.build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableSetTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableSetTypeAdapter.kt
new file mode 100644
index 00000000..1cd97dfb
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/factory/ImmutableSetTypeAdapter.kt
@@ -0,0 +1,46 @@
+package ru.dbotthepony.kstarbound.io.json.factory
+
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableSet
+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
+
+class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableSet<E>>() {
+	override fun write(out: JsonWriter, value: ImmutableSet<E>) {
+		if (value.size == 1) {
+			elementAdapter.write(out, value.first())
+			return
+		}
+
+		out.beginArray()
+
+		for (v in value) {
+			elementAdapter.write(out, v)
+		}
+
+		out.endArray()
+	}
+
+	override fun read(reader: JsonReader): ImmutableSet<E> {
+		if (reader.peek() != JsonToken.BEGIN_ARRAY) {
+			// не массив, возможно упрощение структуры "a": [value] -> "a": value
+			return ImmutableSet.of(elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls"))
+		}
+
+		reader.beginArray()
+
+		val builder = ImmutableSet.Builder<E>()
+
+		while (reader.peek() != JsonToken.END_ARRAY) {
+			val readObject = elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls")
+			builder.add(readObject)
+		}
+
+		reader.endArray()
+
+		return builder.build()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/util/LazyTypeProvider.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/util/LazyTypeProvider.kt
index 53aab928..1b79210c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/util/LazyTypeProvider.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/util/LazyTypeProvider.kt
@@ -5,8 +5,8 @@ import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
 import ru.dbotthepony.kstarbound.Starbound
 
-class LazyTypeProvider<T : Any?>(private val bound: Class<T>) : TypeAdapter<T>() {
-	private val resolved by lazy { Starbound.getTypeAdapter(bound) }
+class LazyTypeProvider<T : Any?>(private val bound: Class<T>, private val resolver: (Class<T>) -> TypeAdapter<T> = Starbound::getTypeAdapter) : TypeAdapter<T>() {
+	private val resolved by lazy { resolver(bound) }
 
 	override fun write(out: JsonWriter, value: T) {
 		resolved.write(out, value)