summaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/utils/json
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/utils/json')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/BaseGsonBuilder.kt37
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/FeatureTogglesByDefaultAdapter.kt89
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/ItemStackTypeAdapter.kt29
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/JsonUtils.kt37
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/NBTTypeAdapter.kt24
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/SimpleStringTypeAdapter.kt29
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/SkippingTypeAdapterFactory.kt32
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/json/SkyHanniTypeAdapters.kt79
8 files changed, 356 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/BaseGsonBuilder.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/BaseGsonBuilder.kt
new file mode 100644
index 000000000..d73edc0b0
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/BaseGsonBuilder.kt
@@ -0,0 +1,37 @@
+package at.hannibal2.skyhanni.utils.json
+
+import at.hannibal2.skyhanni.data.IslandType
+import at.hannibal2.skyhanni.features.fishing.trophy.TrophyRarity
+import at.hannibal2.skyhanni.utils.KotlinTypeAdapterFactory
+import at.hannibal2.skyhanni.utils.LorenzRarity
+import at.hannibal2.skyhanni.utils.LorenzVec
+import at.hannibal2.skyhanni.utils.NEUInternalName
+import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import at.hannibal2.skyhanni.utils.tracker.SkyHanniTracker
+import com.google.gson.GsonBuilder
+import io.github.notenoughupdates.moulconfig.observer.PropertyTypeAdapterFactory
+import net.minecraft.item.ItemStack
+import java.util.UUID
+
+object BaseGsonBuilder {
+ fun gson(): GsonBuilder = GsonBuilder().setPrettyPrinting()
+ .excludeFieldsWithoutExposeAnnotation()
+ .serializeSpecialFloatingPointValues()
+ .registerTypeAdapterFactory(PropertyTypeAdapterFactory())
+ .registerTypeAdapterFactory(KotlinTypeAdapterFactory())
+ .registerTypeAdapter(UUID::class.java, SkyHanniTypeAdapters.UUID.nullSafe())
+ .registerTypeAdapter(LorenzVec::class.java, SkyHanniTypeAdapters.VEC_STRING.nullSafe())
+ .registerTypeAdapter(TrophyRarity::class.java, SkyHanniTypeAdapters.TROPHY_RARITY.nullSafe())
+ .registerTypeAdapter(ItemStack::class.java, SkyHanniTypeAdapters.NEU_ITEMSTACK.nullSafe())
+ .registerTypeAdapter(NEUInternalName::class.java, SkyHanniTypeAdapters.INTERNAL_NAME.nullSafe())
+ .registerTypeAdapter(LorenzRarity::class.java, SkyHanniTypeAdapters.RARITY.nullSafe())
+ .registerTypeAdapter(IslandType::class.java, SkyHanniTypeAdapters.ISLAND_TYPE.nullSafe())
+ .registerTypeAdapter(
+ SkyHanniTracker.DefaultDisplayMode::class.java,
+ SkyHanniTypeAdapters.TRACKER_DISPLAY_MODE.nullSafe()
+ )
+ .registerTypeAdapter(SimpleTimeMark::class.java, SkyHanniTypeAdapters.TIME_MARK.nullSafe())
+ .enableComplexMapKeySerialization()
+
+ fun lenientGson(): GsonBuilder = gson().registerTypeAdapterFactory(SkippingTypeAdapterFactory)
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/FeatureTogglesByDefaultAdapter.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/FeatureTogglesByDefaultAdapter.kt
new file mode 100644
index 000000000..bcc9c2edf
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/FeatureTogglesByDefaultAdapter.kt
@@ -0,0 +1,89 @@
+package at.hannibal2.skyhanni.utils.json
+
+import at.hannibal2.skyhanni.config.FeatureToggle
+import at.hannibal2.skyhanni.utils.ReflectionUtils.getDeclaredFieldOrNull
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import com.google.gson.TypeAdapter
+import com.google.gson.TypeAdapterFactory
+import com.google.gson.internal.bind.JsonTreeReader
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonToken
+import com.google.gson.stream.JsonWriter
+import java.lang.reflect.Field
+import java.lang.reflect.Type
+import com.google.gson.internal.`$Gson$Types` as InternalGsonTypes
+
+object FeatureTogglesByDefaultAdapter : TypeAdapterFactory {
+ fun <T> getType(typeToken: TypeToken<T>, field: Field): Type {
+ return InternalGsonTypes.resolve(typeToken.type, typeToken.rawType, field.genericType)
+ }
+
+ class Adapter<T>(
+ val originalWrite: TypeAdapter<T>,
+ val clazz: Class<T>,
+ val gson: Gson,
+ val type: TypeToken<T>,
+ ) : TypeAdapter<T>() {
+ override fun write(out: JsonWriter, value: T) {
+ // Delegate the original config write, since that one is unchanged
+ originalWrite.write(out, value)
+ }
+
+ override fun read(reader: JsonReader): T {
+ reader.beginObject()
+ // Create a default initialized instance
+ val obj = clazz.newInstance()
+
+ // Overwrite the default with true (or false) for feature toggles
+ clazz.fields.forEach {
+ val featureToggle = it.getAnnotation(FeatureToggle::class.java)
+ val adapt = gson.getAdapter(TypeToken.get(getType(type, it)))
+ if (featureToggle != null)
+ it.set(obj, adapt.read(JsonTreeReader(JsonPrimitive(featureToggle.trueIsEnabled))))
+ if (adapt is Adapter) {
+ it.set(obj, adapt.read(JsonTreeReader(JsonObject())))
+ }
+ }
+
+ // Read the actual JSON Object
+ while (reader.peek() != JsonToken.END_OBJECT) {
+ // IllegalStateException: Expected NAME but was BOOLEAN
+ if (reader.peek() != JsonToken.NAME) {
+ reader.skipValue()
+ continue
+ }
+ val name = reader.nextName()
+ val field = clazz.getDeclaredFieldOrNull(name)
+ if (field == null) {
+ println("field is in config file, but not in object file: $name")
+ continue
+ }
+ val fieldType = gson.getAdapter(TypeToken.get(getType(type, field)))
+ // Read the field data
+ val data = fieldType.read(reader)
+ // Set the field or override the feature toggle with the saved data, leaving only the unset feature toggles to deviate from their defaults
+ field.set(obj, data)
+ }
+
+ reader.endObject()
+ return obj
+ }
+ }
+
+ override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>): TypeAdapter<T>? {
+ gson!!
+ val t = type.rawType
+
+ // Check if this object has any feature toggles present
+ if (t.fields.none {
+ it.isAnnotationPresent(FeatureToggle::class.java) ||
+ gson.getAdapter(TypeToken.get(getType(type, it))) is Adapter
+ }) return null
+
+ val originalWrite = gson.getDelegateAdapter(this, type)
+ return Adapter(originalWrite, t as Class<T>, gson, type)
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/ItemStackTypeAdapter.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/ItemStackTypeAdapter.kt
new file mode 100644
index 000000000..1940b03c0
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/ItemStackTypeAdapter.kt
@@ -0,0 +1,29 @@
+package at.hannibal2.skyhanni.utils.json
+
+import com.google.gson.Gson
+import com.google.gson.TypeAdapter
+import com.google.gson.TypeAdapterFactory
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NBTTagCompound
+
+object ItemStackTypeAdapterFactory : TypeAdapterFactory {
+
+ override fun <T : Any> create(gson: Gson?, type: TypeToken<T>): TypeAdapter<T>? {
+ if (type.rawType == ItemStack::class.java) {
+ val nbtCompoundTypeAdapter = gson!!.getAdapter(NBTTagCompound::class.java)
+ return object : TypeAdapter<ItemStack>() {
+ override fun write(out: JsonWriter, value: ItemStack) {
+ nbtCompoundTypeAdapter.write(out, value.serializeNBT())
+ }
+
+ override fun read(reader: JsonReader): ItemStack {
+ return ItemStack.loadItemStackFromNBT(nbtCompoundTypeAdapter.read(reader))
+ }
+ } as TypeAdapter<T>
+ }
+ return null
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/JsonUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/JsonUtils.kt
new file mode 100644
index 000000000..750bd96aa
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/JsonUtils.kt
@@ -0,0 +1,37 @@
+package at.hannibal2.skyhanni.utils.json
+
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import java.io.Reader
+import kotlin.reflect.jvm.javaType
+import kotlin.reflect.typeOf
+
+inline fun <reified T : Any> Gson.fromJson(string: String): T = this.fromJson(string, typeOf<T>().javaType)
+
+inline fun <reified T : Any> Gson.fromJson(jsonElement: JsonElement): T =
+ this.fromJson(jsonElement, typeOf<T>().javaType)
+
+inline fun <reified T : Any> Gson.fromJson(reader: Reader): T = this.fromJson(reader, typeOf<T>().javaType)
+
+/**
+ * Straight forward deep copy. This is included in gson as well, but different versions have it exposed privately instead of publicly,
+ * so this reimplementation is here as an always public alternative.
+ */
+fun JsonElement.shDeepCopy(): JsonElement {
+ return when (this) {
+ is JsonObject -> JsonObject().also {
+ for (entry in this.entrySet())
+ it.add(entry.key, entry.value.shDeepCopy())
+ }
+
+ is JsonArray -> JsonArray().also {
+ for (entry in this) {
+ it.add(entry.shDeepCopy())
+ }
+ }
+
+ else -> this
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/NBTTypeAdapter.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/NBTTypeAdapter.kt
new file mode 100644
index 000000000..e2e782636
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/NBTTypeAdapter.kt
@@ -0,0 +1,24 @@
+package at.hannibal2.skyhanni.utils.json
+
+import com.google.gson.TypeAdapter
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+import net.minecraft.nbt.CompressedStreamTools
+import net.minecraft.nbt.NBTTagCompound
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.util.Base64
+
+object NBTTypeAdapter : TypeAdapter<NBTTagCompound>() {
+
+ override fun write(out: JsonWriter, value: NBTTagCompound) {
+ val baos = ByteArrayOutputStream()
+ CompressedStreamTools.writeCompressed(value, baos)
+ out.value(Base64.getEncoder().encode(baos.toByteArray()).decodeToString())
+ }
+
+ override fun read(reader: JsonReader): NBTTagCompound {
+ val bais = ByteArrayInputStream(Base64.getDecoder().decode(reader.nextString()))
+ return CompressedStreamTools.readCompressed(bais)
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/SimpleStringTypeAdapter.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/SimpleStringTypeAdapter.kt
new file mode 100644
index 000000000..21f601932
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/SimpleStringTypeAdapter.kt
@@ -0,0 +1,29 @@
+package at.hannibal2.skyhanni.utils.json
+
+import com.google.gson.TypeAdapter
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+
+class SimpleStringTypeAdapter<T>(
+ val serializer: T.() -> String,
+ val deserializer: String.() -> T
+) : TypeAdapter<T>() {
+
+ override fun write(writer: JsonWriter, value: T) {
+ writer.value(serializer(value))
+ }
+
+ override fun read(reader: JsonReader): T {
+ return deserializer(reader.nextString())
+ }
+
+ companion object {
+
+ inline fun <reified T : Enum<T>> forEnum(): SimpleStringTypeAdapter<T> {
+ return SimpleStringTypeAdapter(
+ { name },
+ { enumValueOf(this.replace(" ", "_").uppercase()) }
+ )
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/SkippingTypeAdapterFactory.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/SkippingTypeAdapterFactory.kt
new file mode 100644
index 000000000..a926976ca
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/SkippingTypeAdapterFactory.kt
@@ -0,0 +1,32 @@
+package at.hannibal2.skyhanni.utils.json
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import com.google.gson.Gson
+import com.google.gson.TypeAdapter
+import com.google.gson.TypeAdapterFactory
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+
+object SkippingTypeAdapterFactory : TypeAdapterFactory {
+
+ override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
+ return SafeTypeAdapter(gson.getDelegateAdapter(this, type))
+ }
+
+ private class SafeTypeAdapter<T>(val parent: TypeAdapter<T>) : TypeAdapter<T>() {
+ override fun write(writer: JsonWriter, value: T) {
+ parent.write(writer, value)
+ }
+
+ override fun read(reader: JsonReader): T? {
+ return try {
+ parent.read(reader)
+ } catch (e: Exception) {
+ SkyHanniMod.logger.warn("Failed to read value from JSON, skipping", e)
+ reader.skipValue()
+ null
+ }
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/json/SkyHanniTypeAdapters.kt b/src/main/java/at/hannibal2/skyhanni/utils/json/SkyHanniTypeAdapters.kt
new file mode 100644
index 000000000..dc50eef74
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/json/SkyHanniTypeAdapters.kt
@@ -0,0 +1,79 @@
+package at.hannibal2.skyhanni.utils.json
+
+import at.hannibal2.skyhanni.data.IslandType
+import at.hannibal2.skyhanni.features.fishing.trophy.TrophyRarity
+import at.hannibal2.skyhanni.features.garden.CropType
+import at.hannibal2.skyhanni.features.garden.pests.PestType
+import at.hannibal2.skyhanni.utils.LorenzRarity
+import at.hannibal2.skyhanni.utils.LorenzVec
+import at.hannibal2.skyhanni.utils.NEUInternalName
+import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName
+import at.hannibal2.skyhanni.utils.NEUItems
+import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.asTimeMark
+import at.hannibal2.skyhanni.utils.tracker.SkyHanniTracker
+import com.google.gson.GsonBuilder
+import com.google.gson.TypeAdapter
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonWriter
+import net.minecraft.item.ItemStack
+import java.util.UUID
+
+object SkyHanniTypeAdapters {
+ val NEU_ITEMSTACK: TypeAdapter<ItemStack> = SimpleStringTypeAdapter(NEUItems::saveNBTData, NEUItems::loadNBTData)
+
+ val UUID: TypeAdapter<UUID> = SimpleStringTypeAdapter(
+ { this.toString() },
+ { java.util.UUID.fromString(this) }
+ )
+
+ val INTERNAL_NAME: TypeAdapter<NEUInternalName> = SimpleStringTypeAdapter(
+ { this.asString() },
+ { this.asInternalName() }
+ )
+
+ val VEC_STRING: TypeAdapter<LorenzVec> = SimpleStringTypeAdapter(
+ { "$x:$y:$z" },
+ { LorenzVec.decodeFromString(this) }
+ )
+
+ val TROPHY_RARITY: TypeAdapter<TrophyRarity> = SimpleStringTypeAdapter(
+ { name },
+ { TrophyRarity.getByName(this) ?: error("Could not parse TrophyRarity from '$this'") }
+ )
+
+ val TIME_MARK: TypeAdapter<SimpleTimeMark> = object : TypeAdapter<SimpleTimeMark>() {
+ override fun write(out: JsonWriter, value: SimpleTimeMark) {
+ out.value(value.toMillis())
+ }
+
+ override fun read(reader: JsonReader): SimpleTimeMark {
+ return reader.nextString().toLong().asTimeMark()
+ }
+ }
+
+ val CROP_TYPE: TypeAdapter<CropType> = SimpleStringTypeAdapter(
+ { name },
+ { CropType.getByName(this) }
+ )
+
+ val PEST_TYPE: TypeAdapter<PestType> = SimpleStringTypeAdapter(
+ { name },
+ { PestType.getByName(this) }
+ )
+
+ val TRACKER_DISPLAY_MODE = SimpleStringTypeAdapter.forEnum<SkyHanniTracker.DefaultDisplayMode>()
+ val ISLAND_TYPE = SimpleStringTypeAdapter.forEnum<IslandType>()
+ val RARITY = SimpleStringTypeAdapter.forEnum<LorenzRarity>()
+
+ inline fun <reified T> GsonBuilder.registerTypeAdapter(
+ crossinline write: (JsonWriter, T) -> Unit,
+ crossinline read: (JsonReader) -> T,
+ ): GsonBuilder {
+ this.registerTypeAdapter(T::class.java, object : TypeAdapter<T>() {
+ override fun write(out: JsonWriter, value: T) = write(out, value)
+ override fun read(reader: JsonReader) = read(reader)
+ }.nullSafe())
+ return this
+ }
+}