From 5c8ddc505d53d0e0557857d86119b0381b371778 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 23 Sep 2022 21:09:28 +0700 Subject: [PATCH] Serializable curried functions This marks final step in migration of research to JSON Fixes #98 --- .../mc/otm/datagen/ResearchData.kt | 11 +- .../mc/otm/OverdriveThatMatters.java | 3 + .../mc/otm/android/AndroidResearch.kt | 8 + .../mc/otm/android/AndroidResearchManager.kt | 10 + .../mc/otm/android/AndroidResearchType.kt | 63 ++++- .../android/feature/NanobotsArmorFeature.kt | 42 +++- .../mc/otm/compat/jei/JEIPlugin.kt | 4 +- .../kotlin/ru/dbotthepony/mc/otm/core/Ext.kt | 2 +- .../mc/otm/core/FriendlyStreams.kt | 84 +++++++ .../mc/otm/data/SerializedFunctionRegistry.kt | 215 ++++++++++++++++++ .../mc/otm/network/FieldSynchronizer.kt | 2 + 11 files changed, 428 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/data/SerializedFunctionRegistry.kt diff --git a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/ResearchData.kt b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/ResearchData.kt index 8fc6176d5..8fa910dd1 100644 --- a/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/ResearchData.kt +++ b/src/data/kotlin/ru/dbotthepony/mc/otm/datagen/ResearchData.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.mc.otm.datagen import ru.dbotthepony.mc.otm.OverdriveThatMatters import ru.dbotthepony.mc.otm.android.AndroidResearchType +import ru.dbotthepony.mc.otm.android.feature.NanobotsArmorFeature import ru.dbotthepony.mc.otm.client.render.ResearchIcons import ru.dbotthepony.mc.otm.core.TextComponent import ru.dbotthepony.mc.otm.core.TranslatableComponent @@ -176,7 +177,10 @@ fun addResearchData(serializer: Consumer, lang: MatteryLang (i + 1) * 6 ) ) - .addFeatureResult(AndroidFeatures.NANOBOTS_ARMOR, 0) // TODO + .addFeatureResult(AndroidFeatures.NANOBOTS_ARMOR, 0, + transformersUp = listOf(NanobotsArmorFeature.STRENGTH_TRANSFORMER_UP.bind(level)), + transformersDown = listOf(NanobotsArmorFeature.STRENGTH_TRANSFORMER_DOWN.bind(level)), + ) .build() }) @@ -199,7 +203,10 @@ fun addResearchData(serializer: Consumer, lang: MatteryLang (i + 1) * 6 ) ) - .addFeatureResult(AndroidFeatures.NANOBOTS_ARMOR, 0) // TODO + .addFeatureResult(AndroidFeatures.NANOBOTS_ARMOR, 0, + transformersUp = listOf(NanobotsArmorFeature.SPEED_TRANSFORMER_UP.bind(level)), + transformersDown = listOf(NanobotsArmorFeature.SPEED_TRANSFORMER_DOWN.bind(level)), + ) .build() }) } diff --git a/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java b/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java index 6c9728c20..c9ef03c05 100644 --- a/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java +++ b/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java @@ -17,6 +17,7 @@ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import ru.dbotthepony.mc.otm.android.AndroidResearchManager; +import ru.dbotthepony.mc.otm.android.feature.NanobotsArmorFeature; import ru.dbotthepony.mc.otm.block.entity.blackhole.ExplosionQueue; import ru.dbotthepony.mc.otm.capability.MatteryCapability; import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability; @@ -99,6 +100,8 @@ public final class OverdriveThatMatters { ClientConfig.INSTANCE.register(); ServerConfig.INSTANCE.register(); + + NanobotsArmorFeature.Companion.register(); } private void setup(final FMLCommonSetupEvent event) { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt index a77ea12c9..bf8a825ea 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearch.kt @@ -63,6 +63,10 @@ class AndroidResearch(val type: AndroidResearchType, val capability: MatteryPlay capability.removeFeature(feature.feature) } else { get.level = level.level + + for (transformer in type.features.first { it.id == feature.feature.registryName }.transformersDown) { + transformer.accept(this to get) + } } } } @@ -88,6 +92,10 @@ class AndroidResearch(val type: AndroidResearchType, val capability: MatteryPlay get.level = feature.level } } + + for (transformer in type.features.first { it.id == feature.feature.registryName }.transformersUp) { + transformer.accept(this to get) + } } } catch(err: Throwable) { oldResearchLevel.clear() 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 522781bca..a5dcbcd08 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchManager.kt @@ -17,9 +17,11 @@ import net.minecraftforge.network.PacketDistributor import org.apache.logging.log4j.LogManager import ru.dbotthepony.mc.otm.MINECRAFT_SERVER import ru.dbotthepony.mc.otm.NULLABLE_MINECRAFT_SERVER +import ru.dbotthepony.mc.otm.OverdriveThatMatters 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.data.SerializedFunctionRegistry import ru.dbotthepony.mc.otm.network.MatteryPacket import ru.dbotthepony.mc.otm.network.RegistryNetworkChannel import ru.dbotthepony.mc.otm.network.enqueueWork @@ -29,6 +31,14 @@ import java.util.LinkedList import java.util.function.Supplier object AndroidResearchManager : SimpleJsonResourceReloadListener(GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(), "otm_android_research"), Iterable { + /** + * Feel free to register functions inside this thing from anywhere in your code + * (registration and querying is completely thread safe). + * + * Just make sure client and server has the same set of functions defined. + */ + val featureResultTransformers = SerializedFunctionRegistry>() + const val DIRECTORY = "otm_android_research" private val LOGGER = LogManager.getLogger() 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 c75ae8d80..ba5e04074 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchType.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/AndroidResearchType.kt @@ -24,6 +24,8 @@ import ru.dbotthepony.mc.otm.core.set import ru.dbotthepony.mc.otm.core.stream import ru.dbotthepony.mc.otm.core.toImmutableList import ru.dbotthepony.mc.otm.data.ItemStackCodec +import ru.dbotthepony.mc.otm.data.SerializedFunctionRegistry +import ru.dbotthepony.mc.otm.data.stream import ru.dbotthepony.mc.otm.registry.MRegistry import java.util.ArrayList import java.util.LinkedList @@ -134,13 +136,26 @@ class AndroidResearchType( data class FeatureReference( val id: ResourceLocation, val level: Int = 0, - val isRigid: Boolean + val isRigid: Boolean, + val transformersUp: Collection>> = listOf(), + val transformersDown: Collection>> = listOf(), ) { fun toJson(): JsonObject { return JsonObject().also { it["id"] = JsonPrimitive(id.toString()) it["level"] = JsonPrimitive(level) it["is_rigid"] = JsonPrimitive(isRigid) + it["functions_up"] = JsonArray().also { + for (transformer in transformersUp) { + it.add(transformer.toJson()) + } + } + + it["functions_down"] = JsonArray().also { + for (transformer in transformersDown) { + it.add(transformer.toJson()) + } + } } } @@ -148,6 +163,8 @@ class AndroidResearchType( buff.writeUtf(id.toString()) buff.writeVarInt(level) buff.writeBoolean(isRigid) + buff.writeCollection(transformersUp) { a, b -> b.toNetwork(a) } + buff.writeCollection(transformersDown) { a, b -> b.toNetwork(a) } } companion object { @@ -155,7 +172,9 @@ class AndroidResearchType( return FeatureReference( ResourceLocation(buff.readUtf()), buff.readVarInt(), - buff.readBoolean() + buff.readBoolean(), + buff.readCollection({ LinkedList() }, AndroidResearchManager.featureResultTransformers::fromNetwork).filterNotNull().toImmutableList(), + buff.readCollection({ LinkedList() }, AndroidResearchManager.featureResultTransformers::fromNetwork).filterNotNull().toImmutableList() ) } @@ -165,7 +184,11 @@ class AndroidResearchType( } else if (value is JsonObject) { return FeatureReference( ResourceLocation((value["id"] as? JsonPrimitive ?: throw JsonSyntaxException("Invalid `id` value")).asString), - (value["level"] as JsonPrimitive?)?.asInt ?: 0, (value["is_rigid"] as JsonPrimitive?)?.asBoolean ?: true) + (value["level"] as JsonPrimitive?)?.asInt ?: 0, + (value["is_rigid"] as JsonPrimitive?)?.asBoolean ?: true, + (value["functions_up"] as JsonArray? ?: JsonArray()).stream().map { AndroidResearchManager.featureResultTransformers.fromJson(it) }.filter { it != null }.collect(ImmutableList.toImmutableList>>()), + (value["functions_down"] as JsonArray? ?: JsonArray()).stream().map { AndroidResearchManager.featureResultTransformers.fromJson(it) }.filter { it != null }.collect(ImmutableList.toImmutableList>>()), + ) } else { throw JsonSyntaxException("Unknown element type ${value::class.qualifiedName}") } @@ -659,19 +682,41 @@ class AndroidResearchType( fun addBlocker(type: AndroidResearchType, rigid: Boolean = true) = addBlocker(type.id, rigid) @JvmOverloads - fun addFeatureResult(id: ResourceLocation, level: Int = 0, rigid: Boolean = false): Builder { - features.add(FeatureReference(id, level, rigid)) + fun addFeatureResult( + id: ResourceLocation, + level: Int = 0, + rigid: Boolean = false, + transformersUp: Collection>> = listOf(), + transformersDown: Collection>> = listOf(), + ): Builder { + features.add(FeatureReference(id, level, rigid, transformersUp, transformersDown)) return this } @JvmOverloads - fun addFeatureResult(feature: AndroidFeatureType<*>, level: Int = 0, rigid: Boolean = true): Builder { - features.add(FeatureReference(feature.registryName ?: throw NullPointerException("Feature $feature does not have registry name"), level, rigid)) + fun addFeatureResult( + feature: AndroidFeatureType<*>, + level: Int = 0, + rigid: Boolean = true, + transformersUp: Collection>> = listOf(), + transformersDown: Collection>> = listOf(), + ): Builder { + features.add(FeatureReference(feature.registryName ?: throw NullPointerException("Feature $feature does not have registry name"), level, rigid, transformersUp, transformersDown)) return this } - fun addFeatureResult(id: ResourceLocation, rigid: Boolean = false): Builder { - features.add(FeatureReference(id, 0, rigid)) + fun addFeatureResult( + id: ResourceLocation, + rigid: Boolean = false, + transformersUp: Collection>> = listOf(), + transformersDown: Collection>> = listOf(), + ): Builder { + features.add(FeatureReference(id, 0, rigid, transformersUp, transformersDown)) + return this + } + + fun addFeatureResult(ref: FeatureReference): Builder { + features.add(ref) return this } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/android/feature/NanobotsArmorFeature.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/android/feature/NanobotsArmorFeature.kt index 1ec0f5e09..250d68319 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/android/feature/NanobotsArmorFeature.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/android/feature/NanobotsArmorFeature.kt @@ -1,9 +1,12 @@ package ru.dbotthepony.mc.otm.android.feature import net.minecraft.nbt.CompoundTag +import net.minecraft.resources.ResourceLocation import net.minecraft.server.level.ServerPlayer import net.minecraftforge.event.entity.living.LivingHurtEvent +import ru.dbotthepony.mc.otm.OverdriveThatMatters import ru.dbotthepony.mc.otm.android.AndroidFeature +import ru.dbotthepony.mc.otm.android.AndroidResearchManager import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability import ru.dbotthepony.mc.otm.capability.extractEnergyInnerExact import ru.dbotthepony.mc.otm.core.ImpreciseFraction @@ -14,10 +17,10 @@ import kotlin.math.roundToInt class NanobotsArmorFeature(android: MatteryPlayerCapability) : AndroidFeature(AndroidFeatures.NANOBOTS_ARMOR, android) { var strength: Int = 0 - set(value) { field = value.coerceIn(0 .. 2) } + set(value) { field = value.coerceIn(0 .. 3) } var speed: Int = 0 - set(value) { field = value.coerceIn(0 .. 2) } + set(value) { field = value.coerceIn(0 .. 3) } private var ticksPassed = 0 private var layers = 0 @@ -86,5 +89,40 @@ class NanobotsArmorFeature(android: MatteryPlayerCapability) : AndroidFeature(An 0.3f, 0.4f, ) + + val STRENGTH_TRANSFORMER_UP = AndroidResearchManager.featureResultTransformers.register(ResourceLocation(OverdriveThatMatters.MOD_ID, "nanobots_armor_strength_up")) + { level: Int -> + if (second is NanobotsArmorFeature && (second as NanobotsArmorFeature).strength == level - 1) { + (second as NanobotsArmorFeature).strength = level + } + } + + val STRENGTH_TRANSFORMER_DOWN = AndroidResearchManager.featureResultTransformers.register(ResourceLocation(OverdriveThatMatters.MOD_ID, "nanobots_armor_strength_down")) + { level: Int -> + if (second is NanobotsArmorFeature && (second as NanobotsArmorFeature).strength == level) { + (second as NanobotsArmorFeature).strength = level - 1 + } + } + + val SPEED_TRANSFORMER_UP = AndroidResearchManager.featureResultTransformers.register(ResourceLocation(OverdriveThatMatters.MOD_ID, "nanobots_armor_speed_up")) + { level: Int -> + if (second is NanobotsArmorFeature && (second as NanobotsArmorFeature).speed == level - 1) { + (second as NanobotsArmorFeature).speed = level + } + } + + val SPEED_TRANSFORMER_DOWN = AndroidResearchManager.featureResultTransformers.register(ResourceLocation(OverdriveThatMatters.MOD_ID, "nanobots_armor_speed_down")) + { level: Int -> + if (second is NanobotsArmorFeature && (second as NanobotsArmorFeature).speed == level) { + (second as NanobotsArmorFeature).speed = level - 1 + } + } + + /** + * Dummy method just for static initializer to execute + */ + fun register() { + + } } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jei/JEIPlugin.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jei/JEIPlugin.kt index 60d54ca09..1498cbb6d 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jei/JEIPlugin.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/compat/jei/JEIPlugin.kt @@ -188,11 +188,11 @@ class JEIPlugin : IModPlugin { val combined = combine.toImmutableList() val newView = object : IRecipeSlotsView { - override fun getSlotViews(): MutableList { + override fun getSlotViews(): List { return combined } - override fun getSlotViews(role: RecipeIngredientRole): MutableList { + override fun getSlotViews(role: RecipeIngredientRole): List { return when (role) { RecipeIngredientRole.INPUT -> filteredInputs RecipeIngredientRole.OUTPUT -> outputs diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Ext.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Ext.kt index c0c69e155..089fc6136 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Ext.kt @@ -349,7 +349,7 @@ operator fun > StateHolder<*, *>.get(value: Property): T { return getValue(value) } -fun List.toImmutableList(): ImmutableList { +fun List.toImmutableList(): List { return ImmutableList.copyOf(this) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/FriendlyStreams.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/FriendlyStreams.kt index b0125637b..f2853f616 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/FriendlyStreams.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/FriendlyStreams.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.mc.otm.core +import com.google.common.collect.ImmutableList import io.netty.handler.codec.EncoderException import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtAccounter @@ -8,9 +9,24 @@ import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack import net.minecraftforge.registries.ForgeRegistries import net.minecraftforge.registries.ForgeRegistry +import ru.dbotthepony.mc.otm.network.BigDecimalValueCodec +import ru.dbotthepony.mc.otm.network.BooleanValueCodec +import ru.dbotthepony.mc.otm.network.ByteValueCodec +import ru.dbotthepony.mc.otm.network.DoubleValueCodec +import ru.dbotthepony.mc.otm.network.FloatValueCodec +import ru.dbotthepony.mc.otm.network.ImpreciseFractionValueCodec +import ru.dbotthepony.mc.otm.network.IntValueCodec +import ru.dbotthepony.mc.otm.network.ItemStackValueCodec +import ru.dbotthepony.mc.otm.network.LongValueCodec +import ru.dbotthepony.mc.otm.network.NetworkValueCodec +import ru.dbotthepony.mc.otm.network.NullValueCodec +import ru.dbotthepony.mc.otm.network.ShortValueCodec +import ru.dbotthepony.mc.otm.network.UUIDValueCodec import java.io.* import java.math.BigDecimal import java.math.BigInteger +import java.util.LinkedList +import java.util.function.Predicate // But seriously, Mojang, why would you need to derive from ByteBuf directly, when you can implement // your own InputStream and OutputStream, since ByteBuf is meant to be operated on most time like a stream anyway? @@ -181,3 +197,71 @@ fun InputStream.readBigDecimal(): BigDecimal { read(bytes) return BigDecimal(BigInteger(bytes), scale) } + +private data class StreamCodec( + val condition: Predicate, + val id: Int, + val codec: NetworkValueCodec +) { + fun read(stream: DataInputStream): T { + return codec.read(stream) + } + + fun write(stream: DataOutputStream, value: Any?) { + codec.write(stream, value as T) + } +} + +private inline fun StreamCodec(id: Int, codec: NetworkValueCodec): StreamCodec { + return StreamCodec({it is T}, id, codec) +} + +private val codecs: List> = ImmutableList.builder>().let { + var codecID = 0 + + it.add(StreamCodec({ it == null }, codecID++, NullValueCodec)) + + it.add(StreamCodec(codecID++, BooleanValueCodec)) + it.add(StreamCodec(codecID++, ByteValueCodec)) + it.add(StreamCodec(codecID++, ShortValueCodec)) + it.add(StreamCodec(codecID++, IntValueCodec)) + it.add(StreamCodec(codecID++, LongValueCodec)) + it.add(StreamCodec(codecID++, FloatValueCodec)) + it.add(StreamCodec(codecID++, DoubleValueCodec)) + it.add(StreamCodec(codecID++, ItemStackValueCodec)) + it.add(StreamCodec(codecID++, ImpreciseFractionValueCodec)) + it.add(StreamCodec(codecID++, BigDecimalValueCodec)) + it.add(StreamCodec(codecID++, UUIDValueCodec)) + + it.build() +} + +class NotSerializableValueException(message: String) : Exception(message) + +/** + * Write arbitrary data to this stream, in exploit-free way + */ +fun DataOutputStream.writeType(value: Any?) { + for (codec in codecs) { + if (codec.condition.test(value)) { + write(codec.id) + codec.write(this, value) + return + } + } + + throw NotSerializableValueException("Value $value <${value?.let { it::class.qualifiedName }}> can not be serialized") +} + +/** + * Read arbitrary data from this stream, in exploit-free way + */ +fun DataInputStream.readType(): Any? { + val id = read() + + if (id >= codecs.size) { + throw IndexOutOfBoundsException("No codec for network type $id") + } + + return codecs[id].read(this) +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/SerializedFunctionRegistry.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/SerializedFunctionRegistry.kt new file mode 100644 index 000000000..a75456aed --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/SerializedFunctionRegistry.kt @@ -0,0 +1,215 @@ +package ru.dbotthepony.mc.otm.data + +import com.google.gson.Gson +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +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.JsonWriter +import io.netty.buffer.ByteBufInputStream +import io.netty.buffer.ByteBufOutputStream +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.resources.ResourceLocation +import ru.dbotthepony.mc.otm.core.readType +import ru.dbotthepony.mc.otm.core.readVarIntLE +import ru.dbotthepony.mc.otm.core.set +import ru.dbotthepony.mc.otm.core.writeType +import ru.dbotthepony.mc.otm.core.writeVarIntLE +import java.io.DataInputStream +import java.io.DataOutputStream +import java.lang.reflect.Type +import java.util.Base64 +import java.util.Collections +import java.util.LinkedList +import java.util.function.Consumer + +class SerializedFunctionRegistry(val gson: Gson = Gson()) : JsonSerializer>, JsonDeserializer>, TypeAdapter>() { + fun interface AnonymousFunction { + fun invoke(receiver: R, arguments: List) + } + + data class BoundFunction( + val function: Function, + val arguments: List + ) : Consumer { + fun toJson(): JsonElement { + return JsonObject().also { + it["function"] = function.toJson() + val stream = FastByteArrayOutputStream() + val dataStream = DataOutputStream(stream) + + stream.writeVarIntLE(arguments.size) + + for (argument in arguments) { + dataStream.writeType(argument) + } + + it["arguments"] = JsonPrimitive(Base64.getEncoder().encode(stream.array.copyOfRange(0, stream.length)).toString(Charsets.UTF_8)) + } + } + + fun toNetwork(buff: FriendlyByteBuf) { + function.toNetwork(buff) + + val stream = DataOutputStream(ByteBufOutputStream(buff)) + stream.writeVarIntLE(arguments.size) + for (argument in arguments) { + stream.writeType(argument) + } + } + + override fun accept(t: R) { + function.body.invoke(t, arguments) + } + } + + data class Function( + val id: ResourceLocation, + val body: AnonymousFunction, + val registry: SerializedFunctionRegistry + ) { + fun bind(vararg arguments: Any?): BoundFunction { + validate(arguments.iterator().withIndex()) + return BoundFunction(this, Collections.unmodifiableList(LinkedList().also { it.addAll(arguments) })) + } + + fun bind(arguments: List): BoundFunction { + validate(arguments.iterator().withIndex()) + return BoundFunction(this, Collections.unmodifiableList(LinkedList().also { it.addAll(arguments) })) + } + + fun toJson(): JsonElement { + return JsonPrimitive(id.toString()) + } + + fun toNetwork(buff: FriendlyByteBuf) { + buff.writeUtf(id.toString()) + } + + companion object { + private fun validate(iterator: Iterator>) { + try { + val stream = DataOutputStream(FastByteArrayOutputStream()) + + for ((i, argument) in iterator) { + try { + stream.writeType(argument) + } catch(err: Exception) { + throw IllegalArgumentException("Argument at $i can not be serialized", err) + } + } + } catch(err: Exception) { + throw IllegalArgumentException("Argument list validation failed", err) + } + } + } + } + + private val map = HashMap>() + + fun register(id: ResourceLocation, function: AnonymousFunction): Function { + synchronized(map) { + return map.computeIfAbsent(id) { Function(id, function, this) } + } + } + + inline fun register(id: ResourceLocation, noinline function: R.(A) -> Unit): Function { + return register(id, AnonymousFunction { r, args -> + check(args.size == 1) { "Invalid amount of arguments. 1 is required" } + function.invoke( + r, + args[0] as? A ?: throw ClassCastException("Argument at 0 is supposed to be ${A::class.qualifiedName}, ${args[0]?.let { it::class.qualifiedName }} given") + ) + }) + } + + inline fun register(id: ResourceLocation, noinline function: R.(A, B) -> Unit): Function { + return register(id, AnonymousFunction { r, args -> + check(args.size == 2) { "Invalid amount of arguments. 2 is required" } + + function.invoke( + r, + args[0] as? A ?: throw ClassCastException("Argument at 0 is supposed to be ${A::class.qualifiedName}, ${args[0]?.let { it::class.qualifiedName }} given"), + args[1] as? B ?: throw ClassCastException("Argument at 1 is supposed to be ${B::class.qualifiedName}, ${args[1]?.let { it::class.qualifiedName }} given"), + ) + }) + } + + inline fun register(id: ResourceLocation, noinline function: R.(A, B, C) -> Unit): Function { + return register(id, AnonymousFunction { r, args -> + check(args.size == 3) { "Invalid amount of arguments. 3 is required" } + + function.invoke( + r, + args[0] as? A ?: throw ClassCastException("Argument at 0 is supposed to be ${A::class.qualifiedName}, ${args[0]?.let { it::class.qualifiedName }} given"), + args[1] as? B ?: throw ClassCastException("Argument at 1 is supposed to be ${B::class.qualifiedName}, ${args[1]?.let { it::class.qualifiedName }} given"), + args[2] as? C ?: throw ClassCastException("Argument at 2 is supposed to be ${B::class.qualifiedName}, ${args[2]?.let { it::class.qualifiedName }} given"), + ) + }) + } + + fun get(id: ResourceLocation): Function? { + synchronized(map) { + return map[id] + } + } + + fun fromNetwork(buff: FriendlyByteBuf): BoundFunction? { + val id = ResourceLocation(buff.readUtf()) + val stream = DataInputStream(ByteBufInputStream(buff)) + + val arguments = LinkedList() + + for (i in 0 until stream.readVarIntLE()) { + arguments.add(stream.readType()) + } + + return map[id]?.bind(arguments) + } + + fun fromJson(value: JsonElement): BoundFunction? { + if (value !is JsonObject) { + return null + } + + val id = value["function"]?.asString ?: return null + val argumentString = value["arguments"]?.asString ?: return null + val stream = DataInputStream(FastByteArrayInputStream(Base64.getDecoder().decode(argumentString))) + val arguments = LinkedList() + + for (i in 0 until stream.readVarIntLE()) { + arguments.add(stream.readType()) + } + + return map[ResourceLocation(id)]?.bind(arguments) + } + + override fun serialize(src: BoundFunction, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return src.toJson() + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): BoundFunction { + return fromJson(json) ?: throw JsonSyntaxException("Function is invalid") + } + + override fun write(out: JsonWriter, value: BoundFunction) { + TypeAdapters.JSON_ELEMENT.write(out, value.toJson()) + } + + override fun read(`in`: JsonReader): BoundFunction { + return fromJson(TypeAdapters.JSON_ELEMENT.read(`in`)) ?: throw JsonSyntaxException("Function is invalid") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/network/FieldSynchronizer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/network/FieldSynchronizer.kt index 1271d5c65..dda57de31 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/network/FieldSynchronizer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/network/FieldSynchronizer.kt @@ -59,6 +59,8 @@ class NetworkValueCodec( } } +val NullValueCodec = NetworkValueCodec({ null }, { _, _ -> }) + val BooleanValueCodec = NetworkValueCodec(DataInputStream::readBoolean, DataOutputStream::writeBoolean) val ByteValueCodec = NetworkValueCodec(DataInputStream::readByte, { s, v -> s.writeByte(v.toInt()) }) val ShortValueCodec = NetworkValueCodec(DataInputStream::readShort, { s, v -> s.writeShort(v.toInt()) })