diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/Codec2RecipeSerializer.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/Codec2RecipeSerializer.kt index 16e1830f7..3ddf52bd2 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/data/Codec2RecipeSerializer.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/Codec2RecipeSerializer.kt @@ -11,6 +11,8 @@ import com.mojang.serialization.JsonOps import net.minecraft.data.recipes.FinishedRecipe import net.minecraft.network.FriendlyByteBuf import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient import net.minecraft.world.item.crafting.Recipe import net.minecraft.world.item.crafting.RecipeSerializer import org.apache.logging.log4j.LogManager @@ -20,51 +22,26 @@ import ru.dbotthepony.mc.otm.core.util.writeBinaryJsonWithCodec import kotlin.collections.ArrayDeque import kotlin.concurrent.getOrSet -class Codec2RecipeSerializer> private constructor( +class Codec2RecipeSerializer>( val empty: S?, - - /** - * [ThreadLocal] because optimization mods can (and probably should) parallelize recipe deserialization, - * since RecipeSerializers are expected to be stateless. [Codec2RecipeSerializer], however, is stateful (threading PoV). - * To make it stateless, [ThreadLocal] is used. - */ - private val idStack: ThreadLocal>, codec: (Codec2RecipeSerializer.Context) -> Codec, ) : Codec, RecipeSerializer { - constructor(empty: S?, codec: (Codec2RecipeSerializer.Context) -> Codec) : this(empty, ThreadLocal(), codec) constructor(supplier: (Codec2RecipeSerializer.Context) -> Codec) : this(null, supplier) - private val codec = codec.invoke(Context()) - private val id: ArrayDeque - get() = idStack.getOrSet { ArrayDeque() } - - inner class Context() { - val id: ResourceLocation - get() = checkNotNull(this@Codec2RecipeSerializer.id.lastOrNull()) { "Not currently deserializing recipe" } - - fun > wrap(other: Codec2RecipeSerializer): Codec { - return object : Codec { - override fun encode(input: O, ops: DynamicOps, prefix: T): DataResult { - try { - other.id.addLast(this@Context.id) - return other.encode(input, ops, prefix) - } finally { - other.id.removeLast() - } - } - - override fun decode(ops: DynamicOps, input: T): DataResult> { - try { - other.id.addLast(this@Context.id) - return other.decode(ops, input) - } finally { - other.id.removeLast() - } - } - } - } + private class CurrentContext { + val idStack = ArrayDeque() + var isNetwork = 0 } + inner class Context { + val id: ResourceLocation + get() = checkNotNull(context.idStack.lastOrNull()) { "Not currently deserializing recipe" } + + val ingredients: Codec get() = ActualIngredientCodec + } + + private val codec = codec.invoke(Context()) + override fun encode(input: S, ops: DynamicOps, prefix: T): DataResult { return codec.encode(input, ops, prefix) } @@ -74,41 +51,47 @@ class Codec2RecipeSerializer> private constructor( } fun > xmap(to: (S) -> O, from: (O) -> S): Codec2RecipeSerializer { - return Codec2RecipeSerializer(empty?.let(to), idStack) { _ -> + return Codec2RecipeSerializer(empty?.let(to)) { _ -> codec.xmap(to, from) } } override fun fromJson(id: ResourceLocation, data: JsonObject): S { try { - this.id.addLast(id) + context.idStack.addLast(id) return decode(JsonOps.INSTANCE, data).get().map( - { - it.first - }, - { - empty ?: throw JsonSyntaxException("Failed to deserialize recipe from JSON: ${it.message()}") - } + { it.first }, + { empty ?: throw JsonSyntaxException("Failed to deserialize recipe from JSON: ${it.message()}") } ) } finally { - this.id.removeLast() + context.idStack.removeLast() } } override fun fromNetwork(id: ResourceLocation, data: FriendlyByteBuf): S? { try { - this.id.addLast(id) + context.idStack.addLast(id) + context.isNetwork++ return data.readBinaryJsonWithCodecIndirect(this) .resultOrPartial { LOGGER.error("Failed to read recipe $id from network: $it") }.orElse(null) } finally { - this.id.removeLast() + context.isNetwork-- + context.idStack.removeLast() } } override fun toNetwork(data: FriendlyByteBuf, recipe: S) { - data.writeBinaryJsonWithCodec(this, recipe) + try { + context.idStack.addLast(recipe.id) + context.isNetwork++ + + data.writeBinaryJsonWithCodec(this, recipe) + } finally { + context.isNetwork-- + context.idStack.removeLast() + } } fun toFinished(recipe: S): FinishedRecipe { @@ -146,7 +129,35 @@ class Codec2RecipeSerializer> private constructor( } } + private object ActualIngredientCodec : Codec { + override fun encode(input: Ingredient, ops: DynamicOps, prefix: T): DataResult { + return if (context.isNetwork > 0) { + networkIngredientCodec.encode(input, ops, prefix) + } else { + IngredientCodec.encode(input, ops, prefix) + } + } + + override fun decode(ops: DynamicOps, input: T): DataResult> { + return if (context.isNetwork > 0) { + networkIngredientCodec.decode(ops, input) + } else { + IngredientCodec.decode(ops, input) + } + } + } + companion object { private val LOGGER = LogManager.getLogger() + private val networkIngredientCodec = Codec.list(ItemStack.CODEC).xmap({ Ingredient.of(it.stream()) }, { it.items.toMutableList() }) + + /** + * [ThreadLocal] because optimization mods can (and probably should) parallelize recipe deserialization, + * since RecipeSerializers are expected to be stateless. [Codec2RecipeSerializer], however, is stateful (threading PoV). + * To make it stateless, [ThreadLocal] is used. + */ + private val contextHolder = ThreadLocal() + private val context: CurrentContext + get() = contextHolder.getOrSet { CurrentContext() } } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientMatrixCodec.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientMatrixCodec.kt index f0ff5b1db..12d7117cd 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientMatrixCodec.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/data/IngredientMatrixCodec.kt @@ -9,24 +9,26 @@ import com.mojang.serialization.codecs.RecordCodecBuilder import net.minecraft.world.item.crafting.Ingredient import ru.dbotthepony.mc.otm.core.collect.allEqual import ru.dbotthepony.mc.otm.core.collect.map +import ru.dbotthepony.mc.otm.core.collect.toList import ru.dbotthepony.mc.otm.core.collect.toStream import ru.dbotthepony.mc.otm.core.stream import ru.dbotthepony.mc.otm.recipe.IIngredientMatrix import ru.dbotthepony.mc.otm.recipe.IngredientMatrix import java.util.function.Supplier -object IngredientMatrixCodec : Codec { - private val ingredientList = Codec.list(IngredientCodec) +class IngredientMatrixCodec(ingredientCodec: Codec) : Codec { + private val ingredientList = Codec.list(ingredientCodec) + private val doubleIngredientList = Codec.list(ingredientList) override fun encode(input: IIngredientMatrix, ops: DynamicOps, prefix: T): DataResult { - return DataResult.success( - ops.createList( - (0 until input.height).stream().map { row -> - ops.createList((0 until input.width).stream().map { column -> - JsonOps.INSTANCE.convertTo(ops, input[column, row].toJson()) - }) - } - ) + return doubleIngredientList.encode( + (0 until input.height).iterator().map { row -> + (0 until input.width).iterator().map { column -> + input[column, row] + }.toList() + }.toList(), + ops, + prefix ) } @@ -45,7 +47,7 @@ object IngredientMatrixCodec : Codec { .flatXmap( { if (it.length == 1) DataResult.success(it[0]) else DataResult.error { "Ingredient key must be exactly 1 symbol in length, '$it' is invalid" } }, { DataResult.success(it.toString()) } - ), IngredientCodec).fieldOf("key").forGetter(Handwritten::key) + ), ingredientCodec).fieldOf("key").forGetter(Handwritten::key) ).apply(it, ::Handwritten) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt index 15024b3b1..a836c3e52 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/MatterEntanglerRecipe.kt @@ -143,18 +143,10 @@ open class MatterEntanglerRecipe( } companion object { - val SERIALIZER = Codec2RecipeSerializer( - MatterEntanglerRecipe( - ResourceLocation(OverdriveThatMatters.MOD_ID, "null"), - IIngredientMatrix.Companion, - Decimal.ZERO, - 0.0, - ItemStack.EMPTY, - ) - ) { context -> + val SERIALIZER = Codec2RecipeSerializer { context -> RecordCodecBuilder.create { it.group( - IngredientMatrixCodec.fieldOf("ingredients").forGetter(MatterEntanglerRecipe::ingredients), + IngredientMatrixCodec(context.ingredients).fieldOf("ingredients").forGetter(MatterEntanglerRecipe::ingredients), DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(MatterEntanglerRecipe::matter), Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(MatterEntanglerRecipe::ticks), ItemStack.CODEC.fieldOf("result").forGetter(MatterEntanglerRecipe::result), diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PainterRecipe.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PainterRecipe.kt index 0e0d4ee77..cfc89caf4 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PainterRecipe.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PainterRecipe.kt @@ -113,12 +113,10 @@ class PainterRecipe( } companion object { - val SERIALIZER = Codec2RecipeSerializer( - PainterRecipe(ResourceLocation(OverdriveThatMatters.MOD_ID, "empty"), Ingredient.EMPTY, ItemStack.EMPTY, setOf()) - ) { context -> + val SERIALIZER = Codec2RecipeSerializer { context -> RecordCodecBuilder.create { it.group( - IngredientCodec.fieldOf("input").forGetter(PainterRecipe::input), + context.ingredients.fieldOf("input").forGetter(PainterRecipe::input), ItemStack.CODEC.fieldOf("output").forGetter(PainterRecipe::output), PredicatedCodecList>( DyeColor.CODEC.xmap({ mapOf(it to 1) }, { it.keys.first() }) to Predicate { it.keys.size == 1 && it.values.first() == 1 }, diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PlatePressRecipe.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PlatePressRecipe.kt index 0cb3d373b..583231dcc 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PlatePressRecipe.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/recipe/PlatePressRecipe.kt @@ -78,11 +78,11 @@ class PlatePressRecipe( fun toFinished() = SERIALIZER.toFinished(this) companion object { - val SERIALIZER = Codec2RecipeSerializer(PlatePressRecipe(ResourceLocation(OverdriveThatMatters.MOD_ID, "empty"), Ingredient.EMPTY, Ingredient.EMPTY, 1)) { context -> + val SERIALIZER = Codec2RecipeSerializer { context -> RecordCodecBuilder.create { it.group( - IngredientCodec.fieldOf("input").forGetter(PlatePressRecipe::input), - IngredientCodec.fieldOf("output").forGetter(PlatePressRecipe::output), + context.ingredients.fieldOf("input").forGetter(PlatePressRecipe::input), + context.ingredients.fieldOf("output").forGetter(PlatePressRecipe::output), Codec.INT.minRange(1).optionalFieldOf("count", 1).forGetter(PlatePressRecipe::count), Codec.INT.minRange(0).optionalFieldOf("workTime", 200).forGetter(PlatePressRecipe::workTime), FloatProvider.CODEC.optionalFieldOf("experience", ConstantFloat.ZERO).forGetter(PlatePressRecipe::experience)