diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchDataProvider.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchDataProvider.kt index ee9b57c9a..98d175bfc 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchDataProvider.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchDataProvider.kt @@ -1,11 +1,14 @@ package ru.dbotthepony.mc.otm.android +import com.google.gson.JsonObject import it.unimi.dsi.fastutil.objects.ObjectArraySet import net.minecraft.data.CachedOutput import net.minecraft.data.DataProvider import net.minecraft.data.PackOutput import net.minecraft.resources.ResourceLocation import net.minecraftforge.data.event.GatherDataEvent +import ru.dbotthepony.mc.otm.core.toJson +import ru.dbotthepony.mc.otm.core.toJsonStrict import ru.dbotthepony.mc.otm.core.util.WriteOnce import java.util.Collections import java.util.LinkedList @@ -56,7 +59,7 @@ open class AndroidResearchDataProvider() : DataProvider { addEverything { if (set.add(it.id)) { - futures.add(DataProvider.saveStable(output, it.toJson(), pathProvider.json(it.id))) + futures.add(DataProvider.saveStable(output, AndroidResearchType.CODEC.toJsonStrict(it).also { (it as JsonObject).remove("id") }, pathProvider.json(it.id))) AndroidResearchManager.put(it) added.add(it) } else { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt index 363d6e12d..7f1672933 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableMap import com.google.gson.GsonBuilder import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException import net.minecraft.client.server.IntegratedServer import net.minecraft.network.FriendlyByteBuf import net.minecraft.resources.ResourceLocation @@ -22,6 +23,13 @@ import ru.dbotthepony.mc.otm.NULLABLE_MINECRAFT_SERVER import ru.dbotthepony.mc.otm.SERVER_IS_LIVE import ru.dbotthepony.mc.otm.capability.matteryPlayer import ru.dbotthepony.mc.otm.client.minecraft +import ru.dbotthepony.mc.otm.core.fromJsonStrict +import ru.dbotthepony.mc.otm.core.fromNetwork +import ru.dbotthepony.mc.otm.core.set +import ru.dbotthepony.mc.otm.core.toJsonStrict +import ru.dbotthepony.mc.otm.core.toNetwork +import ru.dbotthepony.mc.otm.core.util.readJson +import ru.dbotthepony.mc.otm.matter.MatterManager import ru.dbotthepony.mc.otm.network.MatteryPacket import ru.dbotthepony.mc.otm.network.RegistryNetworkChannel import ru.dbotthepony.mc.otm.network.enqueueWork @@ -67,7 +75,13 @@ object AndroidResearchManager : SimpleJsonResourceReloadListener(GsonBuilder().s continue } - builder.put(k, AndroidResearchType.fromJson(v, k)) + v["id"] = k.toString() + + try { + builder.put(k, AndroidResearchType.CODEC.fromJsonStrict(v)) + } catch(err: RuntimeException) { + throw JsonSyntaxException("Caught an exception while decoding android research $k", err) + } } researchMap = builder.build() @@ -101,7 +115,8 @@ object AndroidResearchManager : SimpleJsonResourceReloadListener(GsonBuilder().s class SyncPacket(val collection: Collection) : MatteryPacket { override fun write(buff: FriendlyByteBuf) { - buff.writeCollection(collection) { a, b -> b.toNetwork(a) } + buff.writeCollection(collection) { a, b -> AndroidResearchType.CODEC.toNetwork(a, b) } + LOGGER.debug("Constructed android research registry packet, ${buff.writerIndex()} bytes in size") } override fun play(context: Supplier) { @@ -130,6 +145,7 @@ object AndroidResearchManager : SimpleJsonResourceReloadListener(GsonBuilder().s } fun readSyncPacket(buff: FriendlyByteBuf): SyncPacket { - return SyncPacket(buff.readCollection({ LinkedList() }, AndroidResearchType.Companion::fromNetwork)) + LOGGER.info("Received android research registry packet, ${buff.readableBytes()} bytes in size") + return SyncPacket(buff.readCollection({ LinkedList() }, { AndroidResearchType.CODEC.fromNetwork(it) })) } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchType.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchType.kt index 2d150b697..b987b6c33 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchType.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchType.kt @@ -7,6 +7,12 @@ import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.JsonSyntaxException import com.google.gson.internal.bind.TypeAdapters +import com.mojang.datafixers.util.Either +import com.mojang.serialization.Codec +import com.mojang.serialization.JsonOps +import com.mojang.serialization.codecs.ListCodec +import com.mojang.serialization.codecs.PairCodec +import com.mojang.serialization.codecs.RecordCodecBuilder import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.chat.Component @@ -33,11 +39,18 @@ import ru.dbotthepony.mc.otm.core.fromJsonStrict import ru.dbotthepony.mc.otm.core.toJsonStrict import ru.dbotthepony.mc.otm.core.util.readJson import ru.dbotthepony.mc.otm.core.util.writeJson +import ru.dbotthepony.mc.otm.data.ComponentCodec +import ru.dbotthepony.mc.otm.data.IngredientCodec +import ru.dbotthepony.mc.otm.data.JsonElementCodec +import ru.dbotthepony.mc.otm.data.simpleCodec import ru.dbotthepony.mc.otm.isClient import java.util.LinkedList +import java.util.Optional +import java.util.function.Function import java.util.stream.Stream import kotlin.collections.ArrayList import kotlin.collections.HashSet +import kotlin.jvm.optionals.getOrNull private fun findPrerequisites( initial: Collection, @@ -97,6 +110,25 @@ class AndroidResearchType( val itemIcon: Item? = null, iconText: Component? = null, ) { + private constructor( + id: ResourceLocation, + prerequisites: Collection, + blockedBy: Collection, + + items: Collection>, + results: Collection, + + description: Collection, + + experienceLevels: Int, + customName: Optional, + + // ok + skinIcon: Optional, + itemIcon: Optional, + iconText: Optional, + ) : this(id, prerequisites, blockedBy, items.stream().map { Pair(it.first, it.second) }.toList(), results, description, experienceLevels, customName.getOrNull(), skinIcon.getOrNull(), itemIcon.getOrNull(), iconText.getOrNull()) + private val iconTextValue = iconText?.copy() val iconText get() = iconTextValue?.copy() @@ -110,38 +142,21 @@ class AndroidResearchType( data class Reference( val id: ResourceLocation, - val isRigid: Boolean + val optional: Boolean = false ) { - fun toJson(): JsonObject { - return JsonObject().also { - it["id"] = JsonPrimitive(id.toString()) - it["is_rigid"] = JsonPrimitive(isRigid) - } - } - - fun toNetwork(buff: FriendlyByteBuf) { - buff.writeUtf(id.toString()) - buff.writeBoolean(isRigid) - } - companion object { - fun fromNetwork(buff: FriendlyByteBuf): Reference { - return Reference( - ResourceLocation(buff.readUtf()), - buff.readBoolean() - ) - } - - fun fromJson(value: JsonElement): Reference { - if (value is JsonPrimitive) { - return Reference(ResourceLocation(value.asString), true) - } else if (value is JsonObject) { - return Reference( - ResourceLocation((value["id"] as? JsonPrimitive ?: throw JsonSyntaxException("Invalid `id` value")).asString), - (value["is_rigid"] as JsonPrimitive?)?.asBoolean ?: true) - } else { - throw JsonSyntaxException("Unknown element type ${value::class.qualifiedName}") - } + val CODEC: Codec by lazy { + Codec + .either(ResourceLocation.CODEC, RecordCodecBuilder.create { + it.group( + ResourceLocation.CODEC.fieldOf("id").forGetter(Reference::id), + Codec.BOOL.optionalFieldOf("optional", false).forGetter(Reference::optional) + ).apply(it, ::Reference) + }) + .xmap( + { c -> c.map(::Reference, Function.identity()) }, + { c -> if (c.optional) Either.right(c) else Either.left(c.id) } + ) } } } @@ -174,11 +189,11 @@ class AndroidResearchType( val blockedBy: List = ImmutableList.copyOf(blockedBy) val resolvedPrerequisites: List by lazy { - ImmutableList.copyOf(this.prerequisites.mapNotNull { AndroidResearchManager[it.id].also { e -> if (e == null && it.isRigid) throw NoSuchElementException("Unable to find research ${it.id}") } }) + ImmutableList.copyOf(this.prerequisites.mapNotNull { AndroidResearchManager[it.id].also { e -> if (e == null && it.optional) throw NoSuchElementException("Unable to find research ${it.id}") } }) } val resolvedBlockedBy: List by lazy { - ImmutableList.copyOf(this.blockedBy.mapNotNull { AndroidResearchManager[it.id].also { e -> if (e == null && it.isRigid) throw NoSuchElementException("Unable to find research ${it.id}") } }) + ImmutableList.copyOf(this.blockedBy.mapNotNull { AndroidResearchManager[it.id].also { e -> if (e == null && it.optional) throw NoSuchElementException("Unable to find research ${it.id}") } }) } private val definedItems: List> = ImmutableList.copyOf(items) @@ -380,141 +395,35 @@ class AndroidResearchType( return customName?.copy() ?: MutableComponent.create(displayContents) } - fun toJson(): JsonElement { - return JsonObject().also { - it["prerequisites"] = JsonArray().also { for (value in prerequisites) it.add(value.toJson()) } - it["blocked_by"] = JsonArray().also { for (value in blockedBy) it.add(value.toJson()) } - it["required_items"] = JsonArray().also { for (item in definedItems) it.add(JsonObject().also { it["count"] = JsonPrimitive(item.second); it["ingredient"] = item.first.toJson() }) } - it["results"] = JsonArray().also { for (result in results) it.add(AndroidResearchResult.CODEC.toJsonStrict(result)) } - it["description"] = JsonArray().also { for (line in description) it.add(AndroidResearchDescription.CODEC.toJsonStrict(line)) } - it["experience"] = JsonPrimitive(experienceLevels) - - if (skinIcon != null) { - it["skin_icon"] = skinIcon - } - - if (itemIcon != null) { - it["item_icon"] = JsonPrimitive(itemIcon.registryName!!.toString()) - } - - if (iconTextValue != null) { - it["icon_text"] = Component.Serializer.toJsonTree(iconTextValue) - } - - if (customName != null) { - it["custom_name"] = Component.Serializer.toJsonTree(customName) - } - } - } - fun validate() { resolvedBlockedBy resolvedPrerequisites } - fun toNetwork(buff: FriendlyByteBuf) { - buff.writeUtf(id.toString()) - buff.writeCollection(prerequisites) { a, b -> b.toNetwork(a) } - buff.writeCollection(blockedBy) { a, b -> b.toNetwork(a) } - buff.writeCollection(definedItems) { a, b -> b.first.toNetwork(a); a.writeVarInt(b.second) } - buff.writeCollection(results) { a, b -> a.writeJson(AndroidResearchResult.CODEC.toJsonStrict(b)) } - buff.writeCollection(description) { a, b -> a.writeJson(AndroidResearchDescription.CODEC.toJsonStrict(b)) } - buff.writeVarInt(experienceLevels) - - buff.writeBoolean(customName != null) - if (customName != null) buff.writeComponent(customName) - - buff.writeBoolean(iconTextValue != null) - if (iconTextValue != null) buff.writeComponent(iconTextValue) - - buff.writeBoolean(skinIcon != null) - if (skinIcon != null) buff.writeUtf(skinIcon.toString()) - - buff.writeBoolean(itemIcon != null) - if (itemIcon != null) buff.writeUtf(itemIcon.registryName!!.toString()) - } - companion object { - fun fromNetwork(buff: FriendlyByteBuf): AndroidResearchType { - val id = ResourceLocation(buff.readUtf()) - val prerequisites = buff.readCollection({ LinkedList() }, Reference::fromNetwork) - val blockedBy = buff.readCollection({ LinkedList() }, Reference::fromNetwork) - val items = buff.readCollection({ LinkedList() }, { Ingredient.fromNetwork(it) to it.readVarInt() }) - val results = buff.readCollection({ LinkedList() }, { AndroidResearchResult.CODEC.fromJsonStrict(it.readJson()) }) - val description = buff.readCollection({ LinkedList() }, { AndroidResearchDescription.CODEC.fromJsonStrict(it.readJson()) }) - val experienceLevels = buff.readVarInt() - - val customName = if (buff.readBoolean()) { - buff.readComponent() - } else { - null + val CODEC: Codec by lazy { + RecordCodecBuilder.create { + it.group( + ResourceLocation.CODEC.fieldOf("id").forGetter(AndroidResearchType::id), + ListCodec(Reference.CODEC).fieldOf("prerequisites").forGetter(AndroidResearchType::prerequisites), + ListCodec(Reference.CODEC).fieldOf("blockedBy").forGetter(AndroidResearchType::blockedBy), + ListCodec( + RecordCodecBuilder.create> { + it.group( + IngredientCodec.fieldOf("item").forGetter { it.first }, + Codec.intRange(1, Int.MAX_VALUE).optionalFieldOf("count", 1).forGetter { it.second } + ).apply(it, ::Pair) + } + ).fieldOf("items").forGetter { it.definedItems }, + ListCodec(AndroidResearchResult.CODEC).fieldOf("results").forGetter(AndroidResearchType::results), + ListCodec(AndroidResearchDescription.CODEC).fieldOf("description").forGetter(AndroidResearchType::description), + Codec.intRange(0, Int.MAX_VALUE).fieldOf("experienceLevels").forGetter(AndroidResearchType::experienceLevels), + ComponentCodec.optionalFieldOf("customName").forGetter { Optional.ofNullable(it.customName) }, + JsonElementCodec.xmap({ it as? JsonObject ?: throw JsonSyntaxException("Not a json object: $it") }, { it }).optionalFieldOf("skinIcon").forGetter { Optional.ofNullable(it.skinIcon) }, + ForgeRegistries.ITEMS.codec.optionalFieldOf("itemIcon").forGetter { Optional.ofNullable(it.itemIcon) }, + ComponentCodec.optionalFieldOf("iconTextValue").forGetter { Optional.ofNullable(it.iconTextValue) }, + ).apply(it, ::AndroidResearchType) } - - val iconTextValue = if (buff.readBoolean()) { - buff.readComponent() - } else { - null - } - - val skinIcon = if (buff.readBoolean()) { - TypeAdapters.JSON_ELEMENT.fromJson(buff.readUtf()) as JsonObject - } else { - null - } - - val itemIcon = if (buff.readBoolean()) { - ForgeRegistries.ITEMS.getValue(ResourceLocation(buff.readUtf()))?.let { if (it == Items.AIR) null else it } - } else { - null - } - - return AndroidResearchType( - id = id, - prerequisites = prerequisites, - blockedBy = blockedBy, - items = items, - results = results, - description = description, - experienceLevels = experienceLevels, - customName = customName, - iconText = iconTextValue, - skinIcon = skinIcon, - itemIcon = itemIcon, - ) - } - - fun fromJson(value: JsonElement, id: ResourceLocation): AndroidResearchType { - if (value !is JsonObject) { - throw JsonSyntaxException("Android research type must be of Json Object") - } - - val prerequisites = value["prerequisites"] as JsonArray? ?: JsonArray() - val blocked_by = value["blocked_by"] as JsonArray? ?: JsonArray() - val items = value["required_items"] as JsonArray? ?: JsonArray() - val features = value["feature_result"] as JsonArray? ?: JsonArray() - val description = value["description"] as JsonArray? ?: JsonArray() - val experience = value["experience"]?.asInt ?: 0 - val customName = value["custom_name"]?.let(Component.Serializer::fromJson) - val iconText = value["icon_text"]?.let(Component.Serializer::fromJson) - val skinIcon = value["skin_icon"] as JsonObject? - val itemIcon = value["item_icon"]?.let { ForgeRegistries.ITEMS.getValue(ResourceLocation(it.asString)).let { if (it == Items.AIR) null else it } } - - return AndroidResearchType( - id = id, - prerequisites = prerequisites.stream().map { Reference.fromJson(it) }.toList(), - blockedBy = blocked_by.stream().map { Reference.fromJson(it) }.toList(), - results = features.stream().map { AndroidResearchResult.CODEC.fromJsonStrict(it) }.toList(), - items = items.stream() - .map { it as? JsonObject ?: throw JsonSyntaxException("One of items is not an JsonObject") } - .map { Ingredient.fromJson(it["ingredient"] ?: throw JsonSyntaxException("Missing ingredient key")) to (it["count"]?.asInt ?: throw JsonSyntaxException("Missing count key")) } - .toList(), - description = description.stream().map { AndroidResearchDescription.CODEC.fromJsonStrict(it) }.toList(), - experienceLevels = experience, - customName = customName, - iconText = iconText, - skinIcon = skinIcon, - itemIcon = itemIcon, - ) } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/UnOverengineering.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/UnOverengineering.kt index 354dd0ee5..f643eb1a9 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/UnOverengineering.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/UnOverengineering.kt @@ -26,6 +26,8 @@ import net.minecraft.world.level.material.Fluid import net.minecraft.world.phys.Vec3 import net.minecraftforge.registries.ForgeRegistries import net.minecraftforge.registries.IForgeRegistry +import ru.dbotthepony.mc.otm.core.util.readJson +import ru.dbotthepony.mc.otm.core.util.writeJson // because doing it inline is ugly fun Codec.fromJson(value: JsonElement): V? { @@ -60,6 +62,14 @@ fun Codec.toNbtStrict(value: V, prefix: Tag = NbtOps.INSTANCE.empty return encode(value, NbtOps.INSTANCE, prefix).get().map({ it }, { throw RuntimeException("Error encoding element: ${it.message()}") }) } +fun Codec.toNetwork(buff: FriendlyByteBuf, value: V) { + buff.writeJson(toJsonStrict(value)) +} + +fun Codec.fromNetwork(buff: FriendlyByteBuf): V { + return fromJsonStrict(buff.readJson()) +} + // 1.19 being 1.19 fun TranslatableComponent(key: String, vararg values: Any): MutableComponent = MutableComponent.create(TranslatableContents(key, null, values)) fun TextComponent(value: String): MutableComponent = MutableComponent.create(LiteralContents(value)) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientCodec.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientCodec.kt new file mode 100644 index 000000000..27c18ade8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientCodec.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.mc.otm.data + +import com.google.gson.JsonSyntaxException +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.DynamicOps +import com.mojang.serialization.JsonOps +import net.minecraft.world.item.crafting.Ingredient + +object IngredientCodec : Codec { + override fun encode(input: Ingredient, ops: DynamicOps, prefix: T): DataResult { + return DataResult.success(JsonOps.INSTANCE.convertTo(ops, input.toJson())) + } + + override fun decode(ops: DynamicOps, input: T): DataResult> { + try { + return DataResult.success(Pair(Ingredient.fromJson(ops.convertTo(JsonOps.INSTANCE, input)), ops.empty())) + } catch (err: JsonSyntaxException) { + return DataResult.error { "Error decoding Ingredient: ${err.message}" } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/JsonElementCodec.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/JsonElementCodec.kt new file mode 100644 index 000000000..783222a0e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/JsonElementCodec.kt @@ -0,0 +1,18 @@ +package ru.dbotthepony.mc.otm.data + +import com.google.gson.JsonElement +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.DynamicOps +import com.mojang.serialization.JsonOps + +object JsonElementCodec : Codec { + override fun encode(input: JsonElement, ops: DynamicOps, prefix: T): DataResult { + return DataResult.success(JsonOps.INSTANCE.convertTo(ops, input)) + } + + override fun decode(ops: DynamicOps, input: T): DataResult> { + return DataResult.success(Pair(ops.convertTo(JsonOps.INSTANCE, input), ops.empty())) + } +}